From 443c581280859b7062e14a14facb16c1556fdd46 Mon Sep 17 00:00:00 2001 From: German Khokhlov Date: Sat, 17 Oct 2026 01:16:59 +0400 Subject: [PATCH] refactor(data): focus projects through the engine; readout follows a tracked visual's follow Task 10b foundation. - focusDeemphasis projects through the engine registered for the scene it is given (engineFromViewer(scene).projector), as iconOrientation already does, else Cesium's SceneTransforms. On Cesium the engine projector is SceneTransforms, so nothing changes there; on Navara the aircraft renderer's scene adapter is registered with the engine. Test with a fake engine. - refreshTrackedReadout accepts a layer's tracked visual that names its follow handle (gevFollowHandle): engines without tracked entities republish the card through it. Test. --- src/data/focusDeemphasis.js | 26 ++++++++++--------- src/data/focusDeemphasis.test.mjs | 43 +++++++++++++++++++++++++++++++ src/data/trackedReadout.js | 9 +++++-- src/data/trackedReadout.test.mjs | 7 +++++ 4 files changed, 71 insertions(+), 14 deletions(-) diff --git a/src/data/focusDeemphasis.js b/src/data/focusDeemphasis.js index 396bbc687..6d39baf04 100644 --- a/src/data/focusDeemphasis.js +++ b/src/data/focusDeemphasis.js @@ -1,4 +1,16 @@ import * as Cesium from 'cesium'; +import { engineFromViewer } from '../globe/cesium/engineFromViewer.js'; + +// A window position through the engine's projector when `scene` has a +// registered engine (a Cesium scene in the app, the Navara aircraft +// renderer's scene adapter), else Cesium's SceneTransforms (bare scenes in +// tests). The two agree on Cesium (the engine projector is SceneTransforms). +function worldToWindow(scene, position, result) { + const projector = engineFromViewer(scene)?.projector; + return projector + ? projector.worldToWindow(position, result) + : Cesium.SceneTransforms.worldToWindowCoordinates(scene, position, result); +} /** * Focus-aware sprite treatment shared by the live contact layers. @@ -208,11 +220,7 @@ export function publishFocusTargetFromCachedPosition({ clearFocusTarget(ownerLayer, id); return null; } - const screen = Cesium.SceneTransforms.worldToWindowCoordinates( - scene, - displayPosition, - _scratchScreen, - ); + const screen = worldToWindow(scene, displayPosition, _scratchScreen); if (!screen || !Number.isFinite(screen.x) || !Number.isFinite(screen.y)) { clearFocusTarget(ownerLayer, id); return null; @@ -480,13 +488,7 @@ export function advanceProjectedSpriteFocus( return _advanceResult; } const screen = - position && scene - ? Cesium.SceneTransforms.worldToWindowCoordinates( - scene, - position, - _scratchScreen, - ) - : null; + position && scene ? worldToWindow(scene, position, _scratchScreen) : null; const cameraDistance = position && camera?.positionWC ? Cesium.Cartesian3.distance(camera.positionWC, position) diff --git a/src/data/focusDeemphasis.test.mjs b/src/data/focusDeemphasis.test.mjs index 7d6600d4b..75698543a 100644 --- a/src/data/focusDeemphasis.test.mjs +++ b/src/data/focusDeemphasis.test.mjs @@ -162,3 +162,46 @@ test('NearFarScalar rendered-size interpolation matches Cesium clamp endpoints', const middle = nearFarScalarValueAtDistance(scalar, 1_000_000); assert.ok(middle > 0.5 && middle < 3); }); + +test('a focus target projects through the engine registered for its scene', async () => { + const { publishFocusTargetFromCachedPosition, clearFocusTarget } = + await import('./focusDeemphasis.js'); + const { registerViewerEngine } = await import( + '../globe/cesium/engineFromViewer.js' + ); + const { createFakeEngine } = await import('../globe/testing/fakeEngine.js'); + const engine = createFakeEngine(); + const projected = []; + engine.projector.worldToWindow = (position, result = {}) => { + projected.push(position); + return Object.assign(result, { x: 300, y: 200 }); + }; + // A scene with no Cesium behind it (the Navara renderer's adapter). + const scene = { frameState: { frameNumber: 7 } }; + const unregister = registerViewerEngine(scene, engine); + try { + const target = publishFocusTargetFromCachedPosition({ + ownerLayer: 'flights', + id: 'abc123', + scene, + camera: { positionWC: { x: 0, y: 0, z: 1000 } }, + displayPosition: { x: 0, y: 0, z: 0 }, + widthPx: 20, + heightPx: 20, + params: { paddingPx: 0 }, + }); + assert.equal(projected.length, 1); + assert.deepEqual(target.screenRect, { + left: 290, + top: 190, + right: 310, + bottom: 210, + }); + assert.equal(target.cameraDistance, 1000); + assert.equal(target.frameNumber, 7); + } finally { + clearFocusTarget('flights'); + unregister(); + engine.destroy(); + } +}); diff --git a/src/data/trackedReadout.js b/src/data/trackedReadout.js index a8056f8a6..e32355f28 100644 --- a/src/data/trackedReadout.js +++ b/src/data/trackedReadout.js @@ -241,8 +241,13 @@ function syncActiveEntity() { * @param {Object} entity Entity whose model changed. */ export function refreshTrackedReadout(entity) { - // A follow handle stands for its label subject. - const subject = _labelSubjects.get(entity) ?? entity; + // A follow handle stands for its label subject; so does a layer's tracked + // visual that names its follow handle (gevFollowHandle, an engine without + // tracked entities: the Navara aircraft renderer). + const subject = + _labelSubjects.get(entity) ?? + _labelSubjects.get(entity?.gevFollowHandle) ?? + entity; if (subject && subject === activeEntity()) publishEntity(subject); } diff --git a/src/data/trackedReadout.test.mjs b/src/data/trackedReadout.test.mjs index c1c697b74..bb4f66b79 100644 --- a/src/data/trackedReadout.test.mjs +++ b/src/data/trackedReadout.test.mjs @@ -333,6 +333,13 @@ test("on a globe engine the readout follows engine.camera.follow's plain-data la recorder.calls.filter(({ op }) => op === 'set').at(-1).entries[0].details, ['409 km'], ); + // So does a layer's tracked visual that names its follow handle. + model = { ...model, details: ['410 km'] }; + refreshTrackedReadout({ gevFollowHandle: handle }); + assert.deepEqual( + recorder.calls.filter(({ op }) => op === 'set').at(-1).entries[0].details, + ['410 km'], + ); handle.stop(); assert.equal(getActiveTrackedReadoutId(), null); } finally {