mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-11 22:02:21 +00:00
Check that a superseded or cancelled launch flyover never takes the Navara camera back
This commit is contained in:
1 parent
d5218c4722
commit
c96d4ecfb1
1 file changed
+185
@@ -0,0 +1,185 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import * as geo from '../../geo/index.js';
|
||||||
|
import { createFakeEngine } from '../../globe/testing/fakeEngine.js';
|
||||||
|
import { WHITE } from '../shared/color.js';
|
||||||
|
import { createLaunchesNavaraRenderer } from './render/navara.js';
|
||||||
|
import { createState } from './state.js';
|
||||||
|
import { createPaths } from './paths.js';
|
||||||
|
import { createCamera } from './camera.js';
|
||||||
|
import { createSelection } from './selection.js';
|
||||||
|
import { createReplay } from './replay.js';
|
||||||
|
import { createIngestion } from './ingestion.js';
|
||||||
|
|
||||||
|
const noop = () => {};
|
||||||
|
const settle = async () => {
|
||||||
|
for (let i = 0; i < 5; i++)
|
||||||
|
await new Promise((resolve) => setImmediate(resolve));
|
||||||
|
};
|
||||||
|
const CAPE = { id: 'cape', name: 'Cape mission', lat: 28.56, lon: -80.58 };
|
||||||
|
const VANDENBERG = {
|
||||||
|
id: 'vandy',
|
||||||
|
name: 'Vandy mission',
|
||||||
|
lat: 34.63,
|
||||||
|
lon: -120.6,
|
||||||
|
};
|
||||||
|
const pad = (launch, height = 0) =>
|
||||||
|
geo.cartesian3.fromDegrees(launch.lon, launch.lat, height);
|
||||||
|
|
||||||
|
function rig() {
|
||||||
|
const engine = createFakeEngine({ kind: 'navara' });
|
||||||
|
engine.fake.setPose({
|
||||||
|
longitude: geo.math.toRadians(-100),
|
||||||
|
latitude: geo.math.toRadians(30),
|
||||||
|
height: 20_000_000,
|
||||||
|
});
|
||||||
|
const renderer = createLaunchesNavaraRenderer(engine);
|
||||||
|
const state = createState({
|
||||||
|
services: {
|
||||||
|
overlays: {
|
||||||
|
setOverlayEntries: noop,
|
||||||
|
setOverlaySourceVisible: noop,
|
||||||
|
clearOverlaySource: noop,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
state._viewer = renderer.view;
|
||||||
|
state._engine = engine;
|
||||||
|
state._renderer = renderer;
|
||||||
|
state._enabled = true;
|
||||||
|
state._dataSource = renderer.createDataSource();
|
||||||
|
state._dataSource.show = true;
|
||||||
|
let feed = [CAPE, VANDENBERG];
|
||||||
|
const source = {
|
||||||
|
getLaunches: async () => feed,
|
||||||
|
getActiveTle: async () => '1 25544U',
|
||||||
|
};
|
||||||
|
const parts = {
|
||||||
|
panel: {
|
||||||
|
clearMissionRosterHover: noop,
|
||||||
|
renderMissionPanel: noop,
|
||||||
|
renderMissionRoster: noop,
|
||||||
|
publishPanel: noop,
|
||||||
|
},
|
||||||
|
overlays: { syncMissionOverlayEntries: noop },
|
||||||
|
orbitRendering: {
|
||||||
|
syncMissionOrbitPrimitiveVisibility: noop,
|
||||||
|
removeMissionOrbitPrimitives: noop,
|
||||||
|
},
|
||||||
|
policyHelpers: { releaseAircraftTracking: noop },
|
||||||
|
model: { normalizeRocketLaunches: (launches) => launches },
|
||||||
|
rendering: {
|
||||||
|
// The launch marker (the pad) and a replay track per mission.
|
||||||
|
addLaunchEntity(launch) {
|
||||||
|
renderer.addEntity(state._dataSource, {
|
||||||
|
id: `rocket-launch:${launch.id}`,
|
||||||
|
position: pad(launch),
|
||||||
|
point: { pixelSize: 8, color: WHITE },
|
||||||
|
});
|
||||||
|
state._replayTracks.set(launch.id, {
|
||||||
|
ascentDurationSec: 600,
|
||||||
|
ascentPath: [pad(launch), pad(launch, 100_000)],
|
||||||
|
animatedOrbitPath: [pad(launch, 100_000), pad(launch, 200_000)],
|
||||||
|
beginReplayFrame: () => ({
|
||||||
|
ascending: true,
|
||||||
|
phaseProgress: 0.2,
|
||||||
|
elapsedSinceStart: 10,
|
||||||
|
}),
|
||||||
|
getReplayState: () => null,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const context = {
|
||||||
|
state,
|
||||||
|
services: { geometry: { getKeyholeGeometry: () => ({ radius: 300 }) } },
|
||||||
|
parts,
|
||||||
|
source,
|
||||||
|
getRenderer: () => renderer,
|
||||||
|
};
|
||||||
|
parts.paths = createPaths(context);
|
||||||
|
parts.camera = createCamera(context);
|
||||||
|
parts.selection = createSelection(context);
|
||||||
|
parts.replay = createReplay(context);
|
||||||
|
parts.ingestion = createIngestion(context);
|
||||||
|
return {
|
||||||
|
engine,
|
||||||
|
state,
|
||||||
|
parts,
|
||||||
|
setFeed: (next) => (feed = next),
|
||||||
|
shown: (id) =>
|
||||||
|
state._dataSource.entities.getById(`rocket-launch:${id}`)?.show,
|
||||||
|
cameraCommands: () =>
|
||||||
|
engine.fake.calls.filter(({ method }) =>
|
||||||
|
[
|
||||||
|
'camera.lookAt',
|
||||||
|
'camera.setView',
|
||||||
|
'camera.flyTo',
|
||||||
|
'camera.flyToSphere',
|
||||||
|
].includes(method),
|
||||||
|
),
|
||||||
|
cleanup() {
|
||||||
|
state._sourceController.abort();
|
||||||
|
parts.replay.stopMissionReplay();
|
||||||
|
renderer.destroy();
|
||||||
|
engine.destroy();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const near = (a, b, metres) => geo.cartesian3.distance(a, b) < metres;
|
||||||
|
|
||||||
|
test('edge:rocket-launches: choosing a second mission mid-flyover leaves the camera to that mission', async () => {
|
||||||
|
const r = rig();
|
||||||
|
try {
|
||||||
|
await r.parts.ingestion.requestMissionUpdate();
|
||||||
|
r.parts.selection.setSelectedMission(CAPE.id);
|
||||||
|
assert.equal(r.parts.replay.startMissionReplay(CAPE.id), true);
|
||||||
|
assert.equal(r.engine.camera.flightActive(), true);
|
||||||
|
r.engine.fake.frames(20); // part-way to the Cape pad
|
||||||
|
r.parts.selection.setSelectedMission(VANDENBERG.id);
|
||||||
|
r.parts.selection.focusLaunchSite(VANDENBERG);
|
||||||
|
r.engine.fake.resetCalls();
|
||||||
|
r.engine.fake.frames(400);
|
||||||
|
|
||||||
|
assert.equal(r.state._replayCameraLaunchId, null);
|
||||||
|
assert.equal(r.state._selectedLaunchId, VANDENBERG.id);
|
||||||
|
// Nothing the old flyover set up moves the camera again: the only command
|
||||||
|
// after the switch is the zoom anchor on the second mission's pad.
|
||||||
|
for (const { method, args } of r.cameraCommands()) {
|
||||||
|
assert.equal(method, 'camera.lookAt');
|
||||||
|
assert.ok(near(args[0], pad(VANDENBERG), 1), 'anchored on Vandenberg');
|
||||||
|
}
|
||||||
|
assert.ok(near(r.engine.fake.lookAtTarget(), pad(VANDENBERG), 1));
|
||||||
|
assert.equal(r.shown(VANDENBERG.id), true);
|
||||||
|
assert.equal(r.shown(CAPE.id), false);
|
||||||
|
} finally {
|
||||||
|
r.cleanup();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('edge:rocket-launches: a launch cancelled mid-flyover stops the flyover and keeps its card and pad', async () => {
|
||||||
|
const r = rig();
|
||||||
|
try {
|
||||||
|
await r.parts.ingestion.requestMissionUpdate();
|
||||||
|
r.parts.selection.setSelectedMission(CAPE.id);
|
||||||
|
r.parts.replay.startMissionReplay(CAPE.id);
|
||||||
|
r.engine.fake.frames(20);
|
||||||
|
// The next snapshot reports the mission scrubbed.
|
||||||
|
r.setFeed([{ ...CAPE, status: 'Scrubbed' }, VANDENBERG]);
|
||||||
|
await r.parts.ingestion.requestMissionUpdate();
|
||||||
|
await settle();
|
||||||
|
const parked = r.engine.camera.pose().position;
|
||||||
|
r.engine.fake.resetCalls();
|
||||||
|
r.engine.fake.frames(400);
|
||||||
|
|
||||||
|
assert.equal(r.state._replayCameraLaunchId, null);
|
||||||
|
assert.equal(r.engine.camera.flightActive(), false);
|
||||||
|
assert.deepEqual(r.cameraCommands(), [], 'the old flyover never resumes');
|
||||||
|
assert.ok(near(r.engine.camera.pose().position, parked, 1));
|
||||||
|
assert.equal(r.state._selectedLaunchId, CAPE.id, 'the card stays');
|
||||||
|
assert.equal(r.shown(CAPE.id), true, 'the pad stays');
|
||||||
|
} finally {
|
||||||
|
r.cleanup();
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in new issue
Block a user