import * as THREE from "three"; import { scaleGalaxyVector, toThreeVector } from "./viewerMath"; import { determineActiveSystemId, focusOnSelection, getCameraFocusWorldPosition, getSystemCameraFocus, resolveSelectionPosition, seedSystemFocusLocal, toDisplayLocalPosition, } from "./viewerCamera"; import { syncFollowStateFromSelection, updateFollowCamera, } from "./viewerControls"; import { computeNodeLocalPosition, resolvePointPosition } from "./viewerWorldPresentation"; import { getAnimatedShipLocalPosition, resolveShipHeading } from "./viewerPresentation"; import type { CameraMode, NodeVisual, PlanetVisual, Selectable, ShipVisual, SystemVisual, WorldState, PovLevel, } from "./viewerTypes"; export interface ViewerNavigationContext { getWorld: () => WorldState | undefined; getWorldTimeSyncMs: () => number; getActiveSystemId: () => string | undefined; setActiveSystemId: (value: string | undefined) => void; onActiveSystemChanged: (oldId: string | undefined, newId: string | undefined) => void; getCameraMode: () => CameraMode; setCameraMode: (value: CameraMode) => void; getCameraTargetShipId: () => string | undefined; setCameraTargetShipId: (value: string | undefined) => void; getCurrentDistance: () => number; getPovLevel: () => PovLevel; getSelectedItems: () => Selectable[]; getOrbitYaw: () => number; getFollowOrbitYaw: () => number; getFollowOrbitPitch: () => number; galaxyAnchor: THREE.Vector3; systemAnchor: THREE.Vector3; galaxyCamera: THREE.PerspectiveCamera; systemCamera: THREE.PerspectiveCamera; shipVisuals: Map; nodeVisuals: Map; planetVisuals: PlanetVisual[]; systemVisuals: Map; followCameraPosition: THREE.Vector3; followCameraFocus: THREE.Vector3; followCameraDirection: THREE.Vector3; followCameraDesiredDirection: THREE.Vector3; followCameraOffset: THREE.Vector3; createWorldPresentationContext: () => any; updatePanels: () => void; updateGamePanel: (mode: string) => void; } export class ViewerNavigationController { constructor(private readonly context: ViewerNavigationContext) {} syncGalaxyAnchorToActiveSystem() { const world = this.context.getWorld(); const activeSystemId = this.context.getActiveSystemId(); const povLevel = this.context.getPovLevel(); if (!world || !activeSystemId || povLevel === "galaxy") { return; } const system = world.systems.get(activeSystemId); if (!system) { return; } this.context.galaxyAnchor.copy(scaleGalaxyVector(toThreeVector(system.galaxyPosition))); } focusOnSelection(selection: Selectable) { focusOnSelection({ world: this.context.getWorld(), selection, worldTimeSyncMs: this.context.getWorldTimeSyncMs(), nodeVisuals: this.context.nodeVisuals, planetVisuals: this.context.planetVisuals, computeNodeLocalPosition: (node, timeSeconds) => computeNodeLocalPosition(this.context.createWorldPresentationContext(), node, timeSeconds), resolvePointPosition: (systemId, celestialId, anchorId) => resolvePointPosition(this.context.createWorldPresentationContext(), systemId, celestialId, anchorId), activeSystemId: this.context.getActiveSystemId(), galaxyAnchor: this.context.galaxyAnchor, systemAnchor: this.context.systemAnchor, }); } resolveSelectionPosition(selection: Selectable) { return resolveSelectionPosition({ world: this.context.getWorld(), selection, worldTimeSyncMs: this.context.getWorldTimeSyncMs(), nodeVisuals: this.context.nodeVisuals, planetVisuals: this.context.planetVisuals, computeNodeLocalPosition: (node, timeSeconds) => computeNodeLocalPosition(this.context.createWorldPresentationContext(), node, timeSeconds), resolvePointPosition: (systemId, celestialId, anchorId) => resolvePointPosition(this.context.createWorldPresentationContext(), systemId, celestialId, anchorId), }); } updateActiveSystem() { const nextActiveSystemId = determineActiveSystemId({ world: this.context.getWorld(), cameraMode: this.context.getCameraMode(), cameraTargetShipId: this.context.getCameraTargetShipId(), currentDistance: this.context.getCurrentDistance(), selectedItems: this.context.getSelectedItems(), galaxyAnchor: this.context.galaxyAnchor, }); const previousSystemId = this.context.getActiveSystemId(); if (nextActiveSystemId === previousSystemId) { return; } if (nextActiveSystemId) { this.seedSystemFocusLocal(nextActiveSystemId); } this.context.setActiveSystemId(nextActiveSystemId); this.context.onActiveSystemChanged(previousSystemId, nextActiveSystemId); this.context.updatePanels(); this.context.updateGamePanel("Live"); } updateFollowCamera(delta: number) { const nextState = updateFollowCamera({ world: this.context.getWorld(), cameraMode: this.context.getCameraMode(), cameraTargetShipId: this.context.getCameraTargetShipId(), shipVisuals: this.context.shipVisuals, currentDistance: this.context.getCurrentDistance(), camera: this.context.systemCamera, followCameraPosition: this.context.followCameraPosition, followCameraFocus: this.context.followCameraFocus, followCameraDirection: this.context.followCameraDirection, followCameraDesiredDirection: this.context.followCameraDesiredDirection, followCameraOffset: this.context.followCameraOffset, systemAnchor: this.context.systemAnchor, delta, followOrbitYaw: this.context.getFollowOrbitYaw(), followOrbitPitch: this.context.getFollowOrbitPitch(), getAnimatedShipLocalPosition, toDisplayLocalPosition: (localPosition) => toDisplayLocalPosition(localPosition), resolveShipHeading: (visual, worldPosition) => resolveShipHeading(visual, worldPosition, this.context.getOrbitYaw()), }); this.context.setCameraMode(nextState.cameraMode); this.context.setCameraTargetShipId(nextState.cameraTargetShipId); return nextState.handled; } syncFollowStateFromSelection() { const nextState = syncFollowStateFromSelection( this.context.getSelectedItems(), this.context.getCameraMode(), this.context.getCameraTargetShipId(), ); this.context.setCameraMode(nextState.cameraMode); this.context.setCameraTargetShipId(nextState.cameraTargetShipId); } getCameraFocusWorldPosition() { return getCameraFocusWorldPosition({ galaxyAnchor: this.context.galaxyAnchor, }); } getSystemCameraFocus() { return getSystemCameraFocus(this.context.systemAnchor); } seedSystemFocusLocal(systemId: string) { seedSystemFocusLocal({ world: this.context.getWorld(), systemId, cameraMode: this.context.getCameraMode(), cameraTargetShipId: this.context.getCameraTargetShipId(), selectedItems: this.context.getSelectedItems(), systemAnchor: this.context.systemAnchor, worldTimeSyncMs: this.context.getWorldTimeSyncMs(), nodeVisuals: this.context.nodeVisuals, planetVisuals: this.context.planetVisuals, computeNodeLocalPosition: (node, timeSeconds) => computeNodeLocalPosition(this.context.createWorldPresentationContext(), node, timeSeconds), resolvePointPosition: (systemIdValue, celestialId, anchorId) => resolvePointPosition(this.context.createWorldPresentationContext(), systemIdValue, celestialId, anchorId), }); } toDisplayLocalPosition(localPosition: THREE.Vector3) { return toDisplayLocalPosition(localPosition); } /** Returns a display position for the system camera, derived from a raw local position in meters. */ toSystemDisplayPosition(localPosition: THREE.Vector3) { return toDisplayLocalPosition(localPosition); } }