mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 11:24:00 +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.
|
||||
assert.match(
|
||||
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',
|
||||
);
|
||||
// Seam 2 is pinned by "Cockpit takeover invalidates deferred work" above.
|
||||
|
||||
@@ -9,6 +9,9 @@ export function createAircraftSceneBinding({
|
||||
enable,
|
||||
}) {
|
||||
let resumeFollow = false;
|
||||
// The tracked aircraft's selection origin outlives the renderer, as the
|
||||
// tracking itself does.
|
||||
let resumeOrigin = 'programmatic';
|
||||
function removeSceneListeners() {
|
||||
parts.tracking._removeClickHandler();
|
||||
services.picking.unregisterPickOwner(id);
|
||||
@@ -58,7 +61,7 @@ export function createAircraftSceneBinding({
|
||||
parts.snapshots.presentRecords();
|
||||
if (flightState._trackedIcao) {
|
||||
parts.tracking._trackFlight(flightState._trackedIcao, {
|
||||
origin: 'share-restore',
|
||||
origin: resumeOrigin,
|
||||
resume: true,
|
||||
follow: resumeFollow,
|
||||
});
|
||||
@@ -84,6 +87,8 @@ export function createAircraftSceneBinding({
|
||||
flightState._trackedCameraFrameStop?.();
|
||||
flightState._trackedCameraFrameStop = null;
|
||||
if (flightState._trackedEntity) {
|
||||
resumeOrigin =
|
||||
flightState._trackedEntity.gevSelectionOrigin ?? 'programmatic';
|
||||
resumeFollow = renderer.follow.isFollowing(flightState._trackedEntity);
|
||||
if (resumeFollow) renderer.follow.release();
|
||||
renderer.tracked.remove(flightState._trackedEntity);
|
||||
|
||||
@@ -3,61 +3,165 @@ import test from 'node:test';
|
||||
import { createAircraftSceneBinding } from './sceneBinding.js';
|
||||
import { LayerBindings } from '../../ui/layerBindings.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;
|
||||
globalThis.window = new EventTarget();
|
||||
t.after(() => {
|
||||
globalThis.window = previous;
|
||||
});
|
||||
const trackedEntityChanged = createEngineEvent();
|
||||
let generation = 1;
|
||||
const shell = new LayerBindings({
|
||||
const shell = {
|
||||
generation: 1,
|
||||
trackedEntityChanged,
|
||||
};
|
||||
const bindings = new LayerBindings({
|
||||
viewer: { trackedEntityChanged },
|
||||
services: {},
|
||||
operations: {
|
||||
_stampNavigation() {
|
||||
generation++;
|
||||
shell.generation++;
|
||||
},
|
||||
},
|
||||
});
|
||||
shell.observeCamera();
|
||||
t.after(() => shell.stop());
|
||||
bindings.observeCamera();
|
||||
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 = {
|
||||
fleet: { attach() {}, setVisible() {}, restoreOrder() {} },
|
||||
models: { setVisible() {} },
|
||||
fleet: {
|
||||
attach() {},
|
||||
detach() {},
|
||||
setVisible() {},
|
||||
restoreOrder() {},
|
||||
},
|
||||
models: { setVisible() {}, destroy() {} },
|
||||
follow: {
|
||||
isFollowing: () => true,
|
||||
release() {},
|
||||
},
|
||||
tracked: { remove() {} },
|
||||
destroy() {},
|
||||
};
|
||||
let restored;
|
||||
const binding = createAircraftSceneBinding({
|
||||
return createAircraftSceneBinding({
|
||||
id: 'flights',
|
||||
flightState: {
|
||||
initialized: true,
|
||||
_trackedIcao: 'a00000',
|
||||
_preloadModel: {},
|
||||
flightState,
|
||||
services: {
|
||||
renderer: { create: () => renderer },
|
||||
render: { releaseContinuousRender() {} },
|
||||
picking: { unregisterPickOwner() {} },
|
||||
},
|
||||
services: { renderer: { create: () => renderer } },
|
||||
parts: {
|
||||
snapshots: { presentRecords() {} },
|
||||
tracking: {
|
||||
_trackFlight(id, options) {
|
||||
restored = { id, ...options };
|
||||
trackedEntityChanged.raiseEvent({
|
||||
gevSelectionOrigin: options.origin,
|
||||
});
|
||||
},
|
||||
_trackFlight: tracked,
|
||||
_removeClickHandler() {},
|
||||
_detachTrail() {},
|
||||
},
|
||||
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.resume, true);
|
||||
assert.equal(
|
||||
generation,
|
||||
shell.generation,
|
||||
1,
|
||||
'restored tracking cannot supersede and clear itself',
|
||||
);
|
||||
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);
|
||||
// Camera follows the tracked entity (the renderer cancels any flight
|
||||
// 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) {
|
||||
flightState._trackedCameraFrameStop = flightState._renderer.follow.start(
|
||||
flightState._trackedEntity,
|
||||
);
|
||||
const entity = 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.
|
||||
|
||||
@@ -971,10 +971,20 @@ export function createTracking({
|
||||
refreshTrackedReadout(flightState._trackedEntity);
|
||||
// Camera follows the tracked entity (the renderer cancels any flight
|
||||
// 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) {
|
||||
flightState._trackedCameraFrameStop = flightState._renderer.follow.start(
|
||||
flightState._trackedEntity,
|
||||
);
|
||||
const entity = 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) {
|
||||
_attachTrail();
|
||||
|
||||
@@ -485,7 +485,16 @@ export function createTracking({
|
||||
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})`);
|
||||
}
|
||||
return {
|
||||
|
||||
@@ -86,13 +86,10 @@ export class LayerBindings {
|
||||
if (this.viewer)
|
||||
this._navigationOwnerChangedRemover =
|
||||
this.viewer.trackedEntityChanged.addEventListener((entity) => {
|
||||
// Rebinding the session's aircraft is part of the pending share
|
||||
// restoration, not a new navigation that supersedes that restore.
|
||||
if (
|
||||
entity &&
|
||||
!this._disposed &&
|
||||
entity.gevSelectionOrigin !== 'share-restore'
|
||||
)
|
||||
// Rebinding the session's subject (a resumed renderer, a share
|
||||
// restore) is part of the pending restoration, not a new navigation
|
||||
// that supersedes it. The mark lasts only while the follow starts.
|
||||
if (entity && !this._disposed && !entity.gevRestoring)
|
||||
this._stampNavigation({ cancelPendingSelection: false });
|
||||
});
|
||||
// Vessel/installation focus flies without ever assigning a tracked entity,
|
||||
|
||||
Reference in new issue
Block a user