refactor(installations): use globe camera contract

This commit is contained in:
German Khokhlov committed 2026-10-28 09:50:17 +04:00
1 parent c58bc9864b
commit f4f0088814
6 files changed
+100 -17

No files matched your search

+7 -8
View File
@@ -1,4 +1,3 @@
import * as Cesium from 'cesium';
import * as geo from '../../geo/index.js';
import { installationFeedback } from '../../data/installationFeedback.js';
import {
@@ -39,7 +38,7 @@ export function createControls({ state: layerState, services, parts, source }) {
* only area that answers "what is near this contact". The window moves
* only after the subject travels ANCHOR_REFRESH_M, and cached tiles make
* that cheap. Passing null returns the layer to viewport loading.
* @param {Cesium.Cartesian3|{latitude:number, longitude:number}|null} position
* @param {object|{latitude:number, longitude:number}|null} position
* Subject position.
* @returns {boolean} Whether a new load was scheduled.
*/
@@ -145,19 +144,19 @@ export function createControls({ state: layerState, services, parts, source }) {
*/
focusById(id) {
const record = layerState.recordById.get(String(id));
if (!record || !layerState.viewer) return false;
if (!record || !layerState.engine?.camera?.flyToSphere) return false;
// No camera flight without a real selection: a flight plus a stale subject
// reads as success to Context navigation and strands NEXT on this item.
if (!parts.selection.selectRecord(record.id)) return false;
layerState.viewer.camera.flyToBoundingSphere(
new Cesium.BoundingSphere(
geo.cartesian3.fromDegrees(
layerState.engine.camera.flyToSphere(
{
center: geo.cartesian3.fromDegrees(
record.longitude,
record.latitude,
parts.rendering.installationSurfaceHeightM(record),
),
18000,
),
radius: 18000,
},
{ duration: 1.4 },
);
return true;
@@ -0,0 +1,36 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import * as geo from '../../geo/index.js';
import { createControls } from './controls.js';
test('focus frames a selected installation through the globe engine camera', () => {
const record = { id: 'osm:node:12', latitude: 70, longitude: 179.8 };
let flight = null;
const state = {
engine: {
camera: {
flyToSphere: (sphere, options) => {
flight = { sphere, options };
},
},
},
recordById: new Map([[record.id, record]]),
};
const controls = createControls({
state,
services: {},
parts: {
selection: { selectRecord: (id) => id === record.id },
rendering: { installationSurfaceHeightM: () => 126 },
},
source: {},
}).methods;
assert.equal(controls.focusById(record.id), true);
assert.deepEqual(
flight.sphere.center,
geo.cartesian3.fromDegrees(179.8, 70, 126),
);
assert.equal(flight.sphere.radius, 18000);
assert.deepEqual(flight.options, { duration: 1.4 });
});
+6 -4
View File
@@ -15,19 +15,20 @@ export function createLifecycle({
const { clearSelectedEntityContextForLayer } = services.context;
const methods = {
init(viewer) {
init(viewer, { engine = null } = {}) {
layerState.viewer = viewer;
layerState.engine = engine;
layerState.dataSource = new Cesium.CustomDataSource(
'military-installations',
);
viewer.dataSources.add(layerState.dataSource);
const camera = engine?.camera ?? viewer.camera;
const cameraChanged = () => {
if (!layerState.contextAnchor) parts.viewport.scheduleLoad();
};
layerState.moveEndRemove =
viewer.camera.moveEnd.addEventListener(cameraChanged);
layerState.moveEndRemove = camera.moveEnd.addEventListener(cameraChanged);
layerState.changedRemove =
viewer.camera.changed?.addEventListener(cameraChanged);
camera.changed?.addEventListener(cameraChanged);
parts.selection.installInteraction(viewer);
},
@@ -100,6 +101,7 @@ export function createLifecycle({
if (layerState.dataSource && viewer)
viewer.dataSources.remove(layerState.dataSource, true);
layerState.dataSource = null;
layerState.engine = null;
},
};
@@ -189,3 +189,45 @@ test('wide views refresh screen decluttering when the globe rectangle stays unch
t.mock.timers.tick(200);
assert.equal(loads, 2, 'a different continent needs fresh projected labels');
});
test('the engine camera drives installation viewport requests without a viewer', (t) => {
t.mock.timers.enable({ apis: ['setTimeout'] });
const rectangle = {
west: Cesium.Math.toRadians(179),
south: Cesium.Math.toRadians(-12),
east: Cesium.Math.toRadians(-178),
north: Cesium.Math.toRadians(16),
};
const state = {
enabled: true,
viewer: null,
engine: {
camera: {
viewRectangle: () => rectangle,
pose: () => ({
position: { x: 1, y: 2, z: 3 },
direction: { x: -1, y: 0, z: 0 },
}),
},
},
};
const loads = [];
const parts = {
ingestion: {
loadInstallations() {
loads.push(parts.viewport.loadArea(null));
},
},
};
parts.viewport = createViewport({ state, parts, services: {}, source: {} });
parts.viewport.scheduleLoad();
t.mock.timers.tick(200);
assert.equal(loads.length, 1);
assert.ok(Math.abs(loads[0].box.south + 12) < 1e-10);
assert.equal(loads[0].box.west, 179);
assert.equal(loads[0].box.north, 16);
assert.equal(loads[0].box.east, -178);
assert.equal(loads[0].coverage.kind, 'viewport');
});
+1
View File
@@ -9,6 +9,7 @@ export function createState({ services }) {
Object.assign(state, {
viewer: null,
engine: null,
dataSource: null,
enabled: false,
records: [],
+8 -5
View File
@@ -23,9 +23,9 @@ export function createViewport({ state: layerState, services, parts, source }) {
}
function viewportBox(viewer) {
const rectangle = viewer?.camera?.computeViewRectangle(
viewer.scene.globe.ellipsoid,
);
const rectangle =
layerState.engine?.camera?.viewRectangle?.() ??
viewer?.camera?.computeViewRectangle(viewer.scene.globe.ellipsoid);
if (!rectangle) return null;
const south = geo.math.toDegrees(rectangle.south);
const north = geo.math.toDegrees(rectangle.north);
@@ -89,14 +89,17 @@ export function createViewport({ state: layerState, services, parts, source }) {
function scheduleLoad() {
if (!layerState.enabled) return;
const { box, coverage } = loadArea(layerState.viewer);
const camera = layerState.viewer.camera;
const pose = layerState.engine?.camera?.pose?.();
const camera = layerState.viewer?.camera;
const position = pose?.position ?? camera?.positionWC;
const direction = pose?.direction ?? camera?.directionWC;
const widePose =
coverage.kind === 'viewport' &&
box &&
(box.east < box.west ||
box.east - box.west > 10 ||
box.north - box.south > 10)
? [camera.positionWC, camera.directionWC]
? [position, direction]
.map((v) =>
v ? [v.x, v.y, v.z].map((n) => n.toFixed(4)).join(',') : '',
)