feat: 3 scene rendering setup
This commit is contained in:
5
.claude/settings.json
Normal file
5
.claude/settings.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"enabledPlugins": {
|
||||||
|
"frontend-design@claude-plugins-official": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,49 +2,15 @@ import * as THREE from "three";
|
|||||||
import {
|
import {
|
||||||
MAX_CAMERA_DISTANCE,
|
MAX_CAMERA_DISTANCE,
|
||||||
MIN_CAMERA_DISTANCE,
|
MIN_CAMERA_DISTANCE,
|
||||||
ZOOM_DISTANCE,
|
NAV_DISTANCE,
|
||||||
} from "./viewerConstants";
|
} from "./viewerConstants";
|
||||||
import { createViewerHud } from "./viewerHud";
|
import { createViewerHud } from "./viewerHud";
|
||||||
import {
|
|
||||||
classifyZoomLevel,
|
|
||||||
computeZoomBlend,
|
|
||||||
formatBytes,
|
|
||||||
inventoryAmount,
|
|
||||||
smoothBand,
|
|
||||||
} from "./viewerMath";
|
|
||||||
import { updatePanFromKeyboard } from "./viewerCamera";
|
import { updatePanFromKeyboard } from "./viewerCamera";
|
||||||
import {
|
import { setShellReticleOpacity } from "./viewerControls";
|
||||||
createCirclePoints,
|
|
||||||
shipLength,
|
|
||||||
shipPresentationColor,
|
|
||||||
shipSize,
|
|
||||||
spatialNodeColor,
|
|
||||||
} from "./viewerSceneAppearance";
|
|
||||||
import {
|
|
||||||
createBackdropStars,
|
|
||||||
createNebulaClouds,
|
|
||||||
createNebulaTexture,
|
|
||||||
} from "./viewerSceneFactory";
|
|
||||||
import {
|
|
||||||
setShellReticleOpacity,
|
|
||||||
} from "./viewerControls";
|
|
||||||
import {
|
|
||||||
recordPerformanceStats,
|
|
||||||
updateNetworkPanel as renderNetworkPanel,
|
|
||||||
updatePerformancePanel as renderPerformancePanel,
|
|
||||||
} from "./viewerTelemetry";
|
|
||||||
import { renderFrame, resizeViewer, stepCamera } from "./viewerRenderLoop";
|
import { renderFrame, resizeViewer, stepCamera } from "./viewerRenderLoop";
|
||||||
import { updatePlanetPresentation } from "./viewerPresentation";
|
import { updateSystemStarPresentation } from "./viewerPresentation";
|
||||||
import {
|
import { resolveFocusedCelestialId } from "./viewerSelection";
|
||||||
renderRecentEvents,
|
import { describeSelectionParent } from "./viewerPanels";
|
||||||
updateGameStatus,
|
|
||||||
updateSystemSummaries,
|
|
||||||
updateWorldPresentation,
|
|
||||||
} from "./viewerWorldPresentation";
|
|
||||||
import {
|
|
||||||
resolveFocusedBubbleId,
|
|
||||||
} from "./viewerSelection";
|
|
||||||
import { describeSelectionParent, updateSystemPanel } from "./viewerPanels";
|
|
||||||
import {
|
import {
|
||||||
createInitialNetworkStats,
|
createInitialNetworkStats,
|
||||||
createInitialPerformanceStats,
|
createInitialPerformanceStats,
|
||||||
@@ -55,68 +21,65 @@ import { ViewerNavigationController } from "./viewerNavigationController";
|
|||||||
import { ViewerSceneDataController } from "./viewerSceneDataController";
|
import { ViewerSceneDataController } from "./viewerSceneDataController";
|
||||||
import { ViewerPresentationController } from "./viewerPresentationController";
|
import { ViewerPresentationController } from "./viewerPresentationController";
|
||||||
import { createViewerControllers, wireViewerEvents } from "./viewerControllerFactory";
|
import { createViewerControllers, wireViewerEvents } from "./viewerControllerFactory";
|
||||||
import type { SceneNode } from "./viewerScenePrimitives";
|
import { toDisplayLocalPosition, getSystemCameraFocus } from "./viewerCamera";
|
||||||
import type { FactionSnapshot, ShipSnapshot } from "./contracts";
|
import { UniverseLayer } from "./viewerUniverseLayer";
|
||||||
|
import { GalaxyLayer } from "./viewerGalaxyLayer";
|
||||||
|
import { SystemLayer } from "./viewerSystemLayer";
|
||||||
|
import { LocalLayer } from "./viewerLocalLayer";
|
||||||
|
import type { FactionSnapshot } from "./contracts";
|
||||||
import type {
|
import type {
|
||||||
BubbleVisual,
|
CelestialVisual,
|
||||||
CameraMode,
|
CameraMode,
|
||||||
ClaimVisual,
|
ClaimVisual,
|
||||||
ConstructionSiteVisual,
|
ConstructionSiteVisual,
|
||||||
DragMode,
|
DragMode,
|
||||||
HistoryWindowState,
|
HistoryWindowState,
|
||||||
MoonVisual,
|
|
||||||
NetworkStats,
|
NetworkStats,
|
||||||
NodeVisual,
|
NodeVisual,
|
||||||
OrbitLineVisual,
|
OrbitLineVisual,
|
||||||
OrbitalAnchor,
|
|
||||||
PerformanceStats,
|
PerformanceStats,
|
||||||
PlanetVisual,
|
|
||||||
PresentationEntry,
|
|
||||||
Selectable,
|
Selectable,
|
||||||
ShipVisual,
|
ShipVisual,
|
||||||
SpatialNodeVisual,
|
|
||||||
StructureVisual,
|
StructureVisual,
|
||||||
SystemSummaryVisual,
|
|
||||||
SystemVisual,
|
SystemVisual,
|
||||||
WorldState,
|
WorldState,
|
||||||
ZoomLevel,
|
PovLevel,
|
||||||
} from "./viewerTypes";
|
} from "./viewerTypes";
|
||||||
|
|
||||||
export class ViewerAppController {
|
export class ViewerAppController {
|
||||||
private readonly container: HTMLElement;
|
private readonly container: HTMLElement;
|
||||||
private readonly renderer = new THREE.WebGLRenderer({ antialias: true });
|
private readonly renderer = new THREE.WebGLRenderer({ antialias: true });
|
||||||
private readonly scene = new THREE.Scene();
|
|
||||||
private readonly camera = new THREE.PerspectiveCamera(50, 1, 0.1, 160000);
|
// ── Three independent rendering layers ───────────────────────────────────
|
||||||
|
private readonly universeLayer = new UniverseLayer();
|
||||||
|
private readonly galaxyLayer = new GalaxyLayer();
|
||||||
|
private readonly systemLayer = new SystemLayer();
|
||||||
|
private readonly localLayer = new LocalLayer();
|
||||||
|
|
||||||
private readonly clock = new THREE.Clock();
|
private readonly clock = new THREE.Clock();
|
||||||
private readonly raycaster = new THREE.Raycaster();
|
private readonly raycaster = new THREE.Raycaster();
|
||||||
private readonly mouse = new THREE.Vector2();
|
private readonly mouse = new THREE.Vector2();
|
||||||
private readonly galaxyFocus = new THREE.Vector3(2200, 0, 300);
|
|
||||||
private readonly systemFocusLocal = new THREE.Vector3();
|
// ── Galaxy-space anchor ───────────────────────────────────────────────────
|
||||||
|
private readonly galaxyAnchor = new THREE.Vector3(2200, 0, 300);
|
||||||
|
// ── System-space anchor ───────────────────────────────────────────────────
|
||||||
|
private readonly systemAnchor = new THREE.Vector3();
|
||||||
|
|
||||||
private readonly cameraOffset = new THREE.Vector3();
|
private readonly cameraOffset = new THREE.Vector3();
|
||||||
private readonly keyState = new Set<string>();
|
private readonly keyState = new Set<string>();
|
||||||
private readonly systemGroup = new THREE.Group();
|
|
||||||
private readonly spatialNodeGroup = new THREE.Group();
|
|
||||||
private readonly bubbleGroup = new THREE.Group();
|
|
||||||
private readonly nodeGroup = new THREE.Group();
|
|
||||||
private readonly stationGroup = new THREE.Group();
|
|
||||||
private readonly claimGroup = new THREE.Group();
|
|
||||||
private readonly constructionSiteGroup = new THREE.Group();
|
|
||||||
private readonly shipGroup = new THREE.Group();
|
|
||||||
private readonly ambienceGroup = new THREE.Group();
|
|
||||||
private readonly gamePanelEl: HTMLDivElement;
|
private readonly gamePanelEl: HTMLDivElement;
|
||||||
private readonly selectableTargets = new Map<THREE.Object3D, Selectable>();
|
|
||||||
private readonly presentationEntries: PresentationEntry[] = [];
|
private readonly celestialVisuals = new Map<string, CelestialVisual>();
|
||||||
private readonly nodeVisuals = new Map<string, NodeVisual>();
|
|
||||||
private readonly spatialNodeVisuals = new Map<string, SpatialNodeVisual>();
|
|
||||||
private readonly bubbleVisuals = new Map<string, BubbleVisual>();
|
|
||||||
private readonly stationVisuals = new Map<string, StructureVisual>();
|
private readonly stationVisuals = new Map<string, StructureVisual>();
|
||||||
private readonly claimVisuals = new Map<string, ClaimVisual>();
|
private readonly claimVisuals = new Map<string, ClaimVisual>();
|
||||||
private readonly constructionSiteVisuals = new Map<string, ConstructionSiteVisual>();
|
private readonly constructionSiteVisuals = new Map<string, ConstructionSiteVisual>();
|
||||||
private readonly shipVisuals = new Map<string, ShipVisual>();
|
private readonly shipVisuals = new Map<string, ShipVisual>();
|
||||||
private readonly systemVisuals = new Map<string, SystemVisual>();
|
private readonly systemVisuals = new Map<string, SystemVisual>();
|
||||||
private readonly systemSummaryVisuals = new Map<string, SystemSummaryVisual>();
|
private readonly nodeVisuals = new Map<string, NodeVisual>();
|
||||||
private readonly planetVisuals: PlanetVisual[] = [];
|
private readonly planetVisuals: any[] = [];
|
||||||
private readonly orbitLines: OrbitLineVisual[] = [];
|
private readonly orbitLines: OrbitLineVisual[] = [];
|
||||||
|
|
||||||
private readonly statusEl: HTMLDivElement;
|
private readonly statusEl: HTMLDivElement;
|
||||||
private readonly gameSummaryEl: HTMLSpanElement;
|
private readonly gameSummaryEl: HTMLSpanElement;
|
||||||
private readonly systemPanelEl: HTMLDivElement;
|
private readonly systemPanelEl: HTMLDivElement;
|
||||||
@@ -145,9 +108,9 @@ export class ViewerAppController {
|
|||||||
|
|
||||||
private selectedItems: Selectable[] = [];
|
private selectedItems: Selectable[] = [];
|
||||||
private worldSignature = "";
|
private worldSignature = "";
|
||||||
private zoomLevel: ZoomLevel = "system";
|
private povLevel: PovLevel = "system";
|
||||||
private currentDistance = ZOOM_DISTANCE.system;
|
private currentDistance = NAV_DISTANCE.system;
|
||||||
private desiredDistance = ZOOM_DISTANCE.system;
|
private desiredDistance = NAV_DISTANCE.system;
|
||||||
private orbitYaw = -2.3;
|
private orbitYaw = -2.3;
|
||||||
private orbitPitch = 0.62;
|
private orbitPitch = 0.62;
|
||||||
private cameraMode: CameraMode = "tactical";
|
private cameraMode: CameraMode = "tactical";
|
||||||
@@ -181,23 +144,7 @@ export class ViewerAppController {
|
|||||||
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
||||||
this.renderer.outputColorSpace = THREE.SRGBColorSpace;
|
this.renderer.outputColorSpace = THREE.SRGBColorSpace;
|
||||||
|
|
||||||
this.scene.background = new THREE.Color(0x040912);
|
|
||||||
this.scene.fog = new THREE.FogExp2(0x040912, 0.00011);
|
|
||||||
this.scene.add(new THREE.AmbientLight(0x90a6c0, 0.55));
|
|
||||||
const keyLight = new THREE.DirectionalLight(0xdcecff, 1.3);
|
|
||||||
keyLight.position.set(1000, 1200, 800);
|
|
||||||
this.scene.add(keyLight);
|
|
||||||
this.scene.add(
|
|
||||||
this.ambienceGroup,
|
|
||||||
this.systemGroup,
|
|
||||||
this.spatialNodeGroup,
|
|
||||||
this.bubbleGroup,
|
|
||||||
this.nodeGroup,
|
|
||||||
this.stationGroup,
|
|
||||||
this.claimGroup,
|
|
||||||
this.constructionSiteGroup,
|
|
||||||
this.shipGroup,
|
|
||||||
);
|
|
||||||
const hud = createViewerHud(document);
|
const hud = createViewerHud(document);
|
||||||
this.gamePanelEl = hud.gamePanelEl;
|
this.gamePanelEl = hud.gamePanelEl;
|
||||||
this.statusEl = hud.statusEl;
|
this.statusEl = hud.statusEl;
|
||||||
@@ -263,12 +210,11 @@ export class ViewerAppController {
|
|||||||
return this.sceneDataController.createWorldPresentationContext({
|
return this.sceneDataController.createWorldPresentationContext({
|
||||||
world: this.world,
|
world: this.world,
|
||||||
activeSystemId: this.activeSystemId,
|
activeSystemId: this.activeSystemId,
|
||||||
zoomLevel: this.zoomLevel,
|
povLevel: this.povLevel,
|
||||||
orbitYaw: this.orbitYaw,
|
orbitYaw: this.orbitYaw,
|
||||||
camera: this.camera,
|
systemCamera: this.systemLayer.camera,
|
||||||
systemFocusLocal: this.systemFocusLocal,
|
systemAnchor: this.systemAnchor,
|
||||||
toDisplayLocalPosition: this.toDisplayLocalPosition.bind(this),
|
toDisplayLocalPosition: (localPosition) => toDisplayLocalPosition(localPosition),
|
||||||
updateSystemDetailVisibility: () => this.navigationController.updateSystemDetailVisibility(),
|
|
||||||
setShellReticleOpacity: (sprite, opacity) => this.setShellReticleOpacity(sprite, opacity),
|
setShellReticleOpacity: (sprite, opacity) => this.setShellReticleOpacity(sprite, opacity),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -285,8 +231,14 @@ export class ViewerAppController {
|
|||||||
renderFrame({
|
renderFrame({
|
||||||
clock: this.clock,
|
clock: this.clock,
|
||||||
renderer: this.renderer,
|
renderer: this.renderer,
|
||||||
scene: this.scene,
|
universeScene: this.universeLayer.scene,
|
||||||
camera: this.camera,
|
galaxyScene: this.galaxyLayer.scene,
|
||||||
|
galaxyCamera: this.galaxyLayer.camera,
|
||||||
|
systemScene: this.systemLayer.scene,
|
||||||
|
systemCamera: this.systemLayer.camera,
|
||||||
|
localScene: this.localLayer.scene,
|
||||||
|
localCamera: this.localLayer.camera,
|
||||||
|
getPovLevel: () => this.povLevel,
|
||||||
updateCamera: (delta) => this.updateCamera(delta),
|
updateCamera: (delta) => this.updateCamera(delta),
|
||||||
updateAmbience: (delta) => this.presentationController.updateAmbience(delta),
|
updateAmbience: (delta) => this.presentationController.updateAmbience(delta),
|
||||||
updatePlanetPresentation: () => this.presentationController.updatePlanetPresentation(),
|
updatePlanetPresentation: () => this.presentationController.updatePlanetPresentation(),
|
||||||
@@ -298,10 +250,13 @@ export class ViewerAppController {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateAmbience(delta: number) {
|
private computeOrbitOffset(): THREE.Vector3 {
|
||||||
this.ambienceGroup.position.copy(this.camera.position);
|
const horizontalDistance = this.currentDistance * Math.cos(this.orbitPitch);
|
||||||
this.ambienceGroup.rotation.y += delta * 0.005;
|
return new THREE.Vector3(
|
||||||
this.ambienceGroup.rotation.x = Math.sin(performance.now() * 0.00003) * 0.015;
|
Math.cos(this.orbitYaw) * horizontalDistance,
|
||||||
|
this.currentDistance * Math.sin(this.orbitPitch),
|
||||||
|
Math.sin(this.orbitYaw) * horizontalDistance,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateCamera(delta: number) {
|
private updateCamera(delta: number) {
|
||||||
@@ -312,25 +267,38 @@ export class ViewerAppController {
|
|||||||
delta,
|
delta,
|
||||||
});
|
});
|
||||||
this.currentDistance = nextState.currentDistance;
|
this.currentDistance = nextState.currentDistance;
|
||||||
this.zoomLevel = nextState.zoomLevel;
|
this.povLevel = nextState.povLevel;
|
||||||
this.orbitPitch = nextState.orbitPitch;
|
this.orbitPitch = nextState.orbitPitch;
|
||||||
this.navigationController.updateActiveSystem();
|
this.navigationController.updateActiveSystem();
|
||||||
|
|
||||||
if (this.cameraMode === "follow" && this.navigationController.updateFollowCamera(delta)) {
|
if (this.cameraMode === "follow" && this.navigationController.updateFollowCamera(delta)) {
|
||||||
|
// Follow camera directly controls systemLayer.camera in updateFollowCamera.
|
||||||
|
// Still update galaxy camera independently.
|
||||||
|
const orbitOffset = this.computeOrbitOffset();
|
||||||
|
this.galaxyLayer.updateCamera(this.galaxyAnchor, orbitOffset);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.updatePanFromKeyboard(delta);
|
this.updatePanFromKeyboard(delta);
|
||||||
this.orbitPitch = THREE.MathUtils.clamp(this.orbitPitch, 0.18, 1.3);
|
this.orbitPitch = THREE.MathUtils.clamp(this.orbitPitch, 0.18, 1.3);
|
||||||
|
|
||||||
const horizontalDistance = this.currentDistance * Math.cos(this.orbitPitch);
|
const orbitOffset = this.computeOrbitOffset();
|
||||||
const focus = this.navigationController.getCameraFocusWorldPosition();
|
|
||||||
this.cameraOffset.set(
|
this.galaxyLayer.updateCamera(this.galaxyAnchor, orbitOffset);
|
||||||
Math.cos(this.orbitYaw) * horizontalDistance,
|
|
||||||
this.currentDistance * Math.sin(this.orbitPitch),
|
if (this.activeSystemId) {
|
||||||
Math.sin(this.orbitYaw) * horizontalDistance,
|
this.systemLayer.updateCamera(getSystemCameraFocus(this.systemAnchor), orbitOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.localLayer.updateCamera(orbitOffset);
|
||||||
|
|
||||||
|
// Update star dot scales in galaxy scene
|
||||||
|
updateSystemStarPresentation(
|
||||||
|
this.systemVisuals,
|
||||||
|
this.activeSystemId,
|
||||||
|
this.galaxyLayer.camera,
|
||||||
|
(sprite, opacity) => this.setShellReticleOpacity(sprite, opacity),
|
||||||
);
|
);
|
||||||
this.camera.position.copy(focus).add(this.cameraOffset);
|
|
||||||
this.camera.lookAt(focus);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private updatePanFromKeyboard(delta: number) {
|
private updatePanFromKeyboard(delta: number) {
|
||||||
@@ -338,10 +306,10 @@ export class ViewerAppController {
|
|||||||
this.keyState,
|
this.keyState,
|
||||||
this.orbitYaw,
|
this.orbitYaw,
|
||||||
this.currentDistance,
|
this.currentDistance,
|
||||||
this.zoomLevel,
|
this.povLevel,
|
||||||
this.activeSystemId,
|
this.activeSystemId,
|
||||||
this.systemFocusLocal,
|
this.systemAnchor,
|
||||||
this.galaxyFocus,
|
this.galaxyAnchor,
|
||||||
delta,
|
delta,
|
||||||
MIN_CAMERA_DISTANCE,
|
MIN_CAMERA_DISTANCE,
|
||||||
MAX_CAMERA_DISTANCE,
|
MAX_CAMERA_DISTANCE,
|
||||||
@@ -352,16 +320,6 @@ export class ViewerAppController {
|
|||||||
this.presentationController.updateSystemSummaries();
|
this.presentationController.updateSystemSummaries();
|
||||||
}
|
}
|
||||||
|
|
||||||
private registerPresentation(
|
|
||||||
detail: SceneNode,
|
|
||||||
icon: SceneNode,
|
|
||||||
hideDetailInUniverse: boolean,
|
|
||||||
hideIconInUniverse = false,
|
|
||||||
systemId?: string,
|
|
||||||
) {
|
|
||||||
this.presentationEntries.push({ detail, icon, systemId, hideDetailInUniverse, hideIconInUniverse });
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderRecentEvents(entityKind: string, entityId: string) {
|
private renderRecentEvents(entityKind: string, entityId: string) {
|
||||||
return this.presentationController.renderRecentEvents(entityKind, entityId);
|
return this.presentationController.renderRecentEvents(entityKind, entityId);
|
||||||
}
|
}
|
||||||
@@ -378,14 +336,16 @@ export class ViewerAppController {
|
|||||||
this.interactionController.refreshHistoryWindows();
|
this.interactionController.refreshHistoryWindows();
|
||||||
}
|
}
|
||||||
|
|
||||||
private resolveFocusedBubbleId() {
|
private resolveFocusedCelestialId() {
|
||||||
return resolveFocusedBubbleId(this.world, this.selectedItems);
|
return resolveFocusedCelestialId(this.world, this.selectedItems);
|
||||||
}
|
}
|
||||||
|
|
||||||
private onResize = () => {
|
private onResize = () => {
|
||||||
resizeViewer({
|
resizeViewer({
|
||||||
renderer: this.renderer,
|
renderer: this.renderer,
|
||||||
camera: this.camera,
|
galaxyCamera: this.galaxyLayer.camera,
|
||||||
|
systemCamera: this.systemLayer.camera,
|
||||||
|
localCamera: this.localLayer.camera,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -398,7 +358,7 @@ export class ViewerAppController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private toDisplayLocalPosition(localPosition: THREE.Vector3, systemId?: string) {
|
private toDisplayLocalPosition(localPosition: THREE.Vector3, systemId?: string) {
|
||||||
return this.navigationController.toDisplayLocalPosition(localPosition, systemId);
|
return toDisplayLocalPosition(localPosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateSystemPanel() {
|
private updateSystemPanel() {
|
||||||
|
|||||||
@@ -11,10 +11,8 @@ export type {
|
|||||||
PlanetSnapshot,
|
PlanetSnapshot,
|
||||||
ResourceNodeSnapshot,
|
ResourceNodeSnapshot,
|
||||||
ResourceNodeDelta,
|
ResourceNodeDelta,
|
||||||
SpatialNodeSnapshot,
|
CelestialSnapshot,
|
||||||
SpatialNodeDelta,
|
CelestialDelta,
|
||||||
LocalBubbleSnapshot,
|
|
||||||
LocalBubbleDelta,
|
|
||||||
} from "./contractsCelestial";
|
} from "./contractsCelestial";
|
||||||
export type {
|
export type {
|
||||||
StationSnapshot,
|
StationSnapshot,
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export interface ResourceNodeSnapshot {
|
|||||||
id: string;
|
id: string;
|
||||||
systemId: string;
|
systemId: string;
|
||||||
localPosition: Vector3Dto;
|
localPosition: Vector3Dto;
|
||||||
anchorNodeId?: string | null;
|
celestialId?: string | null;
|
||||||
sourceKind: string;
|
sourceKind: string;
|
||||||
oreRemaining: number;
|
oreRemaining: number;
|
||||||
maxOre: number;
|
maxOre: number;
|
||||||
@@ -41,28 +41,15 @@ export interface ResourceNodeSnapshot {
|
|||||||
|
|
||||||
export interface ResourceNodeDelta extends ResourceNodeSnapshot {}
|
export interface ResourceNodeDelta extends ResourceNodeSnapshot {}
|
||||||
|
|
||||||
export interface SpatialNodeSnapshot {
|
export interface CelestialSnapshot {
|
||||||
id: string;
|
id: string;
|
||||||
systemId: string;
|
systemId: string;
|
||||||
kind: string;
|
kind: string;
|
||||||
localPosition: Vector3Dto;
|
orbitalAnchor: Vector3Dto;
|
||||||
bubbleId: string;
|
localSpaceRadius: number;
|
||||||
parentNodeId?: string | null;
|
parentNodeId?: string | null;
|
||||||
occupyingStructureId?: string | null;
|
occupyingStructureId?: string | null;
|
||||||
orbitReferenceId?: string | null;
|
orbitReferenceId?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SpatialNodeDelta extends SpatialNodeSnapshot {}
|
export interface CelestialDelta extends CelestialSnapshot {}
|
||||||
|
|
||||||
export interface LocalBubbleSnapshot {
|
|
||||||
id: string;
|
|
||||||
nodeId: string;
|
|
||||||
systemId: string;
|
|
||||||
radius: number;
|
|
||||||
occupantShipIds: string[];
|
|
||||||
occupantStationIds: string[];
|
|
||||||
occupantClaimIds: string[];
|
|
||||||
occupantConstructionSiteIds: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LocalBubbleDelta extends LocalBubbleSnapshot {}
|
|
||||||
|
|||||||
@@ -18,9 +18,7 @@ export interface StationSnapshot {
|
|||||||
category: string;
|
category: string;
|
||||||
systemId: string;
|
systemId: string;
|
||||||
localPosition: Vector3Dto;
|
localPosition: Vector3Dto;
|
||||||
nodeId?: string | null;
|
celestialId?: string | null;
|
||||||
bubbleId?: string | null;
|
|
||||||
anchorNodeId?: string | null;
|
|
||||||
color: string;
|
color: string;
|
||||||
dockedShips: number;
|
dockedShips: number;
|
||||||
dockedShipIds: string[];
|
dockedShipIds: string[];
|
||||||
@@ -45,8 +43,7 @@ export interface ClaimSnapshot {
|
|||||||
id: string;
|
id: string;
|
||||||
factionId: string;
|
factionId: string;
|
||||||
systemId: string;
|
systemId: string;
|
||||||
nodeId: string;
|
celestialId: string;
|
||||||
bubbleId: string;
|
|
||||||
state: string;
|
state: string;
|
||||||
health: number;
|
health: number;
|
||||||
placedAtUtc: string;
|
placedAtUtc: string;
|
||||||
@@ -59,8 +56,7 @@ export interface ConstructionSiteSnapshot {
|
|||||||
id: string;
|
id: string;
|
||||||
factionId: string;
|
factionId: string;
|
||||||
systemId: string;
|
systemId: string;
|
||||||
nodeId: string;
|
celestialId: string;
|
||||||
bubbleId: string;
|
|
||||||
targetKind: string;
|
targetKind: string;
|
||||||
targetDefinitionId: string;
|
targetDefinitionId: string;
|
||||||
blueprintId?: string | null;
|
blueprintId?: string | null;
|
||||||
|
|||||||
@@ -15,8 +15,7 @@ export interface ShipSnapshot {
|
|||||||
behaviorPhase: string | null;
|
behaviorPhase: string | null;
|
||||||
controllerTaskKind: string;
|
controllerTaskKind: string;
|
||||||
commanderObjective: string | null;
|
commanderObjective: string | null;
|
||||||
nodeId?: string | null;
|
celestialId?: string | null;
|
||||||
bubbleId?: string | null;
|
|
||||||
dockedStationId?: string | null;
|
dockedStationId?: string | null;
|
||||||
commanderId?: string | null;
|
commanderId?: string | null;
|
||||||
policySetId?: string | null;
|
policySetId?: string | null;
|
||||||
@@ -42,8 +41,7 @@ export interface ShipActionProgressSnapshot {
|
|||||||
export interface ShipSpatialStateSnapshot {
|
export interface ShipSpatialStateSnapshot {
|
||||||
spaceLayer: string;
|
spaceLayer: string;
|
||||||
currentSystemId: string;
|
currentSystemId: string;
|
||||||
currentNodeId?: string | null;
|
currentCelestialId?: string | null;
|
||||||
currentBubbleId?: string | null;
|
|
||||||
localPosition?: Vector3Dto | null;
|
localPosition?: Vector3Dto | null;
|
||||||
systemPosition?: Vector3Dto | null;
|
systemPosition?: Vector3Dto | null;
|
||||||
movementRegime: string;
|
movementRegime: string;
|
||||||
|
|||||||
@@ -9,12 +9,10 @@ import type {
|
|||||||
FactionSnapshot,
|
FactionSnapshot,
|
||||||
} from "./contractsFactions";
|
} from "./contractsFactions";
|
||||||
import type {
|
import type {
|
||||||
LocalBubbleDelta,
|
CelestialDelta,
|
||||||
LocalBubbleSnapshot,
|
CelestialSnapshot,
|
||||||
ResourceNodeDelta,
|
ResourceNodeDelta,
|
||||||
ResourceNodeSnapshot,
|
ResourceNodeSnapshot,
|
||||||
SpatialNodeDelta,
|
|
||||||
SpatialNodeSnapshot,
|
|
||||||
SystemSnapshot,
|
SystemSnapshot,
|
||||||
} from "./contractsCelestial";
|
} from "./contractsCelestial";
|
||||||
import type {
|
import type {
|
||||||
@@ -37,8 +35,7 @@ export interface WorldSnapshot {
|
|||||||
orbitalSimulation: OrbitalSimulationSnapshot;
|
orbitalSimulation: OrbitalSimulationSnapshot;
|
||||||
generatedAtUtc: string;
|
generatedAtUtc: string;
|
||||||
systems: SystemSnapshot[];
|
systems: SystemSnapshot[];
|
||||||
spatialNodes: SpatialNodeSnapshot[];
|
celestials: CelestialSnapshot[];
|
||||||
localBubbles: LocalBubbleSnapshot[];
|
|
||||||
nodes: ResourceNodeSnapshot[];
|
nodes: ResourceNodeSnapshot[];
|
||||||
stations: import("./contractsInfrastructure").StationSnapshot[];
|
stations: import("./contractsInfrastructure").StationSnapshot[];
|
||||||
claims: ClaimSnapshot[];
|
claims: ClaimSnapshot[];
|
||||||
@@ -57,8 +54,7 @@ export interface WorldDelta {
|
|||||||
generatedAtUtc: string;
|
generatedAtUtc: string;
|
||||||
requiresSnapshotRefresh: boolean;
|
requiresSnapshotRefresh: boolean;
|
||||||
events: SimulationEventRecord[];
|
events: SimulationEventRecord[];
|
||||||
spatialNodes: SpatialNodeDelta[];
|
celestials: CelestialDelta[];
|
||||||
localBubbles: LocalBubbleDelta[];
|
|
||||||
nodes: ResourceNodeDelta[];
|
nodes: ResourceNodeDelta[];
|
||||||
stations: import("./contractsInfrastructure").StationDelta[];
|
stations: import("./contractsInfrastructure").StationDelta[];
|
||||||
claims: ClaimDelta[];
|
claims: ClaimDelta[];
|
||||||
@@ -85,7 +81,7 @@ export interface SimulationEventRecord {
|
|||||||
export interface ObserverScope {
|
export interface ObserverScope {
|
||||||
scopeKind: string;
|
scopeKind: string;
|
||||||
systemId?: string | null;
|
systemId?: string | null;
|
||||||
bubbleId?: string | null;
|
celestialId?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OrbitalSimulationSnapshot {
|
export interface OrbitalSimulationSnapshot {
|
||||||
|
|||||||
@@ -1,19 +1,14 @@
|
|||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import { ACTIVE_SYSTEM_CAPTURE_RADIUS, ACTIVE_SYSTEM_DETAIL_SCALE, GALAXY_PARALLAX_FACTOR } from "./viewerConstants";
|
import { ACTIVE_SYSTEM_CAPTURE_RADIUS, ACTIVE_SYSTEM_DETAIL_SCALE } from "./viewerConstants";
|
||||||
import { KILOMETERS_PER_AU, computePlanetLocalPosition, currentWorldTimeSeconds, scaleGalaxyVector, scaleLocalVector, toThreeVector } from "./viewerMath";
|
import { DISPLAY_UNITS_PER_KILOMETER, KILOMETERS_PER_AU, computePlanetLocalPosition, currentWorldTimeSeconds, scaleGalaxyVector, scaleLocalVector, toThreeVector } from "./viewerMath";
|
||||||
import { resolveSelectableSystemId } from "./viewerSelection";
|
import { resolveSelectableSystemId } from "./viewerSelection";
|
||||||
import type {
|
import type {
|
||||||
BubbleVisual,
|
|
||||||
ClaimVisual,
|
|
||||||
ConstructionSiteVisual,
|
|
||||||
NodeVisual,
|
NodeVisual,
|
||||||
PlanetVisual,
|
PlanetVisual,
|
||||||
Selectable,
|
Selectable,
|
||||||
ShipVisual,
|
ShipVisual,
|
||||||
SpatialNodeVisual,
|
|
||||||
StructureVisual,
|
|
||||||
WorldState,
|
WorldState,
|
||||||
ZoomLevel,
|
PovLevel,
|
||||||
} from "./viewerTypes";
|
} from "./viewerTypes";
|
||||||
|
|
||||||
interface ResolveSelectionPositionParams {
|
interface ResolveSelectionPositionParams {
|
||||||
@@ -23,14 +18,13 @@ interface ResolveSelectionPositionParams {
|
|||||||
nodeVisuals: Map<string, NodeVisual>;
|
nodeVisuals: Map<string, NodeVisual>;
|
||||||
planetVisuals: PlanetVisual[];
|
planetVisuals: PlanetVisual[];
|
||||||
computeNodeLocalPosition: (visual: NodeVisual, timeSeconds: number) => THREE.Vector3;
|
computeNodeLocalPosition: (visual: NodeVisual, timeSeconds: number) => THREE.Vector3;
|
||||||
resolveBubblePosition: (bubbleId: string) => THREE.Vector3 | undefined;
|
resolvePointPosition: (systemId: string, celestialId?: string | null) => THREE.Vector3;
|
||||||
resolvePointPosition: (systemId: string, nodeId?: string | null) => THREE.Vector3;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FocusOnSelectionParams extends ResolveSelectionPositionParams {
|
interface FocusOnSelectionParams extends ResolveSelectionPositionParams {
|
||||||
activeSystemId?: string;
|
activeSystemId?: string;
|
||||||
galaxyFocus: THREE.Vector3;
|
galaxyAnchor: THREE.Vector3;
|
||||||
systemFocusLocal: THREE.Vector3;
|
systemAnchor: THREE.Vector3;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DetermineActiveSystemParams {
|
interface DetermineActiveSystemParams {
|
||||||
@@ -39,7 +33,7 @@ interface DetermineActiveSystemParams {
|
|||||||
cameraTargetShipId?: string;
|
cameraTargetShipId?: string;
|
||||||
currentDistance: number;
|
currentDistance: number;
|
||||||
selectedItems: Selectable[];
|
selectedItems: Selectable[];
|
||||||
galaxyFocus: THREE.Vector3;
|
galaxyAnchor: THREE.Vector3;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SeedSystemFocusParams {
|
interface SeedSystemFocusParams {
|
||||||
@@ -48,38 +42,30 @@ interface SeedSystemFocusParams {
|
|||||||
cameraMode: "tactical" | "follow";
|
cameraMode: "tactical" | "follow";
|
||||||
cameraTargetShipId?: string;
|
cameraTargetShipId?: string;
|
||||||
selectedItems: Selectable[];
|
selectedItems: Selectable[];
|
||||||
systemFocusLocal: THREE.Vector3;
|
systemAnchor: THREE.Vector3;
|
||||||
worldTimeSyncMs: number;
|
worldTimeSyncMs: number;
|
||||||
nodeVisuals: Map<string, NodeVisual>;
|
nodeVisuals: Map<string, NodeVisual>;
|
||||||
planetVisuals: PlanetVisual[];
|
planetVisuals: PlanetVisual[];
|
||||||
computeNodeLocalPosition: (visual: NodeVisual, timeSeconds: number) => THREE.Vector3;
|
computeNodeLocalPosition: (visual: NodeVisual, timeSeconds: number) => THREE.Vector3;
|
||||||
resolveBubblePosition: (bubbleId: string) => THREE.Vector3 | undefined;
|
resolvePointPosition: (systemId: string, celestialId?: string | null) => THREE.Vector3;
|
||||||
resolvePointPosition: (systemId: string, nodeId?: string | null) => THREE.Vector3;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CameraFocusParams {
|
interface CameraFocusParams {
|
||||||
world: WorldState | undefined;
|
galaxyAnchor: THREE.Vector3;
|
||||||
activeSystemId?: string;
|
|
||||||
galaxyFocus: THREE.Vector3;
|
|
||||||
systemFocusLocal: THREE.Vector3;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DisplayLocalPositionParams {
|
export function getSystemCameraFocus(systemAnchor: THREE.Vector3): THREE.Vector3 {
|
||||||
world: WorldState | undefined;
|
return systemAnchor.clone().multiplyScalar(DISPLAY_UNITS_PER_KILOMETER * ACTIVE_SYSTEM_DETAIL_SCALE);
|
||||||
systemId?: string;
|
|
||||||
activeSystemId?: string;
|
|
||||||
localPosition: THREE.Vector3;
|
|
||||||
systemFocusLocal: THREE.Vector3;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updatePanFromKeyboard(
|
export function updatePanFromKeyboard(
|
||||||
keyState: Set<string>,
|
keyState: Set<string>,
|
||||||
orbitYaw: number,
|
orbitYaw: number,
|
||||||
currentDistance: number,
|
currentDistance: number,
|
||||||
zoomLevel: ZoomLevel,
|
povLevel: PovLevel,
|
||||||
activeSystemId: string | undefined,
|
activeSystemId: string | undefined,
|
||||||
systemFocusLocal: THREE.Vector3,
|
systemAnchor: THREE.Vector3,
|
||||||
galaxyFocus: THREE.Vector3,
|
galaxyAnchor: THREE.Vector3,
|
||||||
delta: number,
|
delta: number,
|
||||||
minimumDistance: number,
|
minimumDistance: number,
|
||||||
maximumDistance: number,
|
maximumDistance: number,
|
||||||
@@ -106,15 +92,15 @@ export function updatePanFromKeyboard(
|
|||||||
const right = new THREE.Vector3(-forward.z, 0, forward.x);
|
const right = new THREE.Vector3(-forward.z, 0, forward.x);
|
||||||
const pan = right.multiplyScalar(move.x).add(forward.multiplyScalar(move.z));
|
const pan = right.multiplyScalar(move.x).add(forward.multiplyScalar(move.z));
|
||||||
if (activeSystemId) {
|
if (activeSystemId) {
|
||||||
const speedKilometers = zoomLevel === "system"
|
const speedKilometers = povLevel === "system"
|
||||||
? THREE.MathUtils.mapLinear(currentDistance, 80, 4000, KILOMETERS_PER_AU * 0.002, KILOMETERS_PER_AU * 0.35)
|
? THREE.MathUtils.mapLinear(currentDistance, 80, 4000, KILOMETERS_PER_AU * 0.002, KILOMETERS_PER_AU * 0.35)
|
||||||
: THREE.MathUtils.mapLinear(currentDistance, minimumDistance, 120, 40, 180000);
|
: THREE.MathUtils.mapLinear(currentDistance, minimumDistance, 120, 40, 180000);
|
||||||
systemFocusLocal.addScaledVector(pan, speedKilometers * delta);
|
systemAnchor.addScaledVector(pan, speedKilometers * delta);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const speed = THREE.MathUtils.mapLinear(currentDistance, minimumDistance, maximumDistance, 320, 6800);
|
const speed = THREE.MathUtils.mapLinear(currentDistance, minimumDistance, maximumDistance, 320, 6800);
|
||||||
galaxyFocus.addScaledVector(pan, speed * delta);
|
galaxyAnchor.addScaledVector(pan, speed * delta);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function determineActiveSystemId(params: DetermineActiveSystemParams): string | undefined {
|
export function determineActiveSystemId(params: DetermineActiveSystemParams): string | undefined {
|
||||||
@@ -124,7 +110,7 @@ export function determineActiveSystemId(params: DetermineActiveSystemParams): st
|
|||||||
cameraTargetShipId,
|
cameraTargetShipId,
|
||||||
currentDistance,
|
currentDistance,
|
||||||
selectedItems,
|
selectedItems,
|
||||||
galaxyFocus,
|
galaxyAnchor,
|
||||||
} = params;
|
} = params;
|
||||||
|
|
||||||
if (!world) {
|
if (!world) {
|
||||||
@@ -165,7 +151,7 @@ export function determineActiveSystemId(params: DetermineActiveSystemParams): st
|
|||||||
let nearestDistance = Number.POSITIVE_INFINITY;
|
let nearestDistance = Number.POSITIVE_INFINITY;
|
||||||
for (const system of world.systems.values()) {
|
for (const system of world.systems.values()) {
|
||||||
const center = scaleGalaxyVector(toThreeVector(system.galaxyPosition));
|
const center = scaleGalaxyVector(toThreeVector(system.galaxyPosition));
|
||||||
const distance = center.distanceTo(galaxyFocus);
|
const distance = center.distanceTo(galaxyAnchor);
|
||||||
if (distance < nearestDistance) {
|
if (distance < nearestDistance) {
|
||||||
nearestDistance = distance;
|
nearestDistance = distance;
|
||||||
nearestSystemId = system.id;
|
nearestSystemId = system.id;
|
||||||
@@ -185,7 +171,6 @@ export function resolveSelectionPosition(params: ResolveSelectionPositionParams)
|
|||||||
nodeVisuals,
|
nodeVisuals,
|
||||||
planetVisuals,
|
planetVisuals,
|
||||||
computeNodeLocalPosition,
|
computeNodeLocalPosition,
|
||||||
resolveBubblePosition,
|
|
||||||
resolvePointPosition,
|
resolvePointPosition,
|
||||||
} = params;
|
} = params;
|
||||||
|
|
||||||
@@ -208,20 +193,17 @@ export function resolveSelectionPosition(params: ResolveSelectionPositionParams)
|
|||||||
? computeNodeLocalPosition(visual, currentWorldTimeSeconds(world, worldTimeSyncMs))
|
? computeNodeLocalPosition(visual, currentWorldTimeSeconds(world, worldTimeSyncMs))
|
||||||
: (node ? toThreeVector(node.localPosition) : undefined);
|
: (node ? toThreeVector(node.localPosition) : undefined);
|
||||||
}
|
}
|
||||||
if (selection.kind === "spatial-node") {
|
if (selection.kind === "celestial") {
|
||||||
const node = world.spatialNodes.get(selection.id);
|
const celestial = world.celestials.get(selection.id);
|
||||||
return node ? toThreeVector(node.localPosition) : undefined;
|
return celestial ? toThreeVector(celestial.orbitalAnchor) : undefined;
|
||||||
}
|
|
||||||
if (selection.kind === "bubble") {
|
|
||||||
return resolveBubblePosition(selection.id);
|
|
||||||
}
|
}
|
||||||
if (selection.kind === "claim") {
|
if (selection.kind === "claim") {
|
||||||
const claim = world.claims.get(selection.id);
|
const claim = world.claims.get(selection.id);
|
||||||
return claim ? resolvePointPosition(claim.systemId, claim.nodeId) : undefined;
|
return claim ? resolvePointPosition(claim.systemId, claim.celestialId) : undefined;
|
||||||
}
|
}
|
||||||
if (selection.kind === "construction-site") {
|
if (selection.kind === "construction-site") {
|
||||||
const site = world.constructionSites.get(selection.id);
|
const site = world.constructionSites.get(selection.id);
|
||||||
return site ? resolvePointPosition(site.systemId, site.nodeId) : undefined;
|
return site ? resolvePointPosition(site.systemId, site.celestialId) : undefined;
|
||||||
}
|
}
|
||||||
if (selection.kind === "planet") {
|
if (selection.kind === "planet") {
|
||||||
const system = world.systems.get(selection.systemId);
|
const system = world.systems.get(selection.systemId);
|
||||||
@@ -242,8 +224,8 @@ export function focusOnSelection(params: FocusOnSelectionParams) {
|
|||||||
world,
|
world,
|
||||||
selection,
|
selection,
|
||||||
activeSystemId,
|
activeSystemId,
|
||||||
galaxyFocus,
|
galaxyAnchor,
|
||||||
systemFocusLocal,
|
systemAnchor,
|
||||||
} = params;
|
} = params;
|
||||||
|
|
||||||
const nextFocus = resolveSelectionPosition(params);
|
const nextFocus = resolveSelectionPosition(params);
|
||||||
@@ -252,8 +234,8 @@ export function focusOnSelection(params: FocusOnSelectionParams) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (selection.kind === "system") {
|
if (selection.kind === "system") {
|
||||||
galaxyFocus.copy(nextFocus);
|
galaxyAnchor.copy(nextFocus);
|
||||||
systemFocusLocal.set(0, 0, 0);
|
systemAnchor.set(0, 0, 0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,18 +243,18 @@ export function focusOnSelection(params: FocusOnSelectionParams) {
|
|||||||
if (selectionSystemId && world) {
|
if (selectionSystemId && world) {
|
||||||
const system = world.systems.get(selectionSystemId);
|
const system = world.systems.get(selectionSystemId);
|
||||||
if (system) {
|
if (system) {
|
||||||
galaxyFocus.copy(scaleGalaxyVector(toThreeVector(system.galaxyPosition)));
|
galaxyAnchor.copy(scaleGalaxyVector(toThreeVector(system.galaxyPosition)));
|
||||||
systemFocusLocal.copy(nextFocus);
|
systemAnchor.copy(nextFocus);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (activeSystemId && resolveSelectableSystemId(world, selection) === activeSystemId) {
|
if (activeSystemId && resolveSelectableSystemId(world, selection) === activeSystemId) {
|
||||||
systemFocusLocal.copy(nextFocus);
|
systemAnchor.copy(nextFocus);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
galaxyFocus.copy(nextFocus);
|
galaxyAnchor.copy(nextFocus);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function seedSystemFocusLocal(params: SeedSystemFocusParams) {
|
export function seedSystemFocusLocal(params: SeedSystemFocusParams) {
|
||||||
@@ -282,7 +264,7 @@ export function seedSystemFocusLocal(params: SeedSystemFocusParams) {
|
|||||||
cameraMode,
|
cameraMode,
|
||||||
cameraTargetShipId,
|
cameraTargetShipId,
|
||||||
selectedItems,
|
selectedItems,
|
||||||
systemFocusLocal,
|
systemAnchor,
|
||||||
} = params;
|
} = params;
|
||||||
|
|
||||||
if (!world) {
|
if (!world) {
|
||||||
@@ -292,7 +274,7 @@ export function seedSystemFocusLocal(params: SeedSystemFocusParams) {
|
|||||||
if (cameraMode === "follow" && cameraTargetShipId) {
|
if (cameraMode === "follow" && cameraTargetShipId) {
|
||||||
const followedShip = world.ships.get(cameraTargetShipId);
|
const followedShip = world.ships.get(cameraTargetShipId);
|
||||||
if (followedShip?.systemId === systemId) {
|
if (followedShip?.systemId === systemId) {
|
||||||
systemFocusLocal.copy(toThreeVector(followedShip.localPosition));
|
systemAnchor.copy(toThreeVector(followedShip.localPosition));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -300,7 +282,7 @@ export function seedSystemFocusLocal(params: SeedSystemFocusParams) {
|
|||||||
const selected = selectedItems[0];
|
const selected = selectedItems[0];
|
||||||
if (selected && resolveSelectableSystemId(world, selected) === systemId) {
|
if (selected && resolveSelectableSystemId(world, selected) === systemId) {
|
||||||
if (selected.kind === "system") {
|
if (selected.kind === "system") {
|
||||||
systemFocusLocal.set(0, 0, 0);
|
systemAnchor.set(0, 0, 0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -311,62 +293,26 @@ export function seedSystemFocusLocal(params: SeedSystemFocusParams) {
|
|||||||
nodeVisuals: params.nodeVisuals,
|
nodeVisuals: params.nodeVisuals,
|
||||||
planetVisuals: params.planetVisuals,
|
planetVisuals: params.planetVisuals,
|
||||||
computeNodeLocalPosition: params.computeNodeLocalPosition,
|
computeNodeLocalPosition: params.computeNodeLocalPosition,
|
||||||
resolveBubblePosition: params.resolveBubblePosition,
|
|
||||||
resolvePointPosition: params.resolvePointPosition,
|
resolvePointPosition: params.resolvePointPosition,
|
||||||
});
|
});
|
||||||
if (selectedPosition) {
|
if (selectedPosition) {
|
||||||
systemFocusLocal.copy(selectedPosition);
|
systemAnchor.copy(selectedPosition);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
systemFocusLocal.set(0, 0, 0);
|
systemAnchor.set(0, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCameraFocusWorldPosition(params: CameraFocusParams): THREE.Vector3 {
|
export function getCameraFocusWorldPosition(params: CameraFocusParams): THREE.Vector3 {
|
||||||
const {
|
return params.galaxyAnchor;
|
||||||
world,
|
|
||||||
activeSystemId,
|
|
||||||
galaxyFocus,
|
|
||||||
systemFocusLocal,
|
|
||||||
} = params;
|
|
||||||
|
|
||||||
if (!activeSystemId || !world) {
|
|
||||||
return galaxyFocus;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const system = world.systems.get(activeSystemId);
|
/**
|
||||||
return system
|
* Convert a local km position to system-scene display coordinates.
|
||||||
? scaleGalaxyVector(toThreeVector(system.galaxyPosition)).add(
|
* System scene coordinate system: star at origin, all positions scaled by
|
||||||
scaleLocalVector(systemFocusLocal).multiplyScalar(ACTIVE_SYSTEM_DETAIL_SCALE * GALAXY_PARALLAX_FACTOR),
|
* DISPLAY_UNITS_PER_KILOMETER * ACTIVE_SYSTEM_DETAIL_SCALE.
|
||||||
)
|
*/
|
||||||
: galaxyFocus;
|
export function toDisplayLocalPosition(localPosition: THREE.Vector3): THREE.Vector3 {
|
||||||
}
|
return localPosition.clone().multiplyScalar(DISPLAY_UNITS_PER_KILOMETER * ACTIVE_SYSTEM_DETAIL_SCALE);
|
||||||
|
|
||||||
export function toDisplayLocalPosition(params: DisplayLocalPositionParams): THREE.Vector3 {
|
|
||||||
const {
|
|
||||||
world,
|
|
||||||
systemId,
|
|
||||||
activeSystemId,
|
|
||||||
localPosition,
|
|
||||||
systemFocusLocal,
|
|
||||||
} = params;
|
|
||||||
|
|
||||||
if (!world || !systemId) {
|
|
||||||
return scaleLocalVector(localPosition);
|
|
||||||
}
|
|
||||||
|
|
||||||
const system = world.systems.get(systemId);
|
|
||||||
if (!system) {
|
|
||||||
return scaleLocalVector(localPosition);
|
|
||||||
}
|
|
||||||
|
|
||||||
const center = scaleGalaxyVector(toThreeVector(system.galaxyPosition));
|
|
||||||
const scaledLocalPosition = scaleLocalVector(localPosition);
|
|
||||||
const scaledSystemFocus = scaleLocalVector(systemFocusLocal);
|
|
||||||
if (systemId !== activeSystemId) {
|
|
||||||
return center.clone().add(scaledLocalPosition);
|
|
||||||
}
|
|
||||||
|
|
||||||
return center.clone().add(scaledLocalPosition.sub(scaledSystemFocus).multiplyScalar(ACTIVE_SYSTEM_DETAIL_SCALE));
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import type { ZoomLevel } from "./viewerTypes";
|
import type { PovLevel } from "./viewerTypes";
|
||||||
|
|
||||||
export const ZOOM_DISTANCE: Record<ZoomLevel, number> = {
|
export const NAV_DISTANCE: Record<PovLevel, number> = {
|
||||||
local: 18,
|
local: 18,
|
||||||
system: 3200,
|
system: 3200,
|
||||||
universe: 32000,
|
galaxy: 32000,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ACTIVE_SYSTEM_DETAIL_SCALE = 10;
|
export const ACTIVE_SYSTEM_DETAIL_SCALE = 10;
|
||||||
@@ -14,10 +14,10 @@ export const STAR_RENDER_SCALE = 0.18;
|
|||||||
export const PLANET_RENDER_SCALE = 0.95;
|
export const PLANET_RENDER_SCALE = 0.95;
|
||||||
export const MOON_RENDER_SCALE = 1.1;
|
export const MOON_RENDER_SCALE = 1.1;
|
||||||
export const MIN_CAMERA_DISTANCE = 2;
|
export const MIN_CAMERA_DISTANCE = 2;
|
||||||
export const MAX_CAMERA_DISTANCE = 52000;
|
export const MAX_CAMERA_DISTANCE = 150000;
|
||||||
|
|
||||||
export interface ZoomBlend {
|
export interface ZoomBlend {
|
||||||
localWeight: number;
|
localWeight: number;
|
||||||
systemWeight: number;
|
systemWeight: number;
|
||||||
universeWeight: number;
|
galaxyWeight: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,28 +14,26 @@ export function createViewerControllers(host: any) {
|
|||||||
getWorldSeed: () => host.world?.seed ?? 1,
|
getWorldSeed: () => host.world?.seed ?? 1,
|
||||||
getWorldTimeSyncMs: () => host.worldTimeSyncMs,
|
getWorldTimeSyncMs: () => host.worldTimeSyncMs,
|
||||||
getWorldPresentationContext: () => host.createWorldPresentationContext(),
|
getWorldPresentationContext: () => host.createWorldPresentationContext(),
|
||||||
systemGroup: host.systemGroup,
|
getActiveSystemId: () => host.activeSystemId,
|
||||||
spatialNodeGroup: host.spatialNodeGroup,
|
galaxySystemGroup: host.galaxyLayer.systemGroup,
|
||||||
bubbleGroup: host.bubbleGroup,
|
systemScene: host.systemLayer.scene,
|
||||||
nodeGroup: host.nodeGroup,
|
celestialGroup: host.systemLayer.celestialGroup,
|
||||||
stationGroup: host.stationGroup,
|
nodeGroup: host.systemLayer.nodeGroup,
|
||||||
claimGroup: host.claimGroup,
|
stationGroup: host.systemLayer.stationGroup,
|
||||||
constructionSiteGroup: host.constructionSiteGroup,
|
claimGroup: host.systemLayer.claimGroup,
|
||||||
shipGroup: host.shipGroup,
|
constructionSiteGroup: host.systemLayer.constructionSiteGroup,
|
||||||
selectableTargets: host.selectableTargets,
|
shipGroup: host.systemLayer.shipGroup,
|
||||||
presentationEntries: host.presentationEntries,
|
galaxySelectableTargets: host.galaxyLayer.selectableTargets,
|
||||||
|
systemSelectableTargets: host.systemLayer.selectableTargets,
|
||||||
systemVisuals: host.systemVisuals,
|
systemVisuals: host.systemVisuals,
|
||||||
systemSummaryVisuals: host.systemSummaryVisuals,
|
|
||||||
planetVisuals: host.planetVisuals,
|
planetVisuals: host.planetVisuals,
|
||||||
orbitLines: host.orbitLines,
|
orbitLines: host.orbitLines,
|
||||||
spatialNodeVisuals: host.spatialNodeVisuals,
|
celestialVisuals: host.celestialVisuals,
|
||||||
bubbleVisuals: host.bubbleVisuals,
|
|
||||||
nodeVisuals: host.nodeVisuals,
|
nodeVisuals: host.nodeVisuals,
|
||||||
stationVisuals: host.stationVisuals,
|
stationVisuals: host.stationVisuals,
|
||||||
claimVisuals: host.claimVisuals,
|
claimVisuals: host.claimVisuals,
|
||||||
constructionSiteVisuals: host.constructionSiteVisuals,
|
constructionSiteVisuals: host.constructionSiteVisuals,
|
||||||
shipVisuals: host.shipVisuals,
|
shipVisuals: host.shipVisuals,
|
||||||
registerPresentation: host.registerPresentation.bind(host),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const navigationController = new ViewerNavigationController({
|
const navigationController = new ViewerNavigationController({
|
||||||
@@ -45,6 +43,9 @@ export function createViewerControllers(host: any) {
|
|||||||
setActiveSystemId: (value) => {
|
setActiveSystemId: (value) => {
|
||||||
host.activeSystemId = value;
|
host.activeSystemId = value;
|
||||||
},
|
},
|
||||||
|
onActiveSystemChanged: (oldId, newId) => {
|
||||||
|
sceneDataController.onActiveSystemChanged(oldId, newId);
|
||||||
|
},
|
||||||
getCameraMode: () => host.cameraMode,
|
getCameraMode: () => host.cameraMode,
|
||||||
setCameraMode: (value) => {
|
setCameraMode: (value) => {
|
||||||
host.cameraMode = value;
|
host.cameraMode = value;
|
||||||
@@ -54,12 +55,13 @@ export function createViewerControllers(host: any) {
|
|||||||
host.cameraTargetShipId = value;
|
host.cameraTargetShipId = value;
|
||||||
},
|
},
|
||||||
getCurrentDistance: () => host.currentDistance,
|
getCurrentDistance: () => host.currentDistance,
|
||||||
getZoomLevel: () => host.zoomLevel,
|
getPovLevel: () => host.povLevel,
|
||||||
getSelectedItems: () => host.selectedItems,
|
getSelectedItems: () => host.selectedItems,
|
||||||
getOrbitYaw: () => host.orbitYaw,
|
getOrbitYaw: () => host.orbitYaw,
|
||||||
galaxyFocus: host.galaxyFocus,
|
galaxyAnchor: host.galaxyAnchor,
|
||||||
systemFocusLocal: host.systemFocusLocal,
|
systemAnchor: host.systemAnchor,
|
||||||
camera: host.camera,
|
galaxyCamera: host.galaxyLayer.camera,
|
||||||
|
systemCamera: host.systemLayer.camera,
|
||||||
shipVisuals: host.shipVisuals,
|
shipVisuals: host.shipVisuals,
|
||||||
nodeVisuals: host.nodeVisuals,
|
nodeVisuals: host.nodeVisuals,
|
||||||
planetVisuals: host.planetVisuals,
|
planetVisuals: host.planetVisuals,
|
||||||
@@ -76,9 +78,12 @@ export function createViewerControllers(host: any) {
|
|||||||
|
|
||||||
const presentationController = new ViewerPresentationController({
|
const presentationController = new ViewerPresentationController({
|
||||||
renderer: host.renderer,
|
renderer: host.renderer,
|
||||||
scene: host.scene,
|
galaxyScene: host.galaxyLayer.scene,
|
||||||
camera: host.camera,
|
galaxyCamera: host.galaxyLayer.camera,
|
||||||
ambienceGroup: host.ambienceGroup,
|
systemCamera: host.systemLayer.camera,
|
||||||
|
galaxyAnchor: host.galaxyAnchor,
|
||||||
|
systemAnchor: host.systemAnchor,
|
||||||
|
ambienceGroup: host.universeLayer.ambienceGroup,
|
||||||
gameSummaryEl: host.gameSummaryEl,
|
gameSummaryEl: host.gameSummaryEl,
|
||||||
networkSummaryEl: host.networkSummaryEl,
|
networkSummaryEl: host.networkSummaryEl,
|
||||||
performanceSummaryEl: host.performanceSummaryEl,
|
performanceSummaryEl: host.performanceSummaryEl,
|
||||||
@@ -94,14 +99,11 @@ export function createViewerControllers(host: any) {
|
|||||||
getActiveSystemId: () => host.activeSystemId,
|
getActiveSystemId: () => host.activeSystemId,
|
||||||
getCameraMode: () => host.cameraMode,
|
getCameraMode: () => host.cameraMode,
|
||||||
getCameraTargetShipId: () => host.cameraTargetShipId,
|
getCameraTargetShipId: () => host.cameraTargetShipId,
|
||||||
getZoomLevel: () => host.zoomLevel,
|
getPovLevel: () => host.povLevel,
|
||||||
getSelectedItems: () => host.selectedItems,
|
getSelectedItems: () => host.selectedItems,
|
||||||
getWorldTimeSyncMs: () => host.worldTimeSyncMs,
|
getWorldTimeSyncMs: () => host.worldTimeSyncMs,
|
||||||
getCurrentDistance: () => host.currentDistance,
|
getCurrentDistance: () => host.currentDistance,
|
||||||
systemFocusLocal: host.systemFocusLocal,
|
|
||||||
planetVisuals: host.planetVisuals,
|
planetVisuals: host.planetVisuals,
|
||||||
systemSummaryVisuals: host.systemSummaryVisuals,
|
|
||||||
presentationEntries: host.presentationEntries,
|
|
||||||
orbitLines: host.orbitLines,
|
orbitLines: host.orbitLines,
|
||||||
systemVisuals: host.systemVisuals,
|
systemVisuals: host.systemVisuals,
|
||||||
createWorldPresentationContext: () => host.createWorldPresentationContext(),
|
createWorldPresentationContext: () => host.createWorldPresentationContext(),
|
||||||
@@ -128,35 +130,33 @@ export function createViewerControllers(host: any) {
|
|||||||
setCurrentStreamScopeKey: (value) => {
|
setCurrentStreamScopeKey: (value) => {
|
||||||
host.currentStreamScopeKey = value;
|
host.currentStreamScopeKey = value;
|
||||||
},
|
},
|
||||||
getZoomLevel: () => host.zoomLevel,
|
getPovLevel: () => host.povLevel,
|
||||||
getActiveSystemId: () => host.activeSystemId,
|
getActiveSystemId: () => host.activeSystemId,
|
||||||
getSelectedItems: () => host.selectedItems,
|
getSelectedItems: () => host.selectedItems,
|
||||||
getCameraMode: () => host.cameraMode,
|
getCameraMode: () => host.cameraMode,
|
||||||
getCameraTargetShipId: () => host.cameraTargetShipId,
|
getCameraTargetShipId: () => host.cameraTargetShipId,
|
||||||
getNetworkStats: () => host.networkStats,
|
getNetworkStats: () => host.networkStats,
|
||||||
getSystemSummaryVisuals: () => host.systemSummaryVisuals,
|
getSystemSummaryVisuals: () => new Map(),
|
||||||
errorEl: host.errorEl,
|
errorEl: host.errorEl,
|
||||||
opsStripEl: host.opsStripEl,
|
opsStripEl: host.opsStripEl,
|
||||||
detailTitleEl: host.detailTitleEl,
|
detailTitleEl: host.detailTitleEl,
|
||||||
detailBodyEl: host.detailBodyEl,
|
detailBodyEl: host.detailBodyEl,
|
||||||
worldLabel: () => host.world?.label ?? "",
|
worldLabel: () => host.world?.label ?? "",
|
||||||
rebuildSystems: (systems) => sceneDataController.rebuildSystems(systems),
|
rebuildSystems: (systems) => sceneDataController.rebuildSystems(systems),
|
||||||
syncSpatialNodes: (nodes) => sceneDataController.syncSpatialNodes(nodes),
|
syncCelestials: (celestials) => sceneDataController.syncCelestials(celestials),
|
||||||
syncLocalBubbles: (bubbles) => sceneDataController.syncLocalBubbles(bubbles),
|
|
||||||
syncNodes: (nodes) => sceneDataController.syncNodes(nodes),
|
syncNodes: (nodes) => sceneDataController.syncNodes(nodes),
|
||||||
syncStations: (stations) => sceneDataController.syncStations(stations),
|
syncStations: (stations) => sceneDataController.syncStations(stations),
|
||||||
syncClaims: (claims) => sceneDataController.syncClaims(claims),
|
syncClaims: (claims) => sceneDataController.syncClaims(claims),
|
||||||
syncConstructionSites: (sites) => sceneDataController.syncConstructionSites(sites),
|
syncConstructionSites: (sites) => sceneDataController.syncConstructionSites(sites),
|
||||||
syncShips: (ships, tickIntervalMs) => sceneDataController.syncShips(ships, tickIntervalMs),
|
syncShips: (ships, tickIntervalMs) => sceneDataController.syncShips(ships, tickIntervalMs),
|
||||||
applySpatialNodeDeltas: (nodes) => sceneDataController.applySpatialNodeDeltas(nodes),
|
applyCelestialDeltas: (celestials) => sceneDataController.applyCelestialDeltas(celestials),
|
||||||
applyLocalBubbleDeltas: (bubbles) => sceneDataController.applyLocalBubbleDeltas(bubbles),
|
|
||||||
applyNodeDeltas: (nodes) => sceneDataController.applyNodeDeltas(nodes),
|
applyNodeDeltas: (nodes) => sceneDataController.applyNodeDeltas(nodes),
|
||||||
applyStationDeltas: (stations) => sceneDataController.applyStationDeltas(stations),
|
applyStationDeltas: (stations) => sceneDataController.applyStationDeltas(stations),
|
||||||
applyClaimDeltas: (claims) => sceneDataController.applyClaimDeltas(claims),
|
applyClaimDeltas: (claims) => sceneDataController.applyClaimDeltas(claims),
|
||||||
applyConstructionSiteDeltas: (sites) => sceneDataController.applyConstructionSiteDeltas(sites),
|
applyConstructionSiteDeltas: (sites) => sceneDataController.applyConstructionSiteDeltas(sites),
|
||||||
applyShipDeltas: (ships, tickIntervalMs) => sceneDataController.applyShipDeltas(ships, tickIntervalMs),
|
applyShipDeltas: (ships, tickIntervalMs) => sceneDataController.applyShipDeltas(ships, tickIntervalMs),
|
||||||
refreshHistoryWindows: () => host.refreshHistoryWindows(),
|
refreshHistoryWindows: () => host.refreshHistoryWindows(),
|
||||||
resolveFocusedBubbleId: () => host.resolveFocusedBubbleId(),
|
resolveFocusedCelestialId: () => host.resolveFocusedCelestialId(),
|
||||||
updateSystemSummaries: () => host.updateSystemSummaries(),
|
updateSystemSummaries: () => host.updateSystemSummaries(),
|
||||||
applyZoomPresentation: () => presentationController.applyZoomPresentation(),
|
applyZoomPresentation: () => presentationController.applyZoomPresentation(),
|
||||||
updateNetworkPanel: () => presentationController.updateNetworkPanel(),
|
updateNetworkPanel: () => presentationController.updateNetworkPanel(),
|
||||||
@@ -193,14 +193,16 @@ export function createViewerControllers(host: any) {
|
|||||||
renderer: host.renderer,
|
renderer: host.renderer,
|
||||||
raycaster: host.raycaster,
|
raycaster: host.raycaster,
|
||||||
mouse: host.mouse,
|
mouse: host.mouse,
|
||||||
camera: host.camera,
|
galaxyCamera: host.galaxyLayer.camera,
|
||||||
selectableTargets: host.selectableTargets,
|
systemCamera: host.systemLayer.camera,
|
||||||
|
galaxySelectableTargets: host.galaxyLayer.selectableTargets,
|
||||||
|
systemSelectableTargets: host.systemLayer.selectableTargets,
|
||||||
hoverLabelEl: host.hoverLabelEl,
|
hoverLabelEl: host.hoverLabelEl,
|
||||||
marqueeEl: host.marqueeEl,
|
marqueeEl: host.marqueeEl,
|
||||||
keyState: host.keyState,
|
keyState: host.keyState,
|
||||||
getWorld: () => host.world,
|
getWorld: () => host.world,
|
||||||
getActiveSystemId: () => host.activeSystemId,
|
getActiveSystemId: () => host.activeSystemId,
|
||||||
getZoomLevel: () => host.zoomLevel,
|
getPovLevel: () => host.povLevel,
|
||||||
getSelectedItems: () => host.selectedItems,
|
getSelectedItems: () => host.selectedItems,
|
||||||
setSelectedItems: (items) => {
|
setSelectedItems: (items) => {
|
||||||
host.selectedItems = items;
|
host.selectedItems = items;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import { MAX_CAMERA_DISTANCE, MIN_CAMERA_DISTANCE, ZOOM_DISTANCE } from "./viewerConstants";
|
import { MAX_CAMERA_DISTANCE, MIN_CAMERA_DISTANCE, NAV_DISTANCE } from "./viewerConstants";
|
||||||
import { scaleGalaxyVector, toDisplayGalaxyVector, toThreeVector } from "./viewerMath";
|
import { scaleGalaxyVector, toThreeVector } from "./viewerMath";
|
||||||
import { rawObject } from "./viewerScenePrimitives";
|
import { rawObject } from "./viewerScenePrimitives";
|
||||||
import { resolveShipWorldPosition } from "./viewerWorldPresentation";
|
import { resolveShipWorldPosition } from "./viewerWorldPresentation";
|
||||||
import type {
|
import type {
|
||||||
@@ -9,7 +9,6 @@ import type {
|
|||||||
ShipVisual,
|
ShipVisual,
|
||||||
SystemVisual,
|
SystemVisual,
|
||||||
WorldState,
|
WorldState,
|
||||||
ZoomLevel,
|
|
||||||
} from "./viewerTypes";
|
} from "./viewerTypes";
|
||||||
|
|
||||||
export function syncFollowStateFromSelection(
|
export function syncFollowStateFromSelection(
|
||||||
@@ -89,7 +88,7 @@ export function updateFollowCamera(params: {
|
|||||||
followCameraDirection: THREE.Vector3;
|
followCameraDirection: THREE.Vector3;
|
||||||
followCameraDesiredDirection: THREE.Vector3;
|
followCameraDesiredDirection: THREE.Vector3;
|
||||||
followCameraOffset: THREE.Vector3;
|
followCameraOffset: THREE.Vector3;
|
||||||
systemFocusLocal: THREE.Vector3;
|
systemAnchor: THREE.Vector3;
|
||||||
delta: number;
|
delta: number;
|
||||||
getAnimatedShipLocalPosition: (visual: ShipVisual) => THREE.Vector3;
|
getAnimatedShipLocalPosition: (visual: ShipVisual) => THREE.Vector3;
|
||||||
toDisplayLocalPosition: (localPosition: THREE.Vector3, systemId?: string) => THREE.Vector3;
|
toDisplayLocalPosition: (localPosition: THREE.Vector3, systemId?: string) => THREE.Vector3;
|
||||||
@@ -106,7 +105,7 @@ export function updateFollowCamera(params: {
|
|||||||
followCameraDirection,
|
followCameraDirection,
|
||||||
followCameraDesiredDirection,
|
followCameraDesiredDirection,
|
||||||
followCameraOffset,
|
followCameraOffset,
|
||||||
systemFocusLocal,
|
systemAnchor,
|
||||||
delta,
|
delta,
|
||||||
getAnimatedShipLocalPosition,
|
getAnimatedShipLocalPosition,
|
||||||
toDisplayLocalPosition,
|
toDisplayLocalPosition,
|
||||||
@@ -143,10 +142,10 @@ export function updateFollowCamera(params: {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (ship.spatialState.movementRegime === "ftl-transit") {
|
if (ship.spatialState.movementRegime === "ftl-transit") {
|
||||||
systemFocusLocal.set(0, 0, 0);
|
systemAnchor.set(0, 0, 0);
|
||||||
const destinationNodeId = ship.spatialState.transit?.destinationNodeId;
|
const destinationNodeId = ship.spatialState.transit?.destinationNodeId;
|
||||||
const destinationNode = destinationNodeId ? world.spatialNodes.get(destinationNodeId) : undefined;
|
const destinationCelestial = destinationNodeId ? world.celestials.get(destinationNodeId) : undefined;
|
||||||
const destinationSystem = destinationNode ? world.systems.get(destinationNode.systemId) : undefined;
|
const destinationSystem = destinationCelestial ? world.systems.get(destinationCelestial.systemId) : undefined;
|
||||||
const originSystem = world.systems.get(ship.systemId);
|
const originSystem = world.systems.get(ship.systemId);
|
||||||
if (originSystem && destinationSystem) {
|
if (originSystem && destinationSystem) {
|
||||||
followCameraDesiredDirection
|
followCameraDesiredDirection
|
||||||
@@ -154,7 +153,7 @@ export function updateFollowCamera(params: {
|
|||||||
.normalize();
|
.normalize();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
systemFocusLocal.lerp(shipLocalPosition, 1 - Math.exp(-delta * 8));
|
systemAnchor.lerp(shipLocalPosition, 1 - Math.exp(-delta * 8));
|
||||||
followCameraDesiredDirection.copy(resolveShipHeading(visual, shipLocalPosition)).normalize();
|
followCameraDesiredDirection.copy(resolveShipHeading(visual, shipLocalPosition)).normalize();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,13 +189,6 @@ export function updateFollowCamera(params: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateSystemDetailVisibility(systemVisuals: Map<string, SystemVisual>, activeSystemId?: string, zoomLevel?: ZoomLevel) {
|
|
||||||
const detailVisible = !!activeSystemId && zoomLevel !== "universe";
|
|
||||||
for (const [systemId, visual] of systemVisuals.entries()) {
|
|
||||||
visual.detailGroup.setVisible(detailVisible && systemId === activeSystemId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setShellReticleOpacity(sprite: SystemVisual["shellReticle"], opacity: number) {
|
export function setShellReticleOpacity(sprite: SystemVisual["shellReticle"], opacity: number) {
|
||||||
sprite.setVisible(opacity > 0.02);
|
sprite.setVisible(opacity > 0.02);
|
||||||
const material = (rawObject(sprite) as THREE.Sprite).material;
|
const material = (rawObject(sprite) as THREE.Sprite).material;
|
||||||
@@ -204,7 +196,7 @@ export function setShellReticleOpacity(sprite: SystemVisual["shellReticle"], opa
|
|||||||
material.needsUpdate = true;
|
material.needsUpdate = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function zoomFromWheel(desiredDistance: number, deltaY: number) {
|
export function navigateFromWheel(desiredDistance: number, deltaY: number) {
|
||||||
const clampedDelta = THREE.MathUtils.clamp(deltaY, -180, 180);
|
const clampedDelta = THREE.MathUtils.clamp(deltaY, -180, 180);
|
||||||
const zoomFactor = Math.exp(clampedDelta * 0.00135);
|
const zoomFactor = Math.exp(clampedDelta * 0.00135);
|
||||||
return THREE.MathUtils.clamp(desiredDistance * zoomFactor, MIN_CAMERA_DISTANCE, MAX_CAMERA_DISTANCE);
|
return THREE.MathUtils.clamp(desiredDistance * zoomFactor, MIN_CAMERA_DISTANCE, MAX_CAMERA_DISTANCE);
|
||||||
@@ -226,21 +218,21 @@ export function applyKeyboardControl(params: {
|
|||||||
cameraMode = "tactical";
|
cameraMode = "tactical";
|
||||||
}
|
}
|
||||||
if (key === "1") {
|
if (key === "1") {
|
||||||
desiredDistance = ZOOM_DISTANCE.local;
|
desiredDistance = NAV_DISTANCE.local;
|
||||||
} else if (key === "2") {
|
} else if (key === "2") {
|
||||||
desiredDistance = ZOOM_DISTANCE.system;
|
desiredDistance = NAV_DISTANCE.system;
|
||||||
} else if (key === "3") {
|
} else if (key === "3") {
|
||||||
desiredDistance = ZOOM_DISTANCE.universe;
|
desiredDistance = NAV_DISTANCE.galaxy;
|
||||||
} else if (key === "=") {
|
} else if (key === "=") {
|
||||||
desiredDistance = desiredDistance <= ZOOM_DISTANCE.system
|
desiredDistance = desiredDistance <= NAV_DISTANCE.system
|
||||||
? ZOOM_DISTANCE.local
|
? NAV_DISTANCE.local
|
||||||
: ZOOM_DISTANCE.system;
|
: NAV_DISTANCE.system;
|
||||||
} else if (key === "-") {
|
} else if (key === "-") {
|
||||||
desiredDistance = desiredDistance >= ZOOM_DISTANCE.system
|
desiredDistance = desiredDistance >= NAV_DISTANCE.system
|
||||||
? ZOOM_DISTANCE.universe
|
? NAV_DISTANCE.galaxy
|
||||||
: ZOOM_DISTANCE.system;
|
: NAV_DISTANCE.system;
|
||||||
} else if (key === "/") {
|
} else if (key === "/") {
|
||||||
desiredDistance = ZOOM_DISTANCE.system;
|
desiredDistance = NAV_DISTANCE.system;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { cameraMode, desiredDistance };
|
return { cameraMode, desiredDistance };
|
||||||
|
|||||||
37
apps/viewer/src/viewerGalaxyLayer.ts
Normal file
37
apps/viewer/src/viewerGalaxyLayer.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import * as THREE from "three";
|
||||||
|
import type { Selectable } from "./viewerTypes";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Galaxy rendering layer — the galaxy map.
|
||||||
|
* Scene coordinate unit: display-unit (light-year scale).
|
||||||
|
* Only visible in galaxy POV, rendered on top of the universe backdrop.
|
||||||
|
* Contains star dots and shell reticles for each system.
|
||||||
|
*/
|
||||||
|
export class GalaxyLayer {
|
||||||
|
readonly scene = new THREE.Scene();
|
||||||
|
readonly camera = new THREE.PerspectiveCamera(50, 1, 0.1, 600000);
|
||||||
|
|
||||||
|
/** Star dots and shell reticles, one per system. */
|
||||||
|
readonly systemGroup = new THREE.Group();
|
||||||
|
|
||||||
|
readonly selectableTargets = new Map<THREE.Object3D, Selectable>();
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.scene.fog = new THREE.FogExp2(0x040912, 0.000035);
|
||||||
|
this.scene.add(new THREE.AmbientLight(0x90a6c0, 0.55));
|
||||||
|
const keyLight = new THREE.DirectionalLight(0xdcecff, 1.3);
|
||||||
|
keyLight.position.set(1000, 1200, 800);
|
||||||
|
this.scene.add(keyLight);
|
||||||
|
this.scene.add(this.systemGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCamera(focus: THREE.Vector3, orbitOffset: THREE.Vector3) {
|
||||||
|
this.camera.position.copy(focus).add(orbitOffset);
|
||||||
|
this.camera.lookAt(focus);
|
||||||
|
}
|
||||||
|
|
||||||
|
onResize(aspect: number) {
|
||||||
|
this.camera.aspect = aspect;
|
||||||
|
this.camera.updateProjectionMatrix();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,27 +2,16 @@ import * as THREE from "three";
|
|||||||
import { describeHoverLabel, getSelectionGroup } from "./viewerSelection";
|
import { describeHoverLabel, getSelectionGroup } from "./viewerSelection";
|
||||||
import { ACTIVE_SYSTEM_DETAIL_SCALE } from "./viewerConstants";
|
import { ACTIVE_SYSTEM_DETAIL_SCALE } from "./viewerConstants";
|
||||||
import { DISPLAY_UNITS_PER_KILOMETER, DISPLAY_UNITS_PER_LIGHT_YEAR, KILOMETERS_PER_AU, formatAdaptiveDistanceFromKilometers, formatSystemDistance } from "./viewerMath";
|
import { DISPLAY_UNITS_PER_KILOMETER, DISPLAY_UNITS_PER_LIGHT_YEAR, KILOMETERS_PER_AU, formatAdaptiveDistanceFromKilometers, formatSystemDistance } from "./viewerMath";
|
||||||
import type { Selectable, SelectionGroup, WorldState, ZoomLevel } from "./viewerTypes";
|
import type { Selectable, SelectionGroup, WorldState, PovLevel } from "./viewerTypes";
|
||||||
|
|
||||||
export interface HoverPickResult {
|
export interface HoverPickResult {
|
||||||
selection: Selectable;
|
selection: Selectable;
|
||||||
object: THREE.Object3D;
|
object: THREE.Object3D;
|
||||||
|
/** Which camera was used for this pick (for distance calculation) */
|
||||||
|
camera: THREE.Camera;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function pickSelectableAtClientPosition(
|
function pickOneCamera(
|
||||||
renderer: THREE.WebGLRenderer,
|
|
||||||
raycaster: THREE.Raycaster,
|
|
||||||
mouse: THREE.Vector2,
|
|
||||||
camera: THREE.Camera,
|
|
||||||
selectableTargets: Map<THREE.Object3D, Selectable>,
|
|
||||||
clientX: number,
|
|
||||||
clientY: number,
|
|
||||||
) {
|
|
||||||
const hit = pickSelectableHitAtClientPosition(renderer, raycaster, mouse, camera, selectableTargets, clientX, clientY);
|
|
||||||
return hit?.selection;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function pickSelectableHitAtClientPosition(
|
|
||||||
renderer: THREE.WebGLRenderer,
|
renderer: THREE.WebGLRenderer,
|
||||||
raycaster: THREE.Raycaster,
|
raycaster: THREE.Raycaster,
|
||||||
mouse: THREE.Vector2,
|
mouse: THREE.Vector2,
|
||||||
@@ -38,29 +27,61 @@ export function pickSelectableHitAtClientPosition(
|
|||||||
const hit = raycaster.intersectObjects([...selectableTargets.keys()], false)[0];
|
const hit = raycaster.intersectObjects([...selectableTargets.keys()], false)[0];
|
||||||
const selection = hit ? selectableTargets.get(hit.object) : undefined;
|
const selection = hit ? selectableTargets.get(hit.object) : undefined;
|
||||||
return hit && selection
|
return hit && selection
|
||||||
? { selection, object: hit.object }
|
? { selection, object: hit.object, camera }
|
||||||
: undefined;
|
: undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function pickSelectableAtClientPosition(
|
||||||
|
renderer: THREE.WebGLRenderer,
|
||||||
|
raycaster: THREE.Raycaster,
|
||||||
|
mouse: THREE.Vector2,
|
||||||
|
galaxyCamera: THREE.Camera,
|
||||||
|
galaxySelectableTargets: Map<THREE.Object3D, Selectable>,
|
||||||
|
systemCamera: THREE.Camera,
|
||||||
|
systemSelectableTargets: Map<THREE.Object3D, Selectable>,
|
||||||
|
clientX: number,
|
||||||
|
clientY: number,
|
||||||
|
) {
|
||||||
|
const hit = pickSelectableHitAtClientPosition(renderer, raycaster, mouse, galaxyCamera, galaxySelectableTargets, systemCamera, systemSelectableTargets, clientX, clientY);
|
||||||
|
return hit?.selection;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pickSelectableHitAtClientPosition(
|
||||||
|
renderer: THREE.WebGLRenderer,
|
||||||
|
raycaster: THREE.Raycaster,
|
||||||
|
mouse: THREE.Vector2,
|
||||||
|
galaxyCamera: THREE.Camera,
|
||||||
|
galaxySelectableTargets: Map<THREE.Object3D, Selectable>,
|
||||||
|
systemCamera: THREE.Camera,
|
||||||
|
systemSelectableTargets: Map<THREE.Object3D, Selectable>,
|
||||||
|
clientX: number,
|
||||||
|
clientY: number,
|
||||||
|
): HoverPickResult | undefined {
|
||||||
|
// Try system camera first (higher priority when in a system)
|
||||||
|
const systemHit = pickOneCamera(renderer, raycaster, mouse, systemCamera, systemSelectableTargets, clientX, clientY);
|
||||||
|
if (systemHit) {
|
||||||
|
return systemHit;
|
||||||
|
}
|
||||||
|
return pickOneCamera(renderer, raycaster, mouse, galaxyCamera, galaxySelectableTargets, clientX, clientY);
|
||||||
|
}
|
||||||
|
|
||||||
export function updateHoverLabel(params: {
|
export function updateHoverLabel(params: {
|
||||||
dragMode?: string;
|
dragMode?: string;
|
||||||
hoverLabelEl: HTMLDivElement;
|
hoverLabelEl: HTMLDivElement;
|
||||||
hoverPick: HoverPickResult | undefined;
|
hoverPick: HoverPickResult | undefined;
|
||||||
activeSystemId?: string;
|
activeSystemId?: string;
|
||||||
zoomLevel: ZoomLevel;
|
povLevel: PovLevel;
|
||||||
world?: WorldState;
|
world?: WorldState;
|
||||||
point: THREE.Vector2;
|
point: THREE.Vector2;
|
||||||
camera: THREE.Camera;
|
|
||||||
}) {
|
}) {
|
||||||
const {
|
const {
|
||||||
dragMode,
|
dragMode,
|
||||||
hoverLabelEl,
|
hoverLabelEl,
|
||||||
hoverPick,
|
hoverPick,
|
||||||
activeSystemId,
|
activeSystemId,
|
||||||
zoomLevel,
|
povLevel,
|
||||||
world,
|
world,
|
||||||
point,
|
point,
|
||||||
camera,
|
|
||||||
} = params;
|
} = params;
|
||||||
|
|
||||||
if (dragMode) {
|
if (dragMode) {
|
||||||
@@ -73,14 +94,14 @@ export function updateHoverLabel(params: {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { selection, object } = hoverPick;
|
const { selection, object, camera } = hoverPick;
|
||||||
const label = describeHoverLabel(world, selection);
|
const label = describeHoverLabel(world, selection);
|
||||||
if (!label) {
|
if (!label) {
|
||||||
hoverLabelEl.hidden = true;
|
hoverLabelEl.hidden = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const distance = formatHoverDistance(camera, object, selection, zoomLevel, activeSystemId);
|
const distance = formatHoverDistance(camera, object, selection, povLevel, activeSystemId);
|
||||||
|
|
||||||
hoverLabelEl.hidden = false;
|
hoverLabelEl.hidden = false;
|
||||||
hoverLabelEl.textContent = `${label}\n${distance}`;
|
hoverLabelEl.textContent = `${label}\n${distance}`;
|
||||||
@@ -92,7 +113,7 @@ function formatHoverDistance(
|
|||||||
camera: THREE.Camera,
|
camera: THREE.Camera,
|
||||||
object: THREE.Object3D,
|
object: THREE.Object3D,
|
||||||
selection: Selectable,
|
selection: Selectable,
|
||||||
zoomLevel: ZoomLevel,
|
povLevel: PovLevel,
|
||||||
activeSystemId?: string,
|
activeSystemId?: string,
|
||||||
) {
|
) {
|
||||||
const worldPosition = object.getWorldPosition(new THREE.Vector3());
|
const worldPosition = object.getWorldPosition(new THREE.Vector3());
|
||||||
@@ -107,14 +128,13 @@ function formatHoverDistance(
|
|||||||
: selection.kind === "ship"
|
: selection.kind === "ship"
|
||||||
|| selection.kind === "station"
|
|| selection.kind === "station"
|
||||||
|| selection.kind === "node"
|
|| selection.kind === "node"
|
||||||
|| selection.kind === "spatial-node"
|
|| selection.kind === "celestial"
|
||||||
|| selection.kind === "bubble"
|
|
||||||
|| selection.kind === "claim"
|
|| selection.kind === "claim"
|
||||||
|| selection.kind === "construction-site";
|
|| selection.kind === "construction-site";
|
||||||
|
|
||||||
if (inActiveSystem && activeSystemId) {
|
if (inActiveSystem && activeSystemId) {
|
||||||
const kilometers = displayDistance / (DISPLAY_UNITS_PER_KILOMETER * ACTIVE_SYSTEM_DETAIL_SCALE);
|
const kilometers = displayDistance / (DISPLAY_UNITS_PER_KILOMETER * ACTIVE_SYSTEM_DETAIL_SCALE);
|
||||||
return zoomLevel === "system"
|
return povLevel === "system"
|
||||||
? formatSystemDistance(kilometers / KILOMETERS_PER_AU)
|
? formatSystemDistance(kilometers / KILOMETERS_PER_AU)
|
||||||
: formatAdaptiveDistanceFromKilometers(kilometers);
|
: formatAdaptiveDistanceFromKilometers(kilometers);
|
||||||
}
|
}
|
||||||
@@ -145,17 +165,17 @@ export function hideMarqueeBox(marqueeEl: HTMLDivElement) {
|
|||||||
|
|
||||||
export function completeMarqueeSelection(params: {
|
export function completeMarqueeSelection(params: {
|
||||||
renderer: THREE.WebGLRenderer;
|
renderer: THREE.WebGLRenderer;
|
||||||
camera: THREE.Camera;
|
systemCamera: THREE.Camera;
|
||||||
dragStart: THREE.Vector2;
|
dragStart: THREE.Vector2;
|
||||||
dragLast: THREE.Vector2;
|
dragLast: THREE.Vector2;
|
||||||
selectableTargets: Map<THREE.Object3D, Selectable>;
|
systemSelectableTargets: Map<THREE.Object3D, Selectable>;
|
||||||
}) {
|
}) {
|
||||||
const {
|
const {
|
||||||
renderer,
|
renderer,
|
||||||
camera,
|
systemCamera,
|
||||||
dragStart,
|
dragStart,
|
||||||
dragLast,
|
dragLast,
|
||||||
selectableTargets,
|
systemSelectableTargets,
|
||||||
} = params;
|
} = params;
|
||||||
|
|
||||||
const bounds = renderer.domElement.getBoundingClientRect();
|
const bounds = renderer.domElement.getBoundingClientRect();
|
||||||
@@ -165,7 +185,7 @@ export function completeMarqueeSelection(params: {
|
|||||||
const maxY = Math.max(dragStart.y, dragLast.y);
|
const maxY = Math.max(dragStart.y, dragLast.y);
|
||||||
const grouped = new Map<SelectionGroup, Selectable[]>();
|
const grouped = new Map<SelectionGroup, Selectable[]>();
|
||||||
|
|
||||||
for (const [object, selectable] of selectableTargets.entries()) {
|
for (const [object, selectable] of systemSelectableTargets.entries()) {
|
||||||
if (object instanceof THREE.Sprite && !object.visible) {
|
if (object instanceof THREE.Sprite && !object.visible) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -175,7 +195,7 @@ export function completeMarqueeSelection(params: {
|
|||||||
|
|
||||||
const worldPosition = new THREE.Vector3();
|
const worldPosition = new THREE.Vector3();
|
||||||
object.getWorldPosition(worldPosition);
|
object.getWorldPosition(worldPosition);
|
||||||
worldPosition.project(camera);
|
worldPosition.project(systemCamera);
|
||||||
const screenX = ((worldPosition.x + 1) * 0.5) * bounds.width;
|
const screenX = ((worldPosition.x + 1) * 0.5) * bounds.width;
|
||||||
const screenY = ((1 - worldPosition.y) * 0.5) * bounds.height;
|
const screenY = ((1 - worldPosition.y) * 0.5) * bounds.height;
|
||||||
if (screenX < minX || screenX > maxX || screenY < minY || screenY > maxY) {
|
if (screenX < minX || screenX > maxX || screenY < minY || screenY > maxY) {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
applyKeyboardControl,
|
applyKeyboardControl,
|
||||||
toggleCameraMode,
|
toggleCameraMode,
|
||||||
zoomFromWheel,
|
navigateFromWheel,
|
||||||
} from "./viewerControls";
|
} from "./viewerControls";
|
||||||
import { ViewerHistoryWindowController } from "./viewerHistoryWindowController";
|
import { ViewerHistoryWindowController } from "./viewerHistoryWindowController";
|
||||||
import type {
|
import type {
|
||||||
@@ -18,21 +18,23 @@ import type {
|
|||||||
DragMode,
|
DragMode,
|
||||||
Selectable,
|
Selectable,
|
||||||
WorldState,
|
WorldState,
|
||||||
ZoomLevel,
|
PovLevel,
|
||||||
} from "./viewerTypes";
|
} from "./viewerTypes";
|
||||||
|
|
||||||
export interface ViewerInteractionContext {
|
export interface ViewerInteractionContext {
|
||||||
renderer: THREE.WebGLRenderer;
|
renderer: THREE.WebGLRenderer;
|
||||||
raycaster: THREE.Raycaster;
|
raycaster: THREE.Raycaster;
|
||||||
mouse: THREE.Vector2;
|
mouse: THREE.Vector2;
|
||||||
camera: THREE.PerspectiveCamera;
|
galaxyCamera: THREE.PerspectiveCamera;
|
||||||
selectableTargets: Map<THREE.Object3D, Selectable>;
|
systemCamera: THREE.PerspectiveCamera;
|
||||||
|
galaxySelectableTargets: Map<THREE.Object3D, Selectable>;
|
||||||
|
systemSelectableTargets: Map<THREE.Object3D, Selectable>;
|
||||||
hoverLabelEl: HTMLDivElement;
|
hoverLabelEl: HTMLDivElement;
|
||||||
marqueeEl: HTMLDivElement;
|
marqueeEl: HTMLDivElement;
|
||||||
keyState: Set<string>;
|
keyState: Set<string>;
|
||||||
getWorld: () => WorldState | undefined;
|
getWorld: () => WorldState | undefined;
|
||||||
getActiveSystemId: () => string | undefined;
|
getActiveSystemId: () => string | undefined;
|
||||||
getZoomLevel: () => ZoomLevel;
|
getPovLevel: () => PovLevel;
|
||||||
getSelectedItems: () => Selectable[];
|
getSelectedItems: () => Selectable[];
|
||||||
setSelectedItems: (items: Selectable[]) => void;
|
setSelectedItems: (items: Selectable[]) => void;
|
||||||
getDragMode: () => DragMode | undefined;
|
getDragMode: () => DragMode | undefined;
|
||||||
@@ -235,7 +237,7 @@ export class ViewerInteractionController {
|
|||||||
|
|
||||||
readonly onWheel = (event: WheelEvent) => {
|
readonly onWheel = (event: WheelEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.context.setDesiredDistance(zoomFromWheel(this.context.getDesiredDistance(), event.deltaY));
|
this.context.setDesiredDistance(navigateFromWheel(this.context.getDesiredDistance(), event.deltaY));
|
||||||
this.context.updateGamePanel("Live");
|
this.context.updateGamePanel("Live");
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -269,10 +271,9 @@ export class ViewerInteractionController {
|
|||||||
hoverLabelEl: this.context.hoverLabelEl,
|
hoverLabelEl: this.context.hoverLabelEl,
|
||||||
hoverPick: this.pickSelectableHitAtClientPosition(event.clientX, event.clientY),
|
hoverPick: this.pickSelectableHitAtClientPosition(event.clientX, event.clientY),
|
||||||
activeSystemId: this.context.getActiveSystemId(),
|
activeSystemId: this.context.getActiveSystemId(),
|
||||||
zoomLevel: this.context.getZoomLevel(),
|
povLevel: this.context.getPovLevel(),
|
||||||
world: this.context.getWorld(),
|
world: this.context.getWorld(),
|
||||||
point: this.context.screenPointFromClient(event.clientX, event.clientY),
|
point: this.context.screenPointFromClient(event.clientX, event.clientY),
|
||||||
camera: this.context.camera,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -300,8 +301,10 @@ export class ViewerInteractionController {
|
|||||||
this.context.renderer,
|
this.context.renderer,
|
||||||
this.context.raycaster,
|
this.context.raycaster,
|
||||||
this.context.mouse,
|
this.context.mouse,
|
||||||
this.context.camera,
|
this.context.galaxyCamera,
|
||||||
this.context.selectableTargets,
|
this.context.galaxySelectableTargets,
|
||||||
|
this.context.systemCamera,
|
||||||
|
this.context.systemSelectableTargets,
|
||||||
clientX,
|
clientX,
|
||||||
clientY,
|
clientY,
|
||||||
);
|
);
|
||||||
@@ -312,8 +315,10 @@ export class ViewerInteractionController {
|
|||||||
this.context.renderer,
|
this.context.renderer,
|
||||||
this.context.raycaster,
|
this.context.raycaster,
|
||||||
this.context.mouse,
|
this.context.mouse,
|
||||||
this.context.camera,
|
this.context.galaxyCamera,
|
||||||
this.context.selectableTargets,
|
this.context.galaxySelectableTargets,
|
||||||
|
this.context.systemCamera,
|
||||||
|
this.context.systemSelectableTargets,
|
||||||
clientX,
|
clientX,
|
||||||
clientY,
|
clientY,
|
||||||
);
|
);
|
||||||
@@ -322,10 +327,10 @@ export class ViewerInteractionController {
|
|||||||
private completeMarqueeSelection() {
|
private completeMarqueeSelection() {
|
||||||
const selection = completeMarqueeSelection({
|
const selection = completeMarqueeSelection({
|
||||||
renderer: this.context.renderer,
|
renderer: this.context.renderer,
|
||||||
camera: this.context.camera,
|
systemCamera: this.context.systemCamera,
|
||||||
dragStart: this.context.dragStart,
|
dragStart: this.context.dragStart,
|
||||||
dragLast: this.context.dragLast,
|
dragLast: this.context.dragLast,
|
||||||
selectableTargets: this.context.selectableTargets,
|
systemSelectableTargets: this.context.systemSelectableTargets,
|
||||||
});
|
});
|
||||||
this.context.setSelectedItems(selection);
|
this.context.setSelectedItems(selection);
|
||||||
this.context.syncFollowStateFromSelection();
|
this.context.syncFollowStateFromSelection();
|
||||||
|
|||||||
24
apps/viewer/src/viewerLocalLayer.ts
Normal file
24
apps/viewer/src/viewerLocalLayer.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import * as THREE from "three";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Local rendering layer.
|
||||||
|
* Scene coordinate unit: reserved for future close-up detail.
|
||||||
|
* Camera far plane covers immediate surroundings.
|
||||||
|
* Currently empty — populated when local-space objects are introduced.
|
||||||
|
*/
|
||||||
|
export class LocalLayer {
|
||||||
|
readonly scene = new THREE.Scene();
|
||||||
|
readonly camera = new THREE.PerspectiveCamera(50, 1, 0.1, 2000);
|
||||||
|
|
||||||
|
private static readonly ORIGIN = new THREE.Vector3(0, 0, 0);
|
||||||
|
|
||||||
|
updateCamera(orbitOffset: THREE.Vector3) {
|
||||||
|
this.camera.position.copy(orbitOffset);
|
||||||
|
this.camera.lookAt(LocalLayer.ORIGIN);
|
||||||
|
}
|
||||||
|
|
||||||
|
onResize(aspect: number) {
|
||||||
|
this.camera.aspect = aspect;
|
||||||
|
this.camera.updateProjectionMatrix();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ import type {
|
|||||||
import type {
|
import type {
|
||||||
OrbitalAnchor,
|
OrbitalAnchor,
|
||||||
WorldState,
|
WorldState,
|
||||||
ZoomLevel,
|
PovLevel,
|
||||||
} from "./viewerTypes";
|
} from "./viewerTypes";
|
||||||
import type { ZoomBlend } from "./viewerConstants";
|
import type { ZoomBlend } from "./viewerConstants";
|
||||||
|
|
||||||
@@ -112,19 +112,19 @@ export function computeZoomBlend(distance: number): ZoomBlend {
|
|||||||
return {
|
return {
|
||||||
localWeight: 1 - localToSystem,
|
localWeight: 1 - localToSystem,
|
||||||
systemWeight: Math.min(localToSystem, 1 - systemToUniverse),
|
systemWeight: Math.min(localToSystem, 1 - systemToUniverse),
|
||||||
universeWeight: systemToUniverse,
|
galaxyWeight: systemToUniverse,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function classifyZoomLevel(distance: number): ZoomLevel {
|
export function classifyPovLevel(distance: number): PovLevel {
|
||||||
const blend = computeZoomBlend(distance);
|
const blend = computeZoomBlend(distance);
|
||||||
if (blend.localWeight >= blend.systemWeight && blend.localWeight >= blend.universeWeight) {
|
if (blend.localWeight >= blend.systemWeight && blend.localWeight >= blend.galaxyWeight) {
|
||||||
return "local";
|
return "local";
|
||||||
}
|
}
|
||||||
if (blend.systemWeight >= blend.universeWeight) {
|
if (blend.systemWeight >= blend.galaxyWeight) {
|
||||||
return "system";
|
return "system";
|
||||||
}
|
}
|
||||||
return "universe";
|
return "galaxy";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toThreeVector(vector: Vector3Dto): THREE.Vector3 {
|
export function toThreeVector(vector: Vector3Dto): THREE.Vector3 {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
determineActiveSystemId,
|
determineActiveSystemId,
|
||||||
focusOnSelection,
|
focusOnSelection,
|
||||||
getCameraFocusWorldPosition,
|
getCameraFocusWorldPosition,
|
||||||
|
getSystemCameraFocus,
|
||||||
resolveSelectionPosition,
|
resolveSelectionPosition,
|
||||||
seedSystemFocusLocal,
|
seedSystemFocusLocal,
|
||||||
toDisplayLocalPosition,
|
toDisplayLocalPosition,
|
||||||
@@ -10,9 +11,8 @@ import {
|
|||||||
import {
|
import {
|
||||||
syncFollowStateFromSelection,
|
syncFollowStateFromSelection,
|
||||||
updateFollowCamera,
|
updateFollowCamera,
|
||||||
updateSystemDetailVisibility,
|
|
||||||
} from "./viewerControls";
|
} from "./viewerControls";
|
||||||
import { computeNodeLocalPosition, resolveBubblePosition, resolvePointPosition } from "./viewerWorldPresentation";
|
import { computeNodeLocalPosition, resolvePointPosition } from "./viewerWorldPresentation";
|
||||||
import { getAnimatedShipLocalPosition, resolveShipHeading } from "./viewerPresentation";
|
import { getAnimatedShipLocalPosition, resolveShipHeading } from "./viewerPresentation";
|
||||||
import type {
|
import type {
|
||||||
CameraMode,
|
CameraMode,
|
||||||
@@ -22,7 +22,7 @@ import type {
|
|||||||
ShipVisual,
|
ShipVisual,
|
||||||
SystemVisual,
|
SystemVisual,
|
||||||
WorldState,
|
WorldState,
|
||||||
ZoomLevel,
|
PovLevel,
|
||||||
} from "./viewerTypes";
|
} from "./viewerTypes";
|
||||||
|
|
||||||
export interface ViewerNavigationContext {
|
export interface ViewerNavigationContext {
|
||||||
@@ -30,17 +30,19 @@ export interface ViewerNavigationContext {
|
|||||||
getWorldTimeSyncMs: () => number;
|
getWorldTimeSyncMs: () => number;
|
||||||
getActiveSystemId: () => string | undefined;
|
getActiveSystemId: () => string | undefined;
|
||||||
setActiveSystemId: (value: string | undefined) => void;
|
setActiveSystemId: (value: string | undefined) => void;
|
||||||
|
onActiveSystemChanged: (oldId: string | undefined, newId: string | undefined) => void;
|
||||||
getCameraMode: () => CameraMode;
|
getCameraMode: () => CameraMode;
|
||||||
setCameraMode: (value: CameraMode) => void;
|
setCameraMode: (value: CameraMode) => void;
|
||||||
getCameraTargetShipId: () => string | undefined;
|
getCameraTargetShipId: () => string | undefined;
|
||||||
setCameraTargetShipId: (value: string | undefined) => void;
|
setCameraTargetShipId: (value: string | undefined) => void;
|
||||||
getCurrentDistance: () => number;
|
getCurrentDistance: () => number;
|
||||||
getZoomLevel: () => ZoomLevel;
|
getPovLevel: () => PovLevel;
|
||||||
getSelectedItems: () => Selectable[];
|
getSelectedItems: () => Selectable[];
|
||||||
getOrbitYaw: () => number;
|
getOrbitYaw: () => number;
|
||||||
galaxyFocus: THREE.Vector3;
|
galaxyAnchor: THREE.Vector3;
|
||||||
systemFocusLocal: THREE.Vector3;
|
systemAnchor: THREE.Vector3;
|
||||||
camera: THREE.PerspectiveCamera;
|
galaxyCamera: THREE.PerspectiveCamera;
|
||||||
|
systemCamera: THREE.PerspectiveCamera;
|
||||||
shipVisuals: Map<string, ShipVisual>;
|
shipVisuals: Map<string, ShipVisual>;
|
||||||
nodeVisuals: Map<string, NodeVisual>;
|
nodeVisuals: Map<string, NodeVisual>;
|
||||||
planetVisuals: PlanetVisual[];
|
planetVisuals: PlanetVisual[];
|
||||||
@@ -66,14 +68,10 @@ export class ViewerNavigationController {
|
|||||||
nodeVisuals: this.context.nodeVisuals,
|
nodeVisuals: this.context.nodeVisuals,
|
||||||
planetVisuals: this.context.planetVisuals,
|
planetVisuals: this.context.planetVisuals,
|
||||||
computeNodeLocalPosition: (node, timeSeconds) => computeNodeLocalPosition(this.context.createWorldPresentationContext(), node, timeSeconds),
|
computeNodeLocalPosition: (node, timeSeconds) => computeNodeLocalPosition(this.context.createWorldPresentationContext(), node, timeSeconds),
|
||||||
resolveBubblePosition: (bubbleId) => {
|
resolvePointPosition: (systemId, celestialId) => resolvePointPosition(this.context.createWorldPresentationContext(), systemId, celestialId),
|
||||||
const bubble = this.context.getWorld()?.localBubbles.get(bubbleId);
|
|
||||||
return bubble ? resolveBubblePosition(this.context.createWorldPresentationContext(), bubble) : undefined;
|
|
||||||
},
|
|
||||||
resolvePointPosition: (systemId, nodeId) => resolvePointPosition(this.context.createWorldPresentationContext(), systemId, nodeId),
|
|
||||||
activeSystemId: this.context.getActiveSystemId(),
|
activeSystemId: this.context.getActiveSystemId(),
|
||||||
galaxyFocus: this.context.galaxyFocus,
|
galaxyAnchor: this.context.galaxyAnchor,
|
||||||
systemFocusLocal: this.context.systemFocusLocal,
|
systemAnchor: this.context.systemAnchor,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,11 +83,7 @@ export class ViewerNavigationController {
|
|||||||
nodeVisuals: this.context.nodeVisuals,
|
nodeVisuals: this.context.nodeVisuals,
|
||||||
planetVisuals: this.context.planetVisuals,
|
planetVisuals: this.context.planetVisuals,
|
||||||
computeNodeLocalPosition: (node, timeSeconds) => computeNodeLocalPosition(this.context.createWorldPresentationContext(), node, timeSeconds),
|
computeNodeLocalPosition: (node, timeSeconds) => computeNodeLocalPosition(this.context.createWorldPresentationContext(), node, timeSeconds),
|
||||||
resolveBubblePosition: (bubbleId) => {
|
resolvePointPosition: (systemId, celestialId) => resolvePointPosition(this.context.createWorldPresentationContext(), systemId, celestialId),
|
||||||
const bubble = this.context.getWorld()?.localBubbles.get(bubbleId);
|
|
||||||
return bubble ? resolveBubblePosition(this.context.createWorldPresentationContext(), bubble) : undefined;
|
|
||||||
},
|
|
||||||
resolvePointPosition: (systemId, nodeId) => resolvePointPosition(this.context.createWorldPresentationContext(), systemId, nodeId),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,9 +94,10 @@ export class ViewerNavigationController {
|
|||||||
cameraTargetShipId: this.context.getCameraTargetShipId(),
|
cameraTargetShipId: this.context.getCameraTargetShipId(),
|
||||||
currentDistance: this.context.getCurrentDistance(),
|
currentDistance: this.context.getCurrentDistance(),
|
||||||
selectedItems: this.context.getSelectedItems(),
|
selectedItems: this.context.getSelectedItems(),
|
||||||
galaxyFocus: this.context.galaxyFocus,
|
galaxyAnchor: this.context.galaxyAnchor,
|
||||||
});
|
});
|
||||||
if (nextActiveSystemId === this.context.getActiveSystemId()) {
|
const previousSystemId = this.context.getActiveSystemId();
|
||||||
|
if (nextActiveSystemId === previousSystemId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,7 +106,7 @@ export class ViewerNavigationController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.context.setActiveSystemId(nextActiveSystemId);
|
this.context.setActiveSystemId(nextActiveSystemId);
|
||||||
this.updateSystemDetailVisibility();
|
this.context.onActiveSystemChanged(previousSystemId, nextActiveSystemId);
|
||||||
this.context.updatePanels();
|
this.context.updatePanels();
|
||||||
this.context.updateGamePanel("Live");
|
this.context.updateGamePanel("Live");
|
||||||
}
|
}
|
||||||
@@ -123,16 +118,16 @@ export class ViewerNavigationController {
|
|||||||
cameraTargetShipId: this.context.getCameraTargetShipId(),
|
cameraTargetShipId: this.context.getCameraTargetShipId(),
|
||||||
shipVisuals: this.context.shipVisuals,
|
shipVisuals: this.context.shipVisuals,
|
||||||
currentDistance: this.context.getCurrentDistance(),
|
currentDistance: this.context.getCurrentDistance(),
|
||||||
camera: this.context.camera,
|
camera: this.context.systemCamera,
|
||||||
followCameraPosition: this.context.followCameraPosition,
|
followCameraPosition: this.context.followCameraPosition,
|
||||||
followCameraFocus: this.context.followCameraFocus,
|
followCameraFocus: this.context.followCameraFocus,
|
||||||
followCameraDirection: this.context.followCameraDirection,
|
followCameraDirection: this.context.followCameraDirection,
|
||||||
followCameraDesiredDirection: this.context.followCameraDesiredDirection,
|
followCameraDesiredDirection: this.context.followCameraDesiredDirection,
|
||||||
followCameraOffset: this.context.followCameraOffset,
|
followCameraOffset: this.context.followCameraOffset,
|
||||||
systemFocusLocal: this.context.systemFocusLocal,
|
systemAnchor: this.context.systemAnchor,
|
||||||
delta,
|
delta,
|
||||||
getAnimatedShipLocalPosition,
|
getAnimatedShipLocalPosition,
|
||||||
toDisplayLocalPosition: (localPosition, systemId) => this.toDisplayLocalPosition(localPosition, systemId),
|
toDisplayLocalPosition: (localPosition) => toDisplayLocalPosition(localPosition),
|
||||||
resolveShipHeading: (visual, worldPosition) => resolveShipHeading(visual, worldPosition, this.context.getOrbitYaw()),
|
resolveShipHeading: (visual, worldPosition) => resolveShipHeading(visual, worldPosition, this.context.getOrbitYaw()),
|
||||||
});
|
});
|
||||||
this.context.setCameraMode(nextState.cameraMode);
|
this.context.setCameraMode(nextState.cameraMode);
|
||||||
@@ -150,19 +145,16 @@ export class ViewerNavigationController {
|
|||||||
this.context.setCameraTargetShipId(nextState.cameraTargetShipId);
|
this.context.setCameraTargetShipId(nextState.cameraTargetShipId);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateSystemDetailVisibility() {
|
|
||||||
updateSystemDetailVisibility(this.context.systemVisuals, this.context.getActiveSystemId(), this.context.getZoomLevel());
|
|
||||||
}
|
|
||||||
|
|
||||||
getCameraFocusWorldPosition() {
|
getCameraFocusWorldPosition() {
|
||||||
return getCameraFocusWorldPosition({
|
return getCameraFocusWorldPosition({
|
||||||
world: this.context.getWorld(),
|
galaxyAnchor: this.context.galaxyAnchor,
|
||||||
activeSystemId: this.context.getActiveSystemId(),
|
|
||||||
galaxyFocus: this.context.galaxyFocus,
|
|
||||||
systemFocusLocal: this.context.systemFocusLocal,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getSystemCameraFocus() {
|
||||||
|
return getSystemCameraFocus(this.context.systemAnchor);
|
||||||
|
}
|
||||||
|
|
||||||
seedSystemFocusLocal(systemId: string) {
|
seedSystemFocusLocal(systemId: string) {
|
||||||
seedSystemFocusLocal({
|
seedSystemFocusLocal({
|
||||||
world: this.context.getWorld(),
|
world: this.context.getWorld(),
|
||||||
@@ -170,26 +162,21 @@ export class ViewerNavigationController {
|
|||||||
cameraMode: this.context.getCameraMode(),
|
cameraMode: this.context.getCameraMode(),
|
||||||
cameraTargetShipId: this.context.getCameraTargetShipId(),
|
cameraTargetShipId: this.context.getCameraTargetShipId(),
|
||||||
selectedItems: this.context.getSelectedItems(),
|
selectedItems: this.context.getSelectedItems(),
|
||||||
systemFocusLocal: this.context.systemFocusLocal,
|
systemAnchor: this.context.systemAnchor,
|
||||||
worldTimeSyncMs: this.context.getWorldTimeSyncMs(),
|
worldTimeSyncMs: this.context.getWorldTimeSyncMs(),
|
||||||
nodeVisuals: this.context.nodeVisuals,
|
nodeVisuals: this.context.nodeVisuals,
|
||||||
planetVisuals: this.context.planetVisuals,
|
planetVisuals: this.context.planetVisuals,
|
||||||
computeNodeLocalPosition: (node, timeSeconds) => computeNodeLocalPosition(this.context.createWorldPresentationContext(), node, timeSeconds),
|
computeNodeLocalPosition: (node, timeSeconds) => computeNodeLocalPosition(this.context.createWorldPresentationContext(), node, timeSeconds),
|
||||||
resolveBubblePosition: (bubbleId) => {
|
resolvePointPosition: (systemIdValue, celestialId) => resolvePointPosition(this.context.createWorldPresentationContext(), systemIdValue, celestialId),
|
||||||
const bubble = this.context.getWorld()?.localBubbles.get(bubbleId);
|
|
||||||
return bubble ? resolveBubblePosition(this.context.createWorldPresentationContext(), bubble) : undefined;
|
|
||||||
},
|
|
||||||
resolvePointPosition: (systemIdValue, nodeId) => resolvePointPosition(this.context.createWorldPresentationContext(), systemIdValue, nodeId),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
toDisplayLocalPosition(localPosition: THREE.Vector3, systemId?: string) {
|
toDisplayLocalPosition(localPosition: THREE.Vector3) {
|
||||||
return toDisplayLocalPosition({
|
return toDisplayLocalPosition(localPosition);
|
||||||
world: this.context.getWorld(),
|
}
|
||||||
systemId,
|
|
||||||
activeSystemId: this.context.getActiveSystemId(),
|
/** Returns a display position for the system camera, derived from a raw local position in km. */
|
||||||
localPosition,
|
toSystemDisplayPosition(localPosition: THREE.Vector3) {
|
||||||
systemFocusLocal: this.context.systemFocusLocal,
|
return toDisplayLocalPosition(localPosition);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { StationSnapshot } from "./contractsInfrastructure";
|
|||||||
import type { FactionSnapshot } from "./contractsFactions";
|
import type { FactionSnapshot } from "./contractsFactions";
|
||||||
import { inventoryAmount } from "./viewerMath";
|
import { inventoryAmount } from "./viewerMath";
|
||||||
import { describeShipCurrentAction, describeShipLocation, describeShipObjective, describeShipState } from "./viewerSelection";
|
import { describeShipCurrentAction, describeShipLocation, describeShipObjective, describeShipState } from "./viewerSelection";
|
||||||
import type { CameraMode, Selectable, WorldState, ZoomLevel } from "./viewerTypes";
|
import type { CameraMode, Selectable, WorldState, PovLevel } from "./viewerTypes";
|
||||||
|
|
||||||
function renderFactionCard(faction: FactionSnapshot): string {
|
function renderFactionCard(faction: FactionSnapshot): string {
|
||||||
const state = faction.goapState;
|
const state = faction.goapState;
|
||||||
@@ -71,14 +71,14 @@ export function renderOpsStrip(
|
|||||||
selectedItems: Selectable[],
|
selectedItems: Selectable[],
|
||||||
cameraMode: CameraMode,
|
cameraMode: CameraMode,
|
||||||
cameraTargetShipId?: string,
|
cameraTargetShipId?: string,
|
||||||
zoomLevel?: ZoomLevel,
|
povLevel?: PovLevel,
|
||||||
activeSystemId?: string,
|
activeSystemId?: string,
|
||||||
) {
|
) {
|
||||||
if (!world) {
|
if (!world) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
const isSystemFiltered = zoomLevel !== "universe" && activeSystemId != null;
|
const isSystemFiltered = povLevel !== "galaxy" && activeSystemId != null;
|
||||||
|
|
||||||
const factionCards = [...world.factions.values()]
|
const factionCards = [...world.factions.values()]
|
||||||
.sort((a, b) => a.label.localeCompare(b.label))
|
.sort((a, b) => a.label.localeCompare(b.label))
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
formatSystemDistance,
|
formatSystemDistance,
|
||||||
inventoryAmount,
|
inventoryAmount,
|
||||||
} from "./viewerMath";
|
} from "./viewerMath";
|
||||||
import { describeOrbitalParent, describeSelectable, describeShipCurrentAction, describeShipObjective, describeShipState, describeSpatialNodePathWithinSystem, getSelectionGroup, renderSystemDetails } from "./viewerSelection";
|
import { describeCelestialPathWithinSystem, describeOrbitalParent, describeSelectable, describeShipCurrentAction, describeShipObjective, describeShipState, getSelectionGroup, renderSystemDetails } from "./viewerSelection";
|
||||||
import type {
|
import type {
|
||||||
CameraMode,
|
CameraMode,
|
||||||
HistoryWindowState,
|
HistoryWindowState,
|
||||||
@@ -20,7 +20,7 @@ import type {
|
|||||||
interface DetailPanelParams {
|
interface DetailPanelParams {
|
||||||
world: WorldState;
|
world: WorldState;
|
||||||
selectedItems: Selectable[];
|
selectedItems: Selectable[];
|
||||||
zoomLevel: string;
|
povLevel: string;
|
||||||
cameraMode: CameraMode;
|
cameraMode: CameraMode;
|
||||||
cameraTargetShipId?: string;
|
cameraTargetShipId?: string;
|
||||||
worldLabel: string;
|
worldLabel: string;
|
||||||
@@ -156,7 +156,7 @@ export function updateDetailPanel(
|
|||||||
const {
|
const {
|
||||||
world,
|
world,
|
||||||
selectedItems,
|
selectedItems,
|
||||||
zoomLevel,
|
povLevel,
|
||||||
cameraMode,
|
cameraMode,
|
||||||
cameraTargetShipId,
|
cameraTargetShipId,
|
||||||
worldLabel,
|
worldLabel,
|
||||||
@@ -166,10 +166,9 @@ export function updateDetailPanel(
|
|||||||
if (selectedItems.length === 0) {
|
if (selectedItems.length === 0) {
|
||||||
detailTitleEl.textContent = worldLabel;
|
detailTitleEl.textContent = worldLabel;
|
||||||
detailBodyEl.innerHTML = `
|
detailBodyEl.innerHTML = `
|
||||||
Zoom ${zoomLevel}<br>
|
Zoom ${povLevel}<br>
|
||||||
Systems ${world.systems.size}<br>
|
Systems ${world.systems.size}<br>
|
||||||
Spatial nodes ${world.spatialNodes.size}<br>
|
Celestials ${world.celestials.size}<br>
|
||||||
Bubbles ${world.localBubbles.size}<br>
|
|
||||||
Stations ${world.stations.size}<br>
|
Stations ${world.stations.size}<br>
|
||||||
Claims ${world.claims.size}<br>
|
Claims ${world.claims.size}<br>
|
||||||
Construction ${world.constructionSites.size}<br>
|
Construction ${world.constructionSites.size}<br>
|
||||||
@@ -294,34 +293,17 @@ export function updateDetailPanel(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selected.kind === "spatial-node") {
|
if (selected.kind === "celestial") {
|
||||||
const node = world.spatialNodes.get(selected.id);
|
const celestial = world.celestials.get(selected.id);
|
||||||
if (!node) {
|
if (!celestial) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const bubble = world.localBubbles.get(node.bubbleId);
|
detailTitleEl.textContent = `${celestial.kind} celestial`;
|
||||||
detailTitleEl.textContent = `${node.kind} node`;
|
|
||||||
detailBodyEl.innerHTML = `
|
detailBodyEl.innerHTML = `
|
||||||
<p>${node.systemId}</p>
|
<p>${celestial.systemId}</p>
|
||||||
<p>Bubble ${node.bubbleId}</p>
|
<p>Parent ${celestial.parentNodeId ?? "none"}<br>Orbit ref ${celestial.orbitReferenceId ?? "none"}</p>
|
||||||
<p>Parent ${node.parentNodeId ?? "none"}<br>Orbit ref ${node.orbitReferenceId ?? "none"}</p>
|
<p>Occupying structure ${celestial.occupyingStructureId ?? "none"}</p>
|
||||||
<p>Occupying structure ${node.occupyingStructureId ?? "none"}</p>
|
<p>Local space radius ${celestial.localSpaceRadius.toFixed(0)} km</p>
|
||||||
<p>Bubble occupants ${bubble ? bubble.occupantShipIds.length + bubble.occupantStationIds.length : 0}</p>
|
|
||||||
`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selected.kind === "bubble") {
|
|
||||||
const bubble = world.localBubbles.get(selected.id);
|
|
||||||
if (!bubble) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
detailTitleEl.textContent = `Bubble ${bubble.id}`;
|
|
||||||
detailBodyEl.innerHTML = `
|
|
||||||
<p>${bubble.systemId}</p>
|
|
||||||
<p>Anchor node ${bubble.nodeId}<br>Radius ${formatLocalDistance(bubble.radius)}</p>
|
|
||||||
<p>Ships ${bubble.occupantShipIds.length}<br>Stations ${bubble.occupantStationIds.length}</p>
|
|
||||||
<p>Claims ${bubble.occupantClaimIds.length}<br>Construction sites ${bubble.occupantConstructionSiteIds.length}</p>
|
|
||||||
`;
|
`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -334,7 +316,7 @@ export function updateDetailPanel(
|
|||||||
detailTitleEl.textContent = `Claim ${claim.id}`;
|
detailTitleEl.textContent = `Claim ${claim.id}`;
|
||||||
detailBodyEl.innerHTML = `
|
detailBodyEl.innerHTML = `
|
||||||
<p>${claim.systemId}</p>
|
<p>${claim.systemId}</p>
|
||||||
<p>Node ${claim.nodeId}<br>Bubble ${claim.bubbleId}</p>
|
<p>Celestial ${claim.celestialId}</p>
|
||||||
<p>State ${claim.state}<br>Health ${claim.health.toFixed(0)}</p>
|
<p>State ${claim.state}<br>Health ${claim.health.toFixed(0)}</p>
|
||||||
<p>Activates ${new Date(claim.activatesAtUtc).toLocaleTimeString()}</p>
|
<p>Activates ${new Date(claim.activatesAtUtc).toLocaleTimeString()}</p>
|
||||||
`;
|
`;
|
||||||
@@ -350,7 +332,7 @@ export function updateDetailPanel(
|
|||||||
detailTitleEl.textContent = `Construction ${site.id}`;
|
detailTitleEl.textContent = `Construction ${site.id}`;
|
||||||
detailBodyEl.innerHTML = `
|
detailBodyEl.innerHTML = `
|
||||||
<p>${site.systemId}</p>
|
<p>${site.systemId}</p>
|
||||||
<p>Node ${site.nodeId}<br>Bubble ${site.bubbleId}</p>
|
<p>Celestial ${site.celestialId}</p>
|
||||||
<p>${site.targetKind} ${site.targetDefinitionId}</p>
|
<p>${site.targetKind} ${site.targetDefinitionId}</p>
|
||||||
<p>State ${site.state}<br>Progress ${(site.progress * 100).toFixed(0)}%</p>
|
<p>State ${site.state}<br>Progress ${(site.progress * 100).toFixed(0)}%</p>
|
||||||
<p>Orders ${orderCount}<br>Assigned constructors ${site.assignedConstructorShipIds.length}</p>
|
<p>Orders ${orderCount}<br>Assigned constructors ${site.assignedConstructorShipIds.length}</p>
|
||||||
@@ -445,8 +427,8 @@ export function describeSelectionParent(
|
|||||||
return "unknown";
|
return "unknown";
|
||||||
}
|
}
|
||||||
|
|
||||||
return station.anchorNodeId
|
return station.celestialId
|
||||||
? describeSpatialNodePathWithinSystem(world, station.systemId, station.anchorNodeId) ?? `${station.systemId} network`
|
? describeCelestialPathWithinSystem(world, station.systemId, station.celestialId) ?? `${station.systemId} network`
|
||||||
: "unknown";
|
: "unknown";
|
||||||
}
|
}
|
||||||
if (selection.kind === "node") {
|
if (selection.kind === "node") {
|
||||||
@@ -454,18 +436,15 @@ export function describeSelectionParent(
|
|||||||
const visual = node ? nodeVisuals.get(selection.id) : undefined;
|
const visual = node ? nodeVisuals.get(selection.id) : undefined;
|
||||||
return describeOrbitalParent(world, node?.systemId, visual?.anchor);
|
return describeOrbitalParent(world, node?.systemId, visual?.anchor);
|
||||||
}
|
}
|
||||||
if (selection.kind === "spatial-node") {
|
if (selection.kind === "celestial") {
|
||||||
const node = world.spatialNodes.get(selection.id);
|
const celestial = world.celestials.get(selection.id);
|
||||||
return node?.parentNodeId ?? `${node?.systemId ?? "unknown"} network`;
|
return celestial?.parentNodeId ?? `${celestial?.systemId ?? "unknown"} network`;
|
||||||
}
|
|
||||||
if (selection.kind === "bubble") {
|
|
||||||
return `${world.localBubbles.get(selection.id)?.nodeId ?? "unknown"} node`;
|
|
||||||
}
|
}
|
||||||
if (selection.kind === "claim") {
|
if (selection.kind === "claim") {
|
||||||
return world.claims.get(selection.id)?.nodeId ?? "unknown";
|
return world.claims.get(selection.id)?.celestialId ?? "unknown";
|
||||||
}
|
}
|
||||||
if (selection.kind === "construction-site") {
|
if (selection.kind === "construction-site") {
|
||||||
return world.constructionSites.get(selection.id)?.nodeId ?? "unknown";
|
return world.constructionSites.get(selection.id)?.celestialId ?? "unknown";
|
||||||
}
|
}
|
||||||
|
|
||||||
return "unknown";
|
return "unknown";
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import { ACTIVE_SYSTEM_DETAIL_SCALE, PROJECTED_GALAXY_RADIUS } from "./viewerConstants";
|
import { ACTIVE_SYSTEM_DETAIL_SCALE, PROJECTED_GALAXY_RADIUS } from "./viewerConstants";
|
||||||
import { computeMoonLocalPosition, computePlanetLocalPosition, currentWorldTimeSeconds, scaleLocalVector } from "./viewerMath";
|
import { computeMoonLocalPosition, computePlanetLocalPosition, currentWorldTimeSeconds, scaleLocalVector } from "./viewerMath";
|
||||||
import { rawObject } from "./viewerScenePrimitives";
|
import type { PlanetVisual, ShipVisual, SystemVisual, WorldState } from "./viewerTypes";
|
||||||
import type { PlanetVisual, ShipVisual, SystemSummaryVisual, SystemVisual, WorldState } from "./viewerTypes";
|
|
||||||
|
|
||||||
export function getAnimatedShipLocalPosition(visual: ShipVisual, now = performance.now()) {
|
export function getAnimatedShipLocalPosition(visual: ShipVisual, now = performance.now()) {
|
||||||
const elapsedMs = now - visual.receivedAtMs;
|
const elapsedMs = now - visual.receivedAtMs;
|
||||||
@@ -26,23 +25,17 @@ export function resolveShipHeading(visual: ShipVisual, worldPosition: THREE.Vect
|
|||||||
export function updatePlanetPresentation(
|
export function updatePlanetPresentation(
|
||||||
world: WorldState | undefined,
|
world: WorldState | undefined,
|
||||||
worldTimeSyncMs: number,
|
worldTimeSyncMs: number,
|
||||||
activeSystemId: string | undefined,
|
|
||||||
systemFocusLocal: THREE.Vector3,
|
|
||||||
planetVisuals: PlanetVisual[],
|
planetVisuals: PlanetVisual[],
|
||||||
) {
|
) {
|
||||||
const nowSeconds = currentWorldTimeSeconds(world, worldTimeSyncMs);
|
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) {
|
for (const visual of planetVisuals) {
|
||||||
const scale = visual.systemId === activeSystemId ? ACTIVE_SYSTEM_DETAIL_SCALE : 1;
|
const position = scaleLocalVector(computePlanetLocalPosition(visual.planet, nowSeconds))
|
||||||
const localPosition = scaleLocalVector(computePlanetLocalPosition(visual.planet, nowSeconds));
|
.multiplyScalar(ACTIVE_SYSTEM_DETAIL_SCALE);
|
||||||
const orbitOffset = visual.systemId === activeSystemId
|
|
||||||
? systemFocusLocal.clone().multiplyScalar(-scale)
|
|
||||||
: new THREE.Vector3();
|
|
||||||
const position = visual.systemId === activeSystemId
|
|
||||||
? localPosition.clone().sub(systemFocusLocal).multiplyScalar(scale)
|
|
||||||
: localPosition.multiplyScalar(scale);
|
|
||||||
|
|
||||||
visual.orbit.setScaleScalar(scale);
|
visual.orbit.setScaleScalar(ACTIVE_SYSTEM_DETAIL_SCALE);
|
||||||
visual.orbit.setPosition(orbitOffset);
|
visual.orbit.setPosition(new THREE.Vector3(0, 0, 0));
|
||||||
visual.mesh.setPosition(position);
|
visual.mesh.setPosition(position);
|
||||||
visual.icon.setPosition(position);
|
visual.icon.setPosition(position);
|
||||||
if (visual.ring) {
|
if (visual.ring) {
|
||||||
@@ -51,56 +44,45 @@ export function updatePlanetPresentation(
|
|||||||
|
|
||||||
for (const [moonIndex, moon] of visual.moons.entries()) {
|
for (const [moonIndex, moon] of visual.moons.entries()) {
|
||||||
moon.orbit.setPosition(position);
|
moon.orbit.setPosition(position);
|
||||||
moon.orbit.setScaleScalar(scale);
|
moon.orbit.setScaleScalar(ACTIVE_SYSTEM_DETAIL_SCALE);
|
||||||
moon.mesh.setPosition(
|
moon.mesh.setPosition(
|
||||||
position.clone().add(
|
position.clone().add(
|
||||||
scaleLocalVector(computeMoonLocalPosition(visual.planet, moonIndex, nowSeconds, world?.seed ?? 1)).multiplyScalar(scale),
|
scaleLocalVector(computeMoonLocalPosition(visual.planet, moonIndex, nowSeconds, world?.seed ?? 1))
|
||||||
|
.multiplyScalar(ACTIVE_SYSTEM_DETAIL_SCALE),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateSystemSummaryPresentation(
|
|
||||||
systemSummaryVisuals: Map<string, SystemSummaryVisual>,
|
|
||||||
camera: THREE.PerspectiveCamera,
|
|
||||||
activeSystemId?: string,
|
|
||||||
) {
|
|
||||||
const distanceScale = activeSystemId ? 0.05 : 0.085;
|
|
||||||
for (const [systemId, visual] of systemSummaryVisuals.entries()) {
|
|
||||||
const worldPosition = visual.sprite.getWorldPosition(new THREE.Vector3());
|
|
||||||
const distance = camera.position.distanceTo(worldPosition);
|
|
||||||
const minimumScale = activeSystemId && systemId !== activeSystemId ? 1200 : 1400;
|
|
||||||
const scale = Math.max(minimumScale, distance * distanceScale);
|
|
||||||
rawObject(visual.sprite).scale.set(scale, scale * 0.3125, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updateSystemStarPresentation(
|
export function updateSystemStarPresentation(
|
||||||
systemVisuals: Map<string, SystemVisual>,
|
systemVisuals: Map<string, SystemVisual>,
|
||||||
activeSystemId: string | undefined,
|
activeSystemId: string | undefined,
|
||||||
systemFocusLocal: THREE.Vector3,
|
galaxyCamera: THREE.PerspectiveCamera,
|
||||||
camera: THREE.PerspectiveCamera,
|
|
||||||
setShellReticleOpacity: (sprite: SystemVisual["shellReticle"], opacity: number) => void,
|
setShellReticleOpacity: (sprite: SystemVisual["shellReticle"], opacity: number) => void,
|
||||||
) {
|
) {
|
||||||
const activeSystem = activeSystemId ? systemVisuals.get(activeSystemId) : undefined;
|
const activeSystem = activeSystemId ? systemVisuals.get(activeSystemId) : undefined;
|
||||||
|
|
||||||
for (const [systemId, visual] of systemVisuals.entries()) {
|
for (const [systemId, visual] of systemVisuals.entries()) {
|
||||||
visual.root.setPosition(visual.galaxyPosition);
|
// galaxyRoot is always at the galaxy position of this system
|
||||||
|
visual.galaxyRoot.setPosition(visual.galaxyPosition);
|
||||||
visual.shellReticle.setScaleScalar(visual.shellReticleBaseScale);
|
visual.shellReticle.setScaleScalar(visual.shellReticleBaseScale);
|
||||||
|
|
||||||
if (!activeSystem) {
|
if (!activeSystem) {
|
||||||
visual.starCluster.setPosition(new THREE.Vector3(0, 0, 0));
|
// Galaxy view: show star dot, hide shell reticle
|
||||||
visual.icon.setPosition(new THREE.Vector3(0, 0, 0));
|
visual.icon.setPosition(new THREE.Vector3(0, 0, 0));
|
||||||
visual.icon.setVisible(true);
|
visual.icon.setVisible(true);
|
||||||
visual.shellReticle.setPosition(new THREE.Vector3(0, 0, 0));
|
visual.shellReticle.setPosition(new THREE.Vector3(0, 0, 0));
|
||||||
visual.shellReticle.setVisible(false);
|
visual.shellReticle.setVisible(false);
|
||||||
setShellReticleOpacity(visual.shellReticle, 0);
|
setShellReticleOpacity(visual.shellReticle, 0);
|
||||||
|
const dotWorldPos = visual.icon.getWorldPosition(new THREE.Vector3());
|
||||||
|
visual.icon.setScaleScalar(galaxyCamera.position.distanceTo(dotWorldPos) * 0.01);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (systemId !== activeSystemId) {
|
if (systemId !== activeSystemId) {
|
||||||
visual.starCluster.setPosition(new THREE.Vector3(0, 0, 0));
|
// 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.setPosition(new THREE.Vector3(0, 0, 0));
|
||||||
visual.icon.setVisible(false);
|
visual.icon.setVisible(false);
|
||||||
visual.shellReticle.setPosition(new THREE.Vector3(0, 0, 0));
|
visual.shellReticle.setPosition(new THREE.Vector3(0, 0, 0));
|
||||||
@@ -108,20 +90,19 @@ export function updateSystemStarPresentation(
|
|||||||
setShellReticleOpacity(visual.shellReticle, 1);
|
setShellReticleOpacity(visual.shellReticle, 1);
|
||||||
const direction = visual.galaxyPosition.clone().sub(activeSystem.galaxyPosition);
|
const direction = visual.galaxyPosition.clone().sub(activeSystem.galaxyPosition);
|
||||||
if (direction.lengthSq() > 0.0001) {
|
if (direction.lengthSq() > 0.0001) {
|
||||||
visual.root.setPosition(
|
visual.galaxyRoot.setPosition(
|
||||||
activeSystem.galaxyPosition.clone().add(direction.normalize().multiplyScalar(PROJECTED_GALAXY_RADIUS)),
|
activeSystem.galaxyPosition.clone().add(direction.normalize().multiplyScalar(PROJECTED_GALAXY_RADIUS)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const reticleWorldPosition = visual.root.getWorldPosition(new THREE.Vector3());
|
const reticleWorldPosition = visual.galaxyRoot.getWorldPosition(new THREE.Vector3());
|
||||||
const reticleDistance = camera.position.distanceTo(reticleWorldPosition);
|
const reticleDistance = galaxyCamera.position.distanceTo(reticleWorldPosition);
|
||||||
const reticleScale = Math.max(900, reticleDistance * 0.032);
|
const reticleScale = Math.max(900, reticleDistance * 0.032);
|
||||||
visual.shellReticle.setScaleScalar(reticleScale);
|
visual.shellReticle.setScaleScalar(reticleScale);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const offset = systemFocusLocal.clone().multiplyScalar(-ACTIVE_SYSTEM_DETAIL_SCALE);
|
// Active system in galaxy view: show star dot, hide shell reticle
|
||||||
visual.starCluster.setPosition(offset);
|
visual.icon.setPosition(new THREE.Vector3(0, 0, 0));
|
||||||
visual.icon.setPosition(offset);
|
|
||||||
visual.icon.setVisible(true);
|
visual.icon.setVisible(true);
|
||||||
visual.shellReticle.setVisible(false);
|
visual.shellReticle.setVisible(false);
|
||||||
setShellReticleOpacity(visual.shellReticle, 0);
|
setShellReticleOpacity(visual.shellReticle, 0);
|
||||||
|
|||||||
@@ -14,8 +14,11 @@ import type { OrbitLineVisual, Selectable } from "./viewerTypes";
|
|||||||
|
|
||||||
export interface ViewerPresentationContext {
|
export interface ViewerPresentationContext {
|
||||||
renderer: THREE.WebGLRenderer;
|
renderer: THREE.WebGLRenderer;
|
||||||
scene: THREE.Scene;
|
galaxyScene: THREE.Scene;
|
||||||
camera: THREE.PerspectiveCamera;
|
galaxyCamera: THREE.PerspectiveCamera;
|
||||||
|
systemCamera: THREE.PerspectiveCamera;
|
||||||
|
galaxyAnchor: THREE.Vector3;
|
||||||
|
systemAnchor: THREE.Vector3;
|
||||||
ambienceGroup: THREE.Group;
|
ambienceGroup: THREE.Group;
|
||||||
gameSummaryEl: HTMLSpanElement;
|
gameSummaryEl: HTMLSpanElement;
|
||||||
networkSummaryEl: HTMLSpanElement;
|
networkSummaryEl: HTMLSpanElement;
|
||||||
@@ -32,14 +35,11 @@ export interface ViewerPresentationContext {
|
|||||||
getActiveSystemId: () => string | undefined;
|
getActiveSystemId: () => string | undefined;
|
||||||
getCameraMode: () => any;
|
getCameraMode: () => any;
|
||||||
getCameraTargetShipId: () => string | undefined;
|
getCameraTargetShipId: () => string | undefined;
|
||||||
getZoomLevel: () => any;
|
getPovLevel: () => any;
|
||||||
getSelectedItems: () => Selectable[];
|
getSelectedItems: () => Selectable[];
|
||||||
getWorldTimeSyncMs: () => number;
|
getWorldTimeSyncMs: () => number;
|
||||||
getCurrentDistance: () => number;
|
getCurrentDistance: () => number;
|
||||||
systemFocusLocal: THREE.Vector3;
|
|
||||||
planetVisuals: any[];
|
planetVisuals: any[];
|
||||||
systemSummaryVisuals: Map<any, any>;
|
|
||||||
presentationEntries: any[];
|
|
||||||
orbitLines: OrbitLineVisual[];
|
orbitLines: OrbitLineVisual[];
|
||||||
systemVisuals: Map<any, any>;
|
systemVisuals: Map<any, any>;
|
||||||
createWorldPresentationContext: () => any;
|
createWorldPresentationContext: () => any;
|
||||||
@@ -55,43 +55,25 @@ export class ViewerPresentationController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateAmbience(delta: number) {
|
updateAmbience(delta: number) {
|
||||||
this.context.ambienceGroup.position.copy(this.context.camera.position);
|
const activeCamera = this.context.getPovLevel() === "galaxy"
|
||||||
|
? this.context.galaxyCamera
|
||||||
|
: this.context.systemCamera;
|
||||||
|
this.context.ambienceGroup.position.copy(activeCamera.position);
|
||||||
this.context.ambienceGroup.rotation.y += delta * 0.005;
|
this.context.ambienceGroup.rotation.y += delta * 0.005;
|
||||||
this.context.ambienceGroup.rotation.x = Math.sin(performance.now() * 0.00003) * 0.015;
|
this.context.ambienceGroup.rotation.x = Math.sin(performance.now() * 0.00003) * 0.015;
|
||||||
}
|
}
|
||||||
|
|
||||||
applyZoomPresentation() {
|
applyZoomPresentation() {
|
||||||
const activeSystemId = this.context.getActiveSystemId();
|
const activeSystemId = this.context.getActiveSystemId();
|
||||||
const zoomLevel = this.context.getZoomLevel();
|
const povLevel = this.context.getPovLevel();
|
||||||
const isUniverse = zoomLevel === "universe";
|
|
||||||
|
|
||||||
for (const entry of this.context.presentationEntries) {
|
|
||||||
const systemId = entry.systemId;
|
|
||||||
const isActiveDetail = !systemId || systemId === activeSystemId;
|
|
||||||
const detailAlpha = entry.hideDetailInUniverse
|
|
||||||
? (!isUniverse && isActiveDetail ? 1 : 0)
|
|
||||||
: 1;
|
|
||||||
const iconAlpha = entry.hideIconInUniverse
|
|
||||||
? (isUniverse ? 1 : 0)
|
|
||||||
: (isUniverse ? 1 : 0);
|
|
||||||
|
|
||||||
entry.detail.setOpacity(detailAlpha);
|
|
||||||
entry.icon.setOpacity(iconAlpha);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Orbit lines: only show for active system in system/local zoom
|
||||||
for (const orbitLine of this.context.orbitLines) {
|
for (const orbitLine of this.context.orbitLines) {
|
||||||
const alpha = this.resolveOrbitLineOpacity(orbitLine, zoomLevel, activeSystemId);
|
const alpha = this.resolveOrbitLineOpacity(orbitLine, povLevel, activeSystemId);
|
||||||
orbitLine.line.setOpacity(alpha);
|
orbitLine.line.setOpacity(alpha);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [systemId, summaryVisual] of this.context.systemSummaryVisuals.entries()) {
|
this.context.galaxyScene.fog = new THREE.FogExp2(0x040912, 0.000035);
|
||||||
const summaryOpacity = isUniverse
|
|
||||||
? 0.96
|
|
||||||
: 0;
|
|
||||||
summaryVisual.sprite.setOpacity(summaryOpacity);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.context.scene.fog = new THREE.FogExp2(0x040912, 0.000035);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateNetworkPanel() {
|
updateNetworkPanel() {
|
||||||
@@ -117,14 +99,12 @@ export class ViewerPresentationController {
|
|||||||
updatePlanetPresentation(
|
updatePlanetPresentation(
|
||||||
world,
|
world,
|
||||||
this.context.getWorldTimeSyncMs(),
|
this.context.getWorldTimeSyncMs(),
|
||||||
this.context.getActiveSystemId(),
|
|
||||||
this.context.systemFocusLocal,
|
|
||||||
this.context.planetVisuals,
|
this.context.planetVisuals,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateSystemSummaries() {
|
updateSystemSummaries() {
|
||||||
updateSystemSummaries(this.context.getWorld(), this.context.systemSummaryVisuals);
|
updateSystemSummaries(this.context.getWorld(), new Map());
|
||||||
}
|
}
|
||||||
|
|
||||||
renderRecentEvents(entityKind: string, entityId: string) {
|
renderRecentEvents(entityKind: string, entityId: string) {
|
||||||
@@ -138,9 +118,11 @@ export class ViewerPresentationController {
|
|||||||
world: this.context.getWorld(),
|
world: this.context.getWorld(),
|
||||||
activeSystemId: this.context.getActiveSystemId(),
|
activeSystemId: this.context.getActiveSystemId(),
|
||||||
cameraMode: this.context.getCameraMode(),
|
cameraMode: this.context.getCameraMode(),
|
||||||
zoomLevel: this.context.getZoomLevel(),
|
povLevel: this.context.getPovLevel(),
|
||||||
selectedItems: this.context.getSelectedItems(),
|
selectedItems: this.context.getSelectedItems(),
|
||||||
mode,
|
mode,
|
||||||
|
galaxyAnchor: this.context.galaxyAnchor,
|
||||||
|
systemAnchor: this.context.systemAnchor,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,14 +148,14 @@ export class ViewerPresentationController {
|
|||||||
return new THREE.Vector2(clientX - bounds.left, clientY - bounds.top);
|
return new THREE.Vector2(clientX - bounds.left, clientY - bounds.top);
|
||||||
}
|
}
|
||||||
|
|
||||||
private resolveOrbitLineOpacity(orbitLine: OrbitLineVisual, zoomLevel: "local" | "system" | "universe", activeSystemId?: string) {
|
private resolveOrbitLineOpacity(orbitLine: OrbitLineVisual, povLevel: "local" | "system" | "galaxy", activeSystemId?: string) {
|
||||||
if (zoomLevel === "universe" || !activeSystemId || orbitLine.systemId !== activeSystemId) {
|
if (povLevel === "galaxy" || !activeSystemId || orbitLine.systemId !== activeSystemId) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const selected = this.context.getSelectedItems();
|
const selected = this.context.getSelectedItems();
|
||||||
const selectedItem = selected.length === 1 ? selected[0] : undefined;
|
const selectedItem = selected.length === 1 ? selected[0] : undefined;
|
||||||
const baseAlpha = zoomLevel === "local" ? 0.55 : 0.9;
|
const baseAlpha = povLevel === "local" ? 0.55 : 0.9;
|
||||||
|
|
||||||
if (selectedItem?.kind === "planet" && selectedItem.systemId === activeSystemId) {
|
if (selectedItem?.kind === "planet" && selectedItem.systemId === activeSystemId) {
|
||||||
return orbitLine.kind === "moon" && orbitLine.planetIndex === selectedItem.planetIndex
|
return orbitLine.kind === "moon" && orbitLine.planetIndex === selectedItem.planetIndex
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import { classifyZoomLevel } from "./viewerMath";
|
import { classifyPovLevel } from "./viewerMath";
|
||||||
import type { PerformanceStats } from "./viewerTypes";
|
import type { PovLevel, PerformanceStats } from "./viewerTypes";
|
||||||
|
|
||||||
export interface RenderFrameParams {
|
export interface RenderFrameParams {
|
||||||
clock: THREE.Clock;
|
clock: THREE.Clock;
|
||||||
renderer: THREE.WebGLRenderer;
|
renderer: THREE.WebGLRenderer;
|
||||||
scene: THREE.Scene;
|
universeScene: THREE.Scene;
|
||||||
camera: THREE.PerspectiveCamera;
|
galaxyScene: THREE.Scene;
|
||||||
|
galaxyCamera: THREE.PerspectiveCamera;
|
||||||
|
systemScene: THREE.Scene;
|
||||||
|
systemCamera: THREE.PerspectiveCamera;
|
||||||
|
localScene: THREE.Scene;
|
||||||
|
localCamera: THREE.PerspectiveCamera;
|
||||||
|
getPovLevel: () => PovLevel;
|
||||||
updateCamera: (delta: number) => void;
|
updateCamera: (delta: number) => void;
|
||||||
updateAmbience: (delta: number) => void;
|
updateAmbience: (delta: number) => void;
|
||||||
updatePlanetPresentation: () => void;
|
updatePlanetPresentation: () => void;
|
||||||
@@ -19,7 +25,9 @@ export interface RenderFrameParams {
|
|||||||
|
|
||||||
export interface ResizeParams {
|
export interface ResizeParams {
|
||||||
renderer: THREE.WebGLRenderer;
|
renderer: THREE.WebGLRenderer;
|
||||||
camera: THREE.PerspectiveCamera;
|
galaxyCamera: THREE.PerspectiveCamera;
|
||||||
|
systemCamera: THREE.PerspectiveCamera;
|
||||||
|
localCamera: THREE.PerspectiveCamera;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CameraStepParams {
|
export interface CameraStepParams {
|
||||||
@@ -38,7 +46,26 @@ export function renderFrame(params: RenderFrameParams) {
|
|||||||
params.updateShipPresentation();
|
params.updateShipPresentation();
|
||||||
params.updateNetworkPanel();
|
params.updateNetworkPanel();
|
||||||
params.applyZoomPresentation();
|
params.applyZoomPresentation();
|
||||||
params.renderer.render(params.scene, params.camera);
|
|
||||||
|
const povLevel = params.getPovLevel();
|
||||||
|
const activeCamera = povLevel === "galaxy" ? params.galaxyCamera : params.systemCamera;
|
||||||
|
params.renderer.autoClear = false;
|
||||||
|
params.renderer.clear();
|
||||||
|
// Universe backdrop — always first, rendered with the active camera so it aligns with the foreground
|
||||||
|
params.renderer.render(params.universeScene, activeCamera);
|
||||||
|
params.renderer.clearDepth();
|
||||||
|
if (povLevel === "galaxy") {
|
||||||
|
// Galaxy map on top of universe backdrop
|
||||||
|
params.renderer.render(params.galaxyScene, params.galaxyCamera);
|
||||||
|
} else if (povLevel === "system") {
|
||||||
|
params.renderer.render(params.systemScene, params.systemCamera);
|
||||||
|
} else {
|
||||||
|
// local: system as mid-ground backdrop, then local on top
|
||||||
|
params.renderer.render(params.systemScene, params.systemCamera);
|
||||||
|
params.renderer.clearDepth();
|
||||||
|
params.renderer.render(params.localScene, params.localCamera);
|
||||||
|
}
|
||||||
|
|
||||||
params.recordPerformanceStats(performance.now() - frameStartedAtMs);
|
params.recordPerformanceStats(performance.now() - frameStartedAtMs);
|
||||||
params.updatePerformancePanel();
|
params.updatePerformancePanel();
|
||||||
}
|
}
|
||||||
@@ -46,14 +73,16 @@ export function renderFrame(params: RenderFrameParams) {
|
|||||||
export function resizeViewer(params: ResizeParams) {
|
export function resizeViewer(params: ResizeParams) {
|
||||||
const width = window.innerWidth;
|
const width = window.innerWidth;
|
||||||
const height = window.innerHeight;
|
const height = window.innerHeight;
|
||||||
params.camera.aspect = width / height;
|
for (const camera of [params.galaxyCamera, params.systemCamera, params.localCamera]) {
|
||||||
params.camera.updateProjectionMatrix();
|
camera.aspect = width / height;
|
||||||
|
camera.updateProjectionMatrix();
|
||||||
|
}
|
||||||
params.renderer.setSize(width, height);
|
params.renderer.setSize(width, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function stepCamera(params: CameraStepParams) {
|
export function stepCamera(params: CameraStepParams) {
|
||||||
const currentDistance = THREE.MathUtils.damp(params.currentDistance, params.desiredDistance, 7.5, params.delta);
|
const currentDistance = THREE.MathUtils.damp(params.currentDistance, params.desiredDistance, 7.5, params.delta);
|
||||||
const zoomLevel = classifyZoomLevel(currentDistance);
|
const povLevel = classifyPovLevel(currentDistance);
|
||||||
const orbitPitch = THREE.MathUtils.clamp(params.orbitPitch, 0.18, 1.3);
|
const orbitPitch = THREE.MathUtils.clamp(params.orbitPitch, 0.18, 1.3);
|
||||||
return { currentDistance, zoomLevel, orbitPitch };
|
return { currentDistance, povLevel, orbitPitch };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export function shipPresentationColor(ship: ShipSnapshot) {
|
|||||||
return shipColor(ship.kind);
|
return shipColor(ship.kind);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function spatialNodeColor(kind: string) {
|
export function celestialColor(kind: string) {
|
||||||
if (kind.includes("lagrange")) {
|
if (kind.includes("lagrange")) {
|
||||||
return "#7fe8ff";
|
return "#7fe8ff";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,55 +1,49 @@
|
|||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import {
|
import {
|
||||||
|
applyCelestialDeltas as applyCelestialDeltaUpdates,
|
||||||
applyClaimDeltas as applyClaimDeltaUpdates,
|
applyClaimDeltas as applyClaimDeltaUpdates,
|
||||||
applyConstructionSiteDeltas as applyConstructionSiteDeltaUpdates,
|
applyConstructionSiteDeltas as applyConstructionSiteDeltaUpdates,
|
||||||
applyLocalBubbleDeltas as applyLocalBubbleDeltaUpdates,
|
|
||||||
applyNodeDeltas as applyNodeDeltaUpdates,
|
applyNodeDeltas as applyNodeDeltaUpdates,
|
||||||
applyShipDeltas as applyShipDeltaUpdates,
|
applyShipDeltas as applyShipDeltaUpdates,
|
||||||
applySpatialNodeDeltas as applySpatialNodeDeltaUpdates,
|
|
||||||
applyStationDeltas as applyStationDeltaUpdates,
|
applyStationDeltas as applyStationDeltaUpdates,
|
||||||
rebuildSystems as rebuildSystemScene,
|
rebuildSystems as rebuildSystemScene,
|
||||||
|
syncCelestials as syncCelestialScene,
|
||||||
syncClaims as syncClaimScene,
|
syncClaims as syncClaimScene,
|
||||||
syncConstructionSites as syncConstructionSiteScene,
|
syncConstructionSites as syncConstructionSiteScene,
|
||||||
syncLocalBubbles as syncBubbleScene,
|
|
||||||
syncNodes as syncNodeScene,
|
syncNodes as syncNodeScene,
|
||||||
syncShips as syncShipScene,
|
syncShips as syncShipScene,
|
||||||
syncSpatialNodes as syncSpatialNodeScene,
|
|
||||||
syncStations as syncStationScene,
|
syncStations as syncStationScene,
|
||||||
} from "./viewerSceneSync";
|
} from "./viewerSceneSync";
|
||||||
import {
|
import {
|
||||||
deriveNodeOrbital,
|
deriveNodeOrbital,
|
||||||
deriveOrbitalFromLocalPosition,
|
deriveOrbitalFromLocalPosition,
|
||||||
resolveBubblePosition,
|
|
||||||
resolveOrbitalAnchor,
|
resolveOrbitalAnchor,
|
||||||
resolvePointPosition,
|
resolvePointPosition,
|
||||||
setBubbleVisualState,
|
|
||||||
} from "./viewerWorldPresentation";
|
} from "./viewerWorldPresentation";
|
||||||
import {
|
import {
|
||||||
createCirclePoints,
|
createCirclePoints,
|
||||||
shipLength,
|
shipLength,
|
||||||
shipPresentationColor,
|
shipPresentationColor,
|
||||||
shipSize,
|
shipSize,
|
||||||
spatialNodeColor,
|
celestialColor,
|
||||||
} from "./viewerSceneAppearance";
|
} from "./viewerSceneAppearance";
|
||||||
import type {
|
import type {
|
||||||
|
CelestialDelta,
|
||||||
|
CelestialSnapshot,
|
||||||
ClaimDelta,
|
ClaimDelta,
|
||||||
ClaimSnapshot,
|
ClaimSnapshot,
|
||||||
ConstructionSiteDelta,
|
ConstructionSiteDelta,
|
||||||
ConstructionSiteSnapshot,
|
ConstructionSiteSnapshot,
|
||||||
LocalBubbleDelta,
|
|
||||||
LocalBubbleSnapshot,
|
|
||||||
ResourceNodeDelta,
|
ResourceNodeDelta,
|
||||||
ResourceNodeSnapshot,
|
ResourceNodeSnapshot,
|
||||||
ShipDelta,
|
ShipDelta,
|
||||||
ShipSnapshot,
|
ShipSnapshot,
|
||||||
SpatialNodeDelta,
|
|
||||||
SpatialNodeSnapshot,
|
|
||||||
StationDelta,
|
StationDelta,
|
||||||
StationSnapshot,
|
StationSnapshot,
|
||||||
SystemSnapshot,
|
SystemSnapshot,
|
||||||
} from "./contracts";
|
} from "./contracts";
|
||||||
import type { OrbitLineVisual, OrbitalAnchor } from "./viewerTypes";
|
import type { OrbitLineVisual, OrbitalAnchor, Selectable } from "./viewerTypes";
|
||||||
import type { SceneNode } from "./viewerScenePrimitives";
|
import { rawObject } from "./viewerScenePrimitives";
|
||||||
|
|
||||||
export interface ViewerSceneDataContext {
|
export interface ViewerSceneDataContext {
|
||||||
documentRef: Document;
|
documentRef: Document;
|
||||||
@@ -58,102 +52,162 @@ export interface ViewerSceneDataContext {
|
|||||||
getWorldSeed: () => number;
|
getWorldSeed: () => number;
|
||||||
getWorldTimeSyncMs: () => number;
|
getWorldTimeSyncMs: () => number;
|
||||||
getWorldPresentationContext: () => any;
|
getWorldPresentationContext: () => any;
|
||||||
systemGroup: THREE.Group;
|
getActiveSystemId: () => string | undefined;
|
||||||
spatialNodeGroup: THREE.Group;
|
galaxySystemGroup: THREE.Group;
|
||||||
bubbleGroup: THREE.Group;
|
systemScene: THREE.Scene;
|
||||||
|
celestialGroup: THREE.Group;
|
||||||
nodeGroup: THREE.Group;
|
nodeGroup: THREE.Group;
|
||||||
stationGroup: THREE.Group;
|
stationGroup: THREE.Group;
|
||||||
claimGroup: THREE.Group;
|
claimGroup: THREE.Group;
|
||||||
constructionSiteGroup: THREE.Group;
|
constructionSiteGroup: THREE.Group;
|
||||||
shipGroup: THREE.Group;
|
shipGroup: THREE.Group;
|
||||||
selectableTargets: Map<any, any>;
|
galaxySelectableTargets: Map<THREE.Object3D, Selectable>;
|
||||||
presentationEntries: any[];
|
systemSelectableTargets: Map<THREE.Object3D, Selectable>;
|
||||||
systemVisuals: Map<any, any>;
|
systemVisuals: Map<any, any>;
|
||||||
systemSummaryVisuals: Map<any, any>;
|
|
||||||
planetVisuals: any[];
|
planetVisuals: any[];
|
||||||
orbitLines: OrbitLineVisual[];
|
orbitLines: OrbitLineVisual[];
|
||||||
spatialNodeVisuals: Map<any, any>;
|
celestialVisuals: Map<any, any>;
|
||||||
bubbleVisuals: Map<any, any>;
|
|
||||||
nodeVisuals: Map<any, any>;
|
nodeVisuals: Map<any, any>;
|
||||||
stationVisuals: Map<any, any>;
|
stationVisuals: Map<any, any>;
|
||||||
claimVisuals: Map<any, any>;
|
claimVisuals: Map<any, any>;
|
||||||
constructionSiteVisuals: Map<any, any>;
|
constructionSiteVisuals: Map<any, any>;
|
||||||
shipVisuals: Map<any, any>;
|
shipVisuals: Map<any, any>;
|
||||||
registerPresentation: (detail: SceneNode, icon: SceneNode, hideDetailInUniverse: boolean, hideIconInUniverse?: boolean, systemId?: string) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ViewerSceneDataController {
|
export class ViewerSceneDataController {
|
||||||
|
private activeSystemRootInScene: THREE.Object3D | undefined;
|
||||||
|
|
||||||
constructor(private readonly context: ViewerSceneDataContext) {}
|
constructor(private readonly context: ViewerSceneDataContext) {}
|
||||||
|
|
||||||
rebuildSystems(systems: SystemSnapshot[]) {
|
rebuildSystems(systems: SystemSnapshot[]) {
|
||||||
|
this.activeSystemRootInScene = undefined;
|
||||||
rebuildSystemScene(this.createSceneSyncContext(), systems);
|
rebuildSystemScene(this.createSceneSyncContext(), systems);
|
||||||
|
// Re-activate the current active system if any
|
||||||
|
const activeId = this.context.getActiveSystemId();
|
||||||
|
if (activeId) {
|
||||||
|
this.activateSystemRoot(activeId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
syncSpatialNodes(nodes: SpatialNodeSnapshot[]) {
|
syncCelestials(celestials: CelestialSnapshot[]) {
|
||||||
syncSpatialNodeScene(this.createSceneSyncContext(), nodes);
|
syncCelestialScene(this.createSceneSyncContext(), celestials, this.context.getActiveSystemId());
|
||||||
}
|
|
||||||
|
|
||||||
syncLocalBubbles(bubbles: LocalBubbleSnapshot[]) {
|
|
||||||
syncBubbleScene(this.createSceneSyncContext(), bubbles);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
syncNodes(nodes: ResourceNodeSnapshot[]) {
|
syncNodes(nodes: ResourceNodeSnapshot[]) {
|
||||||
syncNodeScene(this.createSceneSyncContext(), nodes);
|
syncNodeScene(this.createSceneSyncContext(), nodes, this.context.getActiveSystemId());
|
||||||
}
|
}
|
||||||
|
|
||||||
syncStations(stations: StationSnapshot[]) {
|
syncStations(stations: StationSnapshot[]) {
|
||||||
syncStationScene(this.createSceneSyncContext(), stations);
|
syncStationScene(this.createSceneSyncContext(), stations, this.context.getActiveSystemId());
|
||||||
}
|
}
|
||||||
|
|
||||||
syncClaims(claims: ClaimSnapshot[]) {
|
syncClaims(claims: ClaimSnapshot[]) {
|
||||||
syncClaimScene(this.createSceneSyncContext(), claims);
|
syncClaimScene(this.createSceneSyncContext(), claims, this.context.getActiveSystemId());
|
||||||
}
|
}
|
||||||
|
|
||||||
syncConstructionSites(sites: ConstructionSiteSnapshot[]) {
|
syncConstructionSites(sites: ConstructionSiteSnapshot[]) {
|
||||||
syncConstructionSiteScene(this.createSceneSyncContext(), sites);
|
syncConstructionSiteScene(this.createSceneSyncContext(), sites, this.context.getActiveSystemId());
|
||||||
}
|
}
|
||||||
|
|
||||||
syncShips(ships: ShipSnapshot[], tickIntervalMs: number) {
|
syncShips(ships: ShipSnapshot[], tickIntervalMs: number) {
|
||||||
syncShipScene(this.createSceneSyncContext(), ships, tickIntervalMs);
|
syncShipScene(this.createSceneSyncContext(), ships, tickIntervalMs, this.context.getActiveSystemId());
|
||||||
}
|
}
|
||||||
|
|
||||||
applySpatialNodeDeltas(nodes: SpatialNodeDelta[]) {
|
applyCelestialDeltas(celestials: CelestialDelta[]) {
|
||||||
applySpatialNodeDeltaUpdates(this.createSceneSyncContext(), nodes);
|
applyCelestialDeltaUpdates(this.createSceneSyncContext(), celestials, this.context.getActiveSystemId());
|
||||||
}
|
|
||||||
|
|
||||||
applyLocalBubbleDeltas(bubbles: LocalBubbleDelta[]) {
|
|
||||||
applyLocalBubbleDeltaUpdates(this.createSceneSyncContext(), bubbles);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
applyNodeDeltas(nodes: ResourceNodeDelta[]) {
|
applyNodeDeltas(nodes: ResourceNodeDelta[]) {
|
||||||
applyNodeDeltaUpdates(this.createSceneSyncContext(), nodes);
|
applyNodeDeltaUpdates(this.createSceneSyncContext(), nodes, this.context.getActiveSystemId());
|
||||||
}
|
}
|
||||||
|
|
||||||
applyStationDeltas(stations: StationDelta[]) {
|
applyStationDeltas(stations: StationDelta[]) {
|
||||||
applyStationDeltaUpdates(this.createSceneSyncContext(), stations);
|
applyStationDeltaUpdates(this.createSceneSyncContext(), stations, this.context.getActiveSystemId());
|
||||||
}
|
}
|
||||||
|
|
||||||
applyClaimDeltas(claims: ClaimDelta[]) {
|
applyClaimDeltas(claims: ClaimDelta[]) {
|
||||||
applyClaimDeltaUpdates(this.createSceneSyncContext(), claims);
|
applyClaimDeltaUpdates(this.createSceneSyncContext(), claims, this.context.getActiveSystemId());
|
||||||
}
|
}
|
||||||
|
|
||||||
applyConstructionSiteDeltas(sites: ConstructionSiteDelta[]) {
|
applyConstructionSiteDeltas(sites: ConstructionSiteDelta[]) {
|
||||||
applyConstructionSiteDeltaUpdates(this.createSceneSyncContext(), sites);
|
applyConstructionSiteDeltaUpdates(this.createSceneSyncContext(), sites, this.context.getActiveSystemId());
|
||||||
}
|
}
|
||||||
|
|
||||||
applyShipDeltas(ships: ShipDelta[], tickIntervalMs: number) {
|
applyShipDeltas(ships: ShipDelta[], tickIntervalMs: number) {
|
||||||
applyShipDeltaUpdates(this.createSceneSyncContext(), ships, tickIntervalMs);
|
applyShipDeltaUpdates(this.createSceneSyncContext(), ships, tickIntervalMs, this.context.getActiveSystemId());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the active system changes. Swaps which system's root is in systemScene
|
||||||
|
* and updates visibility of all system-filtered objects.
|
||||||
|
*/
|
||||||
|
onActiveSystemChanged(oldSystemId: string | undefined, newSystemId: string | undefined) {
|
||||||
|
// Remove old system's root from systemScene
|
||||||
|
if (this.activeSystemRootInScene) {
|
||||||
|
this.context.systemScene.remove(this.activeSystemRootInScene);
|
||||||
|
this.activeSystemRootInScene = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new system's root to systemScene
|
||||||
|
if (newSystemId) {
|
||||||
|
this.activateSystemRoot(newSystemId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update visibility of all system-filtered objects
|
||||||
|
this.updateSystemObjectVisibility(newSystemId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private activateSystemRoot(systemId: string) {
|
||||||
|
const visual = this.context.systemVisuals.get(systemId);
|
||||||
|
if (!visual) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const threeObj = rawObject(visual.systemRoot);
|
||||||
|
this.context.systemScene.add(threeObj);
|
||||||
|
this.activeSystemRootInScene = threeObj;
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateSystemObjectVisibility(activeSystemId: string | undefined) {
|
||||||
|
for (const visual of this.context.celestialVisuals.values()) {
|
||||||
|
const isActive = visual.systemId === activeSystemId;
|
||||||
|
visual.mesh.setVisible(isActive);
|
||||||
|
visual.icon.setVisible(isActive);
|
||||||
|
}
|
||||||
|
for (const visual of this.context.nodeVisuals.values()) {
|
||||||
|
const isActive = visual.systemId === activeSystemId;
|
||||||
|
visual.mesh.setVisible(isActive);
|
||||||
|
visual.icon.setVisible(isActive);
|
||||||
|
}
|
||||||
|
for (const visual of this.context.stationVisuals.values()) {
|
||||||
|
const isActive = visual.systemId === activeSystemId;
|
||||||
|
visual.mesh.setVisible(isActive);
|
||||||
|
visual.icon.setVisible(isActive);
|
||||||
|
}
|
||||||
|
for (const visual of this.context.claimVisuals.values()) {
|
||||||
|
const isActive = visual.systemId === activeSystemId;
|
||||||
|
visual.mesh.setVisible(isActive);
|
||||||
|
visual.icon.setVisible(isActive);
|
||||||
|
}
|
||||||
|
for (const visual of this.context.constructionSiteVisuals.values()) {
|
||||||
|
const isActive = visual.systemId === activeSystemId;
|
||||||
|
visual.mesh.setVisible(isActive);
|
||||||
|
visual.icon.setVisible(isActive);
|
||||||
|
}
|
||||||
|
for (const visual of this.context.shipVisuals.values()) {
|
||||||
|
const isActive = visual.systemId === activeSystemId;
|
||||||
|
visual.mesh.setVisible(isActive);
|
||||||
|
visual.icon.setVisible(isActive);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
createWorldPresentationContext(overrides: {
|
createWorldPresentationContext(overrides: {
|
||||||
world: any;
|
world: any;
|
||||||
activeSystemId?: string;
|
activeSystemId?: string;
|
||||||
zoomLevel: any;
|
povLevel: any;
|
||||||
orbitYaw: number;
|
orbitYaw: number;
|
||||||
camera: THREE.PerspectiveCamera;
|
systemCamera: THREE.PerspectiveCamera;
|
||||||
systemFocusLocal: THREE.Vector3;
|
systemAnchor: THREE.Vector3;
|
||||||
toDisplayLocalPosition: (localPosition: THREE.Vector3, systemId?: string) => THREE.Vector3;
|
toDisplayLocalPosition: (localPosition: THREE.Vector3) => THREE.Vector3;
|
||||||
updateSystemDetailVisibility: () => void;
|
|
||||||
setShellReticleOpacity: (sprite: any, opacity: number) => void;
|
setShellReticleOpacity: (sprite: any, opacity: number) => void;
|
||||||
}) {
|
}) {
|
||||||
return {
|
return {
|
||||||
@@ -161,21 +215,20 @@ export class ViewerSceneDataController {
|
|||||||
worldTimeSyncMs: this.context.getWorldTimeSyncMs(),
|
worldTimeSyncMs: this.context.getWorldTimeSyncMs(),
|
||||||
worldSeed: this.context.getWorldSeed(),
|
worldSeed: this.context.getWorldSeed(),
|
||||||
activeSystemId: overrides.activeSystemId,
|
activeSystemId: overrides.activeSystemId,
|
||||||
zoomLevel: overrides.zoomLevel,
|
povLevel: overrides.povLevel,
|
||||||
orbitYaw: overrides.orbitYaw,
|
orbitYaw: overrides.orbitYaw,
|
||||||
camera: overrides.camera,
|
camera: overrides.systemCamera,
|
||||||
systemFocusLocal: overrides.systemFocusLocal,
|
systemAnchor: overrides.systemAnchor,
|
||||||
shipVisuals: this.context.shipVisuals,
|
shipVisuals: this.context.shipVisuals,
|
||||||
nodeVisuals: this.context.nodeVisuals,
|
nodeVisuals: this.context.nodeVisuals,
|
||||||
spatialNodeVisuals: this.context.spatialNodeVisuals,
|
celestialVisuals: this.context.celestialVisuals,
|
||||||
bubbleVisuals: this.context.bubbleVisuals,
|
|
||||||
stationVisuals: this.context.stationVisuals,
|
stationVisuals: this.context.stationVisuals,
|
||||||
claimVisuals: this.context.claimVisuals,
|
claimVisuals: this.context.claimVisuals,
|
||||||
constructionSiteVisuals: this.context.constructionSiteVisuals,
|
constructionSiteVisuals: this.context.constructionSiteVisuals,
|
||||||
systemVisuals: this.context.systemVisuals,
|
systemVisuals: this.context.systemVisuals,
|
||||||
systemSummaryVisuals: this.context.systemSummaryVisuals,
|
systemSummaryVisuals: new Map(),
|
||||||
toDisplayLocalPosition: overrides.toDisplayLocalPosition,
|
toDisplayLocalPosition: overrides.toDisplayLocalPosition,
|
||||||
updateSystemDetailVisibility: overrides.updateSystemDetailVisibility,
|
updateSystemDetailVisibility: () => {},
|
||||||
setShellReticleOpacity: overrides.setShellReticleOpacity,
|
setShellReticleOpacity: overrides.setShellReticleOpacity,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -187,39 +240,33 @@ export class ViewerSceneDataController {
|
|||||||
orbitalSimulationSpeed: this.context.getOrbitalSimulationSpeed(),
|
orbitalSimulationSpeed: this.context.getOrbitalSimulationSpeed(),
|
||||||
worldSeed: this.context.getWorldSeed(),
|
worldSeed: this.context.getWorldSeed(),
|
||||||
worldTimeSyncMs: this.context.getWorldTimeSyncMs(),
|
worldTimeSyncMs: this.context.getWorldTimeSyncMs(),
|
||||||
systemGroup: this.context.systemGroup,
|
galaxySystemGroup: this.context.galaxySystemGroup,
|
||||||
spatialNodeGroup: this.context.spatialNodeGroup,
|
celestialGroup: this.context.celestialGroup,
|
||||||
bubbleGroup: this.context.bubbleGroup,
|
|
||||||
nodeGroup: this.context.nodeGroup,
|
nodeGroup: this.context.nodeGroup,
|
||||||
stationGroup: this.context.stationGroup,
|
stationGroup: this.context.stationGroup,
|
||||||
claimGroup: this.context.claimGroup,
|
claimGroup: this.context.claimGroup,
|
||||||
constructionSiteGroup: this.context.constructionSiteGroup,
|
constructionSiteGroup: this.context.constructionSiteGroup,
|
||||||
shipGroup: this.context.shipGroup,
|
shipGroup: this.context.shipGroup,
|
||||||
selectableTargets: this.context.selectableTargets,
|
galaxySelectableTargets: this.context.galaxySelectableTargets,
|
||||||
presentationEntries: this.context.presentationEntries,
|
systemSelectableTargets: this.context.systemSelectableTargets,
|
||||||
systemVisuals: this.context.systemVisuals,
|
systemVisuals: this.context.systemVisuals,
|
||||||
systemSummaryVisuals: this.context.systemSummaryVisuals,
|
|
||||||
planetVisuals: this.context.planetVisuals,
|
planetVisuals: this.context.planetVisuals,
|
||||||
orbitLines: this.context.orbitLines,
|
orbitLines: this.context.orbitLines,
|
||||||
spatialNodeVisuals: this.context.spatialNodeVisuals,
|
celestialVisuals: this.context.celestialVisuals,
|
||||||
bubbleVisuals: this.context.bubbleVisuals,
|
|
||||||
nodeVisuals: this.context.nodeVisuals,
|
nodeVisuals: this.context.nodeVisuals,
|
||||||
stationVisuals: this.context.stationVisuals,
|
stationVisuals: this.context.stationVisuals,
|
||||||
claimVisuals: this.context.claimVisuals,
|
claimVisuals: this.context.claimVisuals,
|
||||||
constructionSiteVisuals: this.context.constructionSiteVisuals,
|
constructionSiteVisuals: this.context.constructionSiteVisuals,
|
||||||
shipVisuals: this.context.shipVisuals,
|
shipVisuals: this.context.shipVisuals,
|
||||||
registerPresentation: this.context.registerPresentation,
|
|
||||||
shipSize,
|
shipSize,
|
||||||
shipLength,
|
shipLength,
|
||||||
shipPresentationColor,
|
shipPresentationColor,
|
||||||
spatialNodeColor,
|
celestialColor,
|
||||||
createCirclePoints,
|
createCirclePoints,
|
||||||
resolveBubblePosition: (bubble: LocalBubbleSnapshot | LocalBubbleDelta) => resolveBubblePosition(this.context.getWorldPresentationContext(), bubble),
|
resolvePointPosition: (systemId: string, celestialId?: string | null) => resolvePointPosition(this.context.getWorldPresentationContext(), systemId, celestialId),
|
||||||
resolvePointPosition: (systemId: string, nodeId?: string | null) => resolvePointPosition(this.context.getWorldPresentationContext(), systemId, nodeId),
|
|
||||||
resolveOrbitalAnchor: (systemId: string, localPosition: THREE.Vector3) => resolveOrbitalAnchor(this.context.getWorldPresentationContext(), systemId, localPosition),
|
resolveOrbitalAnchor: (systemId: string, localPosition: THREE.Vector3) => resolveOrbitalAnchor(this.context.getWorldPresentationContext(), systemId, localPosition),
|
||||||
deriveNodeOrbital: (node: ResourceNodeSnapshot | ResourceNodeDelta, anchor: OrbitalAnchor) => deriveNodeOrbital(this.context.getWorldPresentationContext(), node, anchor),
|
deriveNodeOrbital: (node: ResourceNodeSnapshot | ResourceNodeDelta, anchor: OrbitalAnchor) => deriveNodeOrbital(this.context.getWorldPresentationContext(), node, anchor),
|
||||||
deriveOrbitalFromLocalPosition: (localPosition: THREE.Vector3, systemId: string, anchor: OrbitalAnchor) => deriveOrbitalFromLocalPosition(this.context.getWorldPresentationContext(), localPosition, systemId, anchor),
|
deriveOrbitalFromLocalPosition: (localPosition: THREE.Vector3, systemId: string, anchor: OrbitalAnchor) => deriveOrbitalFromLocalPosition(this.context.getWorldPresentationContext(), localPosition, systemId, anchor),
|
||||||
setBubbleVisualState,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,17 +5,16 @@ import {
|
|||||||
STAR_RENDER_SCALE,
|
STAR_RENDER_SCALE,
|
||||||
} from "./viewerConstants";
|
} from "./viewerConstants";
|
||||||
import type {
|
import type {
|
||||||
|
CelestialSnapshot,
|
||||||
ClaimSnapshot,
|
ClaimSnapshot,
|
||||||
ConstructionSiteSnapshot,
|
ConstructionSiteSnapshot,
|
||||||
LocalBubbleSnapshot,
|
|
||||||
PlanetSnapshot,
|
PlanetSnapshot,
|
||||||
ResourceNodeSnapshot,
|
ResourceNodeSnapshot,
|
||||||
ShipSnapshot,
|
ShipSnapshot,
|
||||||
SpatialNodeSnapshot,
|
|
||||||
StationSnapshot,
|
StationSnapshot,
|
||||||
SystemSnapshot,
|
SystemSnapshot,
|
||||||
} from "./contracts";
|
} from "./contracts";
|
||||||
import type { MoonVisual, SystemSummaryVisual } from "./viewerTypes";
|
import type { MoonVisual } from "./viewerTypes";
|
||||||
import {
|
import {
|
||||||
celestialRenderRadius,
|
celestialRenderRadius,
|
||||||
computeMoonOrbitRadius,
|
computeMoonOrbitRadius,
|
||||||
@@ -46,10 +45,10 @@ export function createNodeMesh(node: ResourceNodeSnapshot): SceneNode {
|
|||||||
return createSceneNode(mesh);
|
return createSceneNode(mesh);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createSpatialNodeMesh(node: SpatialNodeSnapshot, spatialNodeColor: (kind: string) => string): SceneNode {
|
export function createCelestialMesh(node: CelestialSnapshot, celestialColor: (kind: string) => string): SceneNode {
|
||||||
const color = spatialNodeColor(node.kind);
|
const color = celestialColor(node.kind);
|
||||||
return createSceneNode(new THREE.Mesh(
|
return createSceneNode(new THREE.Mesh(
|
||||||
new THREE.OctahedronGeometry(10, 0),
|
new THREE.OctahedronGeometry(0.08, 0),
|
||||||
new THREE.MeshStandardMaterial({
|
new THREE.MeshStandardMaterial({
|
||||||
color,
|
color,
|
||||||
emissive: new THREE.Color(color).multiplyScalar(0.16),
|
emissive: new THREE.Color(color).multiplyScalar(0.16),
|
||||||
@@ -59,23 +58,6 @@ export function createSpatialNodeMesh(node: SpatialNodeSnapshot, spatialNodeColo
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createBubbleRing(
|
|
||||||
bubble: LocalBubbleSnapshot,
|
|
||||||
localPosition: THREE.Vector3,
|
|
||||||
createCirclePoints: (radius: number, segments: number) => THREE.Vector3[],
|
|
||||||
): SceneNode {
|
|
||||||
const ring = new THREE.LineLoop(
|
|
||||||
new THREE.BufferGeometry().setFromPoints(createCirclePoints(Math.max(bubble.radius, 60), 64)),
|
|
||||||
new THREE.LineBasicMaterial({
|
|
||||||
color: 0x6ed6ff,
|
|
||||||
transparent: true,
|
|
||||||
opacity: 0.32,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
ring.position.copy(localPosition);
|
|
||||||
return createSceneNode(ring);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createClaimMesh(claim: ClaimSnapshot): SceneNode {
|
export function createClaimMesh(claim: ClaimSnapshot): SceneNode {
|
||||||
return createSceneNode(new THREE.Mesh(
|
return createSceneNode(new THREE.Mesh(
|
||||||
new THREE.ConeGeometry(9, 20, 4),
|
new THREE.ConeGeometry(9, 20, 4),
|
||||||
@@ -363,20 +345,34 @@ export function createTacticalIcon(documentRef: Document, color: string, size: n
|
|||||||
return createSceneNode(sprite);
|
return createSceneNode(sprite);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createSystemSummaryVisual(documentRef: Document, anchor: THREE.Vector3): SystemSummaryVisual {
|
|
||||||
|
export function createStarDot(documentRef: Document, color: string): SceneNode {
|
||||||
const canvas = documentRef.createElement("canvas");
|
const canvas = documentRef.createElement("canvas");
|
||||||
canvas.width = 512;
|
canvas.width = 32;
|
||||||
canvas.height = 160;
|
canvas.height = 32;
|
||||||
|
const context = canvas.getContext("2d");
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("Unable to create star dot canvas");
|
||||||
|
}
|
||||||
|
|
||||||
|
context.clearRect(0, 0, 32, 32);
|
||||||
|
context.fillStyle = color;
|
||||||
|
context.beginPath();
|
||||||
|
context.arc(16, 16, 12, 0, Math.PI * 2);
|
||||||
|
context.fill();
|
||||||
|
|
||||||
const texture = new THREE.CanvasTexture(canvas);
|
const texture = new THREE.CanvasTexture(canvas);
|
||||||
const sprite = createSceneNode(new THREE.Sprite(new THREE.SpriteMaterial({
|
const sprite = new THREE.Sprite(new THREE.SpriteMaterial({
|
||||||
map: texture,
|
map: texture,
|
||||||
transparent: true,
|
transparent: true,
|
||||||
depthWrite: false,
|
depthWrite: false,
|
||||||
depthTest: false,
|
depthTest: false,
|
||||||
})));
|
color: "#ffffff",
|
||||||
sprite.object.scale.set(520, 160, 1);
|
fog: false,
|
||||||
sprite.setVisible(false);
|
}));
|
||||||
return { sprite, texture, anchor };
|
sprite.scale.setScalar(4);
|
||||||
|
sprite.visible = false;
|
||||||
|
return createSceneNode(sprite);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createShellReticle(documentRef: Document, color: string, size: number): SceneNode {
|
export function createShellReticle(documentRef: Document, color: string, size: number): SceneNode {
|
||||||
|
|||||||
@@ -1,36 +1,33 @@
|
|||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import {
|
import {
|
||||||
|
ACTIVE_SYSTEM_DETAIL_SCALE,
|
||||||
PLANET_RENDER_SCALE,
|
PLANET_RENDER_SCALE,
|
||||||
STAR_RENDER_SCALE,
|
STAR_RENDER_SCALE,
|
||||||
} from "./viewerConstants";
|
} from "./viewerConstants";
|
||||||
|
import { DISPLAY_UNITS_PER_KILOMETER } from "./viewerMath";
|
||||||
import type {
|
import type {
|
||||||
BubbleVisual,
|
CelestialVisual,
|
||||||
ClaimVisual,
|
ClaimVisual,
|
||||||
ConstructionSiteVisual,
|
ConstructionSiteVisual,
|
||||||
NodeVisual,
|
NodeVisual,
|
||||||
OrbitLineVisual,
|
OrbitLineVisual,
|
||||||
PlanetVisual,
|
PlanetVisual,
|
||||||
PresentationEntry,
|
|
||||||
Selectable,
|
Selectable,
|
||||||
ShipVisual,
|
ShipVisual,
|
||||||
SpatialNodeVisual,
|
|
||||||
StructureVisual,
|
StructureVisual,
|
||||||
SystemSummaryVisual,
|
|
||||||
SystemVisual,
|
SystemVisual,
|
||||||
} from "./viewerTypes";
|
} from "./viewerTypes";
|
||||||
import type {
|
import type {
|
||||||
|
CelestialDelta,
|
||||||
|
CelestialSnapshot,
|
||||||
ClaimDelta,
|
ClaimDelta,
|
||||||
ClaimSnapshot,
|
ClaimSnapshot,
|
||||||
ConstructionSiteDelta,
|
ConstructionSiteDelta,
|
||||||
ConstructionSiteSnapshot,
|
ConstructionSiteSnapshot,
|
||||||
LocalBubbleDelta,
|
|
||||||
LocalBubbleSnapshot,
|
|
||||||
ResourceNodeDelta,
|
ResourceNodeDelta,
|
||||||
ResourceNodeSnapshot,
|
ResourceNodeSnapshot,
|
||||||
ShipDelta,
|
ShipDelta,
|
||||||
ShipSnapshot,
|
ShipSnapshot,
|
||||||
SpatialNodeDelta,
|
|
||||||
SpatialNodeSnapshot,
|
|
||||||
StationDelta,
|
StationDelta,
|
||||||
StationSnapshot,
|
StationSnapshot,
|
||||||
SystemSnapshot,
|
SystemSnapshot,
|
||||||
@@ -45,7 +42,6 @@ import {
|
|||||||
} from "./viewerMath";
|
} from "./viewerMath";
|
||||||
import { getAnimatedShipLocalPosition } from "./viewerPresentation";
|
import { getAnimatedShipLocalPosition } from "./viewerPresentation";
|
||||||
import {
|
import {
|
||||||
createBubbleRing,
|
|
||||||
createClaimMesh,
|
createClaimMesh,
|
||||||
createConstructionSiteMesh,
|
createConstructionSiteMesh,
|
||||||
createMoonVisuals,
|
createMoonVisuals,
|
||||||
@@ -54,10 +50,10 @@ import {
|
|||||||
createPlanetRing,
|
createPlanetRing,
|
||||||
createShellReticle,
|
createShellReticle,
|
||||||
createShipMesh,
|
createShipMesh,
|
||||||
createSpatialNodeMesh,
|
createCelestialMesh,
|
||||||
createStarCluster,
|
createStarCluster,
|
||||||
|
createStarDot,
|
||||||
createStationMesh,
|
createStationMesh,
|
||||||
createSystemSummaryVisual,
|
|
||||||
createTacticalIcon,
|
createTacticalIcon,
|
||||||
} from "./viewerSceneFactory";
|
} from "./viewerSceneFactory";
|
||||||
import {
|
import {
|
||||||
@@ -68,47 +64,41 @@ import {
|
|||||||
} from "./viewerScenePrimitives";
|
} from "./viewerScenePrimitives";
|
||||||
import type { SceneNode } from "./viewerScenePrimitives";
|
import type { SceneNode } from "./viewerScenePrimitives";
|
||||||
|
|
||||||
|
/** Scale a local km position to system-scene display coordinates. */
|
||||||
|
function toSystemPos(localPosition: THREE.Vector3): THREE.Vector3 {
|
||||||
|
return localPosition.clone().multiplyScalar(DISPLAY_UNITS_PER_KILOMETER * ACTIVE_SYSTEM_DETAIL_SCALE);
|
||||||
|
}
|
||||||
|
|
||||||
interface SceneSyncContext {
|
interface SceneSyncContext {
|
||||||
documentRef: Document;
|
documentRef: Document;
|
||||||
worldOrbitalTimeSeconds?: number;
|
worldOrbitalTimeSeconds?: number;
|
||||||
orbitalSimulationSpeed: number;
|
orbitalSimulationSpeed: number;
|
||||||
worldSeed: number;
|
worldSeed: number;
|
||||||
worldTimeSyncMs: number;
|
worldTimeSyncMs: number;
|
||||||
systemGroup: THREE.Group;
|
galaxySystemGroup: THREE.Group;
|
||||||
spatialNodeGroup: THREE.Group;
|
celestialGroup: THREE.Group;
|
||||||
bubbleGroup: THREE.Group;
|
|
||||||
nodeGroup: THREE.Group;
|
nodeGroup: THREE.Group;
|
||||||
stationGroup: THREE.Group;
|
stationGroup: THREE.Group;
|
||||||
claimGroup: THREE.Group;
|
claimGroup: THREE.Group;
|
||||||
constructionSiteGroup: THREE.Group;
|
constructionSiteGroup: THREE.Group;
|
||||||
shipGroup: THREE.Group;
|
shipGroup: THREE.Group;
|
||||||
selectableTargets: Map<THREE.Object3D, Selectable>;
|
galaxySelectableTargets: Map<THREE.Object3D, Selectable>;
|
||||||
presentationEntries: PresentationEntry[];
|
systemSelectableTargets: Map<THREE.Object3D, Selectable>;
|
||||||
systemVisuals: Map<string, SystemVisual>;
|
systemVisuals: Map<string, SystemVisual>;
|
||||||
systemSummaryVisuals: Map<string, SystemSummaryVisual>;
|
|
||||||
planetVisuals: PlanetVisual[];
|
planetVisuals: PlanetVisual[];
|
||||||
orbitLines: OrbitLineVisual[];
|
orbitLines: OrbitLineVisual[];
|
||||||
spatialNodeVisuals: Map<string, SpatialNodeVisual>;
|
celestialVisuals: Map<string, CelestialVisual>;
|
||||||
bubbleVisuals: Map<string, BubbleVisual>;
|
|
||||||
nodeVisuals: Map<string, NodeVisual>;
|
nodeVisuals: Map<string, NodeVisual>;
|
||||||
stationVisuals: Map<string, StructureVisual>;
|
stationVisuals: Map<string, StructureVisual>;
|
||||||
claimVisuals: Map<string, ClaimVisual>;
|
claimVisuals: Map<string, ClaimVisual>;
|
||||||
constructionSiteVisuals: Map<string, ConstructionSiteVisual>;
|
constructionSiteVisuals: Map<string, ConstructionSiteVisual>;
|
||||||
shipVisuals: Map<string, ShipVisual>;
|
shipVisuals: Map<string, ShipVisual>;
|
||||||
registerPresentation: (
|
|
||||||
detail: SceneNode,
|
|
||||||
icon: SceneNode,
|
|
||||||
hideDetailInUniverse: boolean,
|
|
||||||
hideIconInUniverse?: boolean,
|
|
||||||
systemId?: string,
|
|
||||||
) => void;
|
|
||||||
shipSize: (ship: ShipSnapshot) => number;
|
shipSize: (ship: ShipSnapshot) => number;
|
||||||
shipLength: (ship: ShipSnapshot) => number;
|
shipLength: (ship: ShipSnapshot) => number;
|
||||||
shipPresentationColor: (ship: ShipSnapshot) => string;
|
shipPresentationColor: (ship: ShipSnapshot) => string;
|
||||||
spatialNodeColor: (kind: string) => string;
|
celestialColor: (kind: string) => string;
|
||||||
createCirclePoints: (radius: number, segments: number) => THREE.Vector3[];
|
createCirclePoints: (radius: number, segments: number) => THREE.Vector3[];
|
||||||
resolveBubblePosition: (bubble: LocalBubbleSnapshot | LocalBubbleDelta) => THREE.Vector3;
|
resolvePointPosition: (systemId: string, celestialId?: string | null) => THREE.Vector3;
|
||||||
resolvePointPosition: (systemId: string, nodeId?: string | null) => THREE.Vector3;
|
|
||||||
resolveOrbitalAnchor: (systemId: string, localPosition: THREE.Vector3) => NodeVisual["anchor"];
|
resolveOrbitalAnchor: (systemId: string, localPosition: THREE.Vector3) => NodeVisual["anchor"];
|
||||||
deriveNodeOrbital: (node: ResourceNodeSnapshot | ResourceNodeDelta, anchor: NodeVisual["anchor"]) => {
|
deriveNodeOrbital: (node: ResourceNodeSnapshot | ResourceNodeDelta, anchor: NodeVisual["anchor"]) => {
|
||||||
radius: number;
|
radius: number;
|
||||||
@@ -120,7 +110,6 @@ interface SceneSyncContext {
|
|||||||
phase: number;
|
phase: number;
|
||||||
inclination: number;
|
inclination: number;
|
||||||
};
|
};
|
||||||
setBubbleVisualState: (visual: BubbleVisual, bubble: LocalBubbleSnapshot | LocalBubbleDelta) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function rebuildSystems(context: SceneSyncContext, systems: SystemSnapshot[]) {
|
export function rebuildSystems(context: SceneSyncContext, systems: SystemSnapshot[]) {
|
||||||
@@ -128,44 +117,37 @@ export function rebuildSystems(context: SceneSyncContext, systems: SystemSnapsho
|
|||||||
? context.worldOrbitalTimeSeconds + ((performance.now() - context.worldTimeSyncMs) / 1000 * context.orbitalSimulationSpeed)
|
? context.worldOrbitalTimeSeconds + ((performance.now() - context.worldTimeSyncMs) / 1000 * context.orbitalSimulationSpeed)
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
context.systemGroup.clear();
|
context.galaxySystemGroup.clear();
|
||||||
context.selectableTargets.clear();
|
context.galaxySelectableTargets.clear();
|
||||||
context.presentationEntries.length = 0;
|
context.systemSelectableTargets.clear();
|
||||||
context.planetVisuals.length = 0;
|
context.planetVisuals.length = 0;
|
||||||
context.orbitLines.length = 0;
|
context.orbitLines.length = 0;
|
||||||
context.systemVisuals.clear();
|
context.systemVisuals.clear();
|
||||||
context.systemSummaryVisuals.clear();
|
|
||||||
|
|
||||||
for (const system of systems) {
|
for (const system of systems) {
|
||||||
const root = createSceneNode(new THREE.Group());
|
// Galaxy root: star dot + shell reticle — lives in galaxyScene
|
||||||
root.setPosition(toDisplayGalaxyVector(system.galaxyPosition));
|
const galaxyRoot = createSceneNode(new THREE.Group());
|
||||||
const detailGroup = createSceneNode(new THREE.Group());
|
galaxyRoot.setPosition(toDisplayGalaxyVector(system.galaxyPosition));
|
||||||
const renderedStarSize = celestialRenderRadius(system.starSize, 0.00018, 0.16, 0.62);
|
|
||||||
|
|
||||||
const starCluster = createStarCluster(system);
|
const systemIcon = createStarDot(context.documentRef, system.starColor);
|
||||||
const systemIcon = createTacticalIcon(context.documentRef, system.starColor, 96);
|
|
||||||
const shellReticle = createShellReticle(context.documentRef, "#ff3b30", 400);
|
const shellReticle = createShellReticle(context.documentRef, "#ff3b30", 400);
|
||||||
const summaryVisual = createSystemSummaryVisual(
|
galaxyRoot.add(systemIcon, shellReticle);
|
||||||
context.documentRef,
|
|
||||||
toDisplayGalaxyVector(system.galaxyPosition).add(new THREE.Vector3(0, renderedStarSize + 140, 0)),
|
registerSelectableTarget(context.galaxySelectableTargets, systemIcon, { kind: "system", id: system.id });
|
||||||
);
|
registerSelectableTarget(context.galaxySelectableTargets, shellReticle, { kind: "system", id: system.id });
|
||||||
summaryVisual.sprite.setPosition(new THREE.Vector3(0, renderedStarSize + 110, 0));
|
|
||||||
root.add(starCluster, systemIcon, shellReticle, summaryVisual.sprite, detailGroup);
|
// System root: star cluster + planet detail group — added to systemScene only when this system is active
|
||||||
context.registerPresentation(starCluster, systemIcon, true);
|
const systemRoot = createSceneNode(new THREE.Group());
|
||||||
context.systemVisuals.set(system.id, {
|
const detailGroup = createSceneNode(new THREE.Group());
|
||||||
root,
|
const starCluster = createStarCluster(system);
|
||||||
|
systemRoot.add(starCluster, detailGroup);
|
||||||
|
|
||||||
|
registerSelectableDescendants(
|
||||||
|
context.systemSelectableTargets,
|
||||||
starCluster,
|
starCluster,
|
||||||
icon: systemIcon,
|
{ kind: "system", id: system.id },
|
||||||
shellReticle,
|
(child) => child instanceof THREE.Mesh,
|
||||||
shellReticleBaseScale: 400,
|
);
|
||||||
detailGroup,
|
|
||||||
summary: summaryVisual,
|
|
||||||
galaxyPosition: toDisplayGalaxyVector(system.galaxyPosition),
|
|
||||||
});
|
|
||||||
context.systemSummaryVisuals.set(system.id, summaryVisual);
|
|
||||||
registerSelectableDescendants(context.selectableTargets, starCluster, { kind: "system", id: system.id }, (child) => child instanceof THREE.Mesh);
|
|
||||||
registerSelectableTarget(context.selectableTargets, systemIcon, { kind: "system", id: system.id });
|
|
||||||
registerSelectableTarget(context.selectableTargets, shellReticle, { kind: "system", id: system.id });
|
|
||||||
|
|
||||||
for (const [planetIndex, planet] of system.planets.entries()) {
|
for (const [planetIndex, planet] of system.planets.entries()) {
|
||||||
const orbit = createPlanetOrbit(planet);
|
const orbit = createPlanetOrbit(planet);
|
||||||
@@ -179,12 +161,13 @@ export function rebuildSystems(context: SceneSyncContext, systems: SystemSnapsho
|
|||||||
emissive: new THREE.Color(planet.color).multiplyScalar(0.04),
|
emissive: new THREE.Color(planet.color).multiplyScalar(0.04),
|
||||||
}),
|
}),
|
||||||
));
|
));
|
||||||
planetMesh.setPosition(scaleLocalVector(computePlanetLocalPosition(planet, worldTimeSeconds)));
|
const initialPos = toSystemPos(computePlanetLocalPosition(planet, worldTimeSeconds));
|
||||||
|
planetMesh.setPosition(initialPos);
|
||||||
const planetIcon = createTacticalIcon(context.documentRef, planet.color, Math.max(24, renderedPlanetRadius * 2));
|
const planetIcon = createTacticalIcon(context.documentRef, planet.color, Math.max(24, renderedPlanetRadius * 2));
|
||||||
planetIcon.setPosition(rawObject(planetMesh).position.clone());
|
planetIcon.setPosition(initialPos);
|
||||||
const ring = planet.hasRing ? createPlanetRing(planet) : undefined;
|
const ring = planet.hasRing ? createPlanetRing(planet) : undefined;
|
||||||
if (ring) {
|
if (ring) {
|
||||||
ring.setPosition(rawObject(planetMesh).position.clone());
|
ring.setPosition(initialPos);
|
||||||
}
|
}
|
||||||
const moons = createMoonVisuals(planet, context.worldSeed);
|
const moons = createMoonVisuals(planet, context.worldSeed);
|
||||||
detailGroup.add(orbit, planetMesh, planetIcon);
|
detailGroup.add(orbit, planetMesh, planetIcon);
|
||||||
@@ -194,8 +177,8 @@ export function rebuildSystems(context: SceneSyncContext, systems: SystemSnapsho
|
|||||||
for (const moon of moons) {
|
for (const moon of moons) {
|
||||||
moon.systemId = system.id;
|
moon.systemId = system.id;
|
||||||
moon.planetIndex = planetIndex;
|
moon.planetIndex = planetIndex;
|
||||||
moon.orbit.setPosition(rawObject(planetMesh).position.clone());
|
moon.orbit.setPosition(initialPos);
|
||||||
moon.mesh.setPosition(rawObject(planetMesh).position.clone());
|
moon.mesh.setPosition(initialPos);
|
||||||
detailGroup.add(moon.orbit, moon.mesh);
|
detailGroup.add(moon.orbit, moon.mesh);
|
||||||
context.orbitLines.push({
|
context.orbitLines.push({
|
||||||
line: moon.orbit,
|
line: moon.orbit,
|
||||||
@@ -203,7 +186,6 @@ export function rebuildSystems(context: SceneSyncContext, systems: SystemSnapsho
|
|||||||
kind: "moon",
|
kind: "moon",
|
||||||
planetIndex,
|
planetIndex,
|
||||||
});
|
});
|
||||||
context.registerPresentation(moon.mesh, planetIcon, true, true, system.id);
|
|
||||||
}
|
}
|
||||||
context.orbitLines.push({
|
context.orbitLines.push({
|
||||||
line: orbit,
|
line: orbit,
|
||||||
@@ -211,68 +193,73 @@ export function rebuildSystems(context: SceneSyncContext, systems: SystemSnapsho
|
|||||||
kind: "planet",
|
kind: "planet",
|
||||||
planetIndex,
|
planetIndex,
|
||||||
});
|
});
|
||||||
context.registerPresentation(planetMesh, planetIcon, true, true, system.id);
|
|
||||||
if (ring) {
|
|
||||||
context.registerPresentation(ring, planetIcon, true, true, system.id);
|
|
||||||
}
|
|
||||||
context.planetVisuals.push({ systemId: system.id, planet, orbit, mesh: planetMesh, icon: planetIcon, ring, moons });
|
context.planetVisuals.push({ systemId: system.id, planet, orbit, mesh: planetMesh, icon: planetIcon, ring, moons });
|
||||||
registerSelectableTarget(context.selectableTargets, planetMesh, { kind: "planet", systemId: system.id, planetIndex });
|
registerSelectableTarget(context.systemSelectableTargets, planetMesh, { kind: "planet", systemId: system.id, planetIndex });
|
||||||
registerSelectableTarget(context.selectableTargets, planetIcon, { kind: "planet", systemId: system.id, planetIndex });
|
registerSelectableTarget(context.systemSelectableTargets, planetIcon, { kind: "planet", systemId: system.id, planetIndex });
|
||||||
}
|
}
|
||||||
|
|
||||||
context.systemGroup.add(rawObject(root));
|
context.systemVisuals.set(system.id, {
|
||||||
|
galaxyRoot,
|
||||||
|
systemRoot,
|
||||||
|
starCluster,
|
||||||
|
icon: systemIcon,
|
||||||
|
shellReticle,
|
||||||
|
shellReticleBaseScale: 400,
|
||||||
|
detailGroup,
|
||||||
|
galaxyPosition: toDisplayGalaxyVector(system.galaxyPosition),
|
||||||
|
});
|
||||||
|
|
||||||
|
context.galaxySystemGroup.add(rawObject(galaxyRoot));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function syncSpatialNodes(context: SceneSyncContext, nodes: SpatialNodeSnapshot[]) {
|
export function syncCelestials(context: SceneSyncContext, celestials: CelestialSnapshot[], activeSystemId?: string) {
|
||||||
context.spatialNodeGroup.clear();
|
context.celestialGroup.clear();
|
||||||
context.spatialNodeVisuals.clear();
|
context.celestialVisuals.clear();
|
||||||
|
|
||||||
for (const node of nodes) {
|
for (const celestial of celestials) {
|
||||||
const mesh = createSpatialNodeMesh(node, context.spatialNodeColor);
|
// Stars, planets, and moons are already rendered by rebuildSystems via SystemSnapshot.
|
||||||
const icon = createTacticalIcon(context.documentRef, context.spatialNodeColor(node.kind), 18);
|
// Only create visual objects for kinds not covered by the system builder.
|
||||||
const localPosition = toThreeVector(node.localPosition);
|
if (celestial.kind === "star" || celestial.kind === "planet" || celestial.kind === "moon") {
|
||||||
mesh.setPosition(localPosition);
|
continue;
|
||||||
icon.setPosition(localPosition);
|
}
|
||||||
context.spatialNodeVisuals.set(node.id, {
|
|
||||||
id: node.id,
|
const mesh = createCelestialMesh(celestial, context.celestialColor);
|
||||||
systemId: node.systemId,
|
const icon = createTacticalIcon(context.documentRef, context.celestialColor(celestial.kind), 18);
|
||||||
|
const orbitalAnchor = toSystemPos(toThreeVector(celestial.orbitalAnchor));
|
||||||
|
mesh.setPosition(orbitalAnchor);
|
||||||
|
icon.setPosition(orbitalAnchor);
|
||||||
|
const isActive = celestial.systemId === activeSystemId;
|
||||||
|
mesh.setVisible(isActive);
|
||||||
|
icon.setVisible(isActive);
|
||||||
|
context.celestialVisuals.set(celestial.id, {
|
||||||
|
id: celestial.id,
|
||||||
|
systemId: celestial.systemId,
|
||||||
mesh,
|
mesh,
|
||||||
icon,
|
icon,
|
||||||
kind: node.kind,
|
kind: celestial.kind,
|
||||||
localPosition,
|
orbitalAnchor,
|
||||||
});
|
});
|
||||||
context.spatialNodeGroup.add(rawObject(mesh), rawObject(icon));
|
context.celestialGroup.add(rawObject(mesh), rawObject(icon));
|
||||||
context.registerPresentation(mesh, icon, true, true, node.systemId);
|
registerSelectableTarget(context.systemSelectableTargets, mesh, { kind: "celestial", id: celestial.id });
|
||||||
registerSelectableTarget(context.selectableTargets, mesh, { kind: "spatial-node", id: node.id });
|
registerSelectableTarget(context.systemSelectableTargets, icon, { kind: "celestial", id: celestial.id });
|
||||||
registerSelectableTarget(context.selectableTargets, icon, { kind: "spatial-node", id: node.id });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function syncLocalBubbles(context: SceneSyncContext, bubbles: LocalBubbleSnapshot[]) {
|
export function syncNodes(context: SceneSyncContext, nodes: ResourceNodeSnapshot[], activeSystemId?: string) {
|
||||||
context.bubbleGroup.clear();
|
|
||||||
context.bubbleVisuals.clear();
|
|
||||||
|
|
||||||
for (const bubble of bubbles) {
|
|
||||||
const localPosition = context.resolveBubblePosition(bubble);
|
|
||||||
const mesh = createBubbleRing(bubble, localPosition, context.createCirclePoints);
|
|
||||||
const visual = { id: bubble.id, systemId: bubble.systemId, mesh, localPosition, radius: bubble.radius };
|
|
||||||
context.setBubbleVisualState(visual, bubble);
|
|
||||||
context.bubbleVisuals.set(bubble.id, visual);
|
|
||||||
context.bubbleGroup.add(rawObject(mesh));
|
|
||||||
registerSelectableTarget(context.selectableTargets, mesh, { kind: "bubble", id: bubble.id });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function syncNodes(context: SceneSyncContext, nodes: ResourceNodeSnapshot[]) {
|
|
||||||
context.nodeGroup.clear();
|
context.nodeGroup.clear();
|
||||||
context.nodeVisuals.clear();
|
context.nodeVisuals.clear();
|
||||||
|
|
||||||
for (const node of nodes) {
|
for (const node of nodes) {
|
||||||
const mesh = createNodeMesh(node);
|
const mesh = createNodeMesh(node);
|
||||||
const icon = createTacticalIcon(context.documentRef, node.sourceKind === "gas-cloud" ? "#7fd6ff" : "#d2b07a", 20);
|
const icon = createTacticalIcon(context.documentRef, node.sourceKind === "gas-cloud" ? "#7fd6ff" : "#d2b07a", 20);
|
||||||
icon.setPosition(rawObject(mesh).position.clone());
|
|
||||||
const localPosition = toThreeVector(node.localPosition);
|
const localPosition = toThreeVector(node.localPosition);
|
||||||
|
const displayPos = toSystemPos(localPosition);
|
||||||
|
mesh.setPosition(displayPos);
|
||||||
|
icon.setPosition(displayPos);
|
||||||
|
const isActive = node.systemId === activeSystemId;
|
||||||
|
mesh.setVisible(isActive);
|
||||||
|
icon.setVisible(isActive);
|
||||||
const anchor = context.resolveOrbitalAnchor(node.systemId, localPosition);
|
const anchor = context.resolveOrbitalAnchor(node.systemId, localPosition);
|
||||||
const orbital = context.deriveNodeOrbital(node, anchor);
|
const orbital = context.deriveNodeOrbital(node, anchor);
|
||||||
context.nodeVisuals.set(node.id, {
|
context.nodeVisuals.set(node.id, {
|
||||||
@@ -287,21 +274,25 @@ export function syncNodes(context: SceneSyncContext, nodes: ResourceNodeSnapshot
|
|||||||
orbitInclination: orbital.inclination,
|
orbitInclination: orbital.inclination,
|
||||||
});
|
});
|
||||||
context.nodeGroup.add(rawObject(mesh), rawObject(icon));
|
context.nodeGroup.add(rawObject(mesh), rawObject(icon));
|
||||||
context.registerPresentation(mesh, icon, true, true, node.systemId);
|
registerSelectableTarget(context.systemSelectableTargets, mesh, { kind: "node", id: node.id });
|
||||||
registerSelectableTarget(context.selectableTargets, mesh, { kind: "node", id: node.id });
|
registerSelectableTarget(context.systemSelectableTargets, icon, { kind: "node", id: node.id });
|
||||||
registerSelectableTarget(context.selectableTargets, icon, { kind: "node", id: node.id });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function syncStations(context: SceneSyncContext, stations: StationSnapshot[]) {
|
export function syncStations(context: SceneSyncContext, stations: StationSnapshot[], activeSystemId?: string) {
|
||||||
context.stationGroup.clear();
|
context.stationGroup.clear();
|
||||||
context.stationVisuals.clear();
|
context.stationVisuals.clear();
|
||||||
|
|
||||||
for (const station of stations) {
|
for (const station of stations) {
|
||||||
const mesh = createStationMesh(station);
|
const mesh = createStationMesh(station);
|
||||||
const icon = createTacticalIcon(context.documentRef, station.color, 26);
|
const icon = createTacticalIcon(context.documentRef, station.color, 26);
|
||||||
icon.setPosition(rawObject(mesh).position.clone());
|
|
||||||
const localPosition = toThreeVector(station.localPosition);
|
const localPosition = toThreeVector(station.localPosition);
|
||||||
|
const displayPos = toSystemPos(localPosition);
|
||||||
|
mesh.setPosition(displayPos);
|
||||||
|
icon.setPosition(displayPos);
|
||||||
|
const isActive = station.systemId === activeSystemId;
|
||||||
|
mesh.setVisible(isActive);
|
||||||
|
icon.setVisible(isActive);
|
||||||
const anchor = context.resolveOrbitalAnchor(station.systemId, localPosition);
|
const anchor = context.resolveOrbitalAnchor(station.systemId, localPosition);
|
||||||
const orbital = context.deriveOrbitalFromLocalPosition(localPosition, station.systemId, anchor);
|
const orbital = context.deriveOrbitalFromLocalPosition(localPosition, station.systemId, anchor);
|
||||||
context.stationVisuals.set(station.id, {
|
context.stationVisuals.set(station.id, {
|
||||||
@@ -316,63 +307,68 @@ export function syncStations(context: SceneSyncContext, stations: StationSnapsho
|
|||||||
localPosition,
|
localPosition,
|
||||||
});
|
});
|
||||||
context.stationGroup.add(rawObject(mesh), rawObject(icon));
|
context.stationGroup.add(rawObject(mesh), rawObject(icon));
|
||||||
context.registerPresentation(mesh, icon, true, true, station.systemId);
|
registerSelectableTarget(context.systemSelectableTargets, mesh, { kind: "station", id: station.id });
|
||||||
registerSelectableTarget(context.selectableTargets, mesh, { kind: "station", id: station.id });
|
registerSelectableTarget(context.systemSelectableTargets, icon, { kind: "station", id: station.id });
|
||||||
registerSelectableTarget(context.selectableTargets, icon, { kind: "station", id: station.id });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function syncClaims(context: SceneSyncContext, claims: ClaimSnapshot[]) {
|
export function syncClaims(context: SceneSyncContext, claims: ClaimSnapshot[], activeSystemId?: string) {
|
||||||
context.claimGroup.clear();
|
context.claimGroup.clear();
|
||||||
context.claimVisuals.clear();
|
context.claimVisuals.clear();
|
||||||
|
|
||||||
for (const claim of claims) {
|
for (const claim of claims) {
|
||||||
const localPosition = context.resolvePointPosition(claim.systemId, claim.nodeId);
|
const localPosition = context.resolvePointPosition(claim.systemId, claim.celestialId);
|
||||||
|
const displayPos = toSystemPos(localPosition);
|
||||||
const mesh = createClaimMesh(claim);
|
const mesh = createClaimMesh(claim);
|
||||||
const icon = createTacticalIcon(context.documentRef, "#ff5b5b", 18);
|
const icon = createTacticalIcon(context.documentRef, "#ff5b5b", 18);
|
||||||
mesh.setPosition(localPosition);
|
mesh.setPosition(displayPos);
|
||||||
icon.setPosition(localPosition);
|
icon.setPosition(displayPos);
|
||||||
|
const isActive = claim.systemId === activeSystemId;
|
||||||
|
mesh.setVisible(isActive);
|
||||||
|
icon.setVisible(isActive);
|
||||||
context.claimVisuals.set(claim.id, {
|
context.claimVisuals.set(claim.id, {
|
||||||
id: claim.id,
|
id: claim.id,
|
||||||
nodeId: claim.nodeId,
|
celestialId: claim.celestialId,
|
||||||
systemId: claim.systemId,
|
systemId: claim.systemId,
|
||||||
mesh,
|
mesh,
|
||||||
icon,
|
icon,
|
||||||
localPosition,
|
localPosition,
|
||||||
});
|
});
|
||||||
context.claimGroup.add(rawObject(mesh), rawObject(icon));
|
context.claimGroup.add(rawObject(mesh), rawObject(icon));
|
||||||
context.registerPresentation(mesh, icon, true, true, claim.systemId);
|
registerSelectableTarget(context.systemSelectableTargets, mesh, { kind: "claim", id: claim.id });
|
||||||
registerSelectableTarget(context.selectableTargets, mesh, { kind: "claim", id: claim.id });
|
registerSelectableTarget(context.systemSelectableTargets, icon, { kind: "claim", id: claim.id });
|
||||||
registerSelectableTarget(context.selectableTargets, icon, { kind: "claim", id: claim.id });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function syncConstructionSites(context: SceneSyncContext, sites: ConstructionSiteSnapshot[]) {
|
export function syncConstructionSites(context: SceneSyncContext, sites: ConstructionSiteSnapshot[], activeSystemId?: string) {
|
||||||
context.constructionSiteGroup.clear();
|
context.constructionSiteGroup.clear();
|
||||||
context.constructionSiteVisuals.clear();
|
context.constructionSiteVisuals.clear();
|
||||||
|
|
||||||
for (const site of sites) {
|
for (const site of sites) {
|
||||||
const localPosition = context.resolvePointPosition(site.systemId, site.nodeId);
|
const localPosition = context.resolvePointPosition(site.systemId, site.celestialId);
|
||||||
|
const displayPos = toSystemPos(localPosition);
|
||||||
const mesh = createConstructionSiteMesh(site);
|
const mesh = createConstructionSiteMesh(site);
|
||||||
const icon = createTacticalIcon(context.documentRef, "#9df29c", 18);
|
const icon = createTacticalIcon(context.documentRef, "#9df29c", 18);
|
||||||
mesh.setPosition(localPosition);
|
mesh.setPosition(displayPos);
|
||||||
icon.setPosition(localPosition);
|
icon.setPosition(displayPos);
|
||||||
|
const isActive = site.systemId === activeSystemId;
|
||||||
|
mesh.setVisible(isActive);
|
||||||
|
icon.setVisible(isActive);
|
||||||
context.constructionSiteVisuals.set(site.id, {
|
context.constructionSiteVisuals.set(site.id, {
|
||||||
id: site.id,
|
id: site.id,
|
||||||
nodeId: site.nodeId,
|
celestialId: site.celestialId,
|
||||||
systemId: site.systemId,
|
systemId: site.systemId,
|
||||||
mesh,
|
mesh,
|
||||||
icon,
|
icon,
|
||||||
localPosition,
|
localPosition,
|
||||||
});
|
});
|
||||||
context.constructionSiteGroup.add(rawObject(mesh), rawObject(icon));
|
context.constructionSiteGroup.add(rawObject(mesh), rawObject(icon));
|
||||||
context.registerPresentation(mesh, icon, true, true, site.systemId);
|
registerSelectableTarget(context.systemSelectableTargets, mesh, { kind: "construction-site", id: site.id });
|
||||||
registerSelectableTarget(context.selectableTargets, mesh, { kind: "construction-site", id: site.id });
|
registerSelectableTarget(context.systemSelectableTargets, icon, { kind: "construction-site", id: site.id });
|
||||||
registerSelectableTarget(context.selectableTargets, icon, { kind: "construction-site", id: site.id });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function syncShips(context: SceneSyncContext, ships: ShipSnapshot[], tickIntervalMs: number) {
|
export function syncShips(context: SceneSyncContext, ships: ShipSnapshot[], tickIntervalMs: number, activeSystemId?: string) {
|
||||||
context.shipGroup.clear();
|
context.shipGroup.clear();
|
||||||
context.shipVisuals.clear();
|
context.shipVisuals.clear();
|
||||||
|
|
||||||
@@ -380,19 +376,23 @@ export function syncShips(context: SceneSyncContext, ships: ShipSnapshot[], tick
|
|||||||
const mesh = createShipMesh(ship, context.shipSize(ship), context.shipLength(ship), context.shipPresentationColor(ship));
|
const mesh = createShipMesh(ship, context.shipSize(ship), context.shipLength(ship), context.shipPresentationColor(ship));
|
||||||
const shipColor = context.shipPresentationColor(ship);
|
const shipColor = context.shipPresentationColor(ship);
|
||||||
const icon = createTacticalIcon(context.documentRef, shipColor, 18);
|
const icon = createTacticalIcon(context.documentRef, shipColor, 18);
|
||||||
const position = toThreeVector(ship.localPosition);
|
const localPosition = toThreeVector(ship.localPosition);
|
||||||
icon.setPosition(position);
|
const displayPos = toSystemPos(localPosition);
|
||||||
|
mesh.setPosition(displayPos);
|
||||||
|
icon.setPosition(displayPos);
|
||||||
icon.setColor(shipColor);
|
icon.setColor(shipColor);
|
||||||
|
const isActive = ship.systemId === activeSystemId;
|
||||||
|
mesh.setVisible(isActive);
|
||||||
|
icon.setVisible(isActive);
|
||||||
context.shipGroup.add(rawObject(mesh), rawObject(icon));
|
context.shipGroup.add(rawObject(mesh), rawObject(icon));
|
||||||
registerSelectableTarget(context.selectableTargets, mesh, { kind: "ship", id: ship.id });
|
registerSelectableTarget(context.systemSelectableTargets, mesh, { kind: "ship", id: ship.id });
|
||||||
registerSelectableTarget(context.selectableTargets, icon, { kind: "ship", id: ship.id });
|
registerSelectableTarget(context.systemSelectableTargets, icon, { kind: "ship", id: ship.id });
|
||||||
context.registerPresentation(mesh, icon, true, true, ship.systemId);
|
|
||||||
context.shipVisuals.set(ship.id, {
|
context.shipVisuals.set(ship.id, {
|
||||||
systemId: ship.systemId,
|
systemId: ship.systemId,
|
||||||
mesh,
|
mesh,
|
||||||
icon,
|
icon,
|
||||||
startPosition: position.clone(),
|
startPosition: localPosition.clone(),
|
||||||
authoritativePosition: position.clone(),
|
authoritativePosition: localPosition.clone(),
|
||||||
targetPosition: toThreeVector(ship.targetLocalPosition),
|
targetPosition: toThreeVector(ship.targetLocalPosition),
|
||||||
velocity: toThreeVector(ship.localVelocity),
|
velocity: toThreeVector(ship.localVelocity),
|
||||||
receivedAtMs: performance.now(),
|
receivedAtMs: performance.now(),
|
||||||
@@ -401,39 +401,30 @@ export function syncShips(context: SceneSyncContext, ships: ShipSnapshot[], tick
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function applySpatialNodeDeltas(context: SceneSyncContext, nodes: SpatialNodeDelta[]) {
|
export function applyCelestialDeltas(context: SceneSyncContext, celestials: CelestialDelta[], activeSystemId?: string) {
|
||||||
for (const node of nodes) {
|
for (const celestial of celestials) {
|
||||||
const visual = context.spatialNodeVisuals.get(node.id);
|
if (celestial.kind === "star" || celestial.kind === "planet" || celestial.kind === "moon") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const visual = context.celestialVisuals.get(celestial.id);
|
||||||
if (!visual) {
|
if (!visual) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
visual.systemId = node.systemId;
|
visual.systemId = celestial.systemId;
|
||||||
visual.kind = node.kind;
|
visual.kind = celestial.kind;
|
||||||
visual.localPosition.copy(toThreeVector(node.localPosition));
|
visual.orbitalAnchor.copy(toSystemPos(toThreeVector(celestial.orbitalAnchor)));
|
||||||
visual.mesh.setPosition(visual.localPosition);
|
visual.mesh.setPosition(visual.orbitalAnchor);
|
||||||
visual.icon.setPosition(visual.localPosition);
|
visual.icon.setPosition(visual.orbitalAnchor);
|
||||||
visual.mesh.setColor(context.spatialNodeColor(node.kind));
|
visual.mesh.setColor(context.celestialColor(celestial.kind));
|
||||||
|
const isActive = visual.systemId === activeSystemId;
|
||||||
|
visual.mesh.setVisible(isActive);
|
||||||
|
visual.icon.setVisible(isActive);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function applyLocalBubbleDeltas(context: SceneSyncContext, bubbles: LocalBubbleDelta[]) {
|
export function applyNodeDeltas(context: SceneSyncContext, nodes: ResourceNodeDelta[], activeSystemId?: string) {
|
||||||
for (const bubble of bubbles) {
|
|
||||||
const visual = context.bubbleVisuals.get(bubble.id);
|
|
||||||
if (!visual) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
visual.systemId = bubble.systemId;
|
|
||||||
visual.radius = bubble.radius;
|
|
||||||
visual.localPosition.copy(context.resolveBubblePosition(bubble));
|
|
||||||
visual.mesh.setPosition(visual.localPosition);
|
|
||||||
visual.mesh.setScaleScalar(Math.max(bubble.radius, 60));
|
|
||||||
context.setBubbleVisualState(visual, bubble);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function applyNodeDeltas(context: SceneSyncContext, nodes: ResourceNodeDelta[]) {
|
|
||||||
for (const node of nodes) {
|
for (const node of nodes) {
|
||||||
const visual = context.nodeVisuals.get(node.id);
|
const visual = context.nodeVisuals.get(node.id);
|
||||||
if (!visual) {
|
if (!visual) {
|
||||||
@@ -449,10 +440,13 @@ export function applyNodeDeltas(context: SceneSyncContext, nodes: ResourceNodeDe
|
|||||||
visual.orbitPhase = orbital.phase;
|
visual.orbitPhase = orbital.phase;
|
||||||
visual.orbitInclination = orbital.inclination;
|
visual.orbitInclination = orbital.inclination;
|
||||||
visual.mesh.setScaleScalar(0.8 + (node.oreRemaining / Math.max(node.maxOre, 1)) * 0.6);
|
visual.mesh.setScaleScalar(0.8 + (node.oreRemaining / Math.max(node.maxOre, 1)) * 0.6);
|
||||||
|
const isActive = visual.systemId === activeSystemId;
|
||||||
|
visual.mesh.setVisible(isActive);
|
||||||
|
visual.icon.setVisible(isActive);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function applyStationDeltas(context: SceneSyncContext, stations: StationDelta[]) {
|
export function applyStationDeltas(context: SceneSyncContext, stations: StationDelta[], activeSystemId?: string) {
|
||||||
for (const station of stations) {
|
for (const station of stations) {
|
||||||
const visual = context.stationVisuals.get(station.id);
|
const visual = context.stationVisuals.get(station.id);
|
||||||
if (!visual) {
|
if (!visual) {
|
||||||
@@ -468,10 +462,13 @@ export function applyStationDeltas(context: SceneSyncContext, stations: StationD
|
|||||||
visual.orbitInclination = orbital.inclination;
|
visual.orbitInclination = orbital.inclination;
|
||||||
visual.mesh.setColor(station.color);
|
visual.mesh.setColor(station.color);
|
||||||
visual.mesh.setEmissive(station.color, 0.1);
|
visual.mesh.setEmissive(station.color, 0.1);
|
||||||
|
const isActive = visual.systemId === activeSystemId;
|
||||||
|
visual.mesh.setVisible(isActive);
|
||||||
|
visual.icon.setVisible(isActive);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function applyClaimDeltas(context: SceneSyncContext, claims: ClaimDelta[]) {
|
export function applyClaimDeltas(context: SceneSyncContext, claims: ClaimDelta[], activeSystemId?: string) {
|
||||||
for (const claim of claims) {
|
for (const claim of claims) {
|
||||||
const visual = context.claimVisuals.get(claim.id);
|
const visual = context.claimVisuals.get(claim.id);
|
||||||
if (!visual) {
|
if (!visual) {
|
||||||
@@ -479,15 +476,19 @@ export function applyClaimDeltas(context: SceneSyncContext, claims: ClaimDelta[]
|
|||||||
}
|
}
|
||||||
|
|
||||||
visual.systemId = claim.systemId;
|
visual.systemId = claim.systemId;
|
||||||
visual.localPosition.copy(context.resolvePointPosition(claim.systemId, claim.nodeId));
|
visual.localPosition.copy(context.resolvePointPosition(claim.systemId, claim.celestialId));
|
||||||
visual.mesh.setPosition(visual.localPosition);
|
const displayPos = toSystemPos(visual.localPosition);
|
||||||
visual.icon.setPosition(visual.localPosition);
|
visual.mesh.setPosition(displayPos);
|
||||||
|
visual.icon.setPosition(displayPos);
|
||||||
visual.mesh.setColor(claim.state === "active" ? "#ff7f50" : "#ff5b5b");
|
visual.mesh.setColor(claim.state === "active" ? "#ff7f50" : "#ff5b5b");
|
||||||
visual.mesh.setEmissive(claim.state === "active" ? "#ffb27d" : "#7a2020");
|
visual.mesh.setEmissive(claim.state === "active" ? "#ffb27d" : "#7a2020");
|
||||||
|
const isActive = visual.systemId === activeSystemId;
|
||||||
|
visual.mesh.setVisible(isActive);
|
||||||
|
visual.icon.setVisible(isActive);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function applyConstructionSiteDeltas(context: SceneSyncContext, sites: ConstructionSiteDelta[]) {
|
export function applyConstructionSiteDeltas(context: SceneSyncContext, sites: ConstructionSiteDelta[], activeSystemId?: string) {
|
||||||
for (const site of sites) {
|
for (const site of sites) {
|
||||||
const visual = context.constructionSiteVisuals.get(site.id);
|
const visual = context.constructionSiteVisuals.get(site.id);
|
||||||
if (!visual) {
|
if (!visual) {
|
||||||
@@ -495,15 +496,19 @@ export function applyConstructionSiteDeltas(context: SceneSyncContext, sites: Co
|
|||||||
}
|
}
|
||||||
|
|
||||||
visual.systemId = site.systemId;
|
visual.systemId = site.systemId;
|
||||||
visual.localPosition.copy(context.resolvePointPosition(site.systemId, site.nodeId));
|
visual.localPosition.copy(context.resolvePointPosition(site.systemId, site.celestialId));
|
||||||
visual.mesh.setPosition(visual.localPosition);
|
const displayPos = toSystemPos(visual.localPosition);
|
||||||
visual.icon.setPosition(visual.localPosition);
|
visual.mesh.setPosition(displayPos);
|
||||||
|
visual.icon.setPosition(displayPos);
|
||||||
visual.mesh.setColor(site.state === "completed" ? "#46d37f" : "#9df29c");
|
visual.mesh.setColor(site.state === "completed" ? "#46d37f" : "#9df29c");
|
||||||
visual.mesh.setScaleScalar(0.75 + site.progress * 0.35);
|
visual.mesh.setScaleScalar(0.75 + site.progress * 0.35);
|
||||||
|
const isActive = visual.systemId === activeSystemId;
|
||||||
|
visual.mesh.setVisible(isActive);
|
||||||
|
visual.icon.setVisible(isActive);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function applyShipDeltas(context: SceneSyncContext, ships: ShipDelta[], tickIntervalMs: number) {
|
export function applyShipDeltas(context: SceneSyncContext, ships: ShipDelta[], tickIntervalMs: number, activeSystemId?: string) {
|
||||||
for (const ship of ships) {
|
for (const ship of ships) {
|
||||||
const visual = context.shipVisuals.get(ship.id);
|
const visual = context.shipVisuals.get(ship.id);
|
||||||
if (!visual) {
|
if (!visual) {
|
||||||
@@ -521,5 +526,8 @@ export function applyShipDeltas(context: SceneSyncContext, ships: ShipDelta[], t
|
|||||||
visual.mesh.setColor(shipColor);
|
visual.mesh.setColor(shipColor);
|
||||||
visual.mesh.setEmissive(shipColor, 0.18);
|
visual.mesh.setEmissive(shipColor, 0.18);
|
||||||
visual.icon.setColor(shipColor);
|
visual.icon.setColor(shipColor);
|
||||||
|
const isActive = visual.systemId === activeSystemId;
|
||||||
|
visual.mesh.setVisible(isActive);
|
||||||
|
visual.icon.setVisible(isActive);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ShipSnapshot, SpatialNodeSnapshot, SystemSnapshot } from "./contracts";
|
import type { CelestialSnapshot, ShipSnapshot, SystemSnapshot } from "./contracts";
|
||||||
import type {
|
import type {
|
||||||
CameraMode,
|
CameraMode,
|
||||||
OrbitalAnchor,
|
OrbitalAnchor,
|
||||||
@@ -21,11 +21,8 @@ export function describeSelectable(world: WorldState | undefined, item: Selectab
|
|||||||
if (item.kind === "node") {
|
if (item.kind === "node") {
|
||||||
return item.id;
|
return item.id;
|
||||||
}
|
}
|
||||||
if (item.kind === "spatial-node") {
|
if (item.kind === "celestial") {
|
||||||
return `${world.spatialNodes.get(item.id)?.kind ?? "node"} ${item.id}`;
|
return `${world.celestials.get(item.id)?.kind ?? "celestial"} ${item.id}`;
|
||||||
}
|
|
||||||
if (item.kind === "bubble") {
|
|
||||||
return `bubble ${item.id}`;
|
|
||||||
}
|
}
|
||||||
if (item.kind === "claim") {
|
if (item.kind === "claim") {
|
||||||
return `claim ${item.id}`;
|
return `claim ${item.id}`;
|
||||||
@@ -53,7 +50,29 @@ export function describeHoverLabel(world: WorldState | undefined, item: Selectab
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (item.kind === "system") {
|
if (item.kind === "system") {
|
||||||
return world.systems.get(item.id)?.label ?? item.id;
|
const system = world.systems.get(item.id);
|
||||||
|
if (!system) {
|
||||||
|
return item.id;
|
||||||
|
}
|
||||||
|
const starLabel = system.starCount > 1 ? `${system.starCount}× ${system.starKind}` : system.starKind;
|
||||||
|
const planetCount = system.planets.length;
|
||||||
|
const shipCount = [...world.ships.values()].filter((s) => s.systemId === item.id).length;
|
||||||
|
const stationCount = [...world.stations.values()].filter((s) => s.systemId === item.id).length;
|
||||||
|
const lines = [
|
||||||
|
system.label,
|
||||||
|
`${starLabel} · ${planetCount} planet${planetCount !== 1 ? "s" : ""}`,
|
||||||
|
];
|
||||||
|
const parts: string[] = [];
|
||||||
|
if (shipCount > 0) {
|
||||||
|
parts.push(`${shipCount} ship${shipCount !== 1 ? "s" : ""}`);
|
||||||
|
}
|
||||||
|
if (stationCount > 0) {
|
||||||
|
parts.push(`${stationCount} station${stationCount !== 1 ? "s" : ""}`);
|
||||||
|
}
|
||||||
|
if (parts.length > 0) {
|
||||||
|
lines.push(parts.join(" · "));
|
||||||
|
}
|
||||||
|
return lines.join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.kind === "planet") {
|
if (item.kind === "planet") {
|
||||||
@@ -68,46 +87,38 @@ export function describeHoverLabel(world: WorldState | undefined, item: Selectab
|
|||||||
return item.id;
|
return item.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
const anchorPath = node.anchorNodeId
|
const anchorPath = node.celestialId
|
||||||
? describeSpatialNodePathWithinSystem(world, node.systemId, node.anchorNodeId)
|
? describeCelestialPathWithinSystem(world, node.systemId, node.celestialId)
|
||||||
: undefined;
|
: undefined;
|
||||||
return anchorPath ? `${anchorPath} / ${node.itemId}` : `${node.systemId} / ${node.itemId}`;
|
return anchorPath ? `${anchorPath} / ${node.itemId}` : `${node.systemId} / ${node.itemId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.kind === "spatial-node") {
|
if (item.kind === "celestial") {
|
||||||
const node = world.spatialNodes.get(item.id);
|
const celestial = world.celestials.get(item.id);
|
||||||
if (!node) {
|
if (!celestial) {
|
||||||
return item.id;
|
return item.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node.kind === "star") {
|
if (celestial.kind === "star") {
|
||||||
const system = world.systems.get(node.systemId);
|
const system = world.systems.get(celestial.systemId);
|
||||||
return system ? `${system.label} star` : `${node.systemId} star`;
|
return system ? `${system.label} star` : `${celestial.systemId} star`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return describeSpatialNodePathWithinSystem(world, node.systemId, node.id) ?? `${node.systemId} / ${node.kind}`;
|
return describeCelestialPathWithinSystem(world, celestial.systemId, celestial.id) ?? `${celestial.systemId} / ${celestial.kind}`;
|
||||||
}
|
|
||||||
|
|
||||||
if (item.kind === "bubble") {
|
|
||||||
const bubble = world.localBubbles.get(item.id);
|
|
||||||
const anchorPath = bubble?.nodeId
|
|
||||||
? describeSpatialNodePathWithinSystem(world, bubble.systemId, bubble.nodeId)
|
|
||||||
: undefined;
|
|
||||||
return anchorPath ? `${anchorPath} bubble` : `Bubble ${item.id}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.kind === "claim") {
|
if (item.kind === "claim") {
|
||||||
const claim = world.claims.get(item.id);
|
const claim = world.claims.get(item.id);
|
||||||
const anchorPath = claim?.nodeId
|
const anchorPath = claim?.celestialId
|
||||||
? describeSpatialNodePathWithinSystem(world, claim.systemId, claim.nodeId)
|
? describeCelestialPathWithinSystem(world, claim.systemId, claim.celestialId)
|
||||||
: undefined;
|
: undefined;
|
||||||
return anchorPath ? `${anchorPath} claim` : `Claim ${item.id}`;
|
return anchorPath ? `${anchorPath} claim` : `Claim ${item.id}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.kind === "construction-site") {
|
if (item.kind === "construction-site") {
|
||||||
const site = world.constructionSites.get(item.id);
|
const site = world.constructionSites.get(item.id);
|
||||||
const anchorPath = site?.nodeId
|
const anchorPath = site?.celestialId
|
||||||
? describeSpatialNodePathWithinSystem(world, site.systemId, site.nodeId)
|
? describeCelestialPathWithinSystem(world, site.systemId, site.celestialId)
|
||||||
: undefined;
|
: undefined;
|
||||||
const siteLabel = site ? (site.blueprintId ?? site.targetDefinitionId) : item.id;
|
const siteLabel = site ? (site.blueprintId ?? site.targetDefinitionId) : item.id;
|
||||||
return anchorPath ? `${anchorPath} / ${siteLabel}` : `Construction ${siteLabel}`;
|
return anchorPath ? `${anchorPath} / ${siteLabel}` : `Construction ${siteLabel}`;
|
||||||
@@ -123,8 +134,6 @@ export function getSelectionGroup(item: Selectable): SelectionGroup {
|
|||||||
if (
|
if (
|
||||||
item.kind === "station"
|
item.kind === "station"
|
||||||
|| item.kind === "node"
|
|| item.kind === "node"
|
||||||
|| item.kind === "spatial-node"
|
|
||||||
|| item.kind === "bubble"
|
|
||||||
|| item.kind === "claim"
|
|| item.kind === "claim"
|
||||||
|| item.kind === "construction-site"
|
|| item.kind === "construction-site"
|
||||||
) {
|
) {
|
||||||
@@ -147,11 +156,8 @@ export function resolveSelectableSystemId(world: WorldState | undefined, selecti
|
|||||||
if (selection.kind === "node") {
|
if (selection.kind === "node") {
|
||||||
return world.nodes.get(selection.id)?.systemId;
|
return world.nodes.get(selection.id)?.systemId;
|
||||||
}
|
}
|
||||||
if (selection.kind === "spatial-node") {
|
if (selection.kind === "celestial") {
|
||||||
return world.spatialNodes.get(selection.id)?.systemId;
|
return world.celestials.get(selection.id)?.systemId;
|
||||||
}
|
|
||||||
if (selection.kind === "bubble") {
|
|
||||||
return world.localBubbles.get(selection.id)?.systemId;
|
|
||||||
}
|
}
|
||||||
if (selection.kind === "claim") {
|
if (selection.kind === "claim") {
|
||||||
return world.claims.get(selection.id)?.systemId;
|
return world.claims.get(selection.id)?.systemId;
|
||||||
@@ -165,29 +171,26 @@ export function resolveSelectableSystemId(world: WorldState | undefined, selecti
|
|||||||
return selection.id;
|
return selection.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveFocusedBubbleId(world: WorldState | undefined, selectedItems: Selectable[]): string | undefined {
|
export function resolveFocusedCelestialId(world: WorldState | undefined, selectedItems: Selectable[]): string | undefined {
|
||||||
if (!world || selectedItems.length !== 1) {
|
if (!world || selectedItems.length !== 1) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const selected = selectedItems[0];
|
const selected = selectedItems[0];
|
||||||
if (selected.kind === "bubble") {
|
if (selected.kind === "celestial") {
|
||||||
return selected.id;
|
return selected.id;
|
||||||
}
|
}
|
||||||
if (selected.kind === "ship") {
|
if (selected.kind === "ship") {
|
||||||
return world.ships.get(selected.id)?.bubbleId ?? world.ships.get(selected.id)?.spatialState.currentBubbleId ?? undefined;
|
return world.ships.get(selected.id)?.spatialState.currentCelestialId ?? world.ships.get(selected.id)?.celestialId ?? undefined;
|
||||||
}
|
}
|
||||||
if (selected.kind === "station") {
|
if (selected.kind === "station") {
|
||||||
return world.stations.get(selected.id)?.bubbleId ?? undefined;
|
return world.stations.get(selected.id)?.celestialId ?? undefined;
|
||||||
}
|
|
||||||
if (selected.kind === "spatial-node") {
|
|
||||||
return world.spatialNodes.get(selected.id)?.bubbleId ?? undefined;
|
|
||||||
}
|
}
|
||||||
if (selected.kind === "claim") {
|
if (selected.kind === "claim") {
|
||||||
return world.claims.get(selected.id)?.bubbleId ?? undefined;
|
return world.claims.get(selected.id)?.celestialId ?? undefined;
|
||||||
}
|
}
|
||||||
if (selected.kind === "construction-site") {
|
if (selected.kind === "construction-site") {
|
||||||
return world.constructionSites.get(selected.id)?.bubbleId ?? undefined;
|
return world.constructionSites.get(selected.id)?.celestialId ?? undefined;
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -232,8 +235,7 @@ export function renderSystemDetails(
|
|||||||
let shipCount = 0;
|
let shipCount = 0;
|
||||||
let stationCount = 0;
|
let stationCount = 0;
|
||||||
let nodeCount = 0;
|
let nodeCount = 0;
|
||||||
let spatialNodeCount = 0;
|
let celestialCount = 0;
|
||||||
let bubbleCount = 0;
|
|
||||||
let claimCount = 0;
|
let claimCount = 0;
|
||||||
let constructionCount = 0;
|
let constructionCount = 0;
|
||||||
let moonCount = 0;
|
let moonCount = 0;
|
||||||
@@ -253,14 +255,9 @@ export function renderSystemDetails(
|
|||||||
nodeCount += 1;
|
nodeCount += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const node of world.spatialNodes.values()) {
|
for (const celestial of world.celestials.values()) {
|
||||||
if (node.systemId === system.id) {
|
if (celestial.systemId === system.id) {
|
||||||
spatialNodeCount += 1;
|
celestialCount += 1;
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const bubble of world.localBubbles.values()) {
|
|
||||||
if (bubble.systemId === system.id) {
|
|
||||||
bubbleCount += 1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const claim of world.claims.values()) {
|
for (const claim of world.claims.values()) {
|
||||||
@@ -285,7 +282,7 @@ export function renderSystemDetails(
|
|||||||
<p>${system.id}${activeContext ? " · active system" : ""}</p>
|
<p>${system.id}${activeContext ? " · active system" : ""}</p>
|
||||||
<p>${system.starKind} · ${system.starCount} star${system.starCount > 1 ? "s" : ""}</p>
|
<p>${system.starKind} · ${system.starCount} star${system.starCount > 1 ? "s" : ""}</p>
|
||||||
<p>Planets ${system.planets.length}<br>Moons ${moonCount}<br>Ships ${shipCount}<br>Stations ${stationCount}</p>
|
<p>Planets ${system.planets.length}<br>Moons ${moonCount}<br>Ships ${shipCount}<br>Stations ${stationCount}</p>
|
||||||
<p>Spatial nodes ${spatialNodeCount}<br>Resource nodes ${nodeCount}<br>Bubbles ${bubbleCount}</p>
|
<p>Celestials ${celestialCount}<br>Resource nodes ${nodeCount}</p>
|
||||||
<p>Claims ${claimCount}<br>Construction sites ${constructionCount}</p>
|
<p>Claims ${claimCount}<br>Construction sites ${constructionCount}</p>
|
||||||
<p>Height ${formatGalaxyDistance(system.galaxyPosition.y)}</p>
|
<p>Height ${formatGalaxyDistance(system.galaxyPosition.y)}</p>
|
||||||
<p>${system.planets.slice(0, 8).map((planet) => `${planet.label} (${planet.planetType})`).join("<br>")}</p>
|
<p>${system.planets.slice(0, 8).map((planet) => `${planet.label} (${planet.planetType})`).join("<br>")}</p>
|
||||||
@@ -308,18 +305,18 @@ export function describeShipState(world: WorldState | undefined, ship: ShipSnaps
|
|||||||
return baseState;
|
return baseState;
|
||||||
}
|
}
|
||||||
|
|
||||||
const destinationNode = world.spatialNodes.get(destinationNodeId);
|
const destinationCelestial = world.celestials.get(destinationNodeId);
|
||||||
if (!destinationNode) {
|
if (!destinationCelestial) {
|
||||||
return `${baseState} -> ${destinationNodeId}`;
|
return `${baseState} -> ${destinationNodeId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (baseState === "warping" || baseState === "spooling-warp") {
|
if (baseState === "warping" || baseState === "spooling-warp") {
|
||||||
const destinationPath = describeSpatialNodePathWithinSystem(world, destinationNode.systemId, destinationNodeId);
|
const destinationPath = describeCelestialPathWithinSystem(world, destinationCelestial.systemId, destinationNodeId);
|
||||||
return `${baseState} -> ${destinationPath ?? destinationNodeId}`;
|
return `${baseState} -> ${destinationPath ?? destinationNodeId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const destinationSystem = world.systems.get(destinationNode.systemId);
|
const destinationSystem = world.systems.get(destinationCelestial.systemId);
|
||||||
return `${baseState} -> ${destinationSystem?.label ?? destinationNode.systemId}`;
|
return `${baseState} -> ${destinationSystem?.label ?? destinationCelestial.systemId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function describeControllerTask(taskKind: string): string {
|
function describeControllerTask(taskKind: string): string {
|
||||||
@@ -381,8 +378,8 @@ export function describeShipLocation(world: WorldState | undefined, ship: ShipSn
|
|||||||
if (ship.dockedStationId) {
|
if (ship.dockedStationId) {
|
||||||
const station = world.stations.get(ship.dockedStationId);
|
const station = world.stations.get(ship.dockedStationId);
|
||||||
if (station) {
|
if (station) {
|
||||||
const anchorPath = station.anchorNodeId
|
const anchorPath = station.celestialId
|
||||||
? describeSpatialNodePathWithinSystem(world, station.systemId, station.anchorNodeId)
|
? describeCelestialPathWithinSystem(world, station.systemId, station.celestialId)
|
||||||
: undefined;
|
: undefined;
|
||||||
return {
|
return {
|
||||||
system: systemLabel,
|
system: systemLabel,
|
||||||
@@ -391,22 +388,11 @@ export function describeShipLocation(world: WorldState | undefined, ship: ShipSn
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentNodeId = ship.spatialState.currentNodeId ?? ship.nodeId;
|
const currentCelestialId = ship.spatialState.currentCelestialId ?? ship.celestialId;
|
||||||
if (currentNodeId) {
|
if (currentCelestialId) {
|
||||||
const nodePath = describeSpatialNodePathWithinSystem(world, systemId, currentNodeId);
|
const celestialPath = describeCelestialPathWithinSystem(world, systemId, currentCelestialId);
|
||||||
if (nodePath) {
|
if (celestialPath) {
|
||||||
return { system: systemLabel, local: nodePath };
|
return { system: systemLabel, local: celestialPath };
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentBubbleId = ship.spatialState.currentBubbleId ?? ship.bubbleId;
|
|
||||||
if (currentBubbleId) {
|
|
||||||
const bubble = world.localBubbles.get(currentBubbleId);
|
|
||||||
if (bubble?.nodeId) {
|
|
||||||
const nodePath = describeSpatialNodePathWithinSystem(world, systemId, bubble.nodeId);
|
|
||||||
if (nodePath) {
|
|
||||||
return { system: systemLabel, local: nodePath };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -415,11 +401,11 @@ export function describeShipLocation(world: WorldState | undefined, ship: ShipSn
|
|||||||
|
|
||||||
export function describeActiveSpace(
|
export function describeActiveSpace(
|
||||||
world: WorldState | undefined,
|
world: WorldState | undefined,
|
||||||
zoomLevel: "local" | "system" | "universe",
|
povLevel: "local" | "system" | "galaxy",
|
||||||
activeSystemId: string | undefined,
|
activeSystemId: string | undefined,
|
||||||
selectedItems: Selectable[],
|
selectedItems: Selectable[],
|
||||||
): string {
|
): string {
|
||||||
if (!world || zoomLevel === "universe") {
|
if (!world || povLevel === "galaxy") {
|
||||||
return "deep-space";
|
return "deep-space";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -428,16 +414,13 @@ export function describeActiveSpace(
|
|||||||
return "deep-space";
|
return "deep-space";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (zoomLevel !== "local") {
|
if (povLevel !== "local") {
|
||||||
return activeSystem.label;
|
return activeSystem.label;
|
||||||
}
|
}
|
||||||
|
|
||||||
const bubbleId = resolveFocusedBubbleId(world, selectedItems);
|
const celestialId = resolveFocusedCelestialId(world, selectedItems);
|
||||||
if (bubbleId) {
|
if (celestialId) {
|
||||||
const bubble = world.localBubbles.get(bubbleId);
|
const localPath = describeCelestialPathWithinSystem(world, activeSystem.id, celestialId);
|
||||||
const localPath = bubble?.nodeId
|
|
||||||
? describeSpatialNodePathWithinSystem(world, activeSystem.id, bubble.nodeId)
|
|
||||||
: undefined;
|
|
||||||
return localPath
|
return localPath
|
||||||
? `${activeSystem.label} / ${localPath}`
|
? `${activeSystem.label} / ${localPath}`
|
||||||
: activeSystem.label;
|
: activeSystem.label;
|
||||||
@@ -454,51 +437,43 @@ export function describeActiveSpace(
|
|||||||
return activeSystem.label;
|
return activeSystem.label;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function describeSpatialNodePathWithinSystem(world: WorldState, systemId: string, nodeId: string): string | undefined {
|
export function describeCelestialPathWithinSystem(world: WorldState, systemId: string, celestialId: string): string | undefined {
|
||||||
const node = world.spatialNodes.get(nodeId);
|
const celestial = world.celestials.get(celestialId);
|
||||||
const system = world.systems.get(systemId);
|
const system = world.systems.get(systemId);
|
||||||
if (!node || !system) {
|
if (!celestial || !system) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node.parentNodeId) {
|
if (celestial.parentNodeId) {
|
||||||
const parentPath = describeSpatialNodePathWithinSystem(world, systemId, node.parentNodeId);
|
const parentPath = describeCelestialPathWithinSystem(world, systemId, celestial.parentNodeId);
|
||||||
const segment = describeSpatialNodeSegment(world, system, node);
|
const segment = describeCelestialSegment(system, celestial);
|
||||||
return parentPath ? `${parentPath}/${segment}` : segment;
|
return parentPath ? `${parentPath}/${segment}` : segment;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node.kind === "star") {
|
if (celestial.kind === "star") {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return describeSpatialNodeSegment(world, system, node);
|
return describeCelestialSegment(system, celestial);
|
||||||
}
|
}
|
||||||
|
|
||||||
function describeSpatialNodeSegment(world: WorldState, system: SystemSnapshot, node: SpatialNodeSnapshot): string {
|
function describeCelestialSegment(system: SystemSnapshot, celestial: CelestialSnapshot): string {
|
||||||
const moonMatch = node.id.match(/-planet-(\d+)-moon-(\d+)$/);
|
const moonMatch = celestial.id.match(/-planet-(\d+)-moon-(\d+)$/);
|
||||||
if (moonMatch) {
|
if (moonMatch) {
|
||||||
const moonIndex = Number.parseInt(moonMatch[2], 10);
|
const moonIndex = Number.parseInt(moonMatch[2], 10);
|
||||||
return `Moon ${moonIndex}`;
|
return `Moon ${moonIndex}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lagrangeMatch = node.id.match(/-planet-\d+-(l[1-5])$/);
|
const lagrangeMatch = celestial.id.match(/-planet-\d+-(l[1-5])$/);
|
||||||
if (lagrangeMatch) {
|
if (lagrangeMatch) {
|
||||||
return lagrangeMatch[1].toUpperCase();
|
return lagrangeMatch[1].toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
const planetMatch = node.id.match(/-planet-(\d+)$/);
|
const planetMatch = celestial.id.match(/-planet-(\d+)$/);
|
||||||
if (planetMatch) {
|
if (planetMatch) {
|
||||||
const planetIndex = Number.parseInt(planetMatch[1], 10) - 1;
|
const planetIndex = Number.parseInt(planetMatch[1], 10) - 1;
|
||||||
return system.planets[planetIndex]?.label ?? `Planet ${planetMatch[1]}`;
|
return system.planets[planetIndex]?.label ?? `Planet ${planetMatch[1]}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node.kind === "station" && node.occupyingStructureId) {
|
return celestial.orbitReferenceId ?? celestial.kind;
|
||||||
return world.stations.get(node.occupyingStructureId)?.label ?? node.occupyingStructureId;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node.kind === "resource-site") {
|
|
||||||
return node.orbitReferenceId ?? "Resource Site";
|
|
||||||
}
|
|
||||||
|
|
||||||
return node.orbitReferenceId ?? node.kind;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,8 +40,7 @@ export function createWorldState(snapshot: WorldSnapshot): WorldState {
|
|||||||
orbitalSimulation: snapshot.orbitalSimulation,
|
orbitalSimulation: snapshot.orbitalSimulation,
|
||||||
generatedAtUtc: snapshot.generatedAtUtc,
|
generatedAtUtc: snapshot.generatedAtUtc,
|
||||||
systems: new Map(snapshot.systems.map((system) => [system.id, system])),
|
systems: new Map(snapshot.systems.map((system) => [system.id, system])),
|
||||||
spatialNodes: new Map(snapshot.spatialNodes.map((node) => [node.id, node])),
|
celestials: new Map(snapshot.celestials.map((celestial) => [celestial.id, celestial])),
|
||||||
localBubbles: new Map(snapshot.localBubbles.map((bubble) => [bubble.id, bubble])),
|
|
||||||
nodes: new Map(snapshot.nodes.map((node) => [node.id, node])),
|
nodes: new Map(snapshot.nodes.map((node) => [node.id, node])),
|
||||||
stations: new Map(snapshot.stations.map((station) => [station.id, station])),
|
stations: new Map(snapshot.stations.map((station) => [station.id, station])),
|
||||||
claims: new Map(snapshot.claims.map((claim) => [claim.id, claim])),
|
claims: new Map(snapshot.claims.map((claim) => [claim.id, claim])),
|
||||||
@@ -62,11 +61,8 @@ export function applyDeltaToWorld(world: WorldState, delta: WorldDelta): boolean
|
|||||||
world.generatedAtUtc = delta.generatedAtUtc;
|
world.generatedAtUtc = delta.generatedAtUtc;
|
||||||
world.recentEvents = [...delta.events, ...world.recentEvents].slice(0, 18);
|
world.recentEvents = [...delta.events, ...world.recentEvents].slice(0, 18);
|
||||||
|
|
||||||
for (const node of delta.spatialNodes) {
|
for (const celestial of delta.celestials) {
|
||||||
world.spatialNodes.set(node.id, node);
|
world.celestials.set(celestial.id, celestial);
|
||||||
}
|
|
||||||
for (const bubble of delta.localBubbles) {
|
|
||||||
world.localBubbles.set(bubble.id, bubble);
|
|
||||||
}
|
}
|
||||||
for (const node of delta.nodes) {
|
for (const node of delta.nodes) {
|
||||||
world.nodes.set(node.id, node);
|
world.nodes.set(node.id, node);
|
||||||
@@ -100,8 +96,7 @@ export function recordDeltaStats(networkStats: NetworkStats, delta: WorldDelta,
|
|||||||
const changedEntities = delta.ships.length
|
const changedEntities = delta.ships.length
|
||||||
+ delta.stations.length
|
+ delta.stations.length
|
||||||
+ delta.nodes.length
|
+ delta.nodes.length
|
||||||
+ delta.spatialNodes.length
|
+ delta.celestials.length
|
||||||
+ delta.localBubbles.length
|
|
||||||
+ delta.claims.length
|
+ delta.claims.length
|
||||||
+ delta.constructionSites.length
|
+ delta.constructionSites.length
|
||||||
+ delta.marketOrders.length
|
+ delta.marketOrders.length
|
||||||
|
|||||||
47
apps/viewer/src/viewerSystemLayer.ts
Normal file
47
apps/viewer/src/viewerSystemLayer.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import * as THREE from "three";
|
||||||
|
import type { Selectable } from "./viewerTypes";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* System rendering layer.
|
||||||
|
* Scene coordinate unit: km * DISPLAY_UNITS_PER_KILOMETER * ACTIVE_SYSTEM_DETAIL_SCALE.
|
||||||
|
* Camera far plane covers a solar system.
|
||||||
|
* Only the active system's objects are visible; inactive system objects are hidden in place.
|
||||||
|
*/
|
||||||
|
export class SystemLayer {
|
||||||
|
readonly scene = new THREE.Scene();
|
||||||
|
readonly camera = new THREE.PerspectiveCamera(50, 1, 0.1, 50000);
|
||||||
|
|
||||||
|
readonly celestialGroup = new THREE.Group();
|
||||||
|
readonly nodeGroup = new THREE.Group();
|
||||||
|
readonly stationGroup = new THREE.Group();
|
||||||
|
readonly claimGroup = new THREE.Group();
|
||||||
|
readonly constructionSiteGroup = new THREE.Group();
|
||||||
|
readonly shipGroup = new THREE.Group();
|
||||||
|
|
||||||
|
readonly selectableTargets = new Map<THREE.Object3D, Selectable>();
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.scene.add(new THREE.AmbientLight(0x90a6c0, 0.55));
|
||||||
|
const keyLight = new THREE.DirectionalLight(0xdcecff, 1.3);
|
||||||
|
keyLight.position.set(1000, 1200, 800);
|
||||||
|
this.scene.add(keyLight);
|
||||||
|
this.scene.add(
|
||||||
|
this.celestialGroup,
|
||||||
|
this.nodeGroup,
|
||||||
|
this.stationGroup,
|
||||||
|
this.claimGroup,
|
||||||
|
this.constructionSiteGroup,
|
||||||
|
this.shipGroup,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCamera(systemFocus: THREE.Vector3, orbitOffset: THREE.Vector3) {
|
||||||
|
this.camera.position.copy(systemFocus).add(orbitOffset);
|
||||||
|
this.camera.lookAt(systemFocus);
|
||||||
|
}
|
||||||
|
|
||||||
|
onResize(aspect: number) {
|
||||||
|
this.camera.aspect = aspect;
|
||||||
|
this.camera.updateProjectionMatrix();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,23 +1,22 @@
|
|||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import type { SceneNode } from "./viewerScenePrimitives";
|
import type { SceneNode } from "./viewerScenePrimitives";
|
||||||
import type {
|
import type {
|
||||||
|
CelestialSnapshot,
|
||||||
ClaimSnapshot,
|
ClaimSnapshot,
|
||||||
ConstructionSiteSnapshot,
|
ConstructionSiteSnapshot,
|
||||||
FactionSnapshot,
|
FactionSnapshot,
|
||||||
LocalBubbleSnapshot,
|
|
||||||
MarketOrderSnapshot,
|
MarketOrderSnapshot,
|
||||||
PlanetSnapshot,
|
PlanetSnapshot,
|
||||||
PolicySetSnapshot,
|
PolicySetSnapshot,
|
||||||
ResourceNodeSnapshot,
|
ResourceNodeSnapshot,
|
||||||
ShipSnapshot,
|
ShipSnapshot,
|
||||||
SimulationEventRecord,
|
SimulationEventRecord,
|
||||||
SpatialNodeSnapshot,
|
|
||||||
StationSnapshot,
|
StationSnapshot,
|
||||||
SystemSnapshot,
|
SystemSnapshot,
|
||||||
OrbitalSimulationSnapshot,
|
OrbitalSimulationSnapshot,
|
||||||
} from "./contracts";
|
} from "./contracts";
|
||||||
|
|
||||||
export type ZoomLevel = "local" | "system" | "universe";
|
export type PovLevel = "local" | "system" | "galaxy";
|
||||||
export type SelectionGroup = "ships" | "structures" | "celestials";
|
export type SelectionGroup = "ships" | "structures" | "celestials";
|
||||||
export type DragMode = "orbit" | "marquee";
|
export type DragMode = "orbit" | "marquee";
|
||||||
export type CameraMode = "tactical" | "follow";
|
export type CameraMode = "tactical" | "follow";
|
||||||
@@ -26,8 +25,7 @@ export type Selectable =
|
|||||||
| { kind: "ship"; id: string }
|
| { kind: "ship"; id: string }
|
||||||
| { kind: "station"; id: string }
|
| { kind: "station"; id: string }
|
||||||
| { kind: "node"; id: string }
|
| { kind: "node"; id: string }
|
||||||
| { kind: "spatial-node"; id: string }
|
| { kind: "celestial"; id: string }
|
||||||
| { kind: "bubble"; id: string }
|
|
||||||
| { kind: "claim"; id: string }
|
| { kind: "claim"; id: string }
|
||||||
| { kind: "construction-site"; id: string }
|
| { kind: "construction-site"; id: string }
|
||||||
| { kind: "system"; id: string }
|
| { kind: "system"; id: string }
|
||||||
@@ -86,26 +84,18 @@ export interface NodeVisual {
|
|||||||
orbitInclination: number;
|
orbitInclination: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SpatialNodeVisual {
|
export interface CelestialVisual {
|
||||||
id: string;
|
id: string;
|
||||||
systemId: string;
|
systemId: string;
|
||||||
mesh: SceneNode;
|
mesh: SceneNode;
|
||||||
icon: SceneNode;
|
icon: SceneNode;
|
||||||
kind: string;
|
kind: string;
|
||||||
localPosition: THREE.Vector3;
|
orbitalAnchor: THREE.Vector3;
|
||||||
}
|
|
||||||
|
|
||||||
export interface BubbleVisual {
|
|
||||||
id: string;
|
|
||||||
systemId: string;
|
|
||||||
mesh: SceneNode;
|
|
||||||
localPosition: THREE.Vector3;
|
|
||||||
radius: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ClaimVisual {
|
export interface ClaimVisual {
|
||||||
id: string;
|
id: string;
|
||||||
nodeId: string;
|
celestialId: string;
|
||||||
systemId: string;
|
systemId: string;
|
||||||
mesh: SceneNode;
|
mesh: SceneNode;
|
||||||
icon: SceneNode;
|
icon: SceneNode;
|
||||||
@@ -114,7 +104,7 @@ export interface ClaimVisual {
|
|||||||
|
|
||||||
export interface ConstructionSiteVisual {
|
export interface ConstructionSiteVisual {
|
||||||
id: string;
|
id: string;
|
||||||
nodeId: string;
|
celestialId: string;
|
||||||
systemId: string;
|
systemId: string;
|
||||||
mesh: SceneNode;
|
mesh: SceneNode;
|
||||||
icon: SceneNode;
|
icon: SceneNode;
|
||||||
@@ -134,13 +124,13 @@ export interface StructureVisual {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface SystemVisual {
|
export interface SystemVisual {
|
||||||
root: SceneNode;
|
galaxyRoot: SceneNode; // lives in galaxyScene (star dot + shell reticle)
|
||||||
|
systemRoot: SceneNode; // added/removed from systemScene when system becomes active/inactive
|
||||||
starCluster: SceneNode;
|
starCluster: SceneNode;
|
||||||
icon: SceneNode;
|
icon: SceneNode; // star dot sprite (child of galaxyRoot)
|
||||||
shellReticle: SceneNode;
|
shellReticle: SceneNode; // reticle sprite (child of galaxyRoot)
|
||||||
shellReticleBaseScale: number;
|
shellReticleBaseScale: number;
|
||||||
detailGroup: SceneNode;
|
detailGroup: SceneNode; // planets + moons (child of systemRoot)
|
||||||
summary: SystemSummaryVisual;
|
|
||||||
galaxyPosition: THREE.Vector3;
|
galaxyPosition: THREE.Vector3;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,8 +143,7 @@ export interface WorldState {
|
|||||||
orbitalSimulation: OrbitalSimulationSnapshot;
|
orbitalSimulation: OrbitalSimulationSnapshot;
|
||||||
generatedAtUtc: string;
|
generatedAtUtc: string;
|
||||||
systems: Map<string, SystemSnapshot>;
|
systems: Map<string, SystemSnapshot>;
|
||||||
spatialNodes: Map<string, SpatialNodeSnapshot>;
|
celestials: Map<string, CelestialSnapshot>;
|
||||||
localBubbles: Map<string, LocalBubbleSnapshot>;
|
|
||||||
nodes: Map<string, ResourceNodeSnapshot>;
|
nodes: Map<string, ResourceNodeSnapshot>;
|
||||||
stations: Map<string, StationSnapshot>;
|
stations: Map<string, StationSnapshot>;
|
||||||
claims: Map<string, ClaimSnapshot>;
|
claims: Map<string, ClaimSnapshot>;
|
||||||
@@ -195,20 +184,6 @@ export interface PerformanceStats {
|
|||||||
lastPanelUpdateAtMs: number;
|
lastPanelUpdateAtMs: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PresentationEntry {
|
|
||||||
detail: SceneNode;
|
|
||||||
icon: SceneNode;
|
|
||||||
systemId?: string;
|
|
||||||
hideDetailInUniverse?: boolean;
|
|
||||||
hideIconInUniverse?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SystemSummaryVisual {
|
|
||||||
sprite: SceneNode;
|
|
||||||
texture: THREE.CanvasTexture;
|
|
||||||
anchor: THREE.Vector3;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface HistoryWindowState {
|
export interface HistoryWindowState {
|
||||||
id: string;
|
id: string;
|
||||||
target: Selectable;
|
target: Selectable;
|
||||||
|
|||||||
25
apps/viewer/src/viewerUniverseLayer.ts
Normal file
25
apps/viewer/src/viewerUniverseLayer.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import * as THREE from "three";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Universe rendering layer — always the first layer rendered.
|
||||||
|
* Contains the infinite backdrop: backdrop stars and nebula clouds.
|
||||||
|
* Has no dedicated camera; rendered with whichever camera is active for the current POV
|
||||||
|
* so the backdrop always aligns with the foreground view.
|
||||||
|
*/
|
||||||
|
export class UniverseLayer {
|
||||||
|
readonly scene = new THREE.Scene();
|
||||||
|
|
||||||
|
/** Backdrop stars and nebula clouds. Follows the active camera to act as a skybox. */
|
||||||
|
readonly ambienceGroup = new THREE.Group();
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.scene.background = new THREE.Color(0x040912);
|
||||||
|
this.scene.add(this.ambienceGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAmbience(activeCamera: THREE.Camera, delta: number) {
|
||||||
|
this.ambienceGroup.position.copy(activeCamera.position);
|
||||||
|
this.ambienceGroup.rotation.y += delta * 0.005;
|
||||||
|
this.ambienceGroup.rotation.x = Math.sin(performance.now() * 0.00003) * 0.015;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,19 +3,17 @@ import { renderOpsStrip } from "./viewerOpsStrip";
|
|||||||
import { updateDetailPanel } from "./viewerPanels";
|
import { updateDetailPanel } from "./viewerPanels";
|
||||||
import { applyDeltaToWorld, cloneFactions, createWorldState, recordDeltaStats } from "./viewerState";
|
import { applyDeltaToWorld, cloneFactions, createWorldState, recordDeltaStats } from "./viewerState";
|
||||||
import type {
|
import type {
|
||||||
|
CelestialDelta,
|
||||||
|
CelestialSnapshot,
|
||||||
ClaimDelta,
|
ClaimDelta,
|
||||||
ClaimSnapshot,
|
ClaimSnapshot,
|
||||||
ConstructionSiteDelta,
|
ConstructionSiteDelta,
|
||||||
ConstructionSiteSnapshot,
|
ConstructionSiteSnapshot,
|
||||||
FactionSnapshot,
|
FactionSnapshot,
|
||||||
LocalBubbleDelta,
|
|
||||||
LocalBubbleSnapshot,
|
|
||||||
ResourceNodeDelta,
|
ResourceNodeDelta,
|
||||||
ResourceNodeSnapshot,
|
ResourceNodeSnapshot,
|
||||||
ShipDelta,
|
ShipDelta,
|
||||||
ShipSnapshot,
|
ShipSnapshot,
|
||||||
SpatialNodeDelta,
|
|
||||||
SpatialNodeSnapshot,
|
|
||||||
StationDelta,
|
StationDelta,
|
||||||
StationSnapshot,
|
StationSnapshot,
|
||||||
SystemSnapshot,
|
SystemSnapshot,
|
||||||
@@ -27,7 +25,7 @@ import type {
|
|||||||
NetworkStats,
|
NetworkStats,
|
||||||
Selectable,
|
Selectable,
|
||||||
WorldState,
|
WorldState,
|
||||||
ZoomLevel,
|
PovLevel,
|
||||||
} from "./viewerTypes";
|
} from "./viewerTypes";
|
||||||
|
|
||||||
export interface ViewerWorldLifecycleContext {
|
export interface ViewerWorldLifecycleContext {
|
||||||
@@ -41,7 +39,7 @@ export interface ViewerWorldLifecycleContext {
|
|||||||
setStream: (stream: EventSource | undefined) => void;
|
setStream: (stream: EventSource | undefined) => void;
|
||||||
getCurrentStreamScopeKey: () => string;
|
getCurrentStreamScopeKey: () => string;
|
||||||
setCurrentStreamScopeKey: (value: string) => void;
|
setCurrentStreamScopeKey: (value: string) => void;
|
||||||
getZoomLevel: () => ZoomLevel;
|
getPovLevel: () => PovLevel;
|
||||||
getActiveSystemId: () => string | undefined;
|
getActiveSystemId: () => string | undefined;
|
||||||
getSelectedItems: () => Selectable[];
|
getSelectedItems: () => Selectable[];
|
||||||
getCameraMode: () => CameraMode;
|
getCameraMode: () => CameraMode;
|
||||||
@@ -54,22 +52,20 @@ export interface ViewerWorldLifecycleContext {
|
|||||||
detailBodyEl: HTMLDivElement;
|
detailBodyEl: HTMLDivElement;
|
||||||
worldLabel: () => string;
|
worldLabel: () => string;
|
||||||
rebuildSystems: (systems: SystemSnapshot[]) => void;
|
rebuildSystems: (systems: SystemSnapshot[]) => void;
|
||||||
syncSpatialNodes: (nodes: SpatialNodeSnapshot[]) => void;
|
syncCelestials: (celestials: CelestialSnapshot[]) => void;
|
||||||
syncLocalBubbles: (bubbles: LocalBubbleSnapshot[]) => void;
|
|
||||||
syncNodes: (nodes: ResourceNodeSnapshot[]) => void;
|
syncNodes: (nodes: ResourceNodeSnapshot[]) => void;
|
||||||
syncStations: (stations: StationSnapshot[]) => void;
|
syncStations: (stations: StationSnapshot[]) => void;
|
||||||
syncClaims: (claims: ClaimSnapshot[]) => void;
|
syncClaims: (claims: ClaimSnapshot[]) => void;
|
||||||
syncConstructionSites: (sites: ConstructionSiteSnapshot[]) => void;
|
syncConstructionSites: (sites: ConstructionSiteSnapshot[]) => void;
|
||||||
syncShips: (ships: ShipSnapshot[], tickIntervalMs: number) => void;
|
syncShips: (ships: ShipSnapshot[], tickIntervalMs: number) => void;
|
||||||
applySpatialNodeDeltas: (nodes: SpatialNodeDelta[]) => void;
|
applyCelestialDeltas: (celestials: CelestialDelta[]) => void;
|
||||||
applyLocalBubbleDeltas: (bubbles: LocalBubbleDelta[]) => void;
|
|
||||||
applyNodeDeltas: (nodes: ResourceNodeDelta[]) => void;
|
applyNodeDeltas: (nodes: ResourceNodeDelta[]) => void;
|
||||||
applyStationDeltas: (stations: StationDelta[]) => void;
|
applyStationDeltas: (stations: StationDelta[]) => void;
|
||||||
applyClaimDeltas: (claims: ClaimDelta[]) => void;
|
applyClaimDeltas: (claims: ClaimDelta[]) => void;
|
||||||
applyConstructionSiteDeltas: (sites: ConstructionSiteDelta[]) => void;
|
applyConstructionSiteDeltas: (sites: ConstructionSiteDelta[]) => void;
|
||||||
applyShipDeltas: (ships: ShipDelta[], tickIntervalMs: number) => void;
|
applyShipDeltas: (ships: ShipDelta[], tickIntervalMs: number) => void;
|
||||||
refreshHistoryWindows: () => void;
|
refreshHistoryWindows: () => void;
|
||||||
resolveFocusedBubbleId: () => string | undefined;
|
resolveFocusedCelestialId: () => string | undefined;
|
||||||
updateSystemSummaries: () => void;
|
updateSystemSummaries: () => void;
|
||||||
applyZoomPresentation: () => void;
|
applyZoomPresentation: () => void;
|
||||||
updateNetworkPanel: () => void;
|
updateNetworkPanel: () => void;
|
||||||
@@ -161,8 +157,7 @@ export class ViewerWorldLifecycle {
|
|||||||
this.context.rebuildSystems(snapshot.systems);
|
this.context.rebuildSystems(snapshot.systems);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.context.syncSpatialNodes(snapshot.spatialNodes);
|
this.context.syncCelestials(snapshot.celestials);
|
||||||
this.context.syncLocalBubbles(snapshot.localBubbles);
|
|
||||||
this.context.syncNodes(snapshot.nodes);
|
this.context.syncNodes(snapshot.nodes);
|
||||||
this.context.syncStations(snapshot.stations);
|
this.context.syncStations(snapshot.stations);
|
||||||
this.context.syncClaims(snapshot.claims);
|
this.context.syncClaims(snapshot.claims);
|
||||||
@@ -182,8 +177,7 @@ export class ViewerWorldLifecycle {
|
|||||||
|
|
||||||
this.context.setWorldTimeSyncMs(performance.now());
|
this.context.setWorldTimeSyncMs(performance.now());
|
||||||
applyDeltaToWorld(world, delta);
|
applyDeltaToWorld(world, delta);
|
||||||
this.context.applySpatialNodeDeltas(delta.spatialNodes);
|
this.context.applyCelestialDeltas(delta.celestials);
|
||||||
this.context.applyLocalBubbleDeltas(delta.localBubbles);
|
|
||||||
this.context.applyNodeDeltas(delta.nodes);
|
this.context.applyNodeDeltas(delta.nodes);
|
||||||
this.context.applyStationDeltas(delta.stations);
|
this.context.applyStationDeltas(delta.stations);
|
||||||
this.context.applyClaimDeltas(delta.claims);
|
this.context.applyClaimDeltas(delta.claims);
|
||||||
@@ -199,7 +193,7 @@ export class ViewerWorldLifecycle {
|
|||||||
this.context.getSelectedItems(),
|
this.context.getSelectedItems(),
|
||||||
this.context.getCameraMode(),
|
this.context.getCameraMode(),
|
||||||
this.context.getCameraTargetShipId(),
|
this.context.getCameraTargetShipId(),
|
||||||
this.context.getZoomLevel(),
|
this.context.getPovLevel(),
|
||||||
this.context.getActiveSystemId(),
|
this.context.getActiveSystemId(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -216,7 +210,7 @@ export class ViewerWorldLifecycle {
|
|||||||
updateDetailPanel(this.context.detailTitleEl, this.context.detailBodyEl, {
|
updateDetailPanel(this.context.detailTitleEl, this.context.detailBodyEl, {
|
||||||
world,
|
world,
|
||||||
selectedItems: this.context.getSelectedItems(),
|
selectedItems: this.context.getSelectedItems(),
|
||||||
zoomLevel: this.context.getZoomLevel(),
|
povLevel: this.context.getPovLevel(),
|
||||||
cameraMode: this.context.getCameraMode(),
|
cameraMode: this.context.getCameraMode(),
|
||||||
cameraTargetShipId: this.context.getCameraTargetShipId(),
|
cameraTargetShipId: this.context.getCameraTargetShipId(),
|
||||||
worldLabel: this.context.worldLabel(),
|
worldLabel: this.context.worldLabel(),
|
||||||
@@ -226,16 +220,16 @@ export class ViewerWorldLifecycle {
|
|||||||
|
|
||||||
private getPreferredStreamScope() {
|
private getPreferredStreamScope() {
|
||||||
const activeSystemId = this.context.getActiveSystemId();
|
const activeSystemId = this.context.getActiveSystemId();
|
||||||
if (this.context.getZoomLevel() === "universe" || !activeSystemId) {
|
if (this.context.getPovLevel() === "galaxy" || !activeSystemId) {
|
||||||
return { scopeKind: "universe" as const };
|
return { scopeKind: "universe" as const };
|
||||||
}
|
}
|
||||||
|
|
||||||
const bubbleId = this.context.resolveFocusedBubbleId();
|
const celestialId = this.context.resolveFocusedCelestialId();
|
||||||
if (this.context.getZoomLevel() === "local" && bubbleId) {
|
if (this.context.getPovLevel() === "local" && celestialId) {
|
||||||
return {
|
return {
|
||||||
scopeKind: "local-bubble" as const,
|
scopeKind: "local-celestial" as const,
|
||||||
systemId: activeSystemId,
|
systemId: activeSystemId,
|
||||||
bubbleId,
|
celestialId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,42 +1,40 @@
|
|||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import {
|
import {
|
||||||
|
DISPLAY_UNITS_PER_KILOMETER,
|
||||||
|
DISPLAY_UNITS_PER_LIGHT_YEAR,
|
||||||
|
KILOMETERS_PER_AU,
|
||||||
computeMoonLocalPosition,
|
computeMoonLocalPosition,
|
||||||
computeMoonSize,
|
computeMoonSize,
|
||||||
computePlanetLocalPosition,
|
computePlanetLocalPosition,
|
||||||
currentWorldTimeSeconds,
|
currentWorldTimeSeconds,
|
||||||
resolveOrbitalAnchorPosition,
|
resolveOrbitalAnchorPosition,
|
||||||
toDisplayGalaxyVector,
|
|
||||||
toThreeVector,
|
toThreeVector,
|
||||||
} from "./viewerMath";
|
} from "./viewerMath";
|
||||||
import { describeActiveSpace } from "./viewerSelection";
|
import { ACTIVE_SYSTEM_DETAIL_SCALE } from "./viewerConstants";
|
||||||
|
import { describeActiveSpace, resolveFocusedCelestialId } from "./viewerSelection";
|
||||||
import {
|
import {
|
||||||
resolveShipHeading,
|
resolveShipHeading,
|
||||||
updateSystemStarPresentation,
|
updateSystemStarPresentation,
|
||||||
updateSystemSummaryPresentation,
|
|
||||||
getAnimatedShipLocalPosition,
|
getAnimatedShipLocalPosition,
|
||||||
} from "./viewerPresentation";
|
} from "./viewerPresentation";
|
||||||
import { rawObject } from "./viewerScenePrimitives";
|
import { rawObject } from "./viewerScenePrimitives";
|
||||||
import type {
|
import type {
|
||||||
LocalBubbleDelta,
|
|
||||||
LocalBubbleSnapshot,
|
|
||||||
ResourceNodeDelta,
|
ResourceNodeDelta,
|
||||||
ResourceNodeSnapshot,
|
ResourceNodeSnapshot,
|
||||||
ShipSnapshot,
|
ShipSnapshot,
|
||||||
} from "./contracts";
|
} from "./contracts";
|
||||||
import type {
|
import type {
|
||||||
BubbleVisual,
|
CelestialVisual,
|
||||||
ClaimVisual,
|
ClaimVisual,
|
||||||
Selectable,
|
Selectable,
|
||||||
ConstructionSiteVisual,
|
ConstructionSiteVisual,
|
||||||
NodeVisual,
|
NodeVisual,
|
||||||
OrbitalAnchor,
|
OrbitalAnchor,
|
||||||
ShipVisual,
|
ShipVisual,
|
||||||
SpatialNodeVisual,
|
|
||||||
StructureVisual,
|
StructureVisual,
|
||||||
SystemSummaryVisual,
|
|
||||||
SystemVisual,
|
SystemVisual,
|
||||||
WorldState,
|
WorldState,
|
||||||
ZoomLevel,
|
PovLevel,
|
||||||
CameraMode,
|
CameraMode,
|
||||||
} from "./viewerTypes";
|
} from "./viewerTypes";
|
||||||
|
|
||||||
@@ -47,23 +45,22 @@ export interface WorldOrbitalContext {
|
|||||||
worldTimeSyncMs: number;
|
worldTimeSyncMs: number;
|
||||||
worldSeed: number;
|
worldSeed: number;
|
||||||
nodeVisuals: Map<string, NodeVisual>;
|
nodeVisuals: Map<string, NodeVisual>;
|
||||||
spatialNodeVisuals: Map<string, SpatialNodeVisual>;
|
celestialVisuals: Map<string, CelestialVisual>;
|
||||||
bubbleVisuals: Map<string, BubbleVisual>;
|
|
||||||
stationVisuals: Map<string, StructureVisual>;
|
stationVisuals: Map<string, StructureVisual>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WorldPresentationContext extends WorldOrbitalContext {
|
export interface WorldPresentationContext extends WorldOrbitalContext {
|
||||||
activeSystemId?: string;
|
activeSystemId?: string;
|
||||||
zoomLevel: ZoomLevel;
|
povLevel: PovLevel;
|
||||||
orbitYaw: number;
|
orbitYaw: number;
|
||||||
camera: THREE.PerspectiveCamera;
|
camera: THREE.PerspectiveCamera;
|
||||||
systemFocusLocal: THREE.Vector3;
|
systemAnchor: THREE.Vector3;
|
||||||
shipVisuals: Map<string, ShipVisual>;
|
shipVisuals: Map<string, ShipVisual>;
|
||||||
claimVisuals: Map<string, ClaimVisual>;
|
claimVisuals: Map<string, ClaimVisual>;
|
||||||
constructionSiteVisuals: Map<string, ConstructionSiteVisual>;
|
constructionSiteVisuals: Map<string, ConstructionSiteVisual>;
|
||||||
systemVisuals: Map<string, SystemVisual>;
|
systemVisuals: Map<string, SystemVisual>;
|
||||||
systemSummaryVisuals: Map<string, SystemSummaryVisual>;
|
systemSummaryVisuals: Map<string, any>;
|
||||||
toDisplayLocalPosition: (localPosition: THREE.Vector3, systemId?: string) => THREE.Vector3;
|
toDisplayLocalPosition: (localPosition: THREE.Vector3) => THREE.Vector3;
|
||||||
updateSystemDetailVisibility: () => void;
|
updateSystemDetailVisibility: () => void;
|
||||||
setShellReticleOpacity: (sprite: SystemVisual["shellReticle"], opacity: number) => void;
|
setShellReticleOpacity: (sprite: SystemVisual["shellReticle"], opacity: number) => void;
|
||||||
}
|
}
|
||||||
@@ -74,15 +71,17 @@ export interface GameStatusParams {
|
|||||||
world?: WorldState;
|
world?: WorldState;
|
||||||
activeSystemId?: string;
|
activeSystemId?: string;
|
||||||
cameraMode: CameraMode;
|
cameraMode: CameraMode;
|
||||||
zoomLevel: ZoomLevel;
|
povLevel: PovLevel;
|
||||||
selectedItems: Selectable[];
|
selectedItems: Selectable[];
|
||||||
mode: string;
|
mode: string;
|
||||||
|
galaxyAnchor?: THREE.Vector3;
|
||||||
|
systemAnchor?: THREE.Vector3;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateWorldPresentation(context: WorldPresentationContext) {
|
export function updateWorldPresentation(context: WorldPresentationContext) {
|
||||||
const now = performance.now();
|
const now = performance.now();
|
||||||
const worldTimeSeconds = currentWorldTimeSeconds(context.world, context.worldTimeSyncMs);
|
const worldTimeSeconds = currentWorldTimeSeconds(context.world, context.worldTimeSyncMs);
|
||||||
const renderMode = resolveRenderSpaceMode(context.activeSystemId, context.zoomLevel);
|
const renderMode = resolveRenderSpaceMode(context.activeSystemId, context.povLevel);
|
||||||
|
|
||||||
for (const [shipId, visual] of context.shipVisuals.entries()) {
|
for (const [shipId, visual] of context.shipVisuals.entries()) {
|
||||||
const ship = context.world?.ships.get(shipId);
|
const ship = context.world?.ships.get(shipId);
|
||||||
@@ -91,7 +90,7 @@ export function updateWorldPresentation(context: WorldPresentationContext) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const worldPosition = getAnimatedShipLocalPosition(visual, now);
|
const worldPosition = getAnimatedShipLocalPosition(visual, now);
|
||||||
const displayPosition = resolveShipWorldPosition(context, ship, visual, worldPosition);
|
const displayPosition = context.toDisplayLocalPosition(worldPosition);
|
||||||
visual.mesh.setPosition(displayPosition);
|
visual.mesh.setPosition(displayPosition);
|
||||||
visual.icon.setPosition(rawObject(visual.mesh).position.clone());
|
visual.icon.setPosition(rawObject(visual.mesh).position.clone());
|
||||||
const shipVisible = isShipVisible(renderMode, context.activeSystemId, ship);
|
const shipVisible = isShipVisible(renderMode, context.activeSystemId, ship);
|
||||||
@@ -105,67 +104,51 @@ export function updateWorldPresentation(context: WorldPresentationContext) {
|
|||||||
|
|
||||||
for (const visual of context.nodeVisuals.values()) {
|
for (const visual of context.nodeVisuals.values()) {
|
||||||
const animatedLocalPosition = computeNodeLocalPosition(context, visual, worldTimeSeconds);
|
const animatedLocalPosition = computeNodeLocalPosition(context, visual, worldTimeSeconds);
|
||||||
visual.mesh.setPosition(context.toDisplayLocalPosition(animatedLocalPosition, visual.systemId));
|
visual.mesh.setPosition(context.toDisplayLocalPosition(animatedLocalPosition));
|
||||||
visual.icon.setPosition(rawObject(visual.mesh).position.clone());
|
visual.icon.setPosition(rawObject(visual.mesh).position.clone());
|
||||||
visual.mesh.setVisible(visual.systemId === context.activeSystemId);
|
visual.mesh.setVisible(visual.systemId === context.activeSystemId);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const visual of context.spatialNodeVisuals.values()) {
|
for (const visual of context.celestialVisuals.values()) {
|
||||||
const animatedLocalPosition = computeSpatialNodeLocalPosition(context, visual, worldTimeSeconds);
|
const animatedLocalPosition = computeCelestialLocalPosition(context, visual, worldTimeSeconds);
|
||||||
visual.mesh.setPosition(context.toDisplayLocalPosition(animatedLocalPosition, visual.systemId));
|
visual.mesh.setPosition(context.toDisplayLocalPosition(animatedLocalPosition));
|
||||||
visual.icon.setPosition(rawObject(visual.mesh).position.clone());
|
visual.icon.setPosition(rawObject(visual.mesh).position.clone());
|
||||||
visual.mesh.setVisible(visual.systemId === context.activeSystemId);
|
visual.mesh.setVisible(visual.systemId === context.activeSystemId);
|
||||||
visual.icon.setVisible(visual.systemId === context.activeSystemId);
|
visual.icon.setVisible(visual.systemId === context.activeSystemId);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const visual of context.bubbleVisuals.values()) {
|
|
||||||
const animatedLocalPosition = resolveBubbleAnimatedLocalPosition(context, visual, worldTimeSeconds);
|
|
||||||
visual.mesh.setPosition(context.toDisplayLocalPosition(animatedLocalPosition, visual.systemId));
|
|
||||||
visual.mesh.setVisible(visual.systemId === context.activeSystemId);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const visual of context.stationVisuals.values()) {
|
for (const visual of context.stationVisuals.values()) {
|
||||||
const animatedLocalPosition = resolveStructureAnimatedLocalPosition(context, visual, worldTimeSeconds);
|
const animatedLocalPosition = resolveStructureAnimatedLocalPosition(context, visual, worldTimeSeconds);
|
||||||
visual.mesh.setPosition(context.toDisplayLocalPosition(animatedLocalPosition, visual.systemId));
|
visual.mesh.setPosition(context.toDisplayLocalPosition(animatedLocalPosition));
|
||||||
visual.icon.setPosition(rawObject(visual.mesh).position.clone());
|
visual.icon.setPosition(rawObject(visual.mesh).position.clone());
|
||||||
visual.mesh.setVisible(visual.systemId === context.activeSystemId);
|
visual.mesh.setVisible(visual.systemId === context.activeSystemId);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const visual of context.claimVisuals.values()) {
|
for (const visual of context.claimVisuals.values()) {
|
||||||
const animatedLocalPosition = computeSpatialNodeLocalPositionById(context, visual.nodeId, worldTimeSeconds) ?? visual.localPosition.clone();
|
const animatedLocalPosition = computeCelestialLocalPositionById(context, visual.celestialId, worldTimeSeconds) ?? visual.localPosition.clone();
|
||||||
visual.mesh.setPosition(context.toDisplayLocalPosition(animatedLocalPosition, visual.systemId));
|
visual.mesh.setPosition(context.toDisplayLocalPosition(animatedLocalPosition));
|
||||||
visual.icon.setPosition(rawObject(visual.mesh).position.clone());
|
visual.icon.setPosition(rawObject(visual.mesh).position.clone());
|
||||||
visual.mesh.setVisible(visual.systemId === context.activeSystemId);
|
visual.mesh.setVisible(visual.systemId === context.activeSystemId);
|
||||||
visual.icon.setVisible(visual.systemId === context.activeSystemId);
|
visual.icon.setVisible(visual.systemId === context.activeSystemId);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const visual of context.constructionSiteVisuals.values()) {
|
for (const visual of context.constructionSiteVisuals.values()) {
|
||||||
const animatedLocalPosition = computeSpatialNodeLocalPositionById(context, visual.nodeId, worldTimeSeconds) ?? visual.localPosition.clone();
|
const animatedLocalPosition = computeCelestialLocalPositionById(context, visual.celestialId, worldTimeSeconds) ?? visual.localPosition.clone();
|
||||||
visual.mesh.setPosition(context.toDisplayLocalPosition(animatedLocalPosition, visual.systemId));
|
visual.mesh.setPosition(context.toDisplayLocalPosition(animatedLocalPosition));
|
||||||
visual.icon.setPosition(rawObject(visual.mesh).position.clone());
|
visual.icon.setPosition(rawObject(visual.mesh).position.clone());
|
||||||
visual.mesh.setVisible(visual.systemId === context.activeSystemId);
|
visual.mesh.setVisible(visual.systemId === context.activeSystemId);
|
||||||
visual.icon.setVisible(visual.systemId === context.activeSystemId);
|
visual.icon.setVisible(visual.systemId === context.activeSystemId);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateSystemStarPresentation(
|
|
||||||
context.systemVisuals,
|
|
||||||
context.activeSystemId,
|
|
||||||
context.systemFocusLocal,
|
|
||||||
context.camera,
|
|
||||||
context.setShellReticleOpacity,
|
|
||||||
);
|
|
||||||
context.updateSystemDetailVisibility();
|
|
||||||
updateSystemSummaryPresentation(context.systemSummaryVisuals, context.camera, context.activeSystemId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type RenderSpaceMode = "galaxy" | "system" | "local";
|
type RenderSpaceMode = "galaxy" | "system" | "local";
|
||||||
|
|
||||||
function resolveRenderSpaceMode(activeSystemId: string | undefined, zoomLevel: ZoomLevel): RenderSpaceMode {
|
function resolveRenderSpaceMode(activeSystemId: string | undefined, povLevel: PovLevel): RenderSpaceMode {
|
||||||
if (!activeSystemId || zoomLevel === "universe") {
|
if (!activeSystemId || povLevel === "galaxy") {
|
||||||
return "galaxy";
|
return "galaxy";
|
||||||
}
|
}
|
||||||
|
|
||||||
return zoomLevel === "local" ? "local" : "system";
|
return povLevel === "local" ? "local" : "system";
|
||||||
}
|
}
|
||||||
|
|
||||||
function isShipVisible(mode: RenderSpaceMode, activeSystemId: string | undefined, ship: ShipSnapshot) {
|
function isShipVisible(mode: RenderSpaceMode, activeSystemId: string | undefined, ship: ShipSnapshot) {
|
||||||
@@ -186,22 +169,11 @@ export function resolveShipWorldPosition(
|
|||||||
visual: ShipVisual,
|
visual: ShipVisual,
|
||||||
animatedLocalPosition = getAnimatedShipLocalPosition(visual),
|
animatedLocalPosition = getAnimatedShipLocalPosition(visual),
|
||||||
) {
|
) {
|
||||||
if (ship.spatialState.movementRegime === "ftl-transit") {
|
// FTL ships are invisible in system scene; just return their last known local position.
|
||||||
const destinationNodeId = ship.spatialState.transit?.destinationNodeId;
|
return context.toDisplayLocalPosition(animatedLocalPosition);
|
||||||
const destinationNode = destinationNodeId ? context.world?.spatialNodes.get(destinationNodeId) : undefined;
|
|
||||||
const originSystem = context.world?.systems.get(ship.systemId);
|
|
||||||
const destinationSystem = destinationNode ? context.world?.systems.get(destinationNode.systemId) : undefined;
|
|
||||||
if (originSystem && destinationSystem) {
|
|
||||||
const origin = toDisplayGalaxyVector(originSystem.galaxyPosition);
|
|
||||||
const destination = toDisplayGalaxyVector(destinationSystem.galaxyPosition);
|
|
||||||
return origin.lerp(destination, THREE.MathUtils.clamp(ship.spatialState.transit?.progress ?? 0, 0, 1));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return context.toDisplayLocalPosition(animatedLocalPosition, ship.systemId);
|
export function updateSystemSummaries(world: WorldState | undefined, systemSummaryVisuals: Map<string, any>) {
|
||||||
}
|
|
||||||
|
|
||||||
export function updateSystemSummaries(world: WorldState | undefined, systemSummaryVisuals: Map<string, SystemSummaryVisual>) {
|
|
||||||
if (!world) {
|
if (!world) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -275,26 +247,50 @@ export function renderRecentEvents(world: WorldState | undefined, entityKind: st
|
|||||||
.join("<br>");
|
.join("<br>");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fmtVec(v: THREE.Vector3, digits: number) {
|
||||||
|
return `${v.x.toFixed(digits)} ${v.y.toFixed(digits)} ${v.z.toFixed(digits)}`;
|
||||||
|
}
|
||||||
|
|
||||||
export function updateGameStatus(params: GameStatusParams) {
|
export function updateGameStatus(params: GameStatusParams) {
|
||||||
const { statusEl, summaryEl, world, activeSystemId, cameraMode, zoomLevel, selectedItems, mode } = params;
|
const { statusEl, summaryEl, world, activeSystemId, cameraMode, povLevel, selectedItems, mode, galaxyAnchor, systemAnchor } = params;
|
||||||
const sequence = world?.sequence ?? 0;
|
const sequence = world?.sequence ?? 0;
|
||||||
const generatedAt = world?.generatedAtUtc
|
const generatedAt = world?.generatedAtUtc
|
||||||
? new Date(world.generatedAtUtc).toLocaleTimeString()
|
? new Date(world.generatedAtUtc).toLocaleTimeString()
|
||||||
: "n/a";
|
: "n/a";
|
||||||
const displayZoomLevel = activeSystemId ? zoomLevel : "universe";
|
const displayPovLevel = activeSystemId ? povLevel : "galaxy";
|
||||||
const activeSpace = describeActiveSpace(world, displayZoomLevel, activeSystemId, selectedItems);
|
const activeSpace = describeActiveSpace(world, displayPovLevel, activeSystemId, selectedItems);
|
||||||
const cameraModeLabel = cameraMode === "follow" ? "follow" : "map";
|
const cameraModeLabel = cameraMode === "follow" ? "follow" : "map";
|
||||||
|
|
||||||
|
// Galaxy space: galaxyAnchor in light-years — changes only during galaxy navigation
|
||||||
|
const galPos = galaxyAnchor
|
||||||
|
? `gal pos: ${fmtVec(galaxyAnchor.clone().divideScalar(DISPLAY_UNITS_PER_LIGHT_YEAR), 2)} ly`
|
||||||
|
: "";
|
||||||
|
// System space: systemAnchor in AU — changes only during system navigation
|
||||||
|
const sysPos = systemAnchor
|
||||||
|
? `sys pos: ${fmtVec(systemAnchor.clone().divideScalar(KILOMETERS_PER_AU), 3)} AU`
|
||||||
|
: "";
|
||||||
|
// Local space: position relative to the focused celestial's orbital anchor in km
|
||||||
|
const focusedCelestialId = resolveFocusedCelestialId(world, selectedItems);
|
||||||
|
const celestialAnchor = focusedCelestialId
|
||||||
|
? world?.celestials.get(focusedCelestialId)?.orbitalAnchor
|
||||||
|
: undefined;
|
||||||
|
const locPos = systemAnchor && celestialAnchor
|
||||||
|
? `loc pos: ${fmtVec(systemAnchor.clone().sub(toThreeVector(celestialAnchor)), 0)} km`
|
||||||
|
: "";
|
||||||
|
|
||||||
statusEl.textContent = [
|
statusEl.textContent = [
|
||||||
`mode: ${mode}`,
|
`mode: ${mode}`,
|
||||||
`camera: ${cameraModeLabel}`,
|
`camera: ${cameraModeLabel}`,
|
||||||
`zoom: ${displayZoomLevel}`,
|
`zoom: ${displayPovLevel}`,
|
||||||
`space: ${activeSpace}`,
|
`space: ${activeSpace}`,
|
||||||
|
galPos,
|
||||||
|
sysPos,
|
||||||
|
locPos,
|
||||||
`sequence: ${sequence}`,
|
`sequence: ${sequence}`,
|
||||||
`snapshot: ${generatedAt}`,
|
`snapshot: ${generatedAt}`,
|
||||||
].join("\n");
|
].filter(Boolean).join("\n");
|
||||||
if (summaryEl) {
|
if (summaryEl) {
|
||||||
summaryEl.textContent = `${mode} | ${displayZoomLevel} | ${activeSpace}`;
|
summaryEl.textContent = `${mode} | ${displayPovLevel} | ${activeSpace}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -372,58 +368,54 @@ export function resolveOrbitalAnchor(context: WorldOrbitalContext, systemId: str
|
|||||||
return bestAnchor;
|
return bestAnchor;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolvePointPosition(context: WorldOrbitalContext, _systemId: string, nodeId?: string | null) {
|
export function resolvePointPosition(context: WorldOrbitalContext, _systemId: string, celestialId?: string | null) {
|
||||||
if (nodeId) {
|
if (celestialId) {
|
||||||
const spatialNode = context.world?.spatialNodes.get(nodeId);
|
const celestial = context.world?.celestials.get(celestialId);
|
||||||
if (spatialNode) {
|
if (celestial) {
|
||||||
return toThreeVector(spatialNode.localPosition);
|
return toThreeVector(celestial.orbitalAnchor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new THREE.Vector3(0, 0, 0);
|
return new THREE.Vector3(0, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveBubblePosition(context: WorldOrbitalContext, bubble: LocalBubbleSnapshot | LocalBubbleDelta) {
|
export function computeCelestialLocalPosition(context: WorldOrbitalContext, visual: CelestialVisual, timeSeconds: number) {
|
||||||
return resolvePointPosition(context, bubble.systemId, bubble.nodeId);
|
return computeCelestialLocalPositionById(context, visual.id, timeSeconds) ?? visual.orbitalAnchor.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function computeSpatialNodeLocalPosition(context: WorldOrbitalContext, visual: SpatialNodeVisual, timeSeconds: number) {
|
export function computeCelestialLocalPositionById(
|
||||||
return computeSpatialNodeLocalPositionById(context, visual.id, timeSeconds) ?? visual.localPosition.clone();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function computeSpatialNodeLocalPositionById(
|
|
||||||
context: WorldOrbitalContext,
|
context: WorldOrbitalContext,
|
||||||
nodeId: string,
|
celestialId: string,
|
||||||
timeSeconds: number,
|
timeSeconds: number,
|
||||||
visiting = new Set<string>(),
|
visiting = new Set<string>(),
|
||||||
): THREE.Vector3 | undefined {
|
): THREE.Vector3 | undefined {
|
||||||
if (!context.world || visiting.has(nodeId)) {
|
if (!context.world || visiting.has(celestialId)) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const node = context.world.spatialNodes.get(nodeId);
|
const celestial = context.world.celestials.get(celestialId);
|
||||||
if (!node) {
|
if (!celestial) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const basePosition = toThreeVector(node.localPosition);
|
const basePosition = toThreeVector(celestial.orbitalAnchor);
|
||||||
if (!node.parentNodeId) {
|
if (!celestial.parentNodeId) {
|
||||||
return basePosition;
|
return basePosition;
|
||||||
}
|
}
|
||||||
|
|
||||||
const parentNode = context.world.spatialNodes.get(node.parentNodeId);
|
const parentCelestial = context.world.celestials.get(celestial.parentNodeId);
|
||||||
if (!parentNode) {
|
if (!parentCelestial) {
|
||||||
return basePosition;
|
return basePosition;
|
||||||
}
|
}
|
||||||
|
|
||||||
visiting.add(nodeId);
|
visiting.add(celestialId);
|
||||||
const parentCurrentPosition = computeSpatialNodeLocalPositionById(context, node.parentNodeId, timeSeconds, visiting);
|
const parentCurrentPosition = computeCelestialLocalPositionById(context, celestial.parentNodeId, timeSeconds, visiting);
|
||||||
visiting.delete(nodeId);
|
visiting.delete(celestialId);
|
||||||
if (!parentCurrentPosition) {
|
if (!parentCurrentPosition) {
|
||||||
return basePosition;
|
return basePosition;
|
||||||
}
|
}
|
||||||
|
|
||||||
const parentInitialPosition = toThreeVector(parentNode.localPosition);
|
const parentInitialPosition = toThreeVector(parentCelestial.orbitalAnchor);
|
||||||
const relativeOffset = basePosition.clone().sub(parentInitialPosition);
|
const relativeOffset = basePosition.clone().sub(parentInitialPosition);
|
||||||
const initialAngle = Math.atan2(parentInitialPosition.z, parentInitialPosition.x);
|
const initialAngle = Math.atan2(parentInitialPosition.z, parentInitialPosition.x);
|
||||||
const currentAngle = Math.atan2(parentCurrentPosition.z, parentCurrentPosition.x);
|
const currentAngle = Math.atan2(parentCurrentPosition.z, parentCurrentPosition.x);
|
||||||
@@ -431,13 +423,6 @@ export function computeSpatialNodeLocalPositionById(
|
|||||||
return parentCurrentPosition.clone().add(rotatedOffset);
|
return parentCurrentPosition.clone().add(rotatedOffset);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setBubbleVisualState(visual: BubbleVisual, bubble: LocalBubbleSnapshot | LocalBubbleDelta) {
|
|
||||||
const intensity = bubble.occupantShipIds.length + bubble.occupantStationIds.length + bubble.occupantConstructionSiteIds.length;
|
|
||||||
const material = (rawObject(visual.mesh) as THREE.LineLoop).material as THREE.LineBasicMaterial;
|
|
||||||
material.opacity = THREE.MathUtils.clamp(0.18 + intensity * 0.05, 0.18, 0.72);
|
|
||||||
material.color.set(intensity > 0 ? "#7fffd4" : "#6ed6ff");
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawCountIcon(
|
function drawCountIcon(
|
||||||
context: CanvasRenderingContext2D,
|
context: CanvasRenderingContext2D,
|
||||||
kind: SummaryIconKind,
|
kind: SummaryIconKind,
|
||||||
@@ -504,24 +489,15 @@ function getOrbitalAnchorPosition(context: WorldOrbitalContext, systemId: string
|
|||||||
return resolveOrbitalAnchorPosition(context.world, systemId, anchor, timeSeconds, context.worldSeed);
|
return resolveOrbitalAnchorPosition(context.world, systemId, anchor, timeSeconds, context.worldSeed);
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveBubbleAnimatedLocalPosition(context: WorldOrbitalContext, visual: BubbleVisual, timeSeconds: number) {
|
|
||||||
const bubble = context.world?.localBubbles.get(visual.id);
|
|
||||||
if (!bubble) {
|
|
||||||
return visual.localPosition.clone();
|
|
||||||
}
|
|
||||||
|
|
||||||
return computeSpatialNodeLocalPositionById(context, bubble.nodeId, timeSeconds) ?? visual.localPosition.clone();
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveStructureAnimatedLocalPosition(context: WorldOrbitalContext, visual: StructureVisual, timeSeconds: number) {
|
function resolveStructureAnimatedLocalPosition(context: WorldOrbitalContext, visual: StructureVisual, timeSeconds: number) {
|
||||||
if (!context.world) {
|
if (!context.world) {
|
||||||
return visual.localPosition.clone();
|
return visual.localPosition.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
const station = context.world.stations.get(visual.id);
|
const station = context.world.stations.get(visual.id);
|
||||||
if (!station?.nodeId) {
|
if (!station?.celestialId) {
|
||||||
return computeStructureLocalPosition(context, visual, timeSeconds, 0.14);
|
return computeStructureLocalPosition(context, visual, timeSeconds, 0.14);
|
||||||
}
|
}
|
||||||
|
|
||||||
return computeSpatialNodeLocalPositionById(context, station.nodeId, timeSeconds) ?? visual.localPosition.clone();
|
return computeCelestialLocalPositionById(context, station.celestialId, timeSeconds) ?? visual.localPosition.clone();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user