import * as THREE from 'three'; export class OrbitPreview { private line: THREE.Line | null = null; private starMesh: THREE.Mesh | null = null; private starGlow: THREE.Sprite | null = null; private planetMesh: THREE.Mesh | null = null; private hzRing: THREE.Line | null = null; private gridHelper: THREE.GridHelper | null = null; private scene: THREE.Scene; private orbitPoints: THREE.Vector3[] = []; private orbitAngle = 0; private orbitSpeed = 0.005; private paramOverlay: HTMLElement | null = null; private parentEl: HTMLElement | null = null; constructor(scene: THREE.Scene) { this.scene = scene; this.addStar(); this.addGrid(); } private addStar(): void { // Solid sphere const geo = new THREE.SphereGeometry(0.18, 24, 16); const mat = new THREE.MeshBasicMaterial({ color: 0xffdd44 }); this.starMesh = new THREE.Mesh(geo, mat); this.scene.add(this.starMesh); // Glow sprite const canvas = document.createElement('canvas'); canvas.width = 64; canvas.height = 64; const ctx = canvas.getContext('2d'); if (ctx) { const grad = ctx.createRadialGradient(32, 32, 2, 32, 32, 32); grad.addColorStop(0, 'rgba(255,221,68,0.6)'); grad.addColorStop(0.4, 'rgba(255,200,50,0.15)'); grad.addColorStop(1, 'rgba(255,200,50,0)'); ctx.fillStyle = grad; ctx.fillRect(0, 0, 64, 64); } const tex = new THREE.CanvasTexture(canvas); const spriteMat = new THREE.SpriteMaterial({ map: tex, transparent: true, blending: THREE.AdditiveBlending }); this.starGlow = new THREE.Sprite(spriteMat); this.starGlow.scale.set(1.2, 1.2, 1); this.scene.add(this.starGlow); } private addGrid(): void { this.gridHelper = new THREE.GridHelper(8, 8, 0x1C2333, 0x151B23); this.gridHelper.position.y = -0.5; this.scene.add(this.gridHelper); } setOrbit( semiMajorAxis: number, eccentricity: number, inclination: number, parentElement?: HTMLElement, ): void { this.disposeLine(); this.disposePlanet(); this.disposeHzRing(); this.disposeOverlay(); const segments = 128; this.orbitPoints = []; const a = semiMajorAxis; const e = Math.min(Math.max(eccentricity, 0), 0.99); const incRad = (inclination * Math.PI) / 180; for (let i = 0; i <= segments; i++) { const theta = (i / segments) * Math.PI * 2; const r = (a * (1 - e * e)) / (1 + e * Math.cos(theta)); const x = r * Math.cos(theta); const z = r * Math.sin(theta) * Math.cos(incRad); const y = r * Math.sin(theta) * Math.sin(incRad); this.orbitPoints.push(new THREE.Vector3(x, y, z)); } // Orbit path const geometry = new THREE.BufferGeometry().setFromPoints(this.orbitPoints); const material = new THREE.LineBasicMaterial({ color: 0x4488ff, transparent: true, opacity: 0.7, }); this.line = new THREE.Line(geometry, material); this.scene.add(this.line); // Planet dot const planetGeo = new THREE.SphereGeometry(0.08, 12, 8); const planetMat = new THREE.MeshStandardMaterial({ color: 0x4488ff, emissive: 0x2244aa, emissiveIntensity: 0.3 }); this.planetMesh = new THREE.Mesh(planetGeo, planetMat); this.planetMesh.position.copy(this.orbitPoints[0]); this.scene.add(this.planetMesh); // Habitable zone ring (0.95-1.37 AU scaled) const hzInner = 0.95 * (a / 1.5); const hzOuter = 1.37 * (a / 1.5); const hzMid = (hzInner + hzOuter) / 2; const hzPts: THREE.Vector3[] = []; for (let i = 0; i <= 64; i++) { const theta = (i / 64) * Math.PI * 2; hzPts.push(new THREE.Vector3(hzMid * Math.cos(theta), -0.48, hzMid * Math.sin(theta))); } const hzGeo = new THREE.BufferGeometry().setFromPoints(hzPts); const hzMat = new THREE.LineBasicMaterial({ color: 0x2ECC71, transparent: true, opacity: 0.25 }); this.hzRing = new THREE.Line(hzGeo, hzMat); this.scene.add(this.hzRing); // Orbit speed based on period (faster for shorter periods) this.orbitSpeed = 0.003 + (1 / (a * 10)) * 0.02; this.orbitAngle = 0; // Param overlay if (parentElement) { this.parentEl = parentElement; this.paramOverlay = document.createElement('div'); this.paramOverlay.style.cssText = 'position:absolute;bottom:8px;left:8px;' + 'background:rgba(11,15,20,0.85);border:1px solid var(--border);border-radius:4px;' + 'padding:6px 10px;font-family:var(--font-mono);font-size:10px;color:var(--text-secondary);' + 'line-height:1.6;z-index:10;pointer-events:none'; this.paramOverlay.innerHTML = `