mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 03:14:00 +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": {
|
"ui-shell": {
|
||||||
"exports": ["./ui/shell"],
|
"exports": ["./ui/shell"],
|
||||||
"modules": [
|
"modules": [
|
||||||
|
"src/globe/contract.js",
|
||||||
"src/hudLayouts.js",
|
"src/hudLayouts.js",
|
||||||
"src/hudLayoutPolicy.js",
|
"src/hudLayoutPolicy.js",
|
||||||
"src/cyberSonar.js",
|
"src/cyberSonar.js",
|
||||||
@@ -2528,6 +2529,27 @@
|
|||||||
"application-layer-construction": {
|
"application-layer-construction": {
|
||||||
"exports": ["./application/layers"],
|
"exports": ["./application/layers"],
|
||||||
"modules": [
|
"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/cyberSonar.js",
|
||||||
"src/data/bhoteKoshiEvent.js",
|
"src/data/bhoteKoshiEvent.js",
|
||||||
"src/ui/imagerySplit.js",
|
"src/ui/imagerySplit.js",
|
||||||
@@ -3064,6 +3086,8 @@
|
|||||||
"application-operations": {
|
"application-operations": {
|
||||||
"exports": ["./application/operations"],
|
"exports": ["./application/operations"],
|
||||||
"modules": [
|
"modules": [
|
||||||
|
"src/services/groundSnap.js",
|
||||||
|
"src/globe/contract.js",
|
||||||
"src/annotations/annotationResolver.js",
|
"src/annotations/annotationResolver.js",
|
||||||
"src/annotations/resolver.js",
|
"src/annotations/resolver.js",
|
||||||
"src/app/operations.js",
|
"src/app/operations.js",
|
||||||
@@ -3095,7 +3119,6 @@
|
|||||||
"src/search/placeSearch.js",
|
"src/search/placeSearch.js",
|
||||||
"src/services/application.js",
|
"src/services/application.js",
|
||||||
"src/services/groundFloor.js",
|
"src/services/groundFloor.js",
|
||||||
"src/services/groundSnap.js",
|
|
||||||
"src/services/meshFloorSampler.js",
|
"src/services/meshFloorSampler.js",
|
||||||
"src/services/requests.js",
|
"src/services/requests.js",
|
||||||
"src/services/terrainHeights.js",
|
"src/services/terrainHeights.js",
|
||||||
@@ -3508,6 +3531,27 @@
|
|||||||
"transit-standalone": {
|
"transit-standalone": {
|
||||||
"exports": ["./layers/transit/standalone"],
|
"exports": ["./layers/transit/standalone"],
|
||||||
"modules": [
|
"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/cyberSonar.js",
|
||||||
"src/app/sourceSlot.js",
|
"src/app/sourceSlot.js",
|
||||||
"src/celestialRing.js",
|
"src/celestialRing.js",
|
||||||
|
|||||||
@@ -111,13 +111,9 @@ export function createCesiumCamera(viewer) {
|
|||||||
fovy: cesium.frustum.fovy,
|
fovy: cesium.frustum.fovy,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
position(result) {
|
// Cesium updates positionWC in place; it is the live object.
|
||||||
const { x, y, z } = cesium.positionWC;
|
get livePosition() {
|
||||||
if (!result) return geo.cartesian3.create(x, y, z);
|
return cesium.positionWC;
|
||||||
result.x = x;
|
|
||||||
result.y = y;
|
|
||||||
result.z = z;
|
|
||||||
return result;
|
|
||||||
},
|
},
|
||||||
height: () => cesium.positionCartographic.height,
|
height: () => cesium.positionCartographic.height,
|
||||||
setView(view) {
|
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();
|
const { camera, cesium } = setup();
|
||||||
cesium.positionCartographic = new Cesium.Cartographic(0.1, 0.2, 1234.5);
|
cesium.positionCartographic = new Cesium.Cartographic(0.1, 0.2, 1234.5);
|
||||||
const result = geo.cartesian3.create();
|
assert.equal(camera.livePosition, cesium.positionWC);
|
||||||
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.height(), 1234.5);
|
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 viewer = createFakeViewer();
|
||||||
const own = { ...viewer.camera, positionWC: new Cesium.Cartesian3(1, 2, 3) };
|
const own = { ...viewer.camera, positionWC: new Cesium.Cartesian3(1, 2, 3) };
|
||||||
const { camera } = createCesiumCamera({ ...viewer, camera: own });
|
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 } = {}) {
|
export function bridgeEvent(source, mapArgs, { unary } = {}) {
|
||||||
const event = createEngineEvent();
|
const event = createEngineEvent();
|
||||||
const forward = unary
|
const forward = unary
|
||||||
? (a, b, c) => event.raiseEvent(unary(a, b, c))
|
? (a, b, c) => event.raiseValue(unary(a, b, c))
|
||||||
: mapArgs
|
: mapArgs
|
||||||
? (...args) => event.raiseEvent(...mapArgs(...args))
|
? (...args) => event.raiseEvent(...mapArgs(...args))
|
||||||
: (...args) => event.raiseEvent(...args);
|
: (...args) => event.raiseEvent(...args);
|
||||||
|
|||||||
+34
-11
@@ -83,9 +83,10 @@
|
|||||||
*
|
*
|
||||||
* @typedef {object} GlobeCamera
|
* @typedef {object} GlobeCamera
|
||||||
* @property {() => CameraPose} pose Fresh values on every call.
|
* @property {() => CameraPose} pose Fresh values on every call.
|
||||||
* @property {(result?: Cartesian3) => Cartesian3} position The pose's world
|
* @property {Cartesian3} livePosition The pose's world position as one
|
||||||
* position; fills and returns `result` when given (no allocation, for
|
* engine-owned object: the same object on every read, holding the current
|
||||||
* per-frame readers), else a fresh value.
|
* 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
|
* @property {() => number} height The camera's height above the WGS84
|
||||||
* ellipsoid (the pose's cartographic height), without building a pose.
|
* ellipsoid (the pose's cartographic height), without building a pose.
|
||||||
* @property {(view: { destination: Cartesian3 | Rectangle, orientation?: Orientation }) => void} setView
|
* @property {(view: { destination: Cartesian3 | Rectangle, orientation?: Orientation }) => void} setView
|
||||||
@@ -209,9 +210,10 @@ export const isEngineEvent = (value) =>
|
|||||||
value != null && value[ENGINE_EVENT] === true;
|
value != null && value[ENGINE_EVENT] === true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An event with the contract's semantics (see EngineEvent). `raiseEvent` is
|
* An event with the contract's semantics (see EngineEvent). `raiseEvent` and
|
||||||
* for engine implementations; a listener that throws propagates, and the
|
* `raiseValue` (one argument, no argument list: for per-frame events) are for
|
||||||
* event stays usable.
|
* engine implementations; a listener that throws propagates, and the event
|
||||||
|
* stays usable.
|
||||||
*/
|
*/
|
||||||
export function createEngineEvent() {
|
export function createEngineEvent() {
|
||||||
let entries = [];
|
let entries = [];
|
||||||
@@ -226,6 +228,13 @@ export function createEngineEvent() {
|
|||||||
else entries.splice(entries.indexOf(entry), 1);
|
else entries.splice(entries.indexOf(entry), 1);
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
const settle = () => {
|
||||||
|
depth--;
|
||||||
|
if (!depth && removedDuringRaise) {
|
||||||
|
entries = entries.filter((entry) => entry.listener);
|
||||||
|
removedDuringRaise = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
const event = {
|
const event = {
|
||||||
get numberOfListeners() {
|
get numberOfListeners() {
|
||||||
return entries.reduce(
|
return entries.reduce(
|
||||||
@@ -259,11 +268,21 @@ export function createEngineEvent() {
|
|||||||
if (listener) listener(...args);
|
if (listener) listener(...args);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
depth--;
|
settle();
|
||||||
if (!depth && removedDuringRaise) {
|
}
|
||||||
entries = entries.filter((entry) => entry.listener);
|
},
|
||||||
removedDuringRaise = false;
|
// 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 },
|
host: { container: isObject, canvas: isObject, size: isFunction },
|
||||||
camera: {
|
camera: {
|
||||||
pose: isFunction,
|
pose: isFunction,
|
||||||
position: isFunction,
|
livePosition: (value) =>
|
||||||
|
value != null &&
|
||||||
|
isNumber(value.x) &&
|
||||||
|
isNumber(value.y) &&
|
||||||
|
isNumber(value.z),
|
||||||
height: isFunction,
|
height: isFunction,
|
||||||
setView: isFunction,
|
setView: isFunction,
|
||||||
flyTo: 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', () => {
|
test('the contract carries what the shell infrastructure needs', () => {
|
||||||
for (const member of [
|
for (const member of [
|
||||||
'camera.position',
|
'camera.livePosition',
|
||||||
'camera.height',
|
'camera.height',
|
||||||
'maps.releasePhotorealTiles',
|
'maps.releasePhotorealTiles',
|
||||||
])
|
])
|
||||||
@@ -196,12 +196,12 @@ test('the contract carries what the shell infrastructure needs', () => {
|
|||||||
const engine = createFakeEngine();
|
const engine = createFakeEngine();
|
||||||
const broken = {
|
const broken = {
|
||||||
...engine,
|
...engine,
|
||||||
camera: { ...engine.camera, position: undefined, height: 1 },
|
camera: { ...engine.camera, livePosition: undefined, height: 1 },
|
||||||
maps: { ...engine.maps, releasePhotorealTiles: undefined },
|
maps: { ...engine.maps, releasePhotorealTiles: undefined },
|
||||||
};
|
};
|
||||||
assert.throws(
|
assert.throws(
|
||||||
() => assertGlobeEngine(broken),
|
() => 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'),
|
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 () => {
|
async () => {
|
||||||
camera.setView({
|
camera.setView({
|
||||||
destination: cartesian3.fromDegrees(-3.7, 40.4, 12000),
|
destination: cartesian3.fromDegrees(-3.7, 40.4, 12000),
|
||||||
orientation: { heading: 0.4, pitch: -0.5, roll: 0 },
|
orientation: { heading: 0.4, pitch: -0.5, roll: 0 },
|
||||||
});
|
});
|
||||||
await settle();
|
await settle();
|
||||||
const pose = camera.pose();
|
const live = camera.livePosition;
|
||||||
const result = cartesian3.create();
|
ensure(isCartesian3(live), 'livePosition is a Cartesian3');
|
||||||
const filled = camera.position(result);
|
|
||||||
ensure(
|
|
||||||
filled === result,
|
|
||||||
'position(result) returns the result it filled',
|
|
||||||
);
|
|
||||||
near(
|
near(
|
||||||
cartesian3.distance(filled, pose.position),
|
cartesian3.distance(live, camera.pose().position),
|
||||||
0,
|
0,
|
||||||
1e-6,
|
1e-6,
|
||||||
'position(result) vs pose().position',
|
'livePosition vs pose().position',
|
||||||
);
|
|
||||||
const fresh = camera.position();
|
|
||||||
ensure(
|
|
||||||
isCartesian3(fresh) && fresh !== filled,
|
|
||||||
'position() returns a fresh Cartesian3',
|
|
||||||
);
|
);
|
||||||
near(
|
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,
|
0,
|
||||||
1e-6,
|
1e-3,
|
||||||
'position() vs pose().position',
|
'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,
|
* @param {{ kind?: 'cesium'|'navara', width?: number, height?: number, fovy?: number,
|
||||||
* longitude?: number, latitude?: number, altitude?: number }} [options]
|
* longitude?: number, latitude?: number, altitude?: number,
|
||||||
* Canvas size in CSS px, angles in radians. The initial pose looks straight
|
* 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).
|
* down from `altitude` metres (10 000 km).
|
||||||
* @returns {import('../contract.js').GlobeEngine & { fake: object }}
|
* @returns {import('../contract.js').GlobeEngine & { fake: object }}
|
||||||
*/
|
*/
|
||||||
@@ -238,6 +240,7 @@ export function createFakeEngine(options = {}) {
|
|||||||
'frames.postRender': createEngineEvent(),
|
'frames.postRender': createEngineEvent(),
|
||||||
};
|
};
|
||||||
const inputs = new Map(INPUT_TYPES.map((type) => [type, new Set()]));
|
const inputs = new Map(INPUT_TYPES.map((type) => [type, new Set()]));
|
||||||
|
const livePosition = cartesian3.create();
|
||||||
const scripts = new Map();
|
const scripts = new Map();
|
||||||
let transform = [...matrix4.IDENTITY];
|
let transform = [...matrix4.IDENTITY];
|
||||||
let percentageChanged = 0.5;
|
let percentageChanged = 0.5;
|
||||||
@@ -602,10 +605,8 @@ export function createFakeEngine(options = {}) {
|
|||||||
fovy,
|
fovy,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
position(result) {
|
get livePosition() {
|
||||||
return result
|
return cartesian3.clone(state.position, livePosition);
|
||||||
? cartesian3.clone(state.position, result)
|
|
||||||
: copy3(state.position);
|
|
||||||
},
|
},
|
||||||
height: () => cartographic.fromCartesian(state.position, WGS84).height,
|
height: () => cartographic.fromCartesian(state.position, WGS84).height,
|
||||||
setView(view) {
|
setView(view) {
|
||||||
@@ -718,8 +719,8 @@ export function createFakeEngine(options = {}) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const host = {
|
const host = {
|
||||||
container: { kind: 'fake-container' },
|
container: options.container ?? { kind: 'fake-container' },
|
||||||
canvas: { kind: 'fake-canvas' },
|
canvas: options.canvas ?? { kind: 'fake-canvas' },
|
||||||
size: () => ({ width, height }),
|
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 engine = createFakeEngine({ altitude: 2_000_000 });
|
||||||
const result = geo.cartesian3.create();
|
const live = engine.camera.livePosition;
|
||||||
assert.equal(engine.camera.position(result), result);
|
|
||||||
assert.ok(
|
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);
|
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 Cesium from 'cesium';
|
||||||
|
import * as geo from '../geo/index.js';
|
||||||
|
import { engineFromViewer } from '../globe/cesium/engineFromViewer.js';
|
||||||
import {
|
import {
|
||||||
createCyberSonarSampler,
|
createCyberSonarSampler,
|
||||||
isCyberContactSonarActive as isCyberSonarActive,
|
isCyberContactSonarActive as isCyberSonarActive,
|
||||||
@@ -27,6 +29,11 @@ import { WORLD_OVERLAY_STYLE } from './worldOverlayTokens.js';
|
|||||||
* @description Shared screen-space renderer/scheduler for world-anchored
|
* @description Shared screen-space renderer/scheduler for world-anchored
|
||||||
* labels and cards. Sources own data, business rules, and bounded candidate
|
* labels and cards. Sources own data, business rules, and bounded candidate
|
||||||
* generation; this host owns projection, final placement, paint, and hits.
|
* 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';
|
const ROOT_ID = 'world-overlay-root';
|
||||||
@@ -131,7 +138,7 @@ export const WORLD_OVERLAY_OCCLUDER_SELECTORS = Object.freeze([
|
|||||||
* @typedef {object} WorldOverlayEntry
|
* @typedef {object} WorldOverlayEntry
|
||||||
* @property {string} id Stable identity within the source.
|
* @property {string} id Stable identity within the source.
|
||||||
* @property {string} source Owning source id.
|
* @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 {'label'|'track'|'card'|'thumbnail'|'selected'|'tracked'} variant
|
||||||
* @property {string} [accessibilityLabel] Accessible name for an actionable card.
|
* @property {string} [accessibilityLabel] Accessible name for an actionable card.
|
||||||
* @property {function():*} [activate] Keyboard/assistive activation callback.
|
* @property {function():*} [activate] Keyboard/assistive activation callback.
|
||||||
@@ -148,8 +155,8 @@ export const WORLD_OVERLAY_OCCLUDER_SELECTORS = Object.freeze([
|
|||||||
* @property {number} [solveIntervalMs=125]
|
* @property {number} [solveIntervalMs=125]
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/** @type {Cesium.Viewer|null} */
|
/** @type {import('../globe/contract.js').GlobeEngine|null} */
|
||||||
let _viewer = null;
|
let _engine = null;
|
||||||
/** @type {HTMLElement|null} */
|
/** @type {HTMLElement|null} */
|
||||||
let _root = null;
|
let _root = null;
|
||||||
/** @type {HTMLCanvasElement|null} */
|
/** @type {HTMLCanvasElement|null} */
|
||||||
@@ -218,13 +225,14 @@ const _domains = new Map();
|
|||||||
const _domainList = [];
|
const _domainList = [];
|
||||||
/** @type {Map<string, object>} */
|
/** @type {Map<string, object>} */
|
||||||
const _records = new Map();
|
const _records = new Map();
|
||||||
const _scratchViewProjection = new Cesium.Matrix4();
|
const _scratchViewProjection = geo.matrix4.create();
|
||||||
const _viewProjectionScalars = {};
|
const _viewProjectionScalars = {};
|
||||||
const _viewport = { width: 0, height: 0 };
|
const _viewport = { width: 0, height: 0 };
|
||||||
|
/** `{ isPointVisible }` over the engine's horizon test, for painters too. */
|
||||||
let _occluder = null;
|
let _occluder = null;
|
||||||
let _occluderCameraX = Number.NaN;
|
/** The engine's live camera position and the camera height, per frame. */
|
||||||
let _occluderCameraY = Number.NaN;
|
let _cameraPosition = null;
|
||||||
let _occluderCameraZ = Number.NaN;
|
let _cameraHeight = Number.NaN;
|
||||||
/** Cached keyhole geometry, versioned by both its box and shared tuning. */
|
/** Cached keyhole geometry, versioned by both its box and shared tuning. */
|
||||||
let _keyhole = null;
|
let _keyhole = null;
|
||||||
let _keyholeWidth = -1;
|
let _keyholeWidth = -1;
|
||||||
@@ -421,6 +429,7 @@ function snapshotCullPosition(entry) {
|
|||||||
const { x, y, z } = candidate;
|
const { x, y, z } = candidate;
|
||||||
if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(z))
|
if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(z))
|
||||||
return null;
|
return null;
|
||||||
|
// Still the sources' own vector type until they move off Cesium (phase 1c).
|
||||||
return new Cesium.Cartesian3(x, y, z);
|
return new Cesium.Cartesian3(x, y, z);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -860,7 +869,11 @@ function invalidateHost({ solve = true, layout = false } = {}) {
|
|||||||
_paintRectCount = 0;
|
_paintRectCount = 0;
|
||||||
_hitRectCount = 0;
|
_hitRectCount = 0;
|
||||||
_paintRectByKey.clear();
|
_paintRectByKey.clear();
|
||||||
_viewer?.scene?.requestRender?.();
|
requestRender();
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestRender() {
|
||||||
|
_engine?.render.request('world-overlay');
|
||||||
}
|
}
|
||||||
|
|
||||||
function inertPaintLaneHandle() {
|
function inertPaintLaneHandle() {
|
||||||
@@ -1227,9 +1240,10 @@ function ensureOverlayDom() {
|
|||||||
_root = document.createElement('div');
|
_root = document.createElement('div');
|
||||||
_root.id = ROOT_ID;
|
_root.id = ROOT_ID;
|
||||||
_root.setAttribute('aria-hidden', 'true');
|
_root.setAttribute('aria-hidden', 'true');
|
||||||
const parent = _viewer.container?.parentElement || document.body;
|
const container = _engine.host.container;
|
||||||
if (_viewer.container?.nextSibling)
|
const parent = container?.parentElement || document.body;
|
||||||
parent.insertBefore(_root, _viewer.container.nextSibling);
|
if (container?.nextSibling)
|
||||||
|
parent.insertBefore(_root, container.nextSibling);
|
||||||
else parent.appendChild(_root);
|
else parent.appendChild(_root);
|
||||||
}
|
}
|
||||||
_canvas =
|
_canvas =
|
||||||
@@ -1249,7 +1263,7 @@ function ensureOverlayDom() {
|
|||||||
// the browser silently discard the blend while the CSS string stayed
|
// the browser silently discard the blend while the CSS string stayed
|
||||||
// `'screen'`. Paint order is expressed purely by z-index: this surface is
|
// `'screen'`. Paint order is expressed purely by z-index: this surface is
|
||||||
// z5, the shared card canvas inside the root is z6.
|
// z5, the shared card canvas inside the root is z6.
|
||||||
const detectionParent = _viewer?.container || document.body;
|
const detectionParent = _engine?.host.container || document.body;
|
||||||
_detectionSurface =
|
_detectionSurface =
|
||||||
detectionParent.querySelector?.(`#${DETECTION_SURFACE_ID}`) ||
|
detectionParent.querySelector?.(`#${DETECTION_SURFACE_ID}`) ||
|
||||||
document.getElementById(DETECTION_SURFACE_ID);
|
document.getElementById(DETECTION_SURFACE_ID);
|
||||||
@@ -1317,15 +1331,10 @@ function sizeCanvasSurface(canvas, ctx, width, height, dpr) {
|
|||||||
* that actually has paint work.
|
* that actually has paint work.
|
||||||
*/
|
*/
|
||||||
function ensureCanvasSize() {
|
function ensureCanvasSize() {
|
||||||
if (!_canvas || !_viewer?.canvas) return false;
|
if (!_canvas || !_engine) return false;
|
||||||
const width = Math.max(
|
const size = _engine.host.size();
|
||||||
0,
|
const width = Math.max(0, Math.round(Number(size.width) || 0));
|
||||||
Math.round(Number(_viewer.canvas.clientWidth) || 0),
|
const height = Math.max(0, Math.round(Number(size.height) || 0));
|
||||||
);
|
|
||||||
const height = Math.max(
|
|
||||||
0,
|
|
||||||
Math.round(Number(_viewer.canvas.clientHeight) || 0),
|
|
||||||
);
|
|
||||||
const dpr = Math.max(1, Number(globalThis.window?.devicePixelRatio) || 1);
|
const dpr = Math.max(1, Number(globalThis.window?.devicePixelRatio) || 1);
|
||||||
const changed =
|
const changed =
|
||||||
_canvas.width !== Math.round(width * dpr) ||
|
_canvas.width !== Math.round(width * dpr) ||
|
||||||
@@ -1450,7 +1459,7 @@ function refreshUiOccluders(timestamp, force = false) {
|
|||||||
_occluderRefreshTimer = setTimeout(
|
_occluderRefreshTimer = setTimeout(
|
||||||
() => {
|
() => {
|
||||||
_occluderRefreshTimer = null;
|
_occluderRefreshTimer = null;
|
||||||
_viewer?.scene?.requestRender?.();
|
requestRender();
|
||||||
},
|
},
|
||||||
Math.max(0, OCCLUDER_REFRESH_MS - elapsed),
|
Math.max(0, OCCLUDER_REFRESH_MS - elapsed),
|
||||||
);
|
);
|
||||||
@@ -1694,12 +1703,7 @@ function overlayHasPaintWork(timestamp = nowMs()) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function prepareProjectionMatrix() {
|
function prepareProjectionMatrix() {
|
||||||
const camera = _viewer.camera;
|
const matrix = _engine.projector.viewProjection(_scratchViewProjection);
|
||||||
const matrix = Cesium.Matrix4.multiply(
|
|
||||||
camera.frustum.projectionMatrix,
|
|
||||||
camera.viewMatrix,
|
|
||||||
_scratchViewProjection,
|
|
||||||
);
|
|
||||||
_viewProjectionScalars.m0 = matrix[0];
|
_viewProjectionScalars.m0 = matrix[0];
|
||||||
_viewProjectionScalars.m1 = matrix[1];
|
_viewProjectionScalars.m1 = matrix[1];
|
||||||
_viewProjectionScalars.m3 = matrix[3];
|
_viewProjectionScalars.m3 = matrix[3];
|
||||||
@@ -1717,17 +1721,9 @@ function prepareProjectionMatrix() {
|
|||||||
|
|
||||||
function prepareCustomPaintFrame(timestamp, keyhole) {
|
function prepareCustomPaintFrame(timestamp, keyhole) {
|
||||||
const viewProjection = prepareProjectionMatrix();
|
const viewProjection = prepareProjectionMatrix();
|
||||||
const camera = _viewer.camera.positionWC;
|
const camera = _engine.camera.livePosition;
|
||||||
if (
|
_cameraPosition = camera;
|
||||||
camera.x !== _occluderCameraX ||
|
_cameraHeight = _engine.camera.height();
|
||||||
camera.y !== _occluderCameraY ||
|
|
||||||
camera.z !== _occluderCameraZ
|
|
||||||
) {
|
|
||||||
_occluder.cameraPosition = camera;
|
|
||||||
_occluderCameraX = camera.x;
|
|
||||||
_occluderCameraY = camera.y;
|
|
||||||
_occluderCameraZ = camera.z;
|
|
||||||
}
|
|
||||||
_customPaintFrame.canvas = _canvas;
|
_customPaintFrame.canvas = _canvas;
|
||||||
_customPaintFrame.surface = _canvas;
|
_customPaintFrame.surface = _canvas;
|
||||||
_customPaintFrame.ctx = _ctx;
|
_customPaintFrame.ctx = _ctx;
|
||||||
@@ -1749,7 +1745,7 @@ function getProjectionRecord(entry) {
|
|||||||
record = {
|
record = {
|
||||||
key: entry._overlayKey,
|
key: entry._overlayKey,
|
||||||
entry,
|
entry,
|
||||||
position: new Cesium.Cartesian3(),
|
position: geo.cartesian3.create(),
|
||||||
screen: { x: 0, y: 0 },
|
screen: { x: 0, y: 0 },
|
||||||
layout: entry._overlayLayout,
|
layout: entry._overlayLayout,
|
||||||
placements: [],
|
placements: [],
|
||||||
@@ -1933,7 +1929,7 @@ function snapshotAndProject(entry, source, viewProjection, keyhole) {
|
|||||||
// scalar steps stay local: the range is computed inline, and the common
|
// 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
|
// "no far limit" fade policy resolves without calling out at all. Both are
|
||||||
// exact restatements of `Cartesian3.distance` / `distanceFade`.
|
// exact restatements of `Cartesian3.distance` / `distanceFade`.
|
||||||
const cameraPosition = _viewer.camera.positionWC;
|
const cameraPosition = _cameraPosition;
|
||||||
const rangeX = cameraPosition.x - record.position.x;
|
const rangeX = cameraPosition.x - record.position.x;
|
||||||
const rangeY = cameraPosition.y - record.position.y;
|
const rangeY = cameraPosition.y - record.position.y;
|
||||||
const rangeZ = cameraPosition.z - record.position.z;
|
const rangeZ = cameraPosition.z - record.position.z;
|
||||||
@@ -1946,7 +1942,7 @@ function snapshotAndProject(entry, source, viewProjection, keyhole) {
|
|||||||
? 1
|
? 1
|
||||||
: 0
|
: 0
|
||||||
: distanceFade(distance, record.distanceOptions);
|
: distanceFade(distance, record.distanceOptions);
|
||||||
const cameraAltitude = _viewer.camera.positionCartographic?.height;
|
const cameraAltitude = _cameraHeight;
|
||||||
record.paintScale = entry.distanceScale
|
record.paintScale = entry.distanceScale
|
||||||
? distanceScale(distance, entry.distanceScale)
|
? distanceScale(distance, entry.distanceScale)
|
||||||
: 1;
|
: 1;
|
||||||
@@ -2607,7 +2603,7 @@ function drawWorldOverlay() {
|
|||||||
clearTimeout(_sonarRenderTimer);
|
clearTimeout(_sonarRenderTimer);
|
||||||
_sonarRenderTimer = null;
|
_sonarRenderTimer = null;
|
||||||
}
|
}
|
||||||
if (_destroyed || !_viewer || !_canvas || !_ctx) return;
|
if (_destroyed || !_engine || !_canvas || !_ctx) return;
|
||||||
const timestamp = nowMs();
|
const timestamp = nowMs();
|
||||||
if (!overlayHasPaintWork(timestamp)) {
|
if (!overlayHasPaintWork(timestamp)) {
|
||||||
resetFrameDiagnostics();
|
resetFrameDiagnostics();
|
||||||
@@ -2653,13 +2649,12 @@ function drawWorldOverlay() {
|
|||||||
if (_paintRectCount > 0 && isCyberSonarActive()) {
|
if (_paintRectCount > 0 && isCyberSonarActive()) {
|
||||||
_sonarRenderTimer = setTimeout(() => {
|
_sonarRenderTimer = setTimeout(() => {
|
||||||
_sonarRenderTimer = null;
|
_sonarRenderTimer = null;
|
||||||
if (!_destroyed && isCyberSonarActive())
|
if (!_destroyed && isCyberSonarActive()) requestRender();
|
||||||
_viewer?.scene?.requestRender?.();
|
|
||||||
}, CYBER_SONAR_RENDER_INTERVAL_MS);
|
}, CYBER_SONAR_RENDER_INTERVAL_MS);
|
||||||
}
|
}
|
||||||
const animationsRemaining =
|
const animationsRemaining =
|
||||||
activeFadeCount(timestamp) + activeLeaderAnimationCount(timestamp);
|
activeFadeCount(timestamp) + activeLeaderAnimationCount(timestamp);
|
||||||
if (animationsRemaining > 0) _viewer.scene.requestRender?.();
|
if (animationsRemaining > 0) requestRender();
|
||||||
}
|
}
|
||||||
|
|
||||||
function createDevFacade() {
|
function createDevFacade() {
|
||||||
@@ -2668,37 +2663,37 @@ function createDevFacade() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the singleton host. Repeated calls for the same viewer are no-op.
|
* Initialize the singleton host. Repeated calls for the same engine are no-op.
|
||||||
* @param {Cesium.Viewer} viewer
|
* @param {import('../globe/contract.js').GlobeEngine | object} target The
|
||||||
|
* globe engine, or a Cesium viewer (its engine: engineFromViewer).
|
||||||
*/
|
*/
|
||||||
export function initWorldOverlay(viewer) {
|
export function initWorldOverlay(target) {
|
||||||
if (!viewer?.scene?.postRender?.addEventListener) {
|
const engine = engineFromViewer(target);
|
||||||
|
if (!engine)
|
||||||
throw new TypeError(
|
throw new TypeError(
|
||||||
'initWorldOverlay requires a Cesium viewer with scene.postRender',
|
'initWorldOverlay requires a globe engine or a Cesium viewer',
|
||||||
);
|
);
|
||||||
}
|
if (_engine === engine && _canvas && _removePostRender) return;
|
||||||
if (_viewer === viewer && _canvas && _removePostRender) return;
|
if (_engine) destroyWorldOverlay();
|
||||||
if (_viewer) destroyWorldOverlay();
|
|
||||||
_destroyed = false;
|
_destroyed = false;
|
||||||
_viewer = viewer;
|
_engine = engine;
|
||||||
ensureOverlayDom();
|
ensureOverlayDom();
|
||||||
_occluder = new Cesium.EllipsoidalOccluder(
|
const { projector } = engine;
|
||||||
Cesium.Ellipsoid.WGS84,
|
_occluder = {
|
||||||
viewer.camera?.positionWC || new Cesium.Cartesian3(),
|
isPointVisible: (position) => projector.horizonVisible(position),
|
||||||
);
|
};
|
||||||
_cockpitActive = !!document.body?.classList?.contains('cockpit-mode');
|
_cockpitActive = !!document.body?.classList?.contains('cockpit-mode');
|
||||||
_cockpitModeHandler = (event) => {
|
_cockpitModeHandler = (event) => {
|
||||||
_cockpitActive = event?.detail?.active === true;
|
_cockpitActive = event?.detail?.active === true;
|
||||||
invalidateHost();
|
invalidateHost();
|
||||||
};
|
};
|
||||||
window.addEventListener('gev:cockpit-mode-changed', _cockpitModeHandler);
|
window.addEventListener('gev:cockpit-mode-changed', _cockpitModeHandler);
|
||||||
_removePostRender =
|
_removePostRender = engine.frames.postRender.addEventListener(() =>
|
||||||
viewer.scene.postRender.addEventListener(drawWorldOverlay);
|
drawWorldOverlay(),
|
||||||
if (viewer.camera?.moveEnd?.addEventListener) {
|
);
|
||||||
_removeMoveEnd = viewer.camera.moveEnd.addEventListener(() =>
|
_removeMoveEnd = engine.camera.moveEnd.addEventListener(() =>
|
||||||
invalidateHost(),
|
invalidateHost(),
|
||||||
);
|
);
|
||||||
}
|
|
||||||
installUiOccluderObservers();
|
installUiOccluderObservers();
|
||||||
// The backing store and the occluder inventory are both deferred to the
|
// 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
|
// 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
|
// Tearing down a host that was never initialized must not arm the
|
||||||
// post-destroy guard: sources are allowed to publish before the first
|
// post-destroy guard: sources are allowed to publish before the first
|
||||||
// `initWorldOverlay`, and that buffering has to survive a stray destroy.
|
// `initWorldOverlay`, and that buffering has to survive a stray destroy.
|
||||||
const hadHost = _viewer !== null;
|
const hadHost = _engine !== null;
|
||||||
_removePostRender?.();
|
_removePostRender?.();
|
||||||
_removePostRender = null;
|
_removePostRender = null;
|
||||||
_removeMoveEnd?.();
|
_removeMoveEnd?.();
|
||||||
@@ -2785,7 +2780,7 @@ export function destroyWorldOverlay() {
|
|||||||
_uiOcclusionRects.length = 0;
|
_uiOcclusionRects.length = 0;
|
||||||
_uiOcclusionRectPool.length = 0;
|
_uiOcclusionRectPool.length = 0;
|
||||||
_destroyed = hadHost || _destroyed;
|
_destroyed = hadHost || _destroyed;
|
||||||
_viewer = null;
|
_engine = null;
|
||||||
_root = null;
|
_root = null;
|
||||||
_canvas = null;
|
_canvas = null;
|
||||||
_ctx = null;
|
_ctx = null;
|
||||||
@@ -2795,9 +2790,8 @@ export function destroyWorldOverlay() {
|
|||||||
_detectionSurface = null;
|
_detectionSurface = null;
|
||||||
_detectionCtx = null;
|
_detectionCtx = null;
|
||||||
_occluder = null;
|
_occluder = null;
|
||||||
_occluderCameraX = Number.NaN;
|
_cameraPosition = null;
|
||||||
_occluderCameraY = Number.NaN;
|
_cameraHeight = Number.NaN;
|
||||||
_occluderCameraZ = Number.NaN;
|
|
||||||
_keyhole = null;
|
_keyhole = null;
|
||||||
_keyholeWidth = -1;
|
_keyholeWidth = -1;
|
||||||
_keyholeHeight = -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');
|
assert.equal(paint(), 1, 'deselect restores the ambient card without re-entry cooldown');
|
||||||
} finally { env.cleanup(); }
|
} 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