mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 19:34:01 +00:00
Reframe a mission after a late active-satellite answer only while nobody has left its orbit view
A flyover, a launch-site view, a cancelled flyover or another mission now clears the pending orbit framing, so a late TLE lookup followed by a data refresh never takes the camera back.
This commit is contained in:
1 parent
f0d7def9c7
commit
057dbe24af
6 files changed
+153
-15
No files matched your search
@@ -26,7 +26,7 @@ const VANDENBERG = {
|
||||
const pad = (launch, height = 0) =>
|
||||
geo.cartesian3.fromDegrees(launch.lon, launch.lat, height);
|
||||
|
||||
function rig() {
|
||||
function rig({ tle = async () => '1 25544U' } = {}) {
|
||||
const engine = createFakeEngine({ kind: 'navara' });
|
||||
engine.fake.setPose({
|
||||
longitude: geo.math.toRadians(-100),
|
||||
@@ -52,7 +52,7 @@ function rig() {
|
||||
let feed = [CAPE, VANDENBERG];
|
||||
const source = {
|
||||
getLaunches: async () => feed,
|
||||
getActiveTle: async () => '1 25544U',
|
||||
getActiveTle: () => tle(),
|
||||
};
|
||||
const parts = {
|
||||
panel: {
|
||||
@@ -119,6 +119,7 @@ function rig() {
|
||||
].includes(method),
|
||||
),
|
||||
cleanup() {
|
||||
parts.ingestion.clearPostTleRetry();
|
||||
state._sourceController.abort();
|
||||
parts.replay.stopMissionReplay();
|
||||
renderer.destroy();
|
||||
@@ -183,3 +184,97 @@ test('edge:rocket-launches: a launch cancelled mid-flyover stops the flyover and
|
||||
r.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
/** An active-satellite lookup that answers only when the test says so. */
|
||||
function lateTle() {
|
||||
let answer;
|
||||
const pending = new Promise((resolve) => (answer = resolve));
|
||||
return { tle: () => pending, answer: () => answer('1 25544U') };
|
||||
}
|
||||
const flights = (r) =>
|
||||
r.cameraCommands().filter(({ method }) => method !== 'camera.lookAt');
|
||||
|
||||
test('edge:rocket-launches: a late TLE answer and a refresh never take the camera from a superseded flyover', async () => {
|
||||
const lookup = lateTle();
|
||||
const r = rig({ tle: lookup.tle });
|
||||
try {
|
||||
await r.parts.ingestion.requestMissionUpdate();
|
||||
r.parts.selection.setSelectedMission(CAPE.id);
|
||||
r.parts.selection.focusMission(CAPE);
|
||||
r.engine.fake.frames(200);
|
||||
r.parts.replay.startMissionReplay(CAPE.id);
|
||||
r.engine.fake.frames(20);
|
||||
// The user moves on to the second mission's launch site.
|
||||
r.parts.selection.setSelectedMission(VANDENBERG.id);
|
||||
r.parts.selection.focusLaunchSite(VANDENBERG);
|
||||
r.engine.fake.frames(400);
|
||||
const parked = r.engine.camera.pose().position;
|
||||
r.engine.fake.resetCalls();
|
||||
lookup.answer();
|
||||
await settle();
|
||||
await r.parts.ingestion.requestMissionUpdate();
|
||||
await settle();
|
||||
r.engine.fake.frames(400);
|
||||
|
||||
assert.deepEqual(flights(r), [], 'no flight after the refresh');
|
||||
assert.ok(near(r.engine.camera.pose().position, parked, 1));
|
||||
assert.equal(r.state._selectedLaunchId, VANDENBERG.id);
|
||||
} finally {
|
||||
r.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test('edge:rocket-launches: a late TLE answer and a refresh never take the camera after a running or cancelled flyover', async () => {
|
||||
for (const cancel of [false, true]) {
|
||||
const lookup = lateTle();
|
||||
const r = rig({ tle: lookup.tle });
|
||||
try {
|
||||
await r.parts.ingestion.requestMissionUpdate();
|
||||
r.parts.selection.setSelectedMission(CAPE.id);
|
||||
r.parts.selection.focusMission(CAPE);
|
||||
r.engine.fake.frames(200);
|
||||
r.parts.replay.startMissionReplay(CAPE.id);
|
||||
r.engine.fake.frames(20);
|
||||
if (cancel) r.parts.replay.stopMissionReplay();
|
||||
lookup.answer();
|
||||
await settle();
|
||||
// The refresh ends a running flyover (its tracks are rebuilt) and must
|
||||
// leave the camera where the flyover put it.
|
||||
const parked = r.engine.camera.pose().position;
|
||||
r.engine.fake.resetCalls();
|
||||
await r.parts.ingestion.requestMissionUpdate();
|
||||
await settle();
|
||||
r.engine.fake.frames(400);
|
||||
|
||||
assert.deepEqual(flights(r), [], `no flight (cancelled: ${cancel})`);
|
||||
assert.ok(near(r.engine.camera.pose().position, parked, 1));
|
||||
assert.equal(r.state._replayCameraLaunchId, null);
|
||||
assert.equal(r.state._selectedLaunchId, CAPE.id, 'the card stays');
|
||||
} finally {
|
||||
r.cleanup();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('edge:rocket-launches: a mission still framed when its late TLE arrives is framed again with the orbit', async () => {
|
||||
const lookup = lateTle();
|
||||
const r = rig({ tle: lookup.tle });
|
||||
try {
|
||||
await r.parts.ingestion.requestMissionUpdate();
|
||||
r.parts.selection.setSelectedMission(CAPE.id);
|
||||
r.parts.selection.focusMission(CAPE);
|
||||
r.engine.fake.frames(200);
|
||||
r.engine.fake.resetCalls();
|
||||
lookup.answer();
|
||||
await settle();
|
||||
await r.parts.ingestion.requestMissionUpdate();
|
||||
await settle();
|
||||
assert.deepEqual(
|
||||
flights(r).map(({ method }) => method),
|
||||
['camera.flyTo'],
|
||||
'the orbit view is framed once',
|
||||
);
|
||||
} finally {
|
||||
r.cleanup();
|
||||
}
|
||||
});
|
||||
@@ -70,7 +70,8 @@ export function createIngestion({
|
||||
return null;
|
||||
layerState._activeTleText = text;
|
||||
layerState._focusAfterActiveLookup = Boolean(
|
||||
layerState._selectedLaunchId,
|
||||
layerState._selectedLaunchId &&
|
||||
layerState._missionFocusIntentId === layerState._selectedLaunchId,
|
||||
);
|
||||
schedulePostTleRetry(token);
|
||||
return text;
|
||||
@@ -210,7 +211,12 @@ export function createIngestion({
|
||||
parts.selection.setSelectedMission(null, false);
|
||||
} else {
|
||||
parts.selection.setSelectedMission(layerState._selectedLaunchId, true);
|
||||
if (layerState._focusAfterActiveLookup) {
|
||||
// Only a mission view nobody has left since the lookup answered: a
|
||||
// flyover, a launch-site view or another mission supersedes it.
|
||||
if (
|
||||
layerState._focusAfterActiveLookup &&
|
||||
layerState._missionFocusIntentId === layerState._selectedLaunchId
|
||||
) {
|
||||
parts.selection.focusMission(
|
||||
launches.find(
|
||||
(launch) => launch.id === layerState._selectedLaunchId,
|
||||
|
||||
@@ -16,7 +16,10 @@ export function createLifecycle({
|
||||
layerState._orbitMatches = 0;
|
||||
this.attachRenderer(viewer, options);
|
||||
},
|
||||
attachRenderer(viewer, { engine = null, waitForCameraRestore = false } = {}) {
|
||||
attachRenderer(
|
||||
viewer,
|
||||
{ engine = null, waitForCameraRestore = false } = {},
|
||||
) {
|
||||
layerState._engine = engine;
|
||||
layerState._renderer = services.renderer.create({ viewer, engine });
|
||||
layerState._viewer = layerState._renderer.view;
|
||||
@@ -59,7 +62,8 @@ export function createLifecycle({
|
||||
holdContinuousRender('rocket-launches');
|
||||
layerState._dataSource.show = true;
|
||||
layerState._orbitMatches = 0;
|
||||
for (const launch of layerState._launches) parts.rendering.addLaunchEntity(launch, layerState._renderedTleText);
|
||||
for (const launch of layerState._launches)
|
||||
parts.rendering.addLaunchEntity(launch, layerState._renderedTleText);
|
||||
parts.selection.showSelectedMission();
|
||||
}
|
||||
layerState._restoringViewport = true;
|
||||
@@ -70,10 +74,18 @@ export function createLifecycle({
|
||||
if (!layerState._restoringViewport) return;
|
||||
layerState._restoringViewport = false;
|
||||
if (layerState._enabled && layerState._replayCameraLaunchId) {
|
||||
parts.replay.startMissionReplay(layerState._replayCameraLaunchId, { resuming: true });
|
||||
parts.replay.startMissionReplay(layerState._replayCameraLaunchId, {
|
||||
resuming: true,
|
||||
});
|
||||
} else if (layerState._enabled && layerState._missionZoomAnchorId) {
|
||||
const launch = layerState._launches.find(item => item.id === layerState._missionZoomAnchorId);
|
||||
if (launch) parts.selection.startMissionZoomAnchor(launch, layerState._missionZoomRange);
|
||||
const launch = layerState._launches.find(
|
||||
(item) => item.id === layerState._missionZoomAnchorId,
|
||||
);
|
||||
if (launch)
|
||||
parts.selection.startMissionZoomAnchor(
|
||||
launch,
|
||||
layerState._missionZoomRange,
|
||||
);
|
||||
}
|
||||
layerState._resumeRenderer?.();
|
||||
layerState._waitForRenderer = layerState._resumeRenderer = null;
|
||||
@@ -81,15 +93,21 @@ export function createLifecycle({
|
||||
|
||||
detachRenderer() {
|
||||
if (!layerState._renderer) return;
|
||||
layerState._waitForRenderer ??= new Promise(resolve => { layerState._resumeRenderer = resolve; });
|
||||
layerState._waitForRenderer ??= new Promise((resolve) => {
|
||||
layerState._resumeRenderer = resolve;
|
||||
});
|
||||
layerState._replayCameraToken++;
|
||||
layerState._replayCameraRemover?.();
|
||||
layerState._missionZoomAnchorRemover?.();
|
||||
layerState._replayCameraRemover = layerState._missionZoomAnchorRemover = null;
|
||||
layerState._replayCameraRemover = layerState._missionZoomAnchorRemover =
|
||||
null;
|
||||
layerState._clickHandler?.destroy();
|
||||
layerState._moveHandler?.destroy();
|
||||
layerState._declutterHandler?.();
|
||||
layerState._clickHandler = layerState._moveHandler = layerState._declutterHandler = null;
|
||||
layerState._clickHandler =
|
||||
layerState._moveHandler =
|
||||
layerState._declutterHandler =
|
||||
null;
|
||||
parts.launchPad.destroyLaunchPadZonePrimitive();
|
||||
parts.orbitRendering.removeMissionOrbitPrimitives();
|
||||
parts.overlays.clearMissionOverlaySources();
|
||||
@@ -99,7 +117,11 @@ export function createLifecycle({
|
||||
layerState._replayTracks.clear();
|
||||
layerState._renderer.removeDataSource(layerState._dataSource);
|
||||
layerState._renderer.destroy();
|
||||
layerState._dataSource = layerState._renderer = layerState._engine = layerState._viewer = null;
|
||||
layerState._dataSource =
|
||||
layerState._renderer =
|
||||
layerState._engine =
|
||||
layerState._viewer =
|
||||
null;
|
||||
if (layerState._enabled) releaseContinuousRender('rocket-launches');
|
||||
},
|
||||
|
||||
@@ -158,6 +180,8 @@ export function createLifecycle({
|
||||
parts.launchPad.hideLaunchPadZone();
|
||||
parts.overlays.hideReplayVehicleOverlay();
|
||||
layerState._selectedLaunchId = null;
|
||||
layerState._missionFocusIntentId = null;
|
||||
layerState._focusAfterActiveLookup = false;
|
||||
parts.overlays.clearMissionOverlaySources();
|
||||
await parts.ingestion.restoreSatelliteDependency();
|
||||
parts.panel.renderMissionPanel();
|
||||
|
||||
@@ -326,13 +326,16 @@ export function createReplay({
|
||||
layerState._replayCameraLaunchId = null;
|
||||
layerState._replayPaused = false;
|
||||
layerState._replayPausedAtMs = null;
|
||||
if (stoppedLaunchId)
|
||||
if (stoppedLaunchId) {
|
||||
layerState._animationStarts.set(stoppedLaunchId, appClock.now());
|
||||
// The flyover moved the camera; the orbit view asked for before it is gone.
|
||||
layerState._missionFocusIntentId = null;
|
||||
}
|
||||
syncReplayButton();
|
||||
parts.overlays.syncMissionOverlayEntries();
|
||||
}
|
||||
|
||||
function startMissionReplay(launchId, {resuming = false} = {}) {
|
||||
function startMissionReplay(launchId, { resuming = false } = {}) {
|
||||
const launch = layerState._launches.find((item) => item.id === launchId);
|
||||
const track = layerState._replayTracks.get(launchId);
|
||||
if (!layerState._viewer || !launch || !track) return false;
|
||||
@@ -345,6 +348,7 @@ export function createReplay({
|
||||
parts.selection.stopMissionZoomAnchor();
|
||||
stopMissionReplay();
|
||||
layerState._replayCameraLaunchId = launchId;
|
||||
layerState._missionFocusIntentId = null;
|
||||
layerState._replayPaused = false;
|
||||
layerState._replayPausedAtMs = null;
|
||||
}
|
||||
|
||||
@@ -68,6 +68,8 @@ export function createSelection({
|
||||
layerState._missionZoomAnchorId !== launchId
|
||||
)
|
||||
stopMissionZoomAnchor();
|
||||
if (layerState._missionFocusIntentId !== launchId)
|
||||
layerState._missionFocusIntentId = null;
|
||||
layerState._selectedLaunchId = launchId;
|
||||
layerState._explicitSelection = Boolean(launchId && isolate);
|
||||
if (launchId) layerState._animationStarts.set(launchId, appClock.now());
|
||||
@@ -122,6 +124,7 @@ export function createSelection({
|
||||
function focusMission(launch) {
|
||||
if (!layerState._viewer || !launch) return;
|
||||
stopMissionZoomAnchor();
|
||||
layerState._missionFocusIntentId = launch.id;
|
||||
layerState._viewer.selectedEntity = layerState._dataSource.entities.getById(
|
||||
`rocket-launch:${launch.id}`,
|
||||
);
|
||||
@@ -198,6 +201,7 @@ export function createSelection({
|
||||
function focusLaunchSite(launch) {
|
||||
if (!layerState._viewer || !launch) return;
|
||||
parts.replay.stopMissionReplay();
|
||||
layerState._missionFocusIntentId = null;
|
||||
stopMissionZoomAnchor();
|
||||
const launchPosition = geo.cartesian3.fromDegrees(launch.lon, launch.lat);
|
||||
layerState._viewer.selectedEntity = layerState._dataSource.entities.getById(
|
||||
|
||||
@@ -100,6 +100,11 @@ export function createState({ services }) {
|
||||
|
||||
state._focusAfterActiveLookup = false;
|
||||
|
||||
// The mission whose orbit view the user asked for last; any later camera
|
||||
// command of this layer (a flyover, a launch-site view) clears it, so a late
|
||||
// active-satellite answer can reframe only a view nobody has left.
|
||||
state._missionFocusIntentId = null;
|
||||
|
||||
state._satelliteStateBeforeMission = null;
|
||||
|
||||
state._satelliteActivationPromise = null;
|
||||
|
||||
Reference in new issue
Block a user