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 {