| import * as THREE from 'three';
|
| import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
| import { setupBatchedTrajectories, TrajectoryAnimator } from './TrajectoryRenderer.js';
|
| import { generateDemoTrajectories } from './DemoGenerator.js';
|
| import { ISSTelemetry } from './ISSTelemetry.js';
|
|
|
|
|
|
|
|
|
| const container = document.getElementById('canvas-container');
|
| const scene = new THREE.Scene();
|
| scene.fog = new THREE.FogExp2(0x0a0a0f, 0.15);
|
|
|
| const camera = new THREE.PerspectiveCamera(
|
| 75,
|
| window.innerWidth / window.innerHeight,
|
| 0.01,
|
| 100
|
| );
|
| camera.position.set(2.2, 1.8, 2.2);
|
|
|
| const renderer = new THREE.WebGLRenderer({
|
| antialias: true,
|
| alpha: true,
|
| powerPreference: 'high-performance',
|
| });
|
| renderer.setSize(window.innerWidth, window.innerHeight);
|
| renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
| renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
| renderer.toneMappingExposure = 1.2;
|
| container.appendChild(renderer.domElement);
|
|
|
|
|
| const controls = new OrbitControls(camera, renderer.domElement);
|
| controls.enableDamping = true;
|
| controls.dampingFactor = 0.05;
|
| controls.autoRotate = true;
|
| controls.autoRotateSpeed = 0.3;
|
| controls.minDistance = 0.5;
|
| controls.maxDistance = 10;
|
|
|
|
|
|
|
|
|
| const sphereGeo = new THREE.SphereGeometry(1, 48, 48);
|
| const wireframe = new THREE.WireframeGeometry(sphereGeo);
|
| const sphereMat = new THREE.LineBasicMaterial({
|
| color: 0x222244,
|
| transparent: true,
|
| opacity: 0.12,
|
| });
|
| const sphere = new THREE.LineSegments(wireframe, sphereMat);
|
| scene.add(sphere);
|
|
|
|
|
| const axesGroup = new THREE.Group();
|
| const axisLen = 1.3;
|
| const axisMat = (color) => new THREE.LineBasicMaterial({ color, transparent: true, opacity: 0.4 });
|
|
|
| function addAxis(dir, color, label) {
|
| const points = [new THREE.Vector3(0, 0, 0), dir.clone().multiplyScalar(axisLen)];
|
| const geo = new THREE.BufferGeometry().setFromPoints(points);
|
| axesGroup.add(new THREE.Line(geo, axisMat(color)));
|
| }
|
| addAxis(new THREE.Vector3(1, 0, 0), 0xff4444, 'X');
|
| addAxis(new THREE.Vector3(0, 1, 0), 0x44ff44, 'Y');
|
| addAxis(new THREE.Vector3(0, 0, 1), 0x4444ff, 'Z');
|
| scene.add(axesGroup);
|
|
|
|
|
| const ringGeo = new THREE.RingGeometry(0.99, 1.01, 64);
|
| const ringMat = new THREE.MeshBasicMaterial({
|
| color: 0x00ffcc,
|
| transparent: true,
|
| opacity: 0.08,
|
| side: THREE.DoubleSide,
|
| });
|
| const ring = new THREE.Mesh(ringGeo, ringMat);
|
| ring.rotation.x = Math.PI / 2;
|
| scene.add(ring);
|
|
|
|
|
| const originGeo = new THREE.SphereGeometry(0.02, 16, 16);
|
| const originMat = new THREE.MeshBasicMaterial({ color: 0xffd700 });
|
| const origin = new THREE.Mesh(originGeo, originMat);
|
| scene.add(origin);
|
|
|
|
|
|
|
|
|
| let animator = null;
|
| let trajectoryMaterial = null;
|
|
|
| const NUM_TRAJECTORIES = 1000;
|
| const NUM_STEPS = 500;
|
|
|
|
|
|
|
|
|
| const issTelemetry = new ISSTelemetry(scene, {
|
| voyagerUrl: 'http://localhost:4299',
|
| hudElement: document.getElementById('hud-iss'),
|
| });
|
|
|
|
|
|
|
|
|
| const hudTrajectories = document.querySelector('#hud-trajectories .value');
|
| const hudSteps = document.querySelector('#hud-steps .value');
|
| const hudProgress = document.querySelector('#hud-progress .value');
|
| const hudFps = document.querySelector('#hud-fps .value');
|
|
|
| let frameCount = 0;
|
| let lastFpsTime = performance.now();
|
| let currentFps = 0;
|
|
|
| function updateHUD() {
|
| if (animator) {
|
| hudProgress.textContent = `${(animator.progress * 100).toFixed(1)}%`;
|
| }
|
| hudFps.textContent = `${currentFps}`;
|
| }
|
|
|
|
|
|
|
|
|
| document.getElementById('btn-play').addEventListener('click', () => {
|
| if (animator) animator.play();
|
| });
|
|
|
| document.getElementById('btn-pause').addEventListener('click', () => {
|
| if (animator) animator.pause();
|
| });
|
|
|
| document.getElementById('btn-reset').addEventListener('click', () => {
|
| if (animator) animator.reset();
|
| });
|
|
|
| document.getElementById('btn-demo').addEventListener('click', () => {
|
| loadDemo();
|
| });
|
|
|
| document.getElementById('btn-iss').addEventListener('click', () => {
|
| if (!issTelemetry.isLive) {
|
| issTelemetry.startPolling(4500);
|
| } else {
|
| issTelemetry.stopPolling();
|
| }
|
| });
|
|
|
| document.getElementById('speed-slider').addEventListener('input', (e) => {
|
| if (animator) {
|
| let rate = parseInt(e.target.value, 10);
|
| if (rate % 2 !== 0) rate++;
|
| animator.setSpeed(rate);
|
| }
|
| });
|
|
|
| document.getElementById('opacity-slider').addEventListener('input', (e) => {
|
| if (trajectoryMaterial) {
|
| trajectoryMaterial.opacity = parseInt(e.target.value, 10) / 100;
|
| }
|
| });
|
|
|
|
|
|
|
|
|
| function loadDemo() {
|
|
|
| if (animator) {
|
| scene.remove(animator.lineSegments);
|
| animator = null;
|
| }
|
|
|
| const buffer = generateDemoTrajectories(NUM_TRAJECTORIES, NUM_STEPS, 0.01, 0.3);
|
| const { lineSegments, maxIndices, material } = setupBatchedTrajectories(
|
| scene, buffer, NUM_TRAJECTORIES, NUM_STEPS
|
| );
|
|
|
| trajectoryMaterial = material;
|
| animator = new TrajectoryAnimator(lineSegments, maxIndices, 6);
|
|
|
| hudTrajectories.textContent = NUM_TRAJECTORIES.toLocaleString();
|
| hudSteps.textContent = NUM_STEPS.toLocaleString();
|
| }
|
|
|
| |
| |
| |
|
|
| export async function loadFromBinary(url, numTrajectories, numSteps) {
|
| if (animator) {
|
| scene.remove(animator.lineSegments);
|
| animator = null;
|
| }
|
|
|
| const response = await fetch(url);
|
| const buffer = await response.arrayBuffer();
|
| const { lineSegments, maxIndices, material } = setupBatchedTrajectories(
|
| scene, buffer, numTrajectories, numSteps
|
| );
|
|
|
| trajectoryMaterial = material;
|
| animator = new TrajectoryAnimator(lineSegments, maxIndices, 6);
|
|
|
| hudTrajectories.textContent = numTrajectories.toLocaleString();
|
| hudSteps.textContent = numSteps.toLocaleString();
|
| }
|
|
|
|
|
| window.loadFromBinary = loadFromBinary;
|
|
|
|
|
|
|
|
|
| function animate() {
|
| requestAnimationFrame(animate);
|
|
|
|
|
| frameCount++;
|
| const now = performance.now();
|
| if (now - lastFpsTime >= 1000) {
|
| currentFps = frameCount;
|
| frameCount = 0;
|
| lastFpsTime = now;
|
| }
|
|
|
|
|
| if (animator) {
|
| animator.tick();
|
| }
|
|
|
|
|
| sphere.rotation.y += 0.0005;
|
|
|
| controls.update();
|
| renderer.render(scene, camera);
|
| updateHUD();
|
| }
|
|
|
|
|
|
|
|
|
| window.addEventListener('resize', () => {
|
| camera.aspect = window.innerWidth / window.innerHeight;
|
| camera.updateProjectionMatrix();
|
| renderer.setSize(window.innerWidth, window.innerHeight);
|
| });
|
|
|
|
|
|
|
|
|
| loadDemo();
|
| animate();
|
|
|