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 * 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. * Focus-aware sprite treatment shared by the live contact layers.
@@ -208,11 +220,7 @@ export function publishFocusTargetFromCachedPosition({
clearFocusTarget(ownerLayer, id); clearFocusTarget(ownerLayer, id);
return null; return null;
} }
const screen = Cesium.SceneTransforms.worldToWindowCoordinates( const screen = worldToWindow(scene, displayPosition, _scratchScreen);
scene,
displayPosition,
_scratchScreen,
);
if (!screen || !Number.isFinite(screen.x) || !Number.isFinite(screen.y)) { if (!screen || !Number.isFinite(screen.x) || !Number.isFinite(screen.y)) {
clearFocusTarget(ownerLayer, id); clearFocusTarget(ownerLayer, id);
return null; return null;
@@ -480,13 +488,7 @@ export function advanceProjectedSpriteFocus(
return _advanceResult; return _advanceResult;
} }
const screen = const screen =
position && scene position && scene ? worldToWindow(scene, position, _scratchScreen) : null;
? Cesium.SceneTransforms.worldToWindowCoordinates(
scene,
position,
_scratchScreen,
)
: null;
const cameraDistance = const cameraDistance =
position && camera?.positionWC position && camera?.positionWC
? Cesium.Cartesian3.distance(camera.positionWC, position) ? 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); const middle = nearFarScalarValueAtDistance(scalar, 1_000_000);
assert.ok(middle > 0.5 && middle < 3); 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. * @param {Object} entity Entity whose model changed.
*/ */
export function refreshTrackedReadout(entity) { export function refreshTrackedReadout(entity) {
// A follow handle stands for its label subject. // A follow handle stands for its label subject; so does a layer's tracked
const subject = _labelSubjects.get(entity) ?? entity; // 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); 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, recorder.calls.filter(({ op }) => op === 'set').at(-1).entries[0].details,
['409 km'], ['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(); handle.stop();
assert.equal(getActiveTrackedReadoutId(), null); assert.equal(getActiveTrackedReadoutId(), null);
} finally { } finally {