mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-10 21:32:23 +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. */
|
/** Release only Space Mission camera ownership, preserving layer and selection state. */
|
||||||
releaseCameraOwnership() {
|
releaseCameraOwnership() {
|
||||||
|
// A failed entry no longer gives back the view it replaced.
|
||||||
|
layerState._entryCameraView = null;
|
||||||
parts.panel.clearMissionRosterHover();
|
parts.panel.clearMissionRosterHover();
|
||||||
parts.replay.stopMissionReplay();
|
parts.replay.stopMissionReplay();
|
||||||
parts.selection.stopMissionZoomAnchor();
|
parts.selection.stopMissionZoomAnchor();
|
||||||
|
|||||||
@@ -57,17 +57,29 @@ function fakeCamera() {
|
|||||||
roll: 0,
|
roll: 0,
|
||||||
flights: 0,
|
flights: 0,
|
||||||
cancelled: 0,
|
cancelled: 0,
|
||||||
|
flight: null,
|
||||||
get positionWC() {
|
get positionWC() {
|
||||||
return camera.position;
|
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.flights += 1;
|
||||||
camera.position = Cesium.Cartesian3.clone(destination);
|
camera.position = Cesium.Cartesian3.clone(destination);
|
||||||
camera.heading = 0;
|
camera.heading = 0;
|
||||||
camera.pitch = -Math.PI / 2;
|
camera.pitch = -Math.PI / 2;
|
||||||
},
|
},
|
||||||
|
land() {
|
||||||
|
const flight = camera.flight;
|
||||||
|
camera.flight = null;
|
||||||
|
flight?.complete?.();
|
||||||
|
},
|
||||||
cancelFlight() {
|
cancelFlight() {
|
||||||
camera.cancelled += 1;
|
camera.cancelled += 1;
|
||||||
|
const flight = camera.flight;
|
||||||
|
camera.flight = null;
|
||||||
|
flight?.cancel?.();
|
||||||
},
|
},
|
||||||
setView({ destination, orientation }) {
|
setView({ destination, orientation }) {
|
||||||
camera.position = Cesium.Cartesian3.clone(destination);
|
camera.position = Cesium.Cartesian3.clone(destination);
|
||||||
@@ -81,11 +93,12 @@ function fakeCamera() {
|
|||||||
|
|
||||||
async function fixture(t) {
|
async function fixture(t) {
|
||||||
const upstream = { status: 429, results: [launchRecord] };
|
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') {
|
if (req.url === '/api/celestrak/active') {
|
||||||
res.end('');
|
res.end('');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (upstream.hold) await upstream.hold;
|
||||||
if (upstream.status !== 200) {
|
if (upstream.status !== 200) {
|
||||||
res.writeHead(upstream.status, { 'Content-Type': 'application/json' });
|
res.writeHead(upstream.status, { 'Content-Type': 'application/json' });
|
||||||
res.end(JSON.stringify({ error: 'Launch Library 2 unavailable' }));
|
res.end(JSON.stringify({ error: 'Launch Library 2 unavailable' }));
|
||||||
@@ -134,8 +147,13 @@ async function fixture(t) {
|
|||||||
},
|
},
|
||||||
replay: { stopMissionReplay: noop, presentReplay: () => null },
|
replay: { stopMissionReplay: noop, presentReplay: () => null },
|
||||||
selection: {
|
selection: {
|
||||||
focusFullGlobe: (target) =>
|
focusFullGlobe(target, flight) {
|
||||||
target.camera.flyTo({ destination: new Cesium.Cartesian3(9, 9, 9) }),
|
target.camera.flyTo({
|
||||||
|
destination: new Cesium.Cartesian3(9, 9, 9),
|
||||||
|
...flight,
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
},
|
||||||
setSelectedMission: noop,
|
setSelectedMission: noop,
|
||||||
focusMission: noop,
|
focusMission: noop,
|
||||||
stopMissionZoomAnchor: 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);
|
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) => {
|
test('the roster reports a failed mission index instead of loading forever', async (t) => {
|
||||||
const f = await fixture(t);
|
const f = await fixture(t);
|
||||||
await f.manager.setEnabled('rocket-launches', true, { origin: 'user' });
|
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 sameCameraView(camera, view) {
|
||||||
function restoreCameraView(viewer, 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;
|
const camera = viewer?.camera;
|
||||||
if (!camera) return;
|
if (!camera) return;
|
||||||
camera.cancelFlight?.();
|
if (!landedView) camera.cancelFlight?.();
|
||||||
|
else if (!sameCameraView(camera, landedView)) return;
|
||||||
camera.setView(view);
|
camera.setView(view);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,9 +246,21 @@ export function createLifecycle({
|
|||||||
parts.panel.renderMissionRoster();
|
parts.panel.renderMissionRoster();
|
||||||
}
|
}
|
||||||
// Kept until the mission index loads, so an entry that never does
|
// 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);
|
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)
|
if (layerState._selectedLaunchId)
|
||||||
parts.selection.setSelectedMission(
|
parts.selection.setSelectedMission(
|
||||||
layerState._selectedLaunchId,
|
layerState._selectedLaunchId,
|
||||||
@@ -246,7 +276,12 @@ export function createLifecycle({
|
|||||||
layerState._enabled = false;
|
layerState._enabled = false;
|
||||||
const entryView = layerState._entryCameraView;
|
const entryView = layerState._entryCameraView;
|
||||||
layerState._entryCameraView = null;
|
layerState._entryCameraView = null;
|
||||||
if (entryView) restoreCameraView(layerState._viewer, entryView);
|
if (entryView)
|
||||||
|
restoreCameraView(
|
||||||
|
layerState._viewer,
|
||||||
|
entryView,
|
||||||
|
layerState._entryLandedView,
|
||||||
|
);
|
||||||
releaseContinuousRender('rocket-launches');
|
releaseContinuousRender('rocket-launches');
|
||||||
layerState._lifecycleToken++;
|
layerState._lifecycleToken++;
|
||||||
layerState._updateDirty = false;
|
layerState._updateDirty = false;
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ export function createSelection({
|
|||||||
}) {
|
}) {
|
||||||
const { getKeyholeGeometry } = services.geometry;
|
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 canvas = viewer?.scene?.canvas;
|
||||||
const height = canvas?.clientHeight || canvas?.height;
|
const height = canvas?.clientHeight || canvas?.height;
|
||||||
const width = canvas?.clientWidth || canvas?.width;
|
const width = canvas?.clientWidth || canvas?.width;
|
||||||
@@ -23,7 +24,7 @@ export function createSelection({
|
|||||||
fovy <= 0 ||
|
fovy <= 0 ||
|
||||||
fovy >= Math.PI
|
fovy >= Math.PI
|
||||||
)
|
)
|
||||||
return;
|
return false;
|
||||||
const earthRadius = Cesium.Ellipsoid.WGS84.maximumRadius;
|
const earthRadius = Cesium.Ellipsoid.WGS84.maximumRadius;
|
||||||
const keyholeRadius = getKeyholeGeometry(width, height).radius;
|
const keyholeRadius = getKeyholeGeometry(width, height).radius;
|
||||||
const targetScreenRadius = keyholeRadius * 0.61;
|
const targetScreenRadius = keyholeRadius * 0.61;
|
||||||
@@ -45,7 +46,10 @@ export function createSelection({
|
|||||||
},
|
},
|
||||||
duration,
|
duration,
|
||||||
easingFunction: Cesium.EasingFunction.CUBIC_IN_OUT,
|
easingFunction: Cesium.EasingFunction.CUBIC_IN_OUT,
|
||||||
|
complete,
|
||||||
|
cancel,
|
||||||
});
|
});
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function entityLaunchId(entity) {
|
function entityLaunchId(entity) {
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ export function createState({ services }) {
|
|||||||
|
|
||||||
state._entryCameraView = null;
|
state._entryCameraView = null;
|
||||||
|
|
||||||
|
state._entryLandedView = null;
|
||||||
|
|
||||||
state._stale = false;
|
state._stale = false;
|
||||||
|
|
||||||
state._orbitMatches = 0;
|
state._orbitMatches = 0;
|
||||||
|
|||||||
Reference in new issue
Block a user