mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
refactor(overlays): use the globe engine
The world overlay host takes its frames (frames.postRender), camera events
(camera.moveEnd), canvas size (host.size), projection matrix
(projector.viewProjection), horizon test (projector.horizonVisible) and
render requests (render.request('world-overlay')) from the globe engine.
initWorldOverlay accepts an engine or, for callers that still hold a
viewer, resolves the viewer's engine through engineFromViewer.
Contract: camera.position(result) becomes camera.livePosition, one
engine-owned object holding the current position, so per-frame readers
and paint lanes keep a live camera position without allocating; engine
events gain raiseValue for argument-list-free frame dispatch. Steady-state
frame allocation is unchanged (worker probe, 30 chunks: 2696/9536/38593
B/frame before and after; detection 473749 -> 408085).
This commit is contained in:
1 parent
58a196374f
commit
aaa6074663
11 files changed
+284
-134
No files matched your search
@@ -550,6 +550,7 @@
|
||||
"ui-shell": {
|
||||
"exports": ["./ui/shell"],
|
||||
"modules": [
|
||||
"src/globe/contract.js",
|
||||
"src/hudLayouts.js",
|
||||
"src/hudLayoutPolicy.js",
|
||||
"src/cyberSonar.js",
|
||||
@@ -2528,6 +2529,27 @@
|
||||
"application-layer-construction": {
|
||||
"exports": ["./application/layers"],
|
||||
"modules": [
|
||||
"src/globe/cesium/engineFromViewer.js",
|
||||
"src/globe/cesium/createCesiumEngine.js",
|
||||
"src/globe/cesium/maps.js",
|
||||
"src/maps/terrain.js",
|
||||
"src/maps/terrainRetry.js",
|
||||
"src/maps/sceneApply.js",
|
||||
"src/maps/credits.js",
|
||||
"src/globe/cesium/surface.js",
|
||||
"src/maps/imagery.js",
|
||||
"src/services/groundSnap.js",
|
||||
"src/maps/google3d.js",
|
||||
"src/globe/cesium/input.js",
|
||||
"src/globe/cesium/convert.js",
|
||||
"src/globe/cesium/camera.js",
|
||||
"src/globe/cesium/frames.js",
|
||||
"src/globe/cesium/picker.js",
|
||||
"src/globe/cesium/events.js",
|
||||
"src/globe/cesium/render.js",
|
||||
"src/globe/cesium/projector.js",
|
||||
"src/globe/cesium/credits.js",
|
||||
"src/globe/contract.js",
|
||||
"src/cyberSonar.js",
|
||||
"src/data/bhoteKoshiEvent.js",
|
||||
"src/ui/imagerySplit.js",
|
||||
@@ -3064,6 +3086,8 @@
|
||||
"application-operations": {
|
||||
"exports": ["./application/operations"],
|
||||
"modules": [
|
||||
"src/services/groundSnap.js",
|
||||
"src/globe/contract.js",
|
||||
"src/annotations/annotationResolver.js",
|
||||
"src/annotations/resolver.js",
|
||||
"src/app/operations.js",
|
||||
@@ -3095,7 +3119,6 @@
|
||||
"src/search/placeSearch.js",
|
||||
"src/services/application.js",
|
||||
"src/services/groundFloor.js",
|
||||
"src/services/groundSnap.js",
|
||||
"src/services/meshFloorSampler.js",
|
||||
"src/services/requests.js",
|
||||
"src/services/terrainHeights.js",
|
||||
@@ -3508,6 +3531,27 @@
|
||||
"transit-standalone": {
|
||||
"exports": ["./layers/transit/standalone"],
|
||||
"modules": [
|
||||
"src/globe/cesium/events.js",
|
||||
"src/globe/cesium/picker.js",
|
||||
"src/globe/cesium/frames.js",
|
||||
"src/globe/cesium/camera.js",
|
||||
"src/globe/cesium/convert.js",
|
||||
"src/globe/cesium/input.js",
|
||||
"src/maps/google3d.js",
|
||||
"src/services/groundSnap.js",
|
||||
"src/maps/imagery.js",
|
||||
"src/globe/cesium/surface.js",
|
||||
"src/maps/credits.js",
|
||||
"src/maps/sceneApply.js",
|
||||
"src/maps/terrainRetry.js",
|
||||
"src/maps/terrain.js",
|
||||
"src/globe/cesium/maps.js",
|
||||
"src/globe/cesium/createCesiumEngine.js",
|
||||
"src/globe/cesium/engineFromViewer.js",
|
||||
"src/globe/cesium/projector.js",
|
||||
"src/globe/cesium/render.js",
|
||||
"src/globe/cesium/credits.js",
|
||||
"src/globe/contract.js",
|
||||
"src/cyberSonar.js",
|
||||
"src/app/sourceSlot.js",
|
||||
"src/celestialRing.js",
|
||||
|
||||
@@ -111,13 +111,9 @@ export function createCesiumCamera(viewer) {
|
||||
fovy: cesium.frustum.fovy,
|
||||
};
|
||||
},
|
||||
position(result) {
|
||||
const { x, y, z } = cesium.positionWC;
|
||||
if (!result) return geo.cartesian3.create(x, y, z);
|
||||
result.x = x;
|
||||
result.y = y;
|
||||
result.z = z;
|
||||
return result;
|
||||
// Cesium updates positionWC in place; it is the live object.
|
||||
get livePosition() {
|
||||
return cesium.positionWC;
|
||||
},
|
||||
height: () => cesium.positionCartographic.height,
|
||||
setView(view) {
|
||||
|
||||
@@ -347,16 +347,10 @@ test('rectangle destinations become a Cesium.Rectangle in flyTo and setView', ()
|
||||
}
|
||||
});
|
||||
|
||||
test('position fills a result from positionWC and height reads the camera cartographic', () => {
|
||||
test('livePosition is the Cesium camera positionWC and height reads its cartographic', () => {
|
||||
const { camera, cesium } = setup();
|
||||
cesium.positionCartographic = new Cesium.Cartographic(0.1, 0.2, 1234.5);
|
||||
const result = geo.cartesian3.create();
|
||||
assert.equal(camera.position(result), result);
|
||||
assert.deepEqual({ ...result }, { x: 7e6, y: 1e5, z: 2e5 });
|
||||
const fresh = camera.position();
|
||||
notCesium(fresh, 'position()');
|
||||
assert.notEqual(fresh, cesium.positionWC);
|
||||
assert.deepEqual({ ...fresh }, { x: 7e6, y: 1e5, z: 2e5 });
|
||||
assert.equal(camera.livePosition, cesium.positionWC);
|
||||
assert.equal(camera.height(), 1234.5);
|
||||
});
|
||||
|
||||
@@ -364,5 +358,5 @@ test('the camera is the viewer camera (Viewer.camera is scene.camera)', () => {
|
||||
const viewer = createFakeViewer();
|
||||
const own = { ...viewer.camera, positionWC: new Cesium.Cartesian3(1, 2, 3) };
|
||||
const { camera } = createCesiumCamera({ ...viewer, camera: own });
|
||||
assert.deepEqual({ ...camera.position() }, { x: 1, y: 2, z: 3 });
|
||||
assert.equal(camera.livePosition, own.positionWC);
|
||||
});
|
||||
@@ -18,7 +18,7 @@ import { createEngineEvent } from '../contract.js';
|
||||
export function bridgeEvent(source, mapArgs, { unary } = {}) {
|
||||
const event = createEngineEvent();
|
||||
const forward = unary
|
||||
? (a, b, c) => event.raiseEvent(unary(a, b, c))
|
||||
? (a, b, c) => event.raiseValue(unary(a, b, c))
|
||||
: mapArgs
|
||||
? (...args) => event.raiseEvent(...mapArgs(...args))
|
||||
: (...args) => event.raiseEvent(...args);
|
||||
|
||||
+34
-11
@@ -83,9 +83,10 @@
|
||||
*
|
||||
* @typedef {object} GlobeCamera
|
||||
* @property {() => CameraPose} pose Fresh values on every call.
|
||||
* @property {(result?: Cartesian3) => Cartesian3} position The pose's world
|
||||
* position; fills and returns `result` when given (no allocation, for
|
||||
* per-frame readers), else a fresh value.
|
||||
* @property {Cartesian3} livePosition The pose's world position as one
|
||||
* engine-owned object: the same object on every read, holding the current
|
||||
* position (at the latest when read). For per-frame readers, which must not
|
||||
* allocate; read-only, copy it to keep a value.
|
||||
* @property {() => number} height The camera's height above the WGS84
|
||||
* ellipsoid (the pose's cartographic height), without building a pose.
|
||||
* @property {(view: { destination: Cartesian3 | Rectangle, orientation?: Orientation }) => void} setView
|
||||
@@ -209,9 +210,10 @@ export const isEngineEvent = (value) =>
|
||||
value != null && value[ENGINE_EVENT] === true;
|
||||
|
||||
/**
|
||||
* An event with the contract's semantics (see EngineEvent). `raiseEvent` is
|
||||
* for engine implementations; a listener that throws propagates, and the
|
||||
* event stays usable.
|
||||
* An event with the contract's semantics (see EngineEvent). `raiseEvent` and
|
||||
* `raiseValue` (one argument, no argument list: for per-frame events) are for
|
||||
* engine implementations; a listener that throws propagates, and the event
|
||||
* stays usable.
|
||||
*/
|
||||
export function createEngineEvent() {
|
||||
let entries = [];
|
||||
@@ -226,6 +228,13 @@ export function createEngineEvent() {
|
||||
else entries.splice(entries.indexOf(entry), 1);
|
||||
return true;
|
||||
};
|
||||
const settle = () => {
|
||||
depth--;
|
||||
if (!depth && removedDuringRaise) {
|
||||
entries = entries.filter((entry) => entry.listener);
|
||||
removedDuringRaise = false;
|
||||
}
|
||||
};
|
||||
const event = {
|
||||
get numberOfListeners() {
|
||||
return entries.reduce(
|
||||
@@ -259,11 +268,21 @@ export function createEngineEvent() {
|
||||
if (listener) listener(...args);
|
||||
}
|
||||
} finally {
|
||||
depth--;
|
||||
if (!depth && removedDuringRaise) {
|
||||
entries = entries.filter((entry) => entry.listener);
|
||||
removedDuringRaise = false;
|
||||
settle();
|
||||
}
|
||||
},
|
||||
// raiseEvent(value) without an argument list, for per-frame events.
|
||||
raiseValue(value) {
|
||||
const snapshot = entries;
|
||||
const length = snapshot.length;
|
||||
depth++;
|
||||
try {
|
||||
for (let index = 0; index < length; index++) {
|
||||
const listener = snapshot[index].listener;
|
||||
if (listener) listener(value);
|
||||
}
|
||||
} finally {
|
||||
settle();
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -282,7 +301,11 @@ const SHAPE = {
|
||||
host: { container: isObject, canvas: isObject, size: isFunction },
|
||||
camera: {
|
||||
pose: isFunction,
|
||||
position: isFunction,
|
||||
livePosition: (value) =>
|
||||
value != null &&
|
||||
isNumber(value.x) &&
|
||||
isNumber(value.y) &&
|
||||
isNumber(value.z),
|
||||
height: isFunction,
|
||||
setView: isFunction,
|
||||
flyTo: isFunction,
|
||||
|
||||
@@ -188,7 +188,7 @@ test('assertGlobeEngine accepts a complete engine and names every missing capabi
|
||||
|
||||
test('the contract carries what the shell infrastructure needs', () => {
|
||||
for (const member of [
|
||||
'camera.position',
|
||||
'camera.livePosition',
|
||||
'camera.height',
|
||||
'maps.releasePhotorealTiles',
|
||||
])
|
||||
@@ -196,12 +196,12 @@ test('the contract carries what the shell infrastructure needs', () => {
|
||||
const engine = createFakeEngine();
|
||||
const broken = {
|
||||
...engine,
|
||||
camera: { ...engine.camera, position: undefined, height: 1 },
|
||||
camera: { ...engine.camera, livePosition: undefined, height: 1 },
|
||||
maps: { ...engine.maps, releasePhotorealTiles: undefined },
|
||||
};
|
||||
assert.throws(
|
||||
() => assertGlobeEngine(broken),
|
||||
/camera\.position.*camera\.height.*maps\.releasePhotorealTiles/,
|
||||
/camera\.livePosition.*camera\.height.*maps\.releasePhotorealTiles/,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -256,3 +256,17 @@ test('the contract suite rejects a raw Cesium-style event that double-registers'
|
||||
failed.join('\n'),
|
||||
);
|
||||
});
|
||||
|
||||
test('raiseValue raises exactly one argument with the event semantics', () => {
|
||||
const event = createEngineEvent();
|
||||
const seen = [];
|
||||
const second = (...values) => seen.push(['second', values]);
|
||||
event.addEventListener((...values) => {
|
||||
seen.push(['first', values]);
|
||||
event.removeEventListener(second);
|
||||
});
|
||||
event.addEventListener(second);
|
||||
event.raiseValue(42);
|
||||
assert.deepEqual(seen, [['first', [42]]]);
|
||||
assert.equal(event.numberOfListeners, 1);
|
||||
});
|
||||
@@ -177,38 +177,40 @@ export function createContractSuite(
|
||||
},
|
||||
],
|
||||
[
|
||||
'camera.position and camera.height: the pose position and height, without a pose',
|
||||
'camera.livePosition and camera.height: the pose position and height, without a pose',
|
||||
async () => {
|
||||
camera.setView({
|
||||
destination: cartesian3.fromDegrees(-3.7, 40.4, 12000),
|
||||
orientation: { heading: 0.4, pitch: -0.5, roll: 0 },
|
||||
});
|
||||
await settle();
|
||||
const pose = camera.pose();
|
||||
const result = cartesian3.create();
|
||||
const filled = camera.position(result);
|
||||
ensure(
|
||||
filled === result,
|
||||
'position(result) returns the result it filled',
|
||||
);
|
||||
const live = camera.livePosition;
|
||||
ensure(isCartesian3(live), 'livePosition is a Cartesian3');
|
||||
near(
|
||||
cartesian3.distance(filled, pose.position),
|
||||
cartesian3.distance(live, camera.pose().position),
|
||||
0,
|
||||
1e-6,
|
||||
'position(result) vs pose().position',
|
||||
);
|
||||
const fresh = camera.position();
|
||||
ensure(
|
||||
isCartesian3(fresh) && fresh !== filled,
|
||||
'position() returns a fresh Cartesian3',
|
||||
'livePosition vs pose().position',
|
||||
);
|
||||
near(
|
||||
cartesian3.distance(fresh, pose.position),
|
||||
camera.height(),
|
||||
camera.pose().cartographic.height,
|
||||
0.01,
|
||||
'height()',
|
||||
);
|
||||
const destination = cartesian3.fromDegrees(2.35, 48.85, 30000);
|
||||
camera.setView({ destination });
|
||||
await settle();
|
||||
ensure(
|
||||
camera.livePosition === live,
|
||||
'livePosition is the same engine-owned object on every read',
|
||||
);
|
||||
near(
|
||||
cartesian3.distance(camera.livePosition, destination),
|
||||
0,
|
||||
1e-6,
|
||||
'position() vs pose().position',
|
||||
1e-3,
|
||||
'livePosition follows the camera',
|
||||
);
|
||||
near(camera.height(), pose.cartographic.height, 0.01, 'height()');
|
||||
},
|
||||
],
|
||||
[
|
||||
|
||||
@@ -210,8 +210,10 @@ function rectangleFromCartographics(points) {
|
||||
|
||||
/**
|
||||
* @param {{ kind?: 'cesium'|'navara', width?: number, height?: number, fovy?: number,
|
||||
* longitude?: number, latitude?: number, altitude?: number }} [options]
|
||||
* Canvas size in CSS px, angles in radians. The initial pose looks straight
|
||||
* longitude?: number, latitude?: number, altitude?: number,
|
||||
* container?: object, canvas?: object }} [options]
|
||||
* Canvas size in CSS px, angles in radians; `container`/`canvas` stand in
|
||||
* for the host elements (DOM mocks for shell tests). The initial pose looks straight
|
||||
* down from `altitude` metres (10 000 km).
|
||||
* @returns {import('../contract.js').GlobeEngine & { fake: object }}
|
||||
*/
|
||||
@@ -238,6 +240,7 @@ export function createFakeEngine(options = {}) {
|
||||
'frames.postRender': createEngineEvent(),
|
||||
};
|
||||
const inputs = new Map(INPUT_TYPES.map((type) => [type, new Set()]));
|
||||
const livePosition = cartesian3.create();
|
||||
const scripts = new Map();
|
||||
let transform = [...matrix4.IDENTITY];
|
||||
let percentageChanged = 0.5;
|
||||
@@ -602,10 +605,8 @@ export function createFakeEngine(options = {}) {
|
||||
fovy,
|
||||
};
|
||||
},
|
||||
position(result) {
|
||||
return result
|
||||
? cartesian3.clone(state.position, result)
|
||||
: copy3(state.position);
|
||||
get livePosition() {
|
||||
return cartesian3.clone(state.position, livePosition);
|
||||
},
|
||||
height: () => cartographic.fromCartesian(state.position, WGS84).height,
|
||||
setView(view) {
|
||||
@@ -718,8 +719,8 @@ export function createFakeEngine(options = {}) {
|
||||
};
|
||||
|
||||
const host = {
|
||||
container: { kind: 'fake-container' },
|
||||
canvas: { kind: 'fake-canvas' },
|
||||
container: options.container ?? { kind: 'fake-container' },
|
||||
canvas: options.canvas ?? { kind: 'fake-canvas' },
|
||||
size: () => ({ width, height }),
|
||||
};
|
||||
|
||||
|
||||
@@ -354,12 +354,27 @@ test('fake drillPick records width and height; maps return promises', async () =
|
||||
}
|
||||
});
|
||||
|
||||
test('fake camera position fills a result and height is the pose height', () => {
|
||||
test('fake livePosition is one object that follows the camera; height is the pose height', () => {
|
||||
const engine = createFakeEngine({ altitude: 2_000_000 });
|
||||
const result = geo.cartesian3.create();
|
||||
assert.equal(engine.camera.position(result), result);
|
||||
const live = engine.camera.livePosition;
|
||||
assert.ok(
|
||||
geo.cartesian3.distance(result, engine.camera.pose().position) < 1e-6,
|
||||
geo.cartesian3.distance(live, engine.camera.pose().position) < 1e-6,
|
||||
);
|
||||
assert.ok(Math.abs(engine.camera.height() - 2_000_000) < 0.01);
|
||||
engine.fake.setPose({ longitude: 0.3, latitude: 0.2, height: 500 });
|
||||
assert.equal(engine.camera.livePosition, live);
|
||||
assert.ok(
|
||||
geo.cartesian3.distance(live, engine.camera.pose().position) < 1e-6,
|
||||
);
|
||||
});
|
||||
|
||||
test('fake host elements can be supplied', () => {
|
||||
const container = { id: 'globe' };
|
||||
const canvas = { id: 'canvas' };
|
||||
const engine = createFakeEngine({ container, canvas });
|
||||
assert.equal(engine.host.container, container);
|
||||
assert.equal(engine.host.canvas, canvas);
|
||||
assert.deepEqual(createFakeEngine().host.container, {
|
||||
kind: 'fake-container',
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,6 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import * as geo from '../geo/index.js';
|
||||
import { engineFromViewer } from '../globe/cesium/engineFromViewer.js';
|
||||
import {
|
||||
createCyberSonarSampler,
|
||||
isCyberContactSonarActive as isCyberSonarActive,
|
||||
@@ -27,6 +29,11 @@ import { WORLD_OVERLAY_STYLE } from './worldOverlayTokens.js';
|
||||
* @description Shared screen-space renderer/scheduler for world-anchored
|
||||
* labels and cards. Sources own data, business rules, and bounded candidate
|
||||
* generation; this host owns projection, final placement, paint, and hits.
|
||||
*
|
||||
* It runs on the page's globe engine (src/globe/contract.js): frames, camera
|
||||
* events, canvas size, projection matrix, horizon test and render requests
|
||||
* all come from the engine. Sources still hand it world positions they
|
||||
* computed themselves.
|
||||
*/
|
||||
|
||||
const ROOT_ID = 'world-overlay-root';
|
||||
@@ -131,7 +138,7 @@ export const WORLD_OVERLAY_OCCLUDER_SELECTORS = Object.freeze([
|
||||
* @typedef {object} WorldOverlayEntry
|
||||
* @property {string} id Stable identity within the source.
|
||||
* @property {string} source Owning source id.
|
||||
* @property {Cesium.Cartesian3|function():Cesium.Cartesian3} position
|
||||
* @property {{x: number, y: number, z: number}|function():{x: number, y: number, z: number}} position
|
||||
* @property {'label'|'track'|'card'|'thumbnail'|'selected'|'tracked'} variant
|
||||
* @property {string} [accessibilityLabel] Accessible name for an actionable card.
|
||||
* @property {function():*} [activate] Keyboard/assistive activation callback.
|
||||
@@ -148,8 +155,8 @@ export const WORLD_OVERLAY_OCCLUDER_SELECTORS = Object.freeze([
|
||||
* @property {number} [solveIntervalMs=125]
|
||||
*/
|
||||
|
||||
/** @type {Cesium.Viewer|null} */
|
||||
let _viewer = null;
|
||||
/** @type {import('../globe/contract.js').GlobeEngine|null} */
|
||||
let _engine = null;
|
||||
/** @type {HTMLElement|null} */
|
||||
let _root = null;
|
||||
/** @type {HTMLCanvasElement|null} */
|
||||
@@ -218,13 +225,14 @@ const _domains = new Map();
|
||||
const _domainList = [];
|
||||
/** @type {Map<string, object>} */
|
||||
const _records = new Map();
|
||||
const _scratchViewProjection = new Cesium.Matrix4();
|
||||
const _scratchViewProjection = geo.matrix4.create();
|
||||
const _viewProjectionScalars = {};
|
||||
const _viewport = { width: 0, height: 0 };
|
||||
/** `{ isPointVisible }` over the engine's horizon test, for painters too. */
|
||||
let _occluder = null;
|
||||
let _occluderCameraX = Number.NaN;
|
||||
let _occluderCameraY = Number.NaN;
|
||||
let _occluderCameraZ = Number.NaN;
|
||||
/** The engine's live camera position and the camera height, per frame. */
|
||||
let _cameraPosition = null;
|
||||
let _cameraHeight = Number.NaN;
|
||||
/** Cached keyhole geometry, versioned by both its box and shared tuning. */
|
||||
let _keyhole = null;
|
||||
let _keyholeWidth = -1;
|
||||
@@ -421,6 +429,7 @@ function snapshotCullPosition(entry) {
|
||||
const { x, y, z } = candidate;
|
||||
if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(z))
|
||||
return null;
|
||||
// Still the sources' own vector type until they move off Cesium (phase 1c).
|
||||
return new Cesium.Cartesian3(x, y, z);
|
||||
}
|
||||
|
||||
@@ -860,7 +869,11 @@ function invalidateHost({ solve = true, layout = false } = {}) {
|
||||
_paintRectCount = 0;
|
||||
_hitRectCount = 0;
|
||||
_paintRectByKey.clear();
|
||||
_viewer?.scene?.requestRender?.();
|
||||
requestRender();
|
||||
}
|
||||
|
||||
function requestRender() {
|
||||
_engine?.render.request('world-overlay');
|
||||
}
|
||||
|
||||
function inertPaintLaneHandle() {
|
||||
@@ -1227,9 +1240,10 @@ function ensureOverlayDom() {
|
||||
_root = document.createElement('div');
|
||||
_root.id = ROOT_ID;
|
||||
_root.setAttribute('aria-hidden', 'true');
|
||||
const parent = _viewer.container?.parentElement || document.body;
|
||||
if (_viewer.container?.nextSibling)
|
||||
parent.insertBefore(_root, _viewer.container.nextSibling);
|
||||
const container = _engine.host.container;
|
||||
const parent = container?.parentElement || document.body;
|
||||
if (container?.nextSibling)
|
||||
parent.insertBefore(_root, container.nextSibling);
|
||||
else parent.appendChild(_root);
|
||||
}
|
||||
_canvas =
|
||||
@@ -1249,7 +1263,7 @@ function ensureOverlayDom() {
|
||||
// the browser silently discard the blend while the CSS string stayed
|
||||
// `'screen'`. Paint order is expressed purely by z-index: this surface is
|
||||
// z5, the shared card canvas inside the root is z6.
|
||||
const detectionParent = _viewer?.container || document.body;
|
||||
const detectionParent = _engine?.host.container || document.body;
|
||||
_detectionSurface =
|
||||
detectionParent.querySelector?.(`#${DETECTION_SURFACE_ID}`) ||
|
||||
document.getElementById(DETECTION_SURFACE_ID);
|
||||
@@ -1317,15 +1331,10 @@ function sizeCanvasSurface(canvas, ctx, width, height, dpr) {
|
||||
* that actually has paint work.
|
||||
*/
|
||||
function ensureCanvasSize() {
|
||||
if (!_canvas || !_viewer?.canvas) return false;
|
||||
const width = Math.max(
|
||||
0,
|
||||
Math.round(Number(_viewer.canvas.clientWidth) || 0),
|
||||
);
|
||||
const height = Math.max(
|
||||
0,
|
||||
Math.round(Number(_viewer.canvas.clientHeight) || 0),
|
||||
);
|
||||
if (!_canvas || !_engine) return false;
|
||||
const size = _engine.host.size();
|
||||
const width = Math.max(0, Math.round(Number(size.width) || 0));
|
||||
const height = Math.max(0, Math.round(Number(size.height) || 0));
|
||||
const dpr = Math.max(1, Number(globalThis.window?.devicePixelRatio) || 1);
|
||||
const changed =
|
||||
_canvas.width !== Math.round(width * dpr) ||
|
||||
@@ -1450,7 +1459,7 @@ function refreshUiOccluders(timestamp, force = false) {
|
||||
_occluderRefreshTimer = setTimeout(
|
||||
() => {
|
||||
_occluderRefreshTimer = null;
|
||||
_viewer?.scene?.requestRender?.();
|
||||
requestRender();
|
||||
},
|
||||
Math.max(0, OCCLUDER_REFRESH_MS - elapsed),
|
||||
);
|
||||
@@ -1694,12 +1703,7 @@ function overlayHasPaintWork(timestamp = nowMs()) {
|
||||
}
|
||||
|
||||
function prepareProjectionMatrix() {
|
||||
const camera = _viewer.camera;
|
||||
const matrix = Cesium.Matrix4.multiply(
|
||||
camera.frustum.projectionMatrix,
|
||||
camera.viewMatrix,
|
||||
_scratchViewProjection,
|
||||
);
|
||||
const matrix = _engine.projector.viewProjection(_scratchViewProjection);
|
||||
_viewProjectionScalars.m0 = matrix[0];
|
||||
_viewProjectionScalars.m1 = matrix[1];
|
||||
_viewProjectionScalars.m3 = matrix[3];
|
||||
@@ -1717,17 +1721,9 @@ function prepareProjectionMatrix() {
|
||||
|
||||
function prepareCustomPaintFrame(timestamp, keyhole) {
|
||||
const viewProjection = prepareProjectionMatrix();
|
||||
const camera = _viewer.camera.positionWC;
|
||||
if (
|
||||
camera.x !== _occluderCameraX ||
|
||||
camera.y !== _occluderCameraY ||
|
||||
camera.z !== _occluderCameraZ
|
||||
) {
|
||||
_occluder.cameraPosition = camera;
|
||||
_occluderCameraX = camera.x;
|
||||
_occluderCameraY = camera.y;
|
||||
_occluderCameraZ = camera.z;
|
||||
}
|
||||
const camera = _engine.camera.livePosition;
|
||||
_cameraPosition = camera;
|
||||
_cameraHeight = _engine.camera.height();
|
||||
_customPaintFrame.canvas = _canvas;
|
||||
_customPaintFrame.surface = _canvas;
|
||||
_customPaintFrame.ctx = _ctx;
|
||||
@@ -1749,7 +1745,7 @@ function getProjectionRecord(entry) {
|
||||
record = {
|
||||
key: entry._overlayKey,
|
||||
entry,
|
||||
position: new Cesium.Cartesian3(),
|
||||
position: geo.cartesian3.create(),
|
||||
screen: { x: 0, y: 0 },
|
||||
layout: entry._overlayLayout,
|
||||
placements: [],
|
||||
@@ -1933,7 +1929,7 @@ function snapshotAndProject(entry, source, viewProjection, keyhole) {
|
||||
// scalar steps stay local: the range is computed inline, and the common
|
||||
// "no far limit" fade policy resolves without calling out at all. Both are
|
||||
// exact restatements of `Cartesian3.distance` / `distanceFade`.
|
||||
const cameraPosition = _viewer.camera.positionWC;
|
||||
const cameraPosition = _cameraPosition;
|
||||
const rangeX = cameraPosition.x - record.position.x;
|
||||
const rangeY = cameraPosition.y - record.position.y;
|
||||
const rangeZ = cameraPosition.z - record.position.z;
|
||||
@@ -1946,7 +1942,7 @@ function snapshotAndProject(entry, source, viewProjection, keyhole) {
|
||||
? 1
|
||||
: 0
|
||||
: distanceFade(distance, record.distanceOptions);
|
||||
const cameraAltitude = _viewer.camera.positionCartographic?.height;
|
||||
const cameraAltitude = _cameraHeight;
|
||||
record.paintScale = entry.distanceScale
|
||||
? distanceScale(distance, entry.distanceScale)
|
||||
: 1;
|
||||
@@ -2607,7 +2603,7 @@ function drawWorldOverlay() {
|
||||
clearTimeout(_sonarRenderTimer);
|
||||
_sonarRenderTimer = null;
|
||||
}
|
||||
if (_destroyed || !_viewer || !_canvas || !_ctx) return;
|
||||
if (_destroyed || !_engine || !_canvas || !_ctx) return;
|
||||
const timestamp = nowMs();
|
||||
if (!overlayHasPaintWork(timestamp)) {
|
||||
resetFrameDiagnostics();
|
||||
@@ -2653,13 +2649,12 @@ function drawWorldOverlay() {
|
||||
if (_paintRectCount > 0 && isCyberSonarActive()) {
|
||||
_sonarRenderTimer = setTimeout(() => {
|
||||
_sonarRenderTimer = null;
|
||||
if (!_destroyed && isCyberSonarActive())
|
||||
_viewer?.scene?.requestRender?.();
|
||||
if (!_destroyed && isCyberSonarActive()) requestRender();
|
||||
}, CYBER_SONAR_RENDER_INTERVAL_MS);
|
||||
}
|
||||
const animationsRemaining =
|
||||
activeFadeCount(timestamp) + activeLeaderAnimationCount(timestamp);
|
||||
if (animationsRemaining > 0) _viewer.scene.requestRender?.();
|
||||
if (animationsRemaining > 0) requestRender();
|
||||
}
|
||||
|
||||
function createDevFacade() {
|
||||
@@ -2668,37 +2663,37 @@ function createDevFacade() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the singleton host. Repeated calls for the same viewer are no-op.
|
||||
* @param {Cesium.Viewer} viewer
|
||||
* Initialize the singleton host. Repeated calls for the same engine are no-op.
|
||||
* @param {import('../globe/contract.js').GlobeEngine | object} target The
|
||||
* globe engine, or a Cesium viewer (its engine: engineFromViewer).
|
||||
*/
|
||||
export function initWorldOverlay(viewer) {
|
||||
if (!viewer?.scene?.postRender?.addEventListener) {
|
||||
export function initWorldOverlay(target) {
|
||||
const engine = engineFromViewer(target);
|
||||
if (!engine)
|
||||
throw new TypeError(
|
||||
'initWorldOverlay requires a Cesium viewer with scene.postRender',
|
||||
'initWorldOverlay requires a globe engine or a Cesium viewer',
|
||||
);
|
||||
}
|
||||
if (_viewer === viewer && _canvas && _removePostRender) return;
|
||||
if (_viewer) destroyWorldOverlay();
|
||||
if (_engine === engine && _canvas && _removePostRender) return;
|
||||
if (_engine) destroyWorldOverlay();
|
||||
_destroyed = false;
|
||||
_viewer = viewer;
|
||||
_engine = engine;
|
||||
ensureOverlayDom();
|
||||
_occluder = new Cesium.EllipsoidalOccluder(
|
||||
Cesium.Ellipsoid.WGS84,
|
||||
viewer.camera?.positionWC || new Cesium.Cartesian3(),
|
||||
);
|
||||
const { projector } = engine;
|
||||
_occluder = {
|
||||
isPointVisible: (position) => projector.horizonVisible(position),
|
||||
};
|
||||
_cockpitActive = !!document.body?.classList?.contains('cockpit-mode');
|
||||
_cockpitModeHandler = (event) => {
|
||||
_cockpitActive = event?.detail?.active === true;
|
||||
invalidateHost();
|
||||
};
|
||||
window.addEventListener('gev:cockpit-mode-changed', _cockpitModeHandler);
|
||||
_removePostRender =
|
||||
viewer.scene.postRender.addEventListener(drawWorldOverlay);
|
||||
if (viewer.camera?.moveEnd?.addEventListener) {
|
||||
_removeMoveEnd = viewer.camera.moveEnd.addEventListener(() =>
|
||||
invalidateHost(),
|
||||
);
|
||||
}
|
||||
_removePostRender = engine.frames.postRender.addEventListener(() =>
|
||||
drawWorldOverlay(),
|
||||
);
|
||||
_removeMoveEnd = engine.camera.moveEnd.addEventListener(() =>
|
||||
invalidateHost(),
|
||||
);
|
||||
installUiOccluderObservers();
|
||||
// The backing store and the occluder inventory are both deferred to the
|
||||
// first frame with real paint work: a dormant host must not hold a
|
||||
@@ -2719,7 +2714,7 @@ export function destroyWorldOverlay() {
|
||||
// Tearing down a host that was never initialized must not arm the
|
||||
// post-destroy guard: sources are allowed to publish before the first
|
||||
// `initWorldOverlay`, and that buffering has to survive a stray destroy.
|
||||
const hadHost = _viewer !== null;
|
||||
const hadHost = _engine !== null;
|
||||
_removePostRender?.();
|
||||
_removePostRender = null;
|
||||
_removeMoveEnd?.();
|
||||
@@ -2785,7 +2780,7 @@ export function destroyWorldOverlay() {
|
||||
_uiOcclusionRects.length = 0;
|
||||
_uiOcclusionRectPool.length = 0;
|
||||
_destroyed = hadHost || _destroyed;
|
||||
_viewer = null;
|
||||
_engine = null;
|
||||
_root = null;
|
||||
_canvas = null;
|
||||
_ctx = null;
|
||||
@@ -2795,9 +2790,8 @@ export function destroyWorldOverlay() {
|
||||
_detectionSurface = null;
|
||||
_detectionCtx = null;
|
||||
_occluder = null;
|
||||
_occluderCameraX = Number.NaN;
|
||||
_occluderCameraY = Number.NaN;
|
||||
_occluderCameraZ = Number.NaN;
|
||||
_cameraPosition = null;
|
||||
_cameraHeight = Number.NaN;
|
||||
_keyhole = null;
|
||||
_keyholeWidth = -1;
|
||||
_keyholeHeight = -1;
|
||||
|
||||
@@ -2297,3 +2297,70 @@ test('promoting an ambient entry to selected paints it once with no fading dupli
|
||||
assert.equal(paint(), 1, 'deselect restores the ambient card without re-entry cooldown');
|
||||
} finally { env.cleanup(); }
|
||||
});
|
||||
|
||||
// --- The host on a globe engine (src/globe contract) -------------------------
|
||||
|
||||
async function fakeEngineHost() {
|
||||
const { createFakeEngine } = await import('../globe/testing/fakeEngine.js');
|
||||
const env = installMockEnvironment({ width: 400, height: 300, dpr: 1 });
|
||||
const engine = createFakeEngine({
|
||||
width: 400,
|
||||
height: 300,
|
||||
container: env.viewer.container,
|
||||
});
|
||||
return { env, engine };
|
||||
}
|
||||
|
||||
test('the host runs on a globe engine: frames, camera events and canvas size', async () => {
|
||||
const { env, engine } = await fakeEngineHost();
|
||||
initWorldOverlay(engine);
|
||||
initWorldOverlay(engine);
|
||||
assert.equal(engine.frames.postRender.numberOfListeners, 1);
|
||||
assert.equal(engine.camera.moveEnd.numberOfListeners, 1);
|
||||
const surface = env.document.getElementById('world-overlay-detection-surface');
|
||||
assert.equal(surface.parentElement, env.viewer.container,
|
||||
'the detection surface hangs off the engine host container');
|
||||
setOverlayEntries('engine', [selectedEntry('CENTRE', { position: Cesium.Cartesian3.fromDegrees(0, 0, 0) })]);
|
||||
engine.fake.frame();
|
||||
const canvas = env.document.getElementById('world-overlay-canvas');
|
||||
assert.equal(canvas.width, 400, 'sized from engine.host.size()');
|
||||
assert.equal(canvas.height, 300);
|
||||
assert.equal(getWorldOverlayDiagnostics().paintedCount, 1);
|
||||
destroyWorldOverlay();
|
||||
assert.equal(engine.frames.postRender.numberOfListeners, 0);
|
||||
assert.equal(engine.camera.moveEnd.numberOfListeners, 0);
|
||||
env.cleanup();
|
||||
});
|
||||
|
||||
test('on a globe engine the host projects with viewProjection and culls with horizonVisible', async () => {
|
||||
const { env, engine } = await fakeEngineHost();
|
||||
initWorldOverlay(engine);
|
||||
setOverlayEntries('engine', [
|
||||
selectedEntry('NEAR', { position: Cesium.Cartesian3.fromDegrees(0, 0, 0), horizonCull: true }),
|
||||
selectedEntry('FAR', { position: Cesium.Cartesian3.fromDegrees(0, 0, 0), horizonCull: true,
|
||||
cullPosition: Cesium.Cartesian3.fromDegrees(180, 0, 0) }),
|
||||
]);
|
||||
engine.fake.frame();
|
||||
assert.equal(getOverlayPaintRect('engine', 'NEAR') !== null, true);
|
||||
assert.equal(getOverlayPaintRect('engine', 'FAR'), null, 'hidden behind the globe');
|
||||
const rect = getOverlayPaintRect('engine', 'NEAR');
|
||||
const centre = engine.projector.worldToWindow(Cesium.Cartesian3.fromDegrees(0, 0, 0));
|
||||
assert.ok(Math.abs(rect.anchorX - centre.x) < 1e-6 && Math.abs(rect.anchorY - centre.y) < 1e-6,
|
||||
'anchored at the projected point');
|
||||
env.cleanup();
|
||||
});
|
||||
|
||||
test('on a globe engine the host hands painters the live camera position and requests renders by reason', async () => {
|
||||
const { env, engine } = await fakeEngineHost();
|
||||
initWorldOverlay(engine);
|
||||
let frame = null;
|
||||
registerWorldOverlayPaintLane('detection', (value) => { frame = value; },
|
||||
{ id: 'detection', active: true, target: 'detection' });
|
||||
setOverlayEntries('engine', [selectedEntry('CENTRE', { position: Cesium.Cartesian3.fromDegrees(0, 0, 0) })]);
|
||||
assert.ok(engine.fake.callsOf('render.request').some(({ args }) => args[0] === 'world-overlay'));
|
||||
engine.fake.frame();
|
||||
assert.equal(frame.cameraPosition, engine.camera.livePosition);
|
||||
assert.equal(frame.occluder.isPointVisible(Cesium.Cartesian3.fromDegrees(180, 0, 0)), false);
|
||||
assert.equal(frame.occluder.isPointVisible(Cesium.Cartesian3.fromDegrees(0, 0, 0)), true);
|
||||
env.cleanup();
|
||||
});
|
||||
Reference in new issue
Block a user