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.
This commit is contained in:
German Khokhlov committed 2026-10-17 01:16:59 +04:00
1 parent 4dfcba8989
commit 443c581280
4 files changed
+71 -14

No files matched your search

+14 -12
View File
@@ -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)
+43
View File
@@ -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();
}
});
+7 -2
View File
@@ -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);
}
+7
View File
@@ -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 {