Files
gods-eye-view/src/layers/launches/lifecycle.js
T
Bilawal Sidhu 9fc8d1966b feat(annotations): draw on the world by hand (#547)
Supersedes #235 by @cora-fresh-labs, ported onto the current component layout with fixes from review.

Verified before merge: format, package boundaries, unit suite, build, and the browser tracking suite; a browser harness for the change where one applies; adversarial review applied.

Future work (not in this PR): hand-drawn marks do not persist across a reload today, and neither do spoken annotations; there is no storage layer for marks. The natural home is Scenes: a drawing that belongs to a shot and persists per shot, so a scene animation can carry its own marks. Needs an owner.
Co-authored-by: Clint Borle <[email protected]>
2026-09-14 19:59:26 -05:00

176 lines
7.1 KiB
JavaScript

import * as Cesium from 'cesium';
import { isPointerFree } from '../../data/inputOwnership.js';
export function createLifecycle({
state: layerState,
services,
parts,
source,
}) {
const { holdContinuousRender, releaseContinuousRender } = services.render;
const methods = {
init(viewer) {
layerState._viewer = viewer;
layerState._enabled = false;
parts.panel.createMissionPanel();
parts.overlays.createReplayVehicleOverlay();
layerState._dataSource = new Cesium.CustomDataSource('rocket-launches');
layerState._dataSource.show = false;
viewer.dataSources.add(layerState._dataSource);
layerState._count = 0;
layerState._lastUpdate = null;
layerState._lastError = null;
layerState._orbitMatches = 0;
parts.overlays.clearMissionOverlaySources();
layerState._clickHandler = new Cesium.ScreenSpaceEventHandler(
viewer.scene.canvas,
);
layerState._clickHandler.setInputAction((movement) => {
// A tool owns the pointer (src/data/inputOwnership.js): yield the click.
if (!isPointerFree()) return;
if (!layerState._enabled || !layerState._dataSource?.show) return;
const entity = viewer.scene
.drillPick(movement.position, 12)
.map((picked) => picked?.id)
.find((candidate) => parts.selection.entityLaunchId(candidate));
const launchId = parts.selection.entityLaunchId(entity);
if (!launchId) return;
parts.selection.setSelectedMission(launchId);
parts.selection.focusMission(
layerState._launches.find((launch) => launch.id === launchId),
);
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
layerState._moveHandler = new Cesium.ScreenSpaceEventHandler(
viewer.scene.canvas,
);
layerState._moveHandler.setInputAction((movement) => {
if (!layerState._enabled || !layerState._dataSource?.show) return;
const missionEntity = viewer.scene
.drillPick(movement.endPosition, 12)
.map((picked) => picked?.id)
.find((candidate) => parts.selection.entityLaunchId(candidate));
viewer.scene.canvas.style.cursor = missionEntity ? 'pointer' : '';
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
// Apply horizon visibility before Cesium traverses and picks the scene.
// A postRender write is one frame late and can remain visibly stale when
// request-on-demand rendering stops after a globe camera move.
layerState._declutterHandler = viewer.scene.preRender.addEventListener(
parts.rendering.updateMissionFrame,
);
parts.launchPad.initLaunchPadZonePrimitive();
},
async enable() {
layerState._sourceController.abort();
layerState._sourceController = new AbortController();
layerState._enabled = true;
holdContinuousRender('rocket-launches'); // per-frame animator (perf wave 2)
layerState._lifecycleToken++;
layerState._postTleRetryCount = 0;
try {
await this._enableBody();
} catch (error) {
// Enable is a transaction: a failed dependency must not leave mission
// UI/datasource visible or the satellite snapshot retained (a retained
// snapshot makes the next enable skip dependency capture entirely).
try {
await this.disable();
} catch (cleanupError) {
console.warn('[Missions] enable rollback failed:', cleanupError);
}
throw error;
}
},
async _enableBody() {
layerState._updateDirty = false;
if (layerState._dataSource) layerState._dataSource.show = true;
parts.orbitRendering.syncMissionOrbitPrimitiveVisibility();
parts.selection.focusFullGlobe(layerState._viewer);
document.getElementById('cockpit-context')?.setAttribute('hidden', '');
if (layerState._selectedLaunchId)
parts.selection.setSelectedMission(
layerState._selectedLaunchId,
layerState._explicitSelection,
);
else parts.selection.setSelectedMission(null, false);
await parts.ingestion.captureSatelliteDependency();
},
async disable() {
layerState._sourceController.abort();
layerState._enabled = false;
releaseContinuousRender('rocket-launches');
layerState._lifecycleToken++;
layerState._updateDirty = false;
parts.ingestion.clearPostTleRetry();
parts.panel.clearMissionRosterHover();
parts.replay.stopMissionReplay();
parts.selection.stopMissionZoomAnchor();
if (layerState._dataSource) layerState._dataSource.show = false;
parts.orbitRendering.syncMissionOrbitPrimitiveVisibility();
if (layerState._viewer?.scene?.canvas)
layerState._viewer.scene.canvas.style.cursor = '';
parts.launchPad.hideLaunchPadZone();
parts.overlays.hideReplayVehicleOverlay();
layerState._selectedLaunchId = null;
parts.overlays.clearMissionOverlaySources();
await parts.ingestion.restoreSatelliteDependency();
parts.panel.renderMissionPanel();
},
async destroy(viewer) {
layerState._sourceController.abort();
releaseContinuousRender('rocket-launches'); // direct-destroy path (perf wave 2 fix)
layerState._enabled = false;
layerState._lifecycleToken++;
layerState._updateDirty = false;
await parts.ingestion.restoreSatelliteDependency();
parts.panel.clearMissionRosterHover();
parts.replay.stopMissionReplay();
parts.selection.stopMissionZoomAnchor();
parts.ingestion.clearPostTleRetry();
if (layerState._clickHandler) layerState._clickHandler.destroy();
layerState._clickHandler = null;
if (layerState._moveHandler) layerState._moveHandler.destroy();
layerState._moveHandler = null;
if (layerState._declutterHandler) layerState._declutterHandler();
layerState._declutterHandler = null;
parts.launchPad.destroyLaunchPadZonePrimitive();
parts.orbitRendering.removeMissionOrbitPrimitives();
parts.overlays.clearMissionOverlaySources();
parts.overlays.destroyReplayVehicleOverlay();
layerState._launches = [];
layerState._missionOverlayRecords.clear();
layerState._animationStarts.clear();
layerState._satelliteTelemetry.clear();
layerState._replayTracks.clear();
layerState._missionPanel?.remove();
layerState._missionPanel = null;
if (layerState._missionRoster) {
layerState._missionRoster.onclick = null;
}
layerState._missionRosterPreviewOwnership = null;
layerState._missionRoster = null;
layerState._viewer = null;
if (layerState._dataSource)
viewer.dataSources.remove(layerState._dataSource, true);
layerState._dataSource = null;
layerState._count = 0;
layerState._orbitMatches = 0;
layerState._lastUpdate = null;
layerState._activeTleText = null;
layerState._activeTlePromise = null;
layerState._activeTlePromiseToken = 0;
layerState._renderedTleText = null;
layerState._postTleRetryCount = 0;
layerState._satelliteStateBeforeMission = null;
layerState._satelliteActivationPromise = null;
layerState._dataManager = null;
},
};
return { methods };
}