SNAPKITTYWEST's picture
chore: push full sov-kernel-monster content from local build
9425aed verified
Raw
History Blame Contribute Delete
11.2 kB
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';
// ═══════════════════════════════════════════════════════════════════
// SCENE SETUP
// ═══════════════════════════════════════════════════════════════════
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);
// Orbit controls β€” cockpit rotation feel
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;
// ═══════════════════════════════════════════════════════════════════
// REFERENCE FRAME β€” BLOCH SPHERE
// ═══════════════════════════════════════════════════════════════════
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);
// Axes
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);
// Grid ring at equator
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);
// Origin marker
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);
// ═══════════════════════════════════════════════════════════════════
// TRAJECTORY STATE
// ═══════════════════════════════════════════════════════════════════
let animator = null;
let trajectoryMaterial = null;
const NUM_TRAJECTORIES = 1000;
const NUM_STEPS = 500;
// ═══════════════════════════════════════════════════════════════════
// ISS TELEMETRY (REAL SATELLITE DATA)
// ═══════════════════════════════════════════════════════════════════
const issTelemetry = new ISSTelemetry(scene, {
voyagerUrl: 'http://localhost:4299',
hudElement: document.getElementById('hud-iss'),
});
// ═══════════════════════════════════════════════════════════════════
// HUD UPDATE
// ═══════════════════════════════════════════════════════════════════
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}`;
}
// ═══════════════════════════════════════════════════════════════════
// CONTROLS
// ═══════════════════════════════════════════════════════════════════
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;
}
});
// ═══════════════════════════════════════════════════════════════════
// DEMO LOADER
// ═══════════════════════════════════════════════════════════════════
function loadDemo() {
// Remove existing trajectory if any
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();
}
/**
* Load trajectory from a binary file URL.
* Call this when serving real data from the Rust backend.
*/
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();
}
// Expose for console usage
window.loadFromBinary = loadFromBinary;
// ═══════════════════════════════════════════════════════════════════
// ANIMATION LOOP
// ═══════════════════════════════════════════════════════════════════
function animate() {
requestAnimationFrame(animate);
// FPS counter
frameCount++;
const now = performance.now();
if (now - lastFpsTime >= 1000) {
currentFps = frameCount;
frameCount = 0;
lastFpsTime = now;
}
// Advance trajectory animation
if (animator) {
animator.tick();
}
// Slow sphere rotation for ambient feel
sphere.rotation.y += 0.0005;
controls.update();
renderer.render(scene, camera);
updateHUD();
}
// ═══════════════════════════════════════════════════════════════════
// RESPONSIVE
// ═══════════════════════════════════════════════════════════════════
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// ═══════════════════════════════════════════════════════════════════
// INIT
// ═══════════════════════════════════════════════════════════════════
loadDemo();
animate();