import * as THREE from "three"; import { ACTIVE_SYSTEM_DETAIL_SCALE, PROJECTED_GALAXY_RADIUS } from "./viewerConstants"; import { computeMoonLocalPosition, computePlanetLocalPosition, currentWorldTimeSeconds, scaleLocalVector } from "./viewerMath"; import type { PlanetVisual, ShipVisual, SystemVisual, WorldState } from "./viewerTypes"; import { rawObject } from "./viewerScenePrimitives"; export const MIN_ICON_PIXELS = 25; export const MAX_ICON_PIXELS = 50; export function iconWorldScale(distToCamera: number, camera: THREE.PerspectiveCamera, pixels: number): number { return pixels * distToCamera * 2 * Math.tan((camera.fov * Math.PI / 180) / 2) / window.innerHeight; } export function getAnimatedShipLocalPosition(visual: ShipVisual, now = performance.now()) { const elapsedMs = now - visual.receivedAtMs; const blendT = THREE.MathUtils.clamp(elapsedMs / visual.blendDurationMs, 0, 1); return new THREE.Vector3().lerpVectors(visual.startPosition, visual.authoritativePosition, blendT); } export function resolveShipHeading(visual: ShipVisual, worldPosition: THREE.Vector3, orbitYaw: number) { const desiredHeading = visual.targetPosition.clone().sub(worldPosition); if (desiredHeading.lengthSq() > 0.01) { return desiredHeading; } if (visual.velocity.lengthSq() > 0.01) { return visual.velocity.clone(); } return new THREE.Vector3(Math.cos(orbitYaw), 0, Math.sin(orbitYaw)); } export function updatePlanetPresentation( world: WorldState | undefined, worldTimeSyncMs: number, planetVisuals: PlanetVisual[], systemCamera: THREE.PerspectiveCamera, ) { const nowSeconds = currentWorldTimeSeconds(world, worldTimeSyncMs); // In systemScene all positions use scaleLocalVector * ACTIVE_SYSTEM_DETAIL_SCALE. // Star is always at origin (0,0,0); orbits are centered there. for (const visual of planetVisuals) { const position = scaleLocalVector(computePlanetLocalPosition(visual.planet, nowSeconds)) .multiplyScalar(ACTIVE_SYSTEM_DETAIL_SCALE); visual.mesh.setPosition(position); visual.icon.setPosition(position); const iconWorldPos = visual.icon.getWorldPosition(new THREE.Vector3()); const distToIcon = systemCamera.position.distanceTo(iconWorldPos); const t = THREE.MathUtils.clamp(distToIcon / 300, 0, 1); const rawScale = visual.iconBaseScale * t * Math.sqrt(t); const planetIconScale = THREE.MathUtils.clamp(rawScale, iconWorldScale(distToIcon, systemCamera, MIN_ICON_PIXELS), iconWorldScale(distToIcon, systemCamera, MAX_ICON_PIXELS)); visual.icon.setScaleScalar(planetIconScale); if (visual.ring) { visual.ring.setPosition(position); } const distToPlanet = systemCamera.position.distanceTo(position); const moonOrbitOpacity = THREE.MathUtils.clamp(1 - distToPlanet / 500, 0, 1) * 0.18; const clusterVisible = distToPlanet < 300; for (const [moonIndex, moon] of visual.moons.entries()) { const moonPos = position.clone().add( scaleLocalVector(computeMoonLocalPosition(visual.planet.moons[moonIndex], nowSeconds)) .multiplyScalar(ACTIVE_SYSTEM_DETAIL_SCALE), ); moon.mesh.setPosition(moonPos); moon.mesh.setVisible(clusterVisible); moon.icon.setPosition(moonPos); moon.icon.setVisible(clusterVisible); if (clusterVisible) { const iconWorldPos = moon.icon.getWorldPosition(new THREE.Vector3()); const moonDist = systemCamera.position.distanceTo(iconWorldPos); const t = THREE.MathUtils.clamp(moonDist / 120, 0, 1); const rawMoonScale = moon.iconBaseScale * t * Math.sqrt(t); const moonIconScale = THREE.MathUtils.clamp(rawMoonScale, iconWorldScale(moonDist, systemCamera, MIN_ICON_PIXELS), iconWorldScale(moonDist, systemCamera, MAX_ICON_PIXELS)); moon.icon.setScaleScalar(moonIconScale); } moon.orbit.setPosition(position); const orbitObj = rawObject(moon.orbit); if (orbitObj instanceof THREE.LineLoop) { (orbitObj.material as THREE.LineBasicMaterial).opacity = moonOrbitOpacity; } } } } export function updateSystemStarPresentation( systemVisuals: Map, activeSystemId: string | undefined, galaxyCamera: THREE.PerspectiveCamera, setShellReticleOpacity: (sprite: SystemVisual["shellReticle"], opacity: number) => void, ) { const activeSystem = activeSystemId ? systemVisuals.get(activeSystemId) : undefined; for (const [systemId, visual] of systemVisuals.entries()) { // galaxyRoot is always at the galaxy position of this system visual.galaxyRoot.setPosition(visual.galaxyPosition); visual.shellReticle.setScaleScalar(visual.shellReticleBaseScale); if (!activeSystem) { // Galaxy view: show star dot, hide shell reticle visual.icon.setPosition(new THREE.Vector3(0, 0, 0)); visual.icon.setVisible(true); visual.shellReticle.setPosition(new THREE.Vector3(0, 0, 0)); visual.shellReticle.setVisible(false); setShellReticleOpacity(visual.shellReticle, 0); const dotWorldPos = visual.icon.getWorldPosition(new THREE.Vector3()); visual.icon.setScaleScalar(galaxyCamera.position.distanceTo(dotWorldPos) * 0.01); continue; } if (systemId !== activeSystemId) { // Other systems in galaxy view while a system is active: show shell reticle projected to edge visual.icon.setPosition(new THREE.Vector3(0, 0, 0)); visual.icon.setVisible(false); visual.shellReticle.setPosition(new THREE.Vector3(0, 0, 0)); visual.shellReticle.setVisible(true); setShellReticleOpacity(visual.shellReticle, 1); const direction = visual.galaxyPosition.clone().sub(activeSystem.galaxyPosition); if (direction.lengthSq() > 0.0001) { visual.galaxyRoot.setPosition( activeSystem.galaxyPosition.clone().add(direction.normalize().multiplyScalar(PROJECTED_GALAXY_RADIUS)), ); } const reticleWorldPosition = visual.galaxyRoot.getWorldPosition(new THREE.Vector3()); const reticleDistance = galaxyCamera.position.distanceTo(reticleWorldPosition); const reticleScale = Math.max(900, reticleDistance * 0.032); visual.shellReticle.setScaleScalar(reticleScale); continue; } // Active system in galaxy view: show star dot, hide shell reticle visual.icon.setPosition(new THREE.Vector3(0, 0, 0)); visual.icon.setVisible(true); visual.shellReticle.setVisible(false); setShellReticleOpacity(visual.shellReticle, 0); } }