mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-12 14:22:21 +00:00
Mark a resumed or share-restored follow only while it starts so a later follow of the same subject is navigation again
The aircraft scene binding re-tracked with origin share-restore, which stayed on the entity: a later trackedEntityChanged for it (cockpit exit, refocus) never stamped navigation, and the aircraft lost its selection origin. Flights, military and satellites now set a transient gevRestoring mark around follow.start, and the aircraft binding resumes with the origin the aircraft was selected by.
This commit is contained in:
1 parent
cbe1d841ea
commit
90d64e6916
7 files changed
+176
-41
No files matched your search
@@ -117,7 +117,7 @@ test('voice Cockpit entry reaches the camera only through stamping seams', () =>
|
|||||||
// Seam 1: a new tracker handoff stamps; restoring the session does not.
|
// Seam 1: a new tracker handoff stamps; restoring the session does not.
|
||||||
assert.match(
|
assert.match(
|
||||||
ui,
|
ui,
|
||||||
/viewer\.trackedEntityChanged\.addEventListener\(\s*\(entity\) => \{[\s\S]*?if \(\s*entity &&\s*!this\._disposed &&\s*entity\.gevSelectionOrigin !== 'share-restore'\s*\)\s*this\._stampNavigation\(\{\s*cancelPendingSelection: false,?\s*\}\);/,
|
/viewer\.trackedEntityChanged\.addEventListener\(\s*\(entity\) => \{[\s\S]*?if \(entity && !this\._disposed && !entity\.gevRestoring\)\s*this\._stampNavigation\(\{\s*cancelPendingSelection: false,?\s*\}\);/,
|
||||||
'tracker handoff must stamp',
|
'tracker handoff must stamp',
|
||||||
);
|
);
|
||||||
// Seam 2 is pinned by "Cockpit takeover invalidates deferred work" above.
|
// Seam 2 is pinned by "Cockpit takeover invalidates deferred work" above.
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ export function createAircraftSceneBinding({
|
|||||||
enable,
|
enable,
|
||||||
}) {
|
}) {
|
||||||
let resumeFollow = false;
|
let resumeFollow = false;
|
||||||
|
// The tracked aircraft's selection origin outlives the renderer, as the
|
||||||
|
// tracking itself does.
|
||||||
|
let resumeOrigin = 'programmatic';
|
||||||
function removeSceneListeners() {
|
function removeSceneListeners() {
|
||||||
parts.tracking._removeClickHandler();
|
parts.tracking._removeClickHandler();
|
||||||
services.picking.unregisterPickOwner(id);
|
services.picking.unregisterPickOwner(id);
|
||||||
@@ -58,7 +61,7 @@ export function createAircraftSceneBinding({
|
|||||||
parts.snapshots.presentRecords();
|
parts.snapshots.presentRecords();
|
||||||
if (flightState._trackedIcao) {
|
if (flightState._trackedIcao) {
|
||||||
parts.tracking._trackFlight(flightState._trackedIcao, {
|
parts.tracking._trackFlight(flightState._trackedIcao, {
|
||||||
origin: 'share-restore',
|
origin: resumeOrigin,
|
||||||
resume: true,
|
resume: true,
|
||||||
follow: resumeFollow,
|
follow: resumeFollow,
|
||||||
});
|
});
|
||||||
@@ -84,6 +87,8 @@ export function createAircraftSceneBinding({
|
|||||||
flightState._trackedCameraFrameStop?.();
|
flightState._trackedCameraFrameStop?.();
|
||||||
flightState._trackedCameraFrameStop = null;
|
flightState._trackedCameraFrameStop = null;
|
||||||
if (flightState._trackedEntity) {
|
if (flightState._trackedEntity) {
|
||||||
|
resumeOrigin =
|
||||||
|
flightState._trackedEntity.gevSelectionOrigin ?? 'programmatic';
|
||||||
resumeFollow = renderer.follow.isFollowing(flightState._trackedEntity);
|
resumeFollow = renderer.follow.isFollowing(flightState._trackedEntity);
|
||||||
if (resumeFollow) renderer.follow.release();
|
if (resumeFollow) renderer.follow.release();
|
||||||
renderer.tracked.remove(flightState._trackedEntity);
|
renderer.tracked.remove(flightState._trackedEntity);
|
||||||
|
|||||||
@@ -3,61 +3,165 @@ import test from 'node:test';
|
|||||||
import { createAircraftSceneBinding } from './sceneBinding.js';
|
import { createAircraftSceneBinding } from './sceneBinding.js';
|
||||||
import { LayerBindings } from '../../ui/layerBindings.js';
|
import { LayerBindings } from '../../ui/layerBindings.js';
|
||||||
import { createEngineEvent } from '../../globe/contract.js';
|
import { createEngineEvent } from '../../globe/contract.js';
|
||||||
|
import { readLayerSource } from '../../testSupport/readLayerSource.mjs';
|
||||||
|
|
||||||
test('resuming an aircraft on Cesium preserves the pending share navigation', (t) => {
|
function observedShell(t) {
|
||||||
const previous = globalThis.window;
|
const previous = globalThis.window;
|
||||||
globalThis.window = new EventTarget();
|
globalThis.window = new EventTarget();
|
||||||
t.after(() => {
|
t.after(() => {
|
||||||
globalThis.window = previous;
|
globalThis.window = previous;
|
||||||
});
|
});
|
||||||
const trackedEntityChanged = createEngineEvent();
|
const trackedEntityChanged = createEngineEvent();
|
||||||
let generation = 1;
|
const shell = {
|
||||||
const shell = new LayerBindings({
|
generation: 1,
|
||||||
|
trackedEntityChanged,
|
||||||
|
};
|
||||||
|
const bindings = new LayerBindings({
|
||||||
viewer: { trackedEntityChanged },
|
viewer: { trackedEntityChanged },
|
||||||
services: {},
|
services: {},
|
||||||
operations: {
|
operations: {
|
||||||
_stampNavigation() {
|
_stampNavigation() {
|
||||||
generation++;
|
shell.generation++;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
shell.observeCamera();
|
bindings.observeCamera();
|
||||||
t.after(() => shell.stop());
|
t.after(() => bindings.stop());
|
||||||
|
return shell;
|
||||||
|
}
|
||||||
|
|
||||||
|
// What the tracking modules do around follow.start on a resume or a share
|
||||||
|
// restore (flights, military and satellites tracking.js).
|
||||||
|
function follow(trackedEntityChanged, entity, restoring) {
|
||||||
|
if (restoring) entity.gevRestoring = true;
|
||||||
|
try {
|
||||||
|
trackedEntityChanged.raiseEvent(entity);
|
||||||
|
} finally {
|
||||||
|
if (restoring) delete entity.gevRestoring;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function binding(flightState, tracked) {
|
||||||
const renderer = {
|
const renderer = {
|
||||||
fleet: { attach() {}, setVisible() {}, restoreOrder() {} },
|
fleet: {
|
||||||
models: { setVisible() {} },
|
attach() {},
|
||||||
|
detach() {},
|
||||||
|
setVisible() {},
|
||||||
|
restoreOrder() {},
|
||||||
|
},
|
||||||
|
models: { setVisible() {}, destroy() {} },
|
||||||
|
follow: {
|
||||||
|
isFollowing: () => true,
|
||||||
|
release() {},
|
||||||
|
},
|
||||||
|
tracked: { remove() {} },
|
||||||
|
destroy() {},
|
||||||
};
|
};
|
||||||
let restored;
|
return createAircraftSceneBinding({
|
||||||
const binding = createAircraftSceneBinding({
|
|
||||||
id: 'flights',
|
id: 'flights',
|
||||||
flightState: {
|
flightState,
|
||||||
initialized: true,
|
services: {
|
||||||
_trackedIcao: 'a00000',
|
renderer: { create: () => renderer },
|
||||||
_preloadModel: {},
|
render: { releaseContinuousRender() {} },
|
||||||
|
picking: { unregisterPickOwner() {} },
|
||||||
},
|
},
|
||||||
services: { renderer: { create: () => renderer } },
|
|
||||||
parts: {
|
parts: {
|
||||||
snapshots: { presentRecords() {} },
|
snapshots: { presentRecords() {} },
|
||||||
tracking: {
|
tracking: {
|
||||||
_trackFlight(id, options) {
|
_trackFlight: tracked,
|
||||||
restored = { id, ...options };
|
_removeClickHandler() {},
|
||||||
trackedEntityChanged.raiseEvent({
|
_detachTrail() {},
|
||||||
gevSelectionOrigin: options.origin,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
rendering: { _releaseTrackedModel() {}, _releaseModels() {} },
|
||||||
|
motion: { _resetTrackedDisplay() {} },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
binding.attachRenderer({});
|
}
|
||||||
|
|
||||||
|
test('resuming an aircraft on Cesium preserves the pending share navigation', (t) => {
|
||||||
|
const shell = observedShell(t);
|
||||||
|
let restored;
|
||||||
|
binding(
|
||||||
|
{ initialized: true, _trackedIcao: 'a00000', _preloadModel: {} },
|
||||||
|
(id, options) => {
|
||||||
|
restored = { id, ...options };
|
||||||
|
follow(
|
||||||
|
shell.trackedEntityChanged,
|
||||||
|
{ gevSelectionOrigin: options.origin },
|
||||||
|
options.resume,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
).attachRenderer({});
|
||||||
assert.equal(restored.id, 'a00000');
|
assert.equal(restored.id, 'a00000');
|
||||||
assert.equal(restored.resume, true);
|
assert.equal(restored.resume, true);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
generation,
|
shell.generation,
|
||||||
1,
|
1,
|
||||||
'restored tracking cannot supersede and clear itself',
|
'restored tracking cannot supersede and clear itself',
|
||||||
);
|
);
|
||||||
for (const origin of ['user', 'voice', 'programmatic']) {
|
for (const origin of ['user', 'voice', 'programmatic']) {
|
||||||
trackedEntityChanged.raiseEvent({ gevSelectionOrigin: origin });
|
follow(shell.trackedEntityChanged, { gevSelectionOrigin: origin }, false);
|
||||||
|
}
|
||||||
|
assert.equal(
|
||||||
|
shell.generation,
|
||||||
|
4,
|
||||||
|
'new tracking still takes navigation authority',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a restored aircraft followed again later is new navigation', (t) => {
|
||||||
|
const shell = observedShell(t);
|
||||||
|
const entity = { gevSelectionOrigin: 'share-restore' };
|
||||||
|
follow(shell.trackedEntityChanged, entity, true);
|
||||||
|
assert.equal(shell.generation, 1);
|
||||||
|
assert.equal('gevRestoring' in entity, false, 'the mark does not stick');
|
||||||
|
// Cockpit exit or a refocus hands the same aircraft back to the camera.
|
||||||
|
shell.trackedEntityChanged.raiseEvent(undefined);
|
||||||
|
shell.trackedEntityChanged.raiseEvent(entity);
|
||||||
|
assert.equal(shell.generation, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('an engine switch resumes the aircraft with the origin it was selected by', () => {
|
||||||
|
const calls = [];
|
||||||
|
const flightState = {
|
||||||
|
initialized: true,
|
||||||
|
_trackedIcao: 'a00000',
|
||||||
|
_preloadModel: null,
|
||||||
|
_modelEpoch: 0,
|
||||||
|
_modelPending: new Map(),
|
||||||
|
_modelGen: new Map(),
|
||||||
|
_billboards: new Map(),
|
||||||
|
_cullPositions: new Map(),
|
||||||
|
_detectionObjects: new Set(),
|
||||||
|
_groundSnap: new Map(),
|
||||||
|
};
|
||||||
|
const scene = binding(flightState, (id, options) => calls.push(options));
|
||||||
|
flightState._preloadModel = {};
|
||||||
|
scene.attachRenderer({});
|
||||||
|
assert.equal(calls[0].origin, 'programmatic');
|
||||||
|
flightState._trackedEntity = { gevSelectionOrigin: 'voice' };
|
||||||
|
scene.detachRenderer({});
|
||||||
|
flightState._preloadModel = {};
|
||||||
|
scene.attachRenderer({});
|
||||||
|
assert.equal(calls[1].origin, 'voice');
|
||||||
|
assert.equal(calls[1].resume, true);
|
||||||
|
assert.equal(calls[1].follow, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('every follow of a resumed or share-restored subject is marked while it starts', () => {
|
||||||
|
for (const [file, resume] of [
|
||||||
|
['flights/tracking.js', 'resume'],
|
||||||
|
['military/tracking.js', 'resume'],
|
||||||
|
['satellites/tracking.js', 'resuming'],
|
||||||
|
]) {
|
||||||
|
const source = readLayerSource(new URL(`../${file}`, import.meta.url));
|
||||||
|
assert.match(
|
||||||
|
source,
|
||||||
|
new RegExp(
|
||||||
|
`const restoring = ${resume} \\|\\| origin === 'share-restore';\\s*if \\(restoring\\) entity\\.gevRestoring = true;\\s*try \\{[\\s\\S]*?follow\\.start\\(entity\\);?\\s*\\} finally \\{\\s*if \\(restoring\\) delete entity\\.gevRestoring;`,
|
||||||
|
),
|
||||||
|
file,
|
||||||
|
);
|
||||||
|
assert.equal(source.match(/follow\.start\(/g).length, 1, file);
|
||||||
}
|
}
|
||||||
assert.equal(generation, 4, 'new tracking still takes navigation authority');
|
|
||||||
});
|
});
|
||||||
@@ -1011,10 +1011,20 @@ export function createTracking({
|
|||||||
refreshTrackedReadout(flightState._trackedEntity);
|
refreshTrackedReadout(flightState._trackedEntity);
|
||||||
// Camera follows the tracked entity (the renderer cancels any flight
|
// Camera follows the tracked entity (the renderer cancels any flight
|
||||||
// first and applies the one exact tracked frame).
|
// first and applies the one exact tracked frame).
|
||||||
|
// A resumed binding or a share restore re-follows the session's own
|
||||||
|
// subject: marked only while the follow starts, so the shell does not
|
||||||
|
// read it as new navigation (LayerBindings.observeCamera) yet any later
|
||||||
|
// follow of the same aircraft still is.
|
||||||
if (follow) {
|
if (follow) {
|
||||||
flightState._trackedCameraFrameStop = flightState._renderer.follow.start(
|
const entity = flightState._trackedEntity;
|
||||||
flightState._trackedEntity,
|
const restoring = resume || origin === 'share-restore';
|
||||||
);
|
if (restoring) entity.gevRestoring = true;
|
||||||
|
try {
|
||||||
|
flightState._trackedCameraFrameStop =
|
||||||
|
flightState._renderer.follow.start(entity);
|
||||||
|
} finally {
|
||||||
|
if (restoring) delete entity.gevRestoring;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Track-history trail (PRD F1): seed from local history + async backfill.
|
// Track-history trail (PRD F1): seed from local history + async backfill.
|
||||||
|
|||||||
@@ -971,10 +971,20 @@ export function createTracking({
|
|||||||
refreshTrackedReadout(flightState._trackedEntity);
|
refreshTrackedReadout(flightState._trackedEntity);
|
||||||
// Camera follows the tracked entity (the renderer cancels any flight
|
// Camera follows the tracked entity (the renderer cancels any flight
|
||||||
// first and applies the one exact tracked frame).
|
// first and applies the one exact tracked frame).
|
||||||
|
// A resumed binding or a share restore re-follows the session's own
|
||||||
|
// subject: marked only while the follow starts, so the shell does not
|
||||||
|
// read it as new navigation (LayerBindings.observeCamera) yet any later
|
||||||
|
// follow of the same aircraft still is.
|
||||||
if (follow) {
|
if (follow) {
|
||||||
flightState._trackedCameraFrameStop = flightState._renderer.follow.start(
|
const entity = flightState._trackedEntity;
|
||||||
flightState._trackedEntity,
|
const restoring = resume || origin === 'share-restore';
|
||||||
);
|
if (restoring) entity.gevRestoring = true;
|
||||||
|
try {
|
||||||
|
flightState._trackedCameraFrameStop =
|
||||||
|
flightState._renderer.follow.start(entity);
|
||||||
|
} finally {
|
||||||
|
if (restoring) delete entity.gevRestoring;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (resume) {
|
if (resume) {
|
||||||
_attachTrail();
|
_attachTrail();
|
||||||
|
|||||||
@@ -485,7 +485,16 @@ export function createTracking({
|
|||||||
layerState._contextRefreshedAtMs = Date.now();
|
layerState._contextRefreshedAtMs = Date.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
getRenderer().follow.start(layerState._trackedEntity);
|
// Resumed or share-restored: not new navigation, only while the follow
|
||||||
|
// starts (see flights/tracking.js).
|
||||||
|
const entity = layerState._trackedEntity;
|
||||||
|
const restoring = resuming || origin === 'share-restore';
|
||||||
|
if (restoring) entity.gevRestoring = true;
|
||||||
|
try {
|
||||||
|
getRenderer().follow.start(entity);
|
||||||
|
} finally {
|
||||||
|
if (restoring) delete entity.gevRestoring;
|
||||||
|
}
|
||||||
console.log(`[Data:Satellites] Tracking ${name} (NORAD ${noradId})`);
|
console.log(`[Data:Satellites] Tracking ${name} (NORAD ${noradId})`);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -86,13 +86,10 @@ export class LayerBindings {
|
|||||||
if (this.viewer)
|
if (this.viewer)
|
||||||
this._navigationOwnerChangedRemover =
|
this._navigationOwnerChangedRemover =
|
||||||
this.viewer.trackedEntityChanged.addEventListener((entity) => {
|
this.viewer.trackedEntityChanged.addEventListener((entity) => {
|
||||||
// Rebinding the session's aircraft is part of the pending share
|
// Rebinding the session's subject (a resumed renderer, a share
|
||||||
// restoration, not a new navigation that supersedes that restore.
|
// restore) is part of the pending restoration, not a new navigation
|
||||||
if (
|
// that supersedes it. The mark lasts only while the follow starts.
|
||||||
entity &&
|
if (entity && !this._disposed && !entity.gevRestoring)
|
||||||
!this._disposed &&
|
|
||||||
entity.gevSelectionOrigin !== 'share-restore'
|
|
||||||
)
|
|
||||||
this._stampNavigation({ cancelPendingSelection: false });
|
this._stampNavigation({ cancelPendingSelection: false });
|
||||||
});
|
});
|
||||||
// Vessel/installation focus flies without ever assigning a tracked entity,
|
// Vessel/installation focus flies without ever assigning a tracked entity,
|
||||||
|
|||||||
Reference in new issue
Block a user