mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-07 11:54:02 +00:00
Keep a newer camera view when a failed Space Missions entry rolls back
This commit is contained in:
1 parent
c64ed40c62
commit
655d93cc92
5 files changed
+154
-12
No files matched your search
Vendored
+2
@@ -12,6 +12,8 @@ export function createControls({ state: layerState, services, parts, source }) {
|
||||
|
||||
/** Release only Space Mission camera ownership, preserving layer and selection state. */
|
||||
releaseCameraOwnership() {
|
||||
// A failed entry no longer gives back the view it replaced.
|
||||
layerState._entryCameraView = null;
|
||||
parts.panel.clearMissionRosterHover();
|
||||
parts.replay.stopMissionReplay();
|
||||
parts.selection.stopMissionZoomAnchor();
|
||||
|
||||
@@ -57,17 +57,29 @@ function fakeCamera() {
|
||||
roll: 0,
|
||||
flights: 0,
|
||||
cancelled: 0,
|
||||
flight: null,
|
||||
get positionWC() {
|
||||
return camera.position;
|
||||
},
|
||||
flyTo({ destination }) {
|
||||
// Like Cesium, a new flight cancels the one in progress.
|
||||
flyTo({ destination, complete, cancel }) {
|
||||
camera.flight?.cancel?.();
|
||||
camera.flight = { complete, cancel };
|
||||
camera.flights += 1;
|
||||
camera.position = Cesium.Cartesian3.clone(destination);
|
||||
camera.heading = 0;
|
||||
camera.pitch = -Math.PI / 2;
|
||||
},
|
||||
land() {
|
||||
const flight = camera.flight;
|
||||
camera.flight = null;
|
||||
flight?.complete?.();
|
||||
},
|
||||
cancelFlight() {
|
||||
camera.cancelled += 1;
|
||||
const flight = camera.flight;
|
||||
camera.flight = null;
|
||||
flight?.cancel?.();
|
||||
},
|
||||
setView({ destination, orientation }) {
|
||||
camera.position = Cesium.Cartesian3.clone(destination);
|
||||
@@ -81,11 +93,12 @@ function fakeCamera() {
|
||||
|
||||
async function fixture(t) {
|
||||
const upstream = { status: 429, results: [launchRecord] };
|
||||
const server = http.createServer((req, res) => {
|
||||
const server = http.createServer(async (req, res) => {
|
||||
if (req.url === '/api/celestrak/active') {
|
||||
res.end('');
|
||||
return;
|
||||
}
|
||||
if (upstream.hold) await upstream.hold;
|
||||
if (upstream.status !== 200) {
|
||||
res.writeHead(upstream.status, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Launch Library 2 unavailable' }));
|
||||
@@ -134,8 +147,13 @@ async function fixture(t) {
|
||||
},
|
||||
replay: { stopMissionReplay: noop, presentReplay: () => null },
|
||||
selection: {
|
||||
focusFullGlobe: (target) =>
|
||||
target.camera.flyTo({ destination: new Cesium.Cartesian3(9, 9, 9) }),
|
||||
focusFullGlobe(target, flight) {
|
||||
target.camera.flyTo({
|
||||
destination: new Cesium.Cartesian3(9, 9, 9),
|
||||
...flight,
|
||||
});
|
||||
return true;
|
||||
},
|
||||
setSelectedMission: noop,
|
||||
focusMission: noop,
|
||||
stopMissionZoomAnchor: noop,
|
||||
@@ -284,6 +302,87 @@ test('a Context-panel Space Missions entry that gets HTTP 429 restores the previ
|
||||
assert.deepEqual(f.view(), before);
|
||||
});
|
||||
|
||||
// Holds the /api/launches reply until the returned release is called.
|
||||
function holdReply(f) {
|
||||
let release;
|
||||
f.upstream.hold = new Promise((resolve) => {
|
||||
release = resolve;
|
||||
});
|
||||
return () => {
|
||||
f.upstream.hold = null;
|
||||
release();
|
||||
};
|
||||
}
|
||||
|
||||
async function untilFlown(f) {
|
||||
for (let i = 0; i < 200 && f.camera.flights === 0; i++)
|
||||
await new Promise((resolve) => setTimeout(resolve, 5));
|
||||
assert.ok(f.camera.flights > 0, 'the entry started its full-globe flight');
|
||||
}
|
||||
|
||||
test('a landed entry flight still gives the view back after a failed first load', async (t) => {
|
||||
const f = await fixture(t);
|
||||
await f.manager.setEnabled('earthquakes', true, { origin: 'user' });
|
||||
const before = f.view();
|
||||
const release = holdReply(f);
|
||||
|
||||
const entering = f.manager.setEnabled('rocket-launches', true, {
|
||||
origin: 'user',
|
||||
});
|
||||
await untilFlown(f);
|
||||
f.camera.land();
|
||||
|
||||
release();
|
||||
assert.equal(await entering, false);
|
||||
await f.settle();
|
||||
assert.deepEqual(f.view(), before);
|
||||
});
|
||||
|
||||
test('a camera move after the entry flight survives a failed first load', async (t) => {
|
||||
const f = await fixture(t);
|
||||
await f.manager.setEnabled('earthquakes', true, { origin: 'user' });
|
||||
const release = holdReply(f);
|
||||
|
||||
const entering = f.manager.setEnabled('rocket-launches', true, {
|
||||
origin: 'user',
|
||||
});
|
||||
await untilFlown(f);
|
||||
f.camera.land();
|
||||
// The user drags the globe while the mission index is still loading.
|
||||
f.camera.setView({
|
||||
destination: new Cesium.Cartesian3(7, 7, 7),
|
||||
orientation: { heading: 0.3, pitch: -0.2, roll: 0 },
|
||||
});
|
||||
const moved = f.view();
|
||||
|
||||
release();
|
||||
assert.equal(await entering, false);
|
||||
await f.settle();
|
||||
assert.deepEqual(f.view(), moved);
|
||||
});
|
||||
|
||||
test('a navigation during a failed first load is not cancelled by the rollback', async (t) => {
|
||||
const f = await fixture(t);
|
||||
await f.manager.setEnabled('earthquakes', true, { origin: 'user' });
|
||||
const release = holdReply(f);
|
||||
|
||||
const entering = f.manager.setEnabled('rocket-launches', true, {
|
||||
origin: 'user',
|
||||
});
|
||||
await untilFlown(f);
|
||||
// Search or Home: navigation releases mission camera ownership, then flies.
|
||||
f.missions.releaseCameraOwnership();
|
||||
f.camera.flyTo({ destination: new Cesium.Cartesian3(5, 5, 5) });
|
||||
const cancelled = f.camera.cancelled;
|
||||
const navigated = f.view();
|
||||
|
||||
release();
|
||||
assert.equal(await entering, false);
|
||||
await f.settle();
|
||||
assert.equal(f.camera.cancelled, cancelled);
|
||||
assert.deepEqual(f.view(), navigated);
|
||||
});
|
||||
|
||||
test('the roster reports a failed mission index instead of loading forever', async (t) => {
|
||||
const f = await fixture(t);
|
||||
await f.manager.setEnabled('rocket-launches', true, { origin: 'user' });
|
||||
|
||||
@@ -125,11 +125,29 @@ function captureCameraView(viewer) {
|
||||
};
|
||||
}
|
||||
|
||||
/** Stop the full-globe flight and return to a captured pose. */
|
||||
function restoreCameraView(viewer, view) {
|
||||
function sameCameraView(camera, view) {
|
||||
return (
|
||||
Cesium.Cartesian3.equalsEpsilon(
|
||||
camera.positionWC,
|
||||
view.destination,
|
||||
0,
|
||||
1,
|
||||
) &&
|
||||
['heading', 'pitch', 'roll'].every((key) =>
|
||||
Cesium.Math.equalsEpsilon(camera[key], view.orientation[key], 0, 1e-6),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return to the pose before entry while the entry's full-globe flight still
|
||||
* holds the camera: in flight, or landed with nothing moved since.
|
||||
*/
|
||||
function restoreCameraView(viewer, view, landedView) {
|
||||
const camera = viewer?.camera;
|
||||
if (!camera) return;
|
||||
camera.cancelFlight?.();
|
||||
if (!landedView) camera.cancelFlight?.();
|
||||
else if (!sameCameraView(camera, landedView)) return;
|
||||
camera.setView(view);
|
||||
}
|
||||
|
||||
@@ -228,9 +246,21 @@ export function createLifecycle({
|
||||
parts.panel.renderMissionRoster();
|
||||
}
|
||||
// Kept until the mission index loads, so an entry that never does
|
||||
// gives the view back.
|
||||
// gives the view back. A newer flight takes the camera and drops it.
|
||||
const token = layerState._lifecycleToken;
|
||||
layerState._entryCameraView = captureCameraView(layerState._viewer);
|
||||
parts.selection.focusFullGlobe(layerState._viewer);
|
||||
layerState._entryLandedView = null;
|
||||
const flying = parts.selection.focusFullGlobe(layerState._viewer, {
|
||||
complete: () => {
|
||||
if (token === layerState._lifecycleToken)
|
||||
layerState._entryLandedView = captureCameraView(layerState._viewer);
|
||||
},
|
||||
cancel: () => {
|
||||
if (token === layerState._lifecycleToken)
|
||||
layerState._entryCameraView = null;
|
||||
},
|
||||
});
|
||||
if (!flying) layerState._entryCameraView = null;
|
||||
if (layerState._selectedLaunchId)
|
||||
parts.selection.setSelectedMission(
|
||||
layerState._selectedLaunchId,
|
||||
@@ -246,7 +276,12 @@ export function createLifecycle({
|
||||
layerState._enabled = false;
|
||||
const entryView = layerState._entryCameraView;
|
||||
layerState._entryCameraView = null;
|
||||
if (entryView) restoreCameraView(layerState._viewer, entryView);
|
||||
if (entryView)
|
||||
restoreCameraView(
|
||||
layerState._viewer,
|
||||
entryView,
|
||||
layerState._entryLandedView,
|
||||
);
|
||||
releaseContinuousRender('rocket-launches');
|
||||
layerState._lifecycleToken++;
|
||||
layerState._updateDirty = false;
|
||||
|
||||
@@ -9,7 +9,8 @@ export function createSelection({
|
||||
}) {
|
||||
const { getKeyholeGeometry } = services.geometry;
|
||||
|
||||
function focusFullGlobe(viewer, duration = 2.4) {
|
||||
/** @returns {boolean} Whether the flight started. */
|
||||
function focusFullGlobe(viewer, { duration = 2.4, complete, cancel } = {}) {
|
||||
const canvas = viewer?.scene?.canvas;
|
||||
const height = canvas?.clientHeight || canvas?.height;
|
||||
const width = canvas?.clientWidth || canvas?.width;
|
||||
@@ -23,7 +24,7 @@ export function createSelection({
|
||||
fovy <= 0 ||
|
||||
fovy >= Math.PI
|
||||
)
|
||||
return;
|
||||
return false;
|
||||
const earthRadius = Cesium.Ellipsoid.WGS84.maximumRadius;
|
||||
const keyholeRadius = getKeyholeGeometry(width, height).radius;
|
||||
const targetScreenRadius = keyholeRadius * 0.61;
|
||||
@@ -45,7 +46,10 @@ export function createSelection({
|
||||
},
|
||||
duration,
|
||||
easingFunction: Cesium.EasingFunction.CUBIC_IN_OUT,
|
||||
complete,
|
||||
cancel,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
function entityLaunchId(entity) {
|
||||
|
||||
@@ -21,6 +21,8 @@ export function createState({ services }) {
|
||||
|
||||
state._entryCameraView = null;
|
||||
|
||||
state._entryLandedView = null;
|
||||
|
||||
state._stale = false;
|
||||
|
||||
state._orbitMatches = 0;
|
||||
|
||||
Reference in new issue
Block a user