mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-12 06:12:18 +00:00
Scope render demand to each scene attachment
Give each scene its own render governor, owner holds and recent idle requests. Make the Cesium render backend own its request mode directly, so the governor drives engine.render on both engines without a dependency back from Cesium. Route layer requests through their attached engine and capture the attachment's governor for camera motions, orbit, annotations and shell controls. Release retained-layer holds before clearing the outgoing engine, and destroy the governor after scene consumers have detached. Preserve Cesium's owner-keyed holds, pre-install buffering, last-release settling frame and one-shot requests during continuous rendering. Navara keeps the same render policy. There is no intended visible change on either engine. Simultaneous scenes no longer share holds, and an old captured service cannot release a replacement attachment's hold. Cover both rules on fake and headless Cesium engines, including an old orbit ending after reattachment. Viewer-reference lines fall from 910 to 891, Navara-kind checks from 2 to 1, and engine-name lines from 33 to 32. All other coupling counts stay unchanged. Checked in the browser, keyless: qa-perf passes on both engines with the same idle, active and release counts as before (24 checks on Cesium, 22 on Navara).
This commit is contained in:
1 parent
4252ed0798
commit
0459ee6d58
87 files changed
+720
-935
No files matched your search
@@ -534,7 +534,7 @@ perform no network requests and discover no additional modules.
|
|||||||
`maps/controller` coordinates scene changes and lifetimes. `maps/imagery`,
|
`maps/controller` coordinates scene changes and lifetimes. `maps/imagery`,
|
||||||
`maps/terrain` and `maps/3d` supply constructors; `maps/defaults` selects the
|
`maps/terrain` and `maps/3d` supply constructors; `maps/defaults` selects the
|
||||||
standard sources and their setup/fallback policy. The standalone facade wires
|
standard sources and their setup/fallback policy. The standalone facade wires
|
||||||
these to the application's render governor.
|
these to their engine's render requests.
|
||||||
|
|
||||||
A registry supplies `sources`, `defaultId`, `unknownId` and optional `recoveryId`.
|
A registry supplies `sources`, `defaultId`, `unknownId` and optional `recoveryId`.
|
||||||
Each source has a user-facing `descriptor`, availability/reason, and either an
|
Each source has a user-facing `descriptor`, availability/reason, and either an
|
||||||
@@ -638,8 +638,11 @@ refresh policy. `app/layerSession.js` defines the retained session fields and
|
|||||||
handoff predicate. `standalone/sceneSwitcher.js` coordinates page handoff;
|
handoff predicate. `standalone/sceneSwitcher.js` coordinates page handoff;
|
||||||
`ui/shellScene.js` owns scene-bound shell construction, while `applicationShell`
|
`ui/shellScene.js` owns scene-bound shell construction, while `applicationShell`
|
||||||
keeps page controls and policy. Request, host and shell queues are not yet one
|
keeps page controls and policy. Request, host and shell queues are not yet one
|
||||||
queue. Overlay, render-governor, tracked-readout and pick services still include
|
queue. Pick ownership, selection dispatch and render demand belong to
|
||||||
module singletons; debug access still exposes the native viewer and tileset.
|
`scene.services`; render demand drives the engine contract, with no dependency
|
||||||
|
from the Cesium render backend to the governor. Overlay and tracked-readout
|
||||||
|
services still include module singletons; debug access still exposes the native
|
||||||
|
viewer and tileset.
|
||||||
|
|
||||||
## UI state owners
|
## UI state owners
|
||||||
|
|
||||||
|
|||||||
@@ -277,8 +277,13 @@ one application, and only the globe-bound parts move to the next engine.
|
|||||||
resolve these services with their attached engine; simultaneous engines do
|
resolve these services with their attached engine; simultaneous engines do
|
||||||
not share owner predicates or click subscriptions. Detach releases both
|
not share owner predicates or click subscriptions. Detach releases both
|
||||||
services before the renderer is parked. Reattachment creates fresh services,
|
services before the renderer is parked. Reattachment creates fresh services,
|
||||||
while retained layers register their current picks again. The overlay host,
|
while retained layers register their current picks again.
|
||||||
render governor and tracked readout still have module-owned state.
|
- **Scene render demand.** `scene.services.render` owns the attachment's holds
|
||||||
|
and recent idle requests. It drives `engine.render` on both engines;
|
||||||
|
Cesium implements its render mode directly. Retained layers identify their
|
||||||
|
attached engine, and scene callbacks capture their governor. Teardown clears
|
||||||
|
only that scene's holds; an old service cannot release a replacement's hold.
|
||||||
|
The overlay host and tracked readout still have module-owned state.
|
||||||
- **The session owns the state.** Catalog, layer models, sources, selection,
|
- **The session owns the state.** Catalog, layer models, sources, selection,
|
||||||
camera mode and URL state outlive a switch. A layer that is a session model
|
camera mode and URL state outlive a switch. A layer that is a session model
|
||||||
gets `attachRenderer(viewer, { engine })` / `detachRenderer()` instead of being rebuilt,
|
gets `attachRenderer(viewer, { engine })` / `detachRenderer()` instead of being rebuilt,
|
||||||
|
|||||||
@@ -547,6 +547,24 @@ the same services on first use. The application destroys them on detach and
|
|||||||
creates fresh services when reattaching a parked engine. Identical layer IDs in
|
creates fresh services when reattaching a parked engine. Identical layer IDs in
|
||||||
simultaneous scenes cannot replace or unregister one another's picks.
|
simultaneous scenes cannot replace or unregister one another's picks.
|
||||||
|
|
||||||
|
## Render demand
|
||||||
|
|
||||||
|
`scene.services.render` owns continuous-render holds and the last 16 idle
|
||||||
|
request reasons for one attachment. `holdContinuousRender(id)` and
|
||||||
|
`releaseContinuousRender(id)` are idempotent per owner; the last release
|
||||||
|
requests one settling frame. `governorRequestRender(reason)` forwards a
|
||||||
|
one-shot request even while another owner holds continuous rendering.
|
||||||
|
The tools call `install()` after scene consumers have registered their holds.
|
||||||
|
|
||||||
|
The governor drives `engine.render` on every engine. Cesium implements
|
||||||
|
`setContinuous` with its scene's `requestRenderMode` and disables automatic
|
||||||
|
simulation-time renders with `maximumRenderTimeChange = Infinity`. Engine
|
||||||
|
internals request their own scene directly and never import the governor.
|
||||||
|
Layer service helpers take their attached engine as the final argument;
|
||||||
|
attachment-bound callbacks can capture `renderGovernorFor(engine)` directly.
|
||||||
|
Destroying the scene clears its holds and makes the captured service inert.
|
||||||
|
A parked engine gets a fresh service when reattached.
|
||||||
|
|
||||||
## Render effects
|
## Render effects
|
||||||
|
|
||||||
The optional `visualStyles`, `bloom` and `contactSonar` capabilities are
|
The optional `visualStyles`, `bloom` and `contactSonar` capabilities are
|
||||||
|
|||||||
@@ -3,14 +3,14 @@
|
|||||||
"cesiumLayerRendererLines": 0,
|
"cesiumLayerRendererLines": 0,
|
||||||
"navaraLayerRendererLines": 189,
|
"navaraLayerRendererLines": 189,
|
||||||
"cesiumImportFiles": 3,
|
"cesiumImportFiles": 3,
|
||||||
"viewerLines": 910,
|
"viewerLines": 891,
|
||||||
"navaraKindChecks": 2,
|
"navaraKindChecks": 1,
|
||||||
"engineSniffCalls": 0,
|
"engineSniffCalls": 0,
|
||||||
"engineExtensionReads": 3,
|
"engineExtensionReads": 3,
|
||||||
"groundEffectPasses": 1,
|
"groundEffectPasses": 1,
|
||||||
"cesiumSdkLoads": 3,
|
"cesiumSdkLoads": 3,
|
||||||
"screenSpaceHandlerFiles": 4,
|
"screenSpaceHandlerFiles": 4,
|
||||||
"engineNameLines": 33
|
"engineNameLines": 32
|
||||||
},
|
},
|
||||||
"measuredAt": "Enforce SDK ownership, relocate annotation and wind backends, and rehearse every shared display"
|
"measuredAt": "Scope render demand to scene attachments and make the Cesium render backend independent"
|
||||||
}
|
}
|
||||||
@@ -191,7 +191,6 @@
|
|||||||
"src/globe/cesium/starrySky.js",
|
"src/globe/cesium/starrySky.js",
|
||||||
"src/sky/stars.js",
|
"src/sky/stars.js",
|
||||||
"src/data/bundledJson.js",
|
"src/data/bundledJson.js",
|
||||||
"src/renderGovernor.js",
|
|
||||||
"src/globe/contract.js",
|
"src/globe/contract.js",
|
||||||
"src/maps/google3d.js"
|
"src/maps/google3d.js"
|
||||||
],
|
],
|
||||||
@@ -980,6 +979,11 @@
|
|||||||
"ui-shell": {
|
"ui-shell": {
|
||||||
"exports": ["./ui/shell"],
|
"exports": ["./ui/shell"],
|
||||||
"modules": [
|
"modules": [
|
||||||
|
"src/renderGovernor.js",
|
||||||
|
"src/data/sceneServices.js",
|
||||||
|
"src/data/pickRegistry.js",
|
||||||
|
"src/data/selectionDispatcher.js",
|
||||||
|
"src/data/trackingGesture.js",
|
||||||
"src/data/headingDegrees.js",
|
"src/data/headingDegrees.js",
|
||||||
"src/data/distanceFormat.js",
|
"src/data/distanceFormat.js",
|
||||||
"src/i18n/marker.js",
|
"src/i18n/marker.js",
|
||||||
|
|||||||
@@ -1226,7 +1226,7 @@ async function main() {
|
|||||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||||
remove();
|
remove();
|
||||||
return { frames, durationMs: performance.now() - started,
|
return { frames, durationMs: performance.now() - started,
|
||||||
governor: getRenderGovernorDiagnostics() };
|
governor: getRenderGovernorDiagnostics(app.engine) };
|
||||||
});
|
});
|
||||||
const geomAfter30s = await serializeGeom(activeId);
|
const geomAfter30s = await serializeGeom(activeId);
|
||||||
evidence.geometry = {
|
evidence.geometry = {
|
||||||
|
|||||||
@@ -190,7 +190,7 @@ try {
|
|||||||
// Use the governor's hold so an asynchronous idle transition cannot
|
// Use the governor's hold so an asynchronous idle transition cannot
|
||||||
// overwrite a raw scene flag during the sustained-render measurement.
|
// overwrite a raw scene flag during the sustained-render measurement.
|
||||||
const { holdContinuousRender } = await import('/src/renderGovernor.js');
|
const { holdContinuousRender } = await import('/src/renderGovernor.js');
|
||||||
holdContinuousRender('qa-military-names-fps');
|
holdContinuousRender('qa-military-names-fps', window.__godsEyeView.engine);
|
||||||
});
|
});
|
||||||
const t0 = await pageNow(page);
|
const t0 = await pageNow(page);
|
||||||
await sleep(3500);
|
await sleep(3500);
|
||||||
@@ -198,7 +198,7 @@ try {
|
|||||||
await page.evaluate(async () => {
|
await page.evaluate(async () => {
|
||||||
const { releaseContinuousRender } =
|
const { releaseContinuousRender } =
|
||||||
await import('/src/renderGovernor.js');
|
await import('/src/renderGovernor.js');
|
||||||
releaseContinuousRender('qa-military-names-fps');
|
releaseContinuousRender('qa-military-names-fps', window.__godsEyeView.engine);
|
||||||
});
|
});
|
||||||
const metrics = frameStats(frames);
|
const metrics = frameStats(frames);
|
||||||
const seconds = frames.reduce((sum, frame) => sum + frame.dt, 0) / 1000;
|
const seconds = frames.reduce((sum, frame) => sum + frame.dt, 0) / 1000;
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import assert from 'node:assert/strict';
|
|||||||
import { FEATURE_SOURCE_METHODS } from '../sources/featureSource.js';
|
import { FEATURE_SOURCE_METHODS } from '../sources/featureSource.js';
|
||||||
import { createAnnotationResolver } from './resolver.js';
|
import { createAnnotationResolver } from './resolver.js';
|
||||||
import { createAnnotationEngine } from './annotationEngine.js';
|
import { createAnnotationEngine } from './annotationEngine.js';
|
||||||
import { _resetRenderGovernorForTest } from '../renderGovernor.js';
|
|
||||||
import { cameraOnlyEngine } from '../testSupport/viewerEngine.mjs';
|
import { cameraOnlyEngine } from '../testSupport/viewerEngine.mjs';
|
||||||
|
|
||||||
function viewerAt(lat, lon, height = 20_000) {
|
function viewerAt(lat, lon, height = 20_000) {
|
||||||
@@ -253,8 +252,6 @@ test('non-admin asks and entity facts keep the existing path', async () => {
|
|||||||
test('the engine keeps every part on the mark, drawn at once or upgraded', async (t) => {
|
test('the engine keeps every part on the mark, drawn at once or upgraded', async (t) => {
|
||||||
globalThis.requestAnimationFrame ??= () => 1;
|
globalThis.requestAnimationFrame ??= () => 1;
|
||||||
globalThis.cancelAnimationFrame ??= () => {};
|
globalThis.cancelAnimationFrame ??= () => {};
|
||||||
_resetRenderGovernorForTest();
|
|
||||||
t.after(() => _resetRenderGovernorForTest());
|
|
||||||
const polygons = [
|
const polygons = [
|
||||||
[
|
[
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -2,11 +2,7 @@ import { isUnavailableCapability } from '../sources/capability.js';
|
|||||||
import { defaultGeospatial } from '../search/defaults.js';
|
import { defaultGeospatial } from '../search/defaults.js';
|
||||||
import * as geo from '../geo/index.js';
|
import * as geo from '../geo/index.js';
|
||||||
import { greatCircleMeters } from '../geoDistance.js';
|
import { greatCircleMeters } from '../geoDistance.js';
|
||||||
import {
|
import { renderGovernorFor } from '../renderGovernor.js';
|
||||||
holdContinuousRender,
|
|
||||||
releaseContinuousRender,
|
|
||||||
governorRequestRender,
|
|
||||||
} from '../renderGovernor.js';
|
|
||||||
import {
|
import {
|
||||||
isRateLimitedOutcome,
|
isRateLimitedOutcome,
|
||||||
resolveAnnotationTarget,
|
resolveAnnotationTarget,
|
||||||
@@ -141,6 +137,7 @@ export function createAnnotationEngine({
|
|||||||
outlineRetryDelaysMs = OUTLINE_RETRY_DELAYS_MS,
|
outlineRetryDelaysMs = OUTLINE_RETRY_DELAYS_MS,
|
||||||
resolveTarget = resolveAnnotationTarget,
|
resolveTarget = resolveAnnotationTarget,
|
||||||
}) {
|
}) {
|
||||||
|
let renderGovernor = renderGovernorFor(globe);
|
||||||
/** @type {Map<string, object>} live annotations keyed by id */
|
/** @type {Map<string, object>} live annotations keyed by id */
|
||||||
const annotations = new Map();
|
const annotations = new Map();
|
||||||
let destroyed = false;
|
let destroyed = false;
|
||||||
@@ -220,8 +217,8 @@ export function createAnnotationEngine({
|
|||||||
// hidden from Draw's list lets the scene idle.
|
// hidden from Draw's list lets the scene idle.
|
||||||
function syncAnnotationHold() {
|
function syncAnnotationHold() {
|
||||||
const visible = [...annotations.values()].some((anno) => !anno.hidden);
|
const visible = [...annotations.values()].some((anno) => !anno.hidden);
|
||||||
if (visible) holdContinuousRender('annotations');
|
if (visible) renderGovernor?.holdContinuousRender('annotations');
|
||||||
else releaseContinuousRender('annotations');
|
else renderGovernor?.releaseContinuousRender('annotations');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -386,7 +383,7 @@ export function createAnnotationEngine({
|
|||||||
syncAnnotationHold();
|
syncAnnotationHold();
|
||||||
// The frame that takes a just-hidden mark off: with every mark hidden
|
// The frame that takes a just-hidden mark off: with every mark hidden
|
||||||
// the hold is gone, and nothing else would draw it.
|
// the hold is gone, and nothing else would draw it.
|
||||||
governorRequestRender('annotations');
|
renderGovernor?.governorRequestRender('annotations');
|
||||||
}
|
}
|
||||||
if (expired) notifyChange();
|
if (expired) notifyChange();
|
||||||
// Stop once the board is STABLE — persistent marks (persist=true is the default)
|
// Stop once the board is STABLE — persistent marks (persist=true is the default)
|
||||||
@@ -1616,7 +1613,7 @@ export function createAnnotationEngine({
|
|||||||
// mark hidden before its first frame has no alpha change to wait for.
|
// mark hidden before its first frame has no alpha change to wait for.
|
||||||
renderer.sync(annotations);
|
renderer.sync(annotations);
|
||||||
syncAnnotationHold();
|
syncAnnotationHold();
|
||||||
governorRequestRender('annotations');
|
renderGovernor?.governorRequestRender('annotations');
|
||||||
ensureTicking();
|
ensureTicking();
|
||||||
notifyChange();
|
notifyChange();
|
||||||
return true;
|
return true;
|
||||||
@@ -1817,6 +1814,8 @@ export function createAnnotationEngine({
|
|||||||
dropQueuedOutlineUpgrades();
|
dropQueuedOutlineUpgrades();
|
||||||
generation++;
|
generation++;
|
||||||
const previous = renderer;
|
const previous = renderer;
|
||||||
|
const previousGovernor = renderGovernor;
|
||||||
|
renderGovernor = null;
|
||||||
renderer = DETACHED_RENDERER;
|
renderer = DETACHED_RENDERER;
|
||||||
globe = null;
|
globe = null;
|
||||||
for (const anno of annotations.values()) {
|
for (const anno of annotations.values()) {
|
||||||
@@ -1830,7 +1829,7 @@ export function createAnnotationEngine({
|
|||||||
try {
|
try {
|
||||||
previous.destroy();
|
previous.destroy();
|
||||||
} finally {
|
} finally {
|
||||||
releaseContinuousRender('annotations');
|
previousGovernor?.releaseContinuousRender('annotations');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
@@ -1841,6 +1840,7 @@ export function createAnnotationEngine({
|
|||||||
if (destroyed) return;
|
if (destroyed) return;
|
||||||
engine.detachRenderer();
|
engine.detachRenderer();
|
||||||
globe = nextGlobe;
|
globe = nextGlobe;
|
||||||
|
renderGovernor = renderGovernorFor(globe);
|
||||||
renderer = nextRenderer ?? DETACHED_RENDERER;
|
renderer = nextRenderer ?? DETACHED_RENDERER;
|
||||||
for (const anno of annotations.values()) {
|
for (const anno of annotations.values()) {
|
||||||
try {
|
try {
|
||||||
@@ -1853,7 +1853,7 @@ export function createAnnotationEngine({
|
|||||||
renderer.sync(annotations);
|
renderer.sync(annotations);
|
||||||
syncAnnotationHold();
|
syncAnnotationHold();
|
||||||
ensureTicking();
|
ensureTicking();
|
||||||
governorRequestRender('annotations');
|
renderGovernor?.governorRequestRender('annotations');
|
||||||
},
|
},
|
||||||
/** Whether a globe draws the marks now. */
|
/** Whether a globe draws the marks now. */
|
||||||
get attached() {
|
get attached() {
|
||||||
@@ -1869,7 +1869,7 @@ export function createAnnotationEngine({
|
|||||||
tickHandle = null;
|
tickHandle = null;
|
||||||
outlineListeners.clear();
|
outlineListeners.clear();
|
||||||
changeListeners.clear();
|
changeListeners.clear();
|
||||||
releaseContinuousRender('annotations');
|
renderGovernor?.releaseContinuousRender('annotations');
|
||||||
renderer.destroy();
|
renderer.destroy();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { createFakeEngine } from '../globe/testing/fakeEngine.js';
|
||||||
import { createStandalonePlaceSearch } from '../standalone/placeSearch.js';
|
import { createStandalonePlaceSearch } from '../standalone/placeSearch.js';
|
||||||
// Voice-annotation resilience contract tests — pure logic, no network, no browser.
|
// Voice-annotation resilience contract tests — pure logic, no network, no browser.
|
||||||
//
|
//
|
||||||
@@ -22,8 +23,7 @@ import {
|
|||||||
import { resolveAnnotationTarget } from './annotationResolver.js';
|
import { resolveAnnotationTarget } from './annotationResolver.js';
|
||||||
import {
|
import {
|
||||||
getRenderGovernorDiagnostics,
|
getRenderGovernorDiagnostics,
|
||||||
installRenderGovernor,
|
renderGovernorFor,
|
||||||
_resetRenderGovernorForTest,
|
|
||||||
} from '../renderGovernor.js';
|
} from '../renderGovernor.js';
|
||||||
|
|
||||||
// A resolver that replays a scripted sequence of outcomes (undefined = transient,
|
// A resolver that replays a scripted sequence of outcomes (undefined = transient,
|
||||||
@@ -241,7 +241,7 @@ test('outline queue: an 8-spec batch keeps at most two FIFO upgrades in flight',
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
const engine = createAnnotationEngine({
|
const engine = createAnnotationEngine({
|
||||||
globe: {},
|
globe: testGlobe,
|
||||||
renderer,
|
renderer,
|
||||||
resolveTarget,
|
resolveTarget,
|
||||||
});
|
});
|
||||||
@@ -313,7 +313,7 @@ test('outline queue: clear drops queued-but-unstarted upgrades without a later f
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
engine = createAnnotationEngine({
|
engine = createAnnotationEngine({
|
||||||
globe: {},
|
globe: testGlobe,
|
||||||
renderer,
|
renderer,
|
||||||
resolveTarget,
|
resolveTarget,
|
||||||
});
|
});
|
||||||
@@ -379,7 +379,7 @@ test('retry: HTTP 429 Retry-After 5s gets one ladder-spaced retry; a second 429
|
|||||||
});
|
});
|
||||||
|
|
||||||
const resolved = await resolveAnnotationTarget({
|
const resolved = await resolveAnnotationTarget({
|
||||||
globe: {},
|
globe: testGlobe,
|
||||||
target: 'PT2 rate-limit fixture',
|
target: 'PT2 rate-limit fixture',
|
||||||
latitude: 12.345,
|
latitude: 12.345,
|
||||||
longitude: 67.89,
|
longitude: 67.89,
|
||||||
@@ -453,7 +453,7 @@ test('retry: a plain HTTP 500 still exhausts the existing 8s/25s transient ladde
|
|||||||
});
|
});
|
||||||
|
|
||||||
const resolved = await resolveAnnotationTarget({
|
const resolved = await resolveAnnotationTarget({
|
||||||
globe: {},
|
globe: testGlobe,
|
||||||
target: 'PT2 transient fixture',
|
target: 'PT2 transient fixture',
|
||||||
latitude: -12.345,
|
latitude: -12.345,
|
||||||
longitude: -67.89,
|
longitude: -67.89,
|
||||||
@@ -711,11 +711,9 @@ const BUILDING_FP = {
|
|||||||
|
|
||||||
test('fresh async building batch reserves slots together and releases failed/cleared installs', async (t) => {
|
test('fresh async building batch reserves slots together and releases failed/cleared installs', async (t) => {
|
||||||
installAnimationFrameStubs(t);
|
installAnimationFrameStubs(t);
|
||||||
_resetRenderGovernorForTest();
|
|
||||||
t.after(() => _resetRenderGovernorForTest());
|
|
||||||
const h = controlledBuildingRenderer();
|
const h = controlledBuildingRenderer();
|
||||||
const engine = createAnnotationEngine({
|
const engine = createAnnotationEngine({
|
||||||
globe: {},
|
globe: testGlobe,
|
||||||
renderer: h.renderer,
|
renderer: h.renderer,
|
||||||
resolveTarget: async ({ target }) => ({
|
resolveTarget: async ({ target }) => ({
|
||||||
...BUILDING_FP,
|
...BUILDING_FP,
|
||||||
@@ -744,7 +742,7 @@ test('fresh async building batch reserves slots together and releases failed/cle
|
|||||||
assert.equal(engine.list()[0].pendingOutline, false);
|
assert.equal(engine.list()[0].pendingOutline, false);
|
||||||
engine.clear();
|
engine.clear();
|
||||||
assert.equal(engine.count(), 0);
|
assert.equal(engine.count(), 0);
|
||||||
assert.ok(!getRenderGovernorDiagnostics().holds.includes('annotations'));
|
assert.ok(!getRenderGovernorDiagnostics(testGlobe).holds.includes('annotations'));
|
||||||
|
|
||||||
const cancelled = engine.annotate([{ type: 'area', target: 'third' }]);
|
const cancelled = engine.annotate([{ type: 'area', target: 'third' }]);
|
||||||
await flushMicrotasks();
|
await flushMicrotasks();
|
||||||
@@ -762,7 +760,7 @@ test('outline workers retain completion ownership and clear cancels resolved-but
|
|||||||
const h = controlledBuildingRenderer();
|
const h = controlledBuildingRenderer();
|
||||||
const started = [];
|
const started = [];
|
||||||
const engine = createAnnotationEngine({
|
const engine = createAnnotationEngine({
|
||||||
globe: {},
|
globe: testGlobe,
|
||||||
renderer: h.renderer,
|
renderer: h.renderer,
|
||||||
resolveTarget: async ({ target }) => ({
|
resolveTarget: async ({ target }) => ({
|
||||||
...FIXED_POINT,
|
...FIXED_POINT,
|
||||||
@@ -805,7 +803,7 @@ test('building recaption timeout retains old route and a newer replacement owns
|
|||||||
installAnimationFrameStubs(t);
|
installAnimationFrameStubs(t);
|
||||||
const h = controlledBuildingRenderer();
|
const h = controlledBuildingRenderer();
|
||||||
const engine = createAnnotationEngine({
|
const engine = createAnnotationEngine({
|
||||||
globe: {},
|
globe: testGlobe,
|
||||||
renderer: h.renderer,
|
renderer: h.renderer,
|
||||||
resolveTarget: async () => ({ ...BUILDING_FP }),
|
resolveTarget: async () => ({ ...BUILDING_FP }),
|
||||||
});
|
});
|
||||||
@@ -860,7 +858,7 @@ test('replacement cleanup failure still removes the old route and retains failed
|
|||||||
installAnimationFrameStubs(t);
|
installAnimationFrameStubs(t);
|
||||||
const h = controlledBuildingRenderer();
|
const h = controlledBuildingRenderer();
|
||||||
const engine = createAnnotationEngine({
|
const engine = createAnnotationEngine({
|
||||||
globe: {}, renderer: h.renderer, resolveTarget: async () => ({ ...BUILDING_FP }),
|
globe: testGlobe, renderer: h.renderer, resolveTarget: async () => ({ ...BUILDING_FP }),
|
||||||
});
|
});
|
||||||
const initial = engine.annotate([{ type: 'area', target: 'Capitol', label: 'Old' }]);
|
const initial = engine.annotate([{ type: 'area', target: 'Capitol', label: 'Old' }]);
|
||||||
await flushMicrotasks();
|
await flushMicrotasks();
|
||||||
@@ -905,7 +903,7 @@ test('identical refresh revokes an older building replacement even if cancellati
|
|||||||
installAnimationFrameStubs(t);
|
installAnimationFrameStubs(t);
|
||||||
const h = controlledBuildingRenderer();
|
const h = controlledBuildingRenderer();
|
||||||
const engine = createAnnotationEngine({
|
const engine = createAnnotationEngine({
|
||||||
globe: {}, renderer: h.renderer, resolveTarget: async () => ({ ...BUILDING_FP }),
|
globe: testGlobe, renderer: h.renderer, resolveTarget: async () => ({ ...BUILDING_FP }),
|
||||||
});
|
});
|
||||||
const initial = engine.annotate([{ type: 'area', target: 'Capitol', label: 'Old' }]);
|
const initial = engine.annotate([{ type: 'area', target: 'Capitol', label: 'Old' }]);
|
||||||
await flushMicrotasks(); h.pending[0].finish(); await initial;
|
await flushMicrotasks(); h.pending[0].finish(); await initial;
|
||||||
@@ -936,7 +934,7 @@ test('successful replacement survives failed old-route retirement and keeps clea
|
|||||||
installAnimationFrameStubs(t);
|
installAnimationFrameStubs(t);
|
||||||
const h = controlledBuildingRenderer();
|
const h = controlledBuildingRenderer();
|
||||||
const engine = createAnnotationEngine({
|
const engine = createAnnotationEngine({
|
||||||
globe: {}, renderer: h.renderer, resolveTarget: async () => ({ ...BUILDING_FP }),
|
globe: testGlobe, renderer: h.renderer, resolveTarget: async () => ({ ...BUILDING_FP }),
|
||||||
});
|
});
|
||||||
const initial = engine.annotate([{ type: 'area', target: 'Capitol', label: 'Old' }]);
|
const initial = engine.annotate([{ type: 'area', target: 'Capitol', label: 'Old' }]);
|
||||||
await flushMicrotasks(); h.pending[0].finish(); await initial;
|
await flushMicrotasks(); h.pending[0].finish(); await initial;
|
||||||
@@ -975,7 +973,7 @@ test('pending fresh predecessor failure cannot erase a newer building intent', a
|
|||||||
globalThis.requestAnimationFrame = (callback) => { frames.push(callback); return frames.length; };
|
globalThis.requestAnimationFrame = (callback) => { frames.push(callback); return frames.length; };
|
||||||
const h = controlledBuildingRenderer();
|
const h = controlledBuildingRenderer();
|
||||||
const engine = createAnnotationEngine({
|
const engine = createAnnotationEngine({
|
||||||
globe: {}, renderer: h.renderer, resolveTarget: async () => ({ ...BUILDING_FP }),
|
globe: testGlobe, renderer: h.renderer, resolveTarget: async () => ({ ...BUILDING_FP }),
|
||||||
});
|
});
|
||||||
const first = engine.annotate([{ type: 'area', target: 'Capitol', label: 'First' }]);
|
const first = engine.annotate([{ type: 'area', target: 'Capitol', label: 'First' }]);
|
||||||
await flushMicrotasks();
|
await flushMicrotasks();
|
||||||
@@ -1004,7 +1002,7 @@ test('duplicate pending building results reflect final failure rather than a dea
|
|||||||
installAnimationFrameStubs(t);
|
installAnimationFrameStubs(t);
|
||||||
const h = controlledBuildingRenderer();
|
const h = controlledBuildingRenderer();
|
||||||
const engine = createAnnotationEngine({
|
const engine = createAnnotationEngine({
|
||||||
globe: {}, renderer: h.renderer, resolveTarget: async () => ({ ...BUILDING_FP }),
|
globe: testGlobe, renderer: h.renderer, resolveTarget: async () => ({ ...BUILDING_FP }),
|
||||||
});
|
});
|
||||||
const spec = { type: 'area', target: 'Capitol', label: 'Same' };
|
const spec = { type: 'area', target: 'Capitol', label: 'Same' };
|
||||||
const batch = engine.annotate([spec, spec]);
|
const batch = engine.annotate([spec, spec]);
|
||||||
@@ -1023,7 +1021,7 @@ test('separate duplicate callers observe the original building install outcome',
|
|||||||
for (const outcome of ['installed', 'failed', 'timeout', 'clear']) {
|
for (const outcome of ['installed', 'failed', 'timeout', 'clear']) {
|
||||||
const h = controlledBuildingRenderer();
|
const h = controlledBuildingRenderer();
|
||||||
const engine = createAnnotationEngine({
|
const engine = createAnnotationEngine({
|
||||||
globe: {},
|
globe: testGlobe,
|
||||||
renderer: h.renderer,
|
renderer: h.renderer,
|
||||||
resolveTarget: async () => ({ ...BUILDING_FP }),
|
resolveTarget: async () => ({ ...BUILDING_FP }),
|
||||||
});
|
});
|
||||||
@@ -1077,7 +1075,7 @@ test('a duplicate joins the physical deferred install but not another outline lo
|
|||||||
const h = controlledBuildingRenderer();
|
const h = controlledBuildingRenderer();
|
||||||
let lookups = 0;
|
let lookups = 0;
|
||||||
const engine = createAnnotationEngine({
|
const engine = createAnnotationEngine({
|
||||||
globe: {},
|
globe: testGlobe,
|
||||||
renderer: h.renderer,
|
renderer: h.renderer,
|
||||||
resolveTarget: async () => ({
|
resolveTarget: async () => ({
|
||||||
...FIXED_POINT,
|
...FIXED_POINT,
|
||||||
@@ -1114,12 +1112,10 @@ test('a duplicate joins the physical deferred install but not another outline lo
|
|||||||
|
|
||||||
test('duplicate-replacement: a renderer throw leaves no phantom mark and no leaked hold', async (t) => {
|
test('duplicate-replacement: a renderer throw leaves no phantom mark and no leaked hold', async (t) => {
|
||||||
installAnimationFrameStubs(t);
|
installAnimationFrameStubs(t);
|
||||||
_resetRenderGovernorForTest();
|
|
||||||
t.after(() => _resetRenderGovernorForTest());
|
|
||||||
|
|
||||||
const { renderer, failAddOnce } = throwingRendererHarness();
|
const { renderer, failAddOnce } = throwingRendererHarness();
|
||||||
const engine = createAnnotationEngine({
|
const engine = createAnnotationEngine({
|
||||||
globe: {},
|
globe: testGlobe,
|
||||||
renderer,
|
renderer,
|
||||||
resolveTarget: async () => ({ ...FIXED_POINT }),
|
resolveTarget: async () => ({ ...FIXED_POINT }),
|
||||||
});
|
});
|
||||||
@@ -1130,7 +1126,7 @@ test('duplicate-replacement: a renderer throw leaves no phantom mark and no leak
|
|||||||
assert.equal(first.drawn, 1);
|
assert.equal(first.drawn, 1);
|
||||||
assert.equal(engine.list().length, 1);
|
assert.equal(engine.list().length, 1);
|
||||||
assert.ok(
|
assert.ok(
|
||||||
getRenderGovernorDiagnostics().holds.includes('annotations'),
|
getRenderGovernorDiagnostics(testGlobe).holds.includes('annotations'),
|
||||||
'a live mark must hold continuous render',
|
'a live mark must hold continuous render',
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1161,19 +1157,17 @@ test('duplicate-replacement: a renderer throw leaves no phantom mark and no leak
|
|||||||
'no phantom annotation survives the failed swap',
|
'no phantom annotation survives the failed swap',
|
||||||
);
|
);
|
||||||
assert.ok(
|
assert.ok(
|
||||||
!getRenderGovernorDiagnostics().holds.includes('annotations'),
|
!getRenderGovernorDiagnostics(testGlobe).holds.includes('annotations'),
|
||||||
'the annotations hold must be released — a leak defeats the idle governor permanently',
|
'the annotations hold must be released — a leak defeats the idle governor permanently',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('fresh path: a renderer throw is rolled back the same way', async (t) => {
|
test('fresh path: a renderer throw is rolled back the same way', async (t) => {
|
||||||
installAnimationFrameStubs(t);
|
installAnimationFrameStubs(t);
|
||||||
_resetRenderGovernorForTest();
|
|
||||||
t.after(() => _resetRenderGovernorForTest());
|
|
||||||
|
|
||||||
const { renderer, failAddOnce } = throwingRendererHarness();
|
const { renderer, failAddOnce } = throwingRendererHarness();
|
||||||
const engine = createAnnotationEngine({
|
const engine = createAnnotationEngine({
|
||||||
globe: {},
|
globe: testGlobe,
|
||||||
renderer,
|
renderer,
|
||||||
resolveTarget: async () => ({ ...FIXED_POINT }),
|
resolveTarget: async () => ({ ...FIXED_POINT }),
|
||||||
});
|
});
|
||||||
@@ -1190,7 +1184,7 @@ test('fresh path: a renderer throw is rolled back the same way', async (t) => {
|
|||||||
'no phantom annotation from a failed first add',
|
'no phantom annotation from a failed first add',
|
||||||
);
|
);
|
||||||
assert.ok(
|
assert.ok(
|
||||||
!getRenderGovernorDiagnostics().holds.includes('annotations'),
|
!getRenderGovernorDiagnostics(testGlobe).holds.includes('annotations'),
|
||||||
'a never-rendered mark must not hold continuous render',
|
'a never-rendered mark must not hold continuous render',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -1232,12 +1226,10 @@ function partialStateRendererHarness() {
|
|||||||
|
|
||||||
test('fresh path: a throw AFTER partial renderer state leaves nothing on the board', async (t) => {
|
test('fresh path: a throw AFTER partial renderer state leaves nothing on the board', async (t) => {
|
||||||
installAnimationFrameStubs(t);
|
installAnimationFrameStubs(t);
|
||||||
_resetRenderGovernorForTest();
|
|
||||||
t.after(() => _resetRenderGovernorForTest());
|
|
||||||
|
|
||||||
const { renderer, failAddOnce, live } = partialStateRendererHarness();
|
const { renderer, failAddOnce, live } = partialStateRendererHarness();
|
||||||
const engine = createAnnotationEngine({
|
const engine = createAnnotationEngine({
|
||||||
globe: {},
|
globe: testGlobe,
|
||||||
renderer,
|
renderer,
|
||||||
resolveTarget: async () => ({ ...FIXED_POINT }),
|
resolveTarget: async () => ({ ...FIXED_POINT }),
|
||||||
});
|
});
|
||||||
@@ -1254,7 +1246,7 @@ test('fresh path: a throw AFTER partial renderer state leaves nothing on the boa
|
|||||||
'the rollback must release what the renderer had already created',
|
'the rollback must release what the renderer had already created',
|
||||||
);
|
);
|
||||||
assert.ok(
|
assert.ok(
|
||||||
!getRenderGovernorDiagnostics().holds.includes('annotations'),
|
!getRenderGovernorDiagnostics(testGlobe).holds.includes('annotations'),
|
||||||
'a never-rendered mark must not hold continuous render',
|
'a never-rendered mark must not hold continuous render',
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1273,12 +1265,10 @@ test('fresh path: a throw AFTER partial renderer state leaves nothing on the boa
|
|||||||
|
|
||||||
test('duplicate-replacement: a throw AFTER partial renderer state leaves nothing on the board', async (t) => {
|
test('duplicate-replacement: a throw AFTER partial renderer state leaves nothing on the board', async (t) => {
|
||||||
installAnimationFrameStubs(t);
|
installAnimationFrameStubs(t);
|
||||||
_resetRenderGovernorForTest();
|
|
||||||
t.after(() => _resetRenderGovernorForTest());
|
|
||||||
|
|
||||||
const { renderer, failAddOnce, live } = partialStateRendererHarness();
|
const { renderer, failAddOnce, live } = partialStateRendererHarness();
|
||||||
const engine = createAnnotationEngine({
|
const engine = createAnnotationEngine({
|
||||||
globe: {},
|
globe: testGlobe,
|
||||||
renderer,
|
renderer,
|
||||||
resolveTarget: async () => ({ ...FIXED_POINT }),
|
resolveTarget: async () => ({ ...FIXED_POINT }),
|
||||||
});
|
});
|
||||||
@@ -1308,7 +1298,7 @@ test('duplicate-replacement: a throw AFTER partial renderer state leaves nothing
|
|||||||
);
|
);
|
||||||
assert.equal(live.size, 0, 'and no half-swapped mark survives on the board');
|
assert.equal(live.size, 0, 'and no half-swapped mark survives on the board');
|
||||||
assert.ok(
|
assert.ok(
|
||||||
!getRenderGovernorDiagnostics().holds.includes('annotations'),
|
!getRenderGovernorDiagnostics(testGlobe).holds.includes('annotations'),
|
||||||
'the annotations hold must be released',
|
'the annotations hold must be released',
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1335,7 +1325,7 @@ test('destroy releases the renderer and prevents subsequent annotation work', as
|
|||||||
renderer.destroy = () => {
|
renderer.destroy = () => {
|
||||||
destroyed++;
|
destroyed++;
|
||||||
};
|
};
|
||||||
const engine = createAnnotationEngine({ globe: {}, renderer });
|
const engine = createAnnotationEngine({ globe: testGlobe, renderer });
|
||||||
const drawn = await engine.annotate([
|
const drawn = await engine.annotate([
|
||||||
{ type: 'pin', longitude: -97.74, latitude: 30.27 },
|
{ type: 'pin', longitude: -97.74, latitude: 30.27 },
|
||||||
]);
|
]);
|
||||||
@@ -1350,7 +1340,7 @@ test('destroy releases the renderer and prevents subsequent annotation work', as
|
|||||||
assert.equal(engine.count(), 0);
|
assert.equal(engine.count(), 0);
|
||||||
engine.destroy();
|
engine.destroy();
|
||||||
assert.equal(destroyed, 1);
|
assert.equal(destroyed, 1);
|
||||||
assert.ok(!getRenderGovernorDiagnostics().holds.includes('annotations'));
|
assert.ok(!getRenderGovernorDiagnostics(testGlobe).holds.includes('annotations'));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('a late annotation resolver cannot redraw after destruction', async (t) => {
|
test('a late annotation resolver cannot redraw after destruction', async (t) => {
|
||||||
@@ -1360,7 +1350,7 @@ test('a late annotation resolver cannot redraw after destruction', async (t) =>
|
|||||||
let release;
|
let release;
|
||||||
let signal;
|
let signal;
|
||||||
const engine = createAnnotationEngine({
|
const engine = createAnnotationEngine({
|
||||||
globe: {},
|
globe: testGlobe,
|
||||||
renderer,
|
renderer,
|
||||||
resolveTarget: (options) => {
|
resolveTarget: (options) => {
|
||||||
signal = options.signal;
|
signal = options.signal;
|
||||||
@@ -1419,13 +1409,11 @@ test('caller cancellation prevents a late annotation and camera mutation', async
|
|||||||
|
|
||||||
test('only an explicit navigation request permits resolving distant annotation targets', async (t) => {
|
test('only an explicit navigation request permits resolving distant annotation targets', async (t) => {
|
||||||
installAnimationFrameStubs(t);
|
installAnimationFrameStubs(t);
|
||||||
_resetRenderGovernorForTest();
|
|
||||||
t.after(() => _resetRenderGovernorForTest());
|
|
||||||
const { renderer } = throwingRendererHarness();
|
const { renderer } = throwingRendererHarness();
|
||||||
renderer.destroy = () => {};
|
renderer.destroy = () => {};
|
||||||
const received = [];
|
const received = [];
|
||||||
const engine = createAnnotationEngine({
|
const engine = createAnnotationEngine({
|
||||||
globe: {},
|
globe: testGlobe,
|
||||||
renderer,
|
renderer,
|
||||||
resolveTarget: async (options) => {
|
resolveTarget: async (options) => {
|
||||||
received.push(options);
|
received.push(options);
|
||||||
@@ -1449,7 +1437,7 @@ test('recaption inherits outline credit from borrowed geometry rather than its n
|
|||||||
const { renderer } = fakeRenderer();
|
const { renderer } = fakeRenderer();
|
||||||
let first = true;
|
let first = true;
|
||||||
const ring = [[-99,31],[-98.99,31],[-98.99,31.01],[-99,31]];
|
const ring = [[-99,31],[-98.99,31],[-98.99,31.01],[-99,31]];
|
||||||
const engine = createAnnotationEngine({ globe: {}, renderer,
|
const engine = createAnnotationEngine({ globe: testGlobe, renderer,
|
||||||
resolveTarget: async () => {
|
resolveTarget: async () => {
|
||||||
if (first) { first = false; return { ...FIXED_POINT, ring, footprintKind: 'area', outlineCredit: 'tiles' }; }
|
if (first) { first = false; return { ...FIXED_POINT, ring, footprintKind: 'area', outlineCredit: 'tiles' }; }
|
||||||
return { ...FIXED_POINT, source: 'osm-local', resolveOutline: async () => null };
|
return { ...FIXED_POINT, source: 'osm-local', resolveOutline: async () => null };
|
||||||
@@ -1467,8 +1455,6 @@ test('recaption inherits outline credit from borrowed geometry rather than its n
|
|||||||
// frame and remove it, and hear about every change to the board.
|
// frame and remove it, and hear about every change to the board.
|
||||||
test('hand-drawn marks: one mark can be hidden, renamed, recoloured, framed and removed', async (t) => {
|
test('hand-drawn marks: one mark can be hidden, renamed, recoloured, framed and removed', async (t) => {
|
||||||
installAnimationFrameStubs(t);
|
installAnimationFrameStubs(t);
|
||||||
_resetRenderGovernorForTest();
|
|
||||||
t.after(() => _resetRenderGovernorForTest());
|
|
||||||
const { renderer, calls } = fakeRenderer();
|
const { renderer, calls } = fakeRenderer();
|
||||||
const flights = [];
|
const flights = [];
|
||||||
const engine = createAnnotationEngine({
|
const engine = createAnnotationEngine({
|
||||||
@@ -1529,11 +1515,9 @@ test('hand-drawn marks: remove, hide, clear, expiry and framing have their effec
|
|||||||
globalThis.cancelAnimationFrame = () => {};
|
globalThis.cancelAnimationFrame = () => {};
|
||||||
let now = 1_000_000;
|
let now = 1_000_000;
|
||||||
t.mock.method(performance, 'now', () => now);
|
t.mock.method(performance, 'now', () => now);
|
||||||
_resetRenderGovernorForTest();
|
|
||||||
t.after(() => {
|
t.after(() => {
|
||||||
globalThis.requestAnimationFrame = originalRequestAnimationFrame;
|
globalThis.requestAnimationFrame = originalRequestAnimationFrame;
|
||||||
globalThis.cancelAnimationFrame = originalCancelAnimationFrame;
|
globalThis.cancelAnimationFrame = originalCancelAnimationFrame;
|
||||||
_resetRenderGovernorForTest();
|
|
||||||
});
|
});
|
||||||
const runFrames = () => {
|
const runFrames = () => {
|
||||||
for (let i = 0; i < 5 && frames.length; i += 1) frames.shift()();
|
for (let i = 0; i < 5 && frames.length; i += 1) frames.shift()();
|
||||||
@@ -1636,10 +1620,8 @@ test('hand-drawn marks: remove, hide, clear, expiry and framing have their effec
|
|||||||
|
|
||||||
test('hand-drawn marks: a voice mark or a hidden mark is not taken as their duplicate', async (t) => {
|
test('hand-drawn marks: a voice mark or a hidden mark is not taken as their duplicate', async (t) => {
|
||||||
installAnimationFrameStubs(t);
|
installAnimationFrameStubs(t);
|
||||||
_resetRenderGovernorForTest();
|
|
||||||
t.after(() => _resetRenderGovernorForTest());
|
|
||||||
const { renderer } = fakeRenderer();
|
const { renderer } = fakeRenderer();
|
||||||
const engine = createAnnotationEngine({ globe: {}, renderer });
|
const engine = createAnnotationEngine({ globe: testGlobe, renderer });
|
||||||
await engine.annotate(
|
await engine.annotate(
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
@@ -1688,11 +1670,9 @@ test('hand-drawn marks: a voice mark or a hidden mark is not taken as their dupl
|
|||||||
|
|
||||||
test('hand-drawn marks: only a manual mark can be restyled', async (t) => {
|
test('hand-drawn marks: only a manual mark can be restyled', async (t) => {
|
||||||
installAnimationFrameStubs(t);
|
installAnimationFrameStubs(t);
|
||||||
_resetRenderGovernorForTest();
|
|
||||||
t.after(() => _resetRenderGovernorForTest());
|
|
||||||
const { renderer } = fakeRenderer();
|
const { renderer } = fakeRenderer();
|
||||||
const engine = createAnnotationEngine({
|
const engine = createAnnotationEngine({
|
||||||
globe: {},
|
globe: testGlobe,
|
||||||
renderer,
|
renderer,
|
||||||
resolveTarget: async () => ({ ...FIXED_POINT }),
|
resolveTarget: async () => ({ ...FIXED_POINT }),
|
||||||
});
|
});
|
||||||
@@ -1707,7 +1687,7 @@ test('a drawn line that rounds up to the next unit is labelled in it, not as 100
|
|||||||
installAnimationFrameStubs(t);
|
installAnimationFrameStubs(t);
|
||||||
const labels = [];
|
const labels = [];
|
||||||
const renderer = { add(anno) { labels.push(anno.label); }, update() {}, remove() {}, sync() {}, destroy() {} };
|
const renderer = { add(anno) { labels.push(anno.label); }, update() {}, remove() {}, sync() {}, destroy() {} };
|
||||||
const engine = createAnnotationEngine({ globe: {}, renderer });
|
const engine = createAnnotationEngine({ globe: testGlobe, renderer });
|
||||||
const metresPerDegree = (6371.0088 * 1000 * Math.PI) / 180;
|
const metresPerDegree = (6371.0088 * 1000 * Math.PI) / 180;
|
||||||
for (const metres of [994, 996, 9940, 9960]) {
|
for (const metres of [994, 996, 9940, 9960]) {
|
||||||
await engine.annotate([
|
await engine.annotate([
|
||||||
@@ -1720,10 +1700,8 @@ test('a drawn line that rounds up to the next unit is labelled in it, not as 100
|
|||||||
|
|
||||||
test('hand-drawn marks: two unnamed pins close together are two pins, whatever their colours', async (t) => {
|
test('hand-drawn marks: two unnamed pins close together are two pins, whatever their colours', async (t) => {
|
||||||
installAnimationFrameStubs(t);
|
installAnimationFrameStubs(t);
|
||||||
_resetRenderGovernorForTest();
|
|
||||||
t.after(() => _resetRenderGovernorForTest());
|
|
||||||
const { renderer } = fakeRenderer();
|
const { renderer } = fakeRenderer();
|
||||||
const engine = createAnnotationEngine({ globe: {}, renderer });
|
const engine = createAnnotationEngine({ globe: testGlobe, renderer });
|
||||||
const pin = (lon, color) => ({ type: 'pin', manual: true, latitude: 30.27, longitude: lon, label: '', color });
|
const pin = (lon, color) => ({ type: 'pin', manual: true, latitude: 30.27, longitude: lon, label: '', color });
|
||||||
await engine.annotate([pin(-97.74, 'primary')], { persist: true });
|
await engine.annotate([pin(-97.74, 'primary')], { persist: true });
|
||||||
await engine.annotate([pin(-97.7402, 'primary')], { persist: true });
|
await engine.annotate([pin(-97.7402, 'primary')], { persist: true });
|
||||||
@@ -1737,36 +1715,33 @@ test('hand-drawn marks: two unnamed pins close together are two pins, whatever t
|
|||||||
|
|
||||||
test('hand-drawn marks: hiding a mark before its first frame takes it off at once', async (t) => {
|
test('hand-drawn marks: hiding a mark before its first frame takes it off at once', async (t) => {
|
||||||
installAnimationFrameStubs(t);
|
installAnimationFrameStubs(t);
|
||||||
_resetRenderGovernorForTest();
|
|
||||||
t.after(() => _resetRenderGovernorForTest());
|
|
||||||
let syncs = 0;
|
let syncs = 0;
|
||||||
const renderer = { add() {}, update() {}, remove() {}, sync() { syncs += 1; }, destroy() {} };
|
const renderer = { add() {}, update() {}, remove() {}, sync() { syncs += 1; }, destroy() {} };
|
||||||
const engine = createAnnotationEngine({ globe: {}, renderer });
|
const engine = createAnnotationEngine({ globe: testGlobe, renderer });
|
||||||
await engine.annotate([{ type: 'pin', manual: true, latitude: 1, longitude: 1 }], { persist: true });
|
await engine.annotate([{ type: 'pin', manual: true, latitude: 1, longitude: 1 }], { persist: true });
|
||||||
const [mark] = engine.list();
|
const [mark] = engine.list();
|
||||||
const before = syncs;
|
const before = syncs;
|
||||||
engine.setHidden(mark.id, true);
|
engine.setHidden(mark.id, true);
|
||||||
assert.ok(syncs > before, 'the renderer hears about it now');
|
assert.ok(syncs > before, 'the renderer hears about it now');
|
||||||
assert.ok(
|
assert.ok(
|
||||||
!getRenderGovernorDiagnostics().holds.includes('annotations'),
|
!getRenderGovernorDiagnostics(testGlobe).holds.includes('annotations'),
|
||||||
'and the render hold goes with the last visible mark',
|
'and the render hold goes with the last visible mark',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('hand-drawn marks: with every mark hidden the scene idles, and showing one wakes it', async (t) => {
|
test('hand-drawn marks: with every mark hidden the scene idles, and showing one wakes it', async (t) => {
|
||||||
installAnimationFrameStubs(t);
|
installAnimationFrameStubs(t);
|
||||||
_resetRenderGovernorForTest();
|
|
||||||
t.after(() => _resetRenderGovernorForTest());
|
|
||||||
let renders = 0;
|
let renders = 0;
|
||||||
installRenderGovernor({ scene: { requestRenderMode: true, requestRender: () => (renders += 1) } });
|
testGlobe.render.request = () => (renders += 1);
|
||||||
|
renderGovernorFor(testGlobe).install();
|
||||||
const { renderer } = fakeRenderer();
|
const { renderer } = fakeRenderer();
|
||||||
const engine = createAnnotationEngine({ globe: {}, renderer });
|
const engine = createAnnotationEngine({ globe: testGlobe, renderer });
|
||||||
await engine.annotate([
|
await engine.annotate([
|
||||||
{ type: 'pin', manual: true, latitude: 1, longitude: 1 },
|
{ type: 'pin', manual: true, latitude: 1, longitude: 1 },
|
||||||
{ type: 'pin', manual: true, latitude: 2, longitude: 2 },
|
{ type: 'pin', manual: true, latitude: 2, longitude: 2 },
|
||||||
], { persist: true });
|
], { persist: true });
|
||||||
const [first, second] = engine.list();
|
const [first, second] = engine.list();
|
||||||
const held = () => getRenderGovernorDiagnostics().holds.includes('annotations');
|
const held = () => getRenderGovernorDiagnostics(testGlobe).holds.includes('annotations');
|
||||||
assert.ok(held(), 'visible marks keep the scene drawing');
|
assert.ok(held(), 'visible marks keep the scene drawing');
|
||||||
engine.setHidden(first.id, true);
|
engine.setHidden(first.id, true);
|
||||||
assert.ok(held(), 'one visible mark is enough');
|
assert.ok(held(), 'one visible mark is enough');
|
||||||
@@ -1780,12 +1755,10 @@ test('hand-drawn marks: with every mark hidden the scene idles, and showing one
|
|||||||
|
|
||||||
test('hand-drawn marks: a restyle the renderer cannot draw takes the mark off and says so', async (t) => {
|
test('hand-drawn marks: a restyle the renderer cannot draw takes the mark off and says so', async (t) => {
|
||||||
installAnimationFrameStubs(t);
|
installAnimationFrameStubs(t);
|
||||||
_resetRenderGovernorForTest();
|
|
||||||
t.after(() => _resetRenderGovernorForTest());
|
|
||||||
const { renderer, failAddOnce } = throwingRendererHarness();
|
const { renderer, failAddOnce } = throwingRendererHarness();
|
||||||
let syncs = 0;
|
let syncs = 0;
|
||||||
renderer.sync = () => (syncs += 1);
|
renderer.sync = () => (syncs += 1);
|
||||||
const engine = createAnnotationEngine({ globe: {}, renderer });
|
const engine = createAnnotationEngine({ globe: testGlobe, renderer });
|
||||||
await engine.annotate([{ type: 'pin', manual: true, latitude: 1, longitude: 1 }], { persist: true });
|
await engine.annotate([{ type: 'pin', manual: true, latitude: 1, longitude: 1 }], { persist: true });
|
||||||
const [mark] = engine.list();
|
const [mark] = engine.list();
|
||||||
let changes = 0;
|
let changes = 0;
|
||||||
@@ -1799,8 +1772,6 @@ test('hand-drawn marks: a restyle the renderer cannot draw takes the mark off an
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('hand-drawn marks: a mark shown again right after it was drawn appears at once, not fading in', async (t) => {
|
test('hand-drawn marks: a mark shown again right after it was drawn appears at once, not fading in', async (t) => {
|
||||||
_resetRenderGovernorForTest();
|
|
||||||
t.after(() => _resetRenderGovernorForTest());
|
|
||||||
const queued = [];
|
const queued = [];
|
||||||
const raf = globalThis.requestAnimationFrame;
|
const raf = globalThis.requestAnimationFrame;
|
||||||
const caf = globalThis.cancelAnimationFrame;
|
const caf = globalThis.cancelAnimationFrame;
|
||||||
@@ -1812,7 +1783,7 @@ test('hand-drawn marks: a mark shown again right after it was drawn appears at o
|
|||||||
});
|
});
|
||||||
const tick = () => queued.splice(0).forEach((callback) => callback());
|
const tick = () => queued.splice(0).forEach((callback) => callback());
|
||||||
const { renderer } = fakeRenderer();
|
const { renderer } = fakeRenderer();
|
||||||
const engine = createAnnotationEngine({ globe: {}, renderer });
|
const engine = createAnnotationEngine({ globe: testGlobe, renderer });
|
||||||
await engine.annotate([{ type: 'pin', manual: true, latitude: 1, longitude: 1 }], { persist: true });
|
await engine.annotate([{ type: 'pin', manual: true, latitude: 1, longitude: 1 }], { persist: true });
|
||||||
const [mark] = engine.list();
|
const [mark] = engine.list();
|
||||||
engine.setHidden(mark.id, true);
|
engine.setHidden(mark.id, true);
|
||||||
@@ -1825,8 +1796,6 @@ test('hand-drawn marks: a mark shown again right after it was drawn appears at o
|
|||||||
|
|
||||||
test('removing one mark whose cleanup fails keeps it addressable for a retry', async (t) => {
|
test('removing one mark whose cleanup fails keeps it addressable for a retry', async (t) => {
|
||||||
installAnimationFrameStubs(t);
|
installAnimationFrameStubs(t);
|
||||||
_resetRenderGovernorForTest();
|
|
||||||
t.after(() => _resetRenderGovernorForTest());
|
|
||||||
const { renderer, calls } = fakeRenderer();
|
const { renderer, calls } = fakeRenderer();
|
||||||
let failRemove = true;
|
let failRemove = true;
|
||||||
renderer.remove = () => {
|
renderer.remove = () => {
|
||||||
@@ -1851,7 +1820,7 @@ test('a building install in the batch does not re-judge marks that never install
|
|||||||
installAnimationFrameStubs(t);
|
installAnimationFrameStubs(t);
|
||||||
const h = controlledBuildingRenderer();
|
const h = controlledBuildingRenderer();
|
||||||
const engine = createAnnotationEngine({
|
const engine = createAnnotationEngine({
|
||||||
globe: {},
|
globe: testGlobe,
|
||||||
renderer: h.renderer,
|
renderer: h.renderer,
|
||||||
resolveTarget: async ({ target }) =>
|
resolveTarget: async ({ target }) =>
|
||||||
target === 'Tower' ? { ...BUILDING_FP, lon: -97.7 } : { ...FIXED_POINT },
|
target === 'Tower' ? { ...BUILDING_FP, lon: -97.7 } : { ...FIXED_POINT },
|
||||||
@@ -1874,7 +1843,7 @@ test('plain recaption keeps the old mark addressable when its removal throws', a
|
|||||||
installAnimationFrameStubs(t);
|
installAnimationFrameStubs(t);
|
||||||
const { renderer } = fakeRenderer();
|
const { renderer } = fakeRenderer();
|
||||||
const engine = createAnnotationEngine({
|
const engine = createAnnotationEngine({
|
||||||
globe: {}, renderer, resolveTarget: async () => ({ ...FIXED_POINT }),
|
globe: testGlobe, renderer, resolveTarget: async () => ({ ...FIXED_POINT }),
|
||||||
});
|
});
|
||||||
await engine.annotate([{ type: 'point', target: 'Capitol', label: 'Capitol' }]);
|
await engine.annotate([{ type: 'point', target: 'Capitol', label: 'Capitol' }]);
|
||||||
const old = engine.list()[0];
|
const old = engine.list()[0];
|
||||||
@@ -1899,3 +1868,7 @@ test('plain recaption keeps the old mark addressable when its removal throws', a
|
|||||||
);
|
);
|
||||||
assert.equal(engine.count(), 0);
|
assert.equal(engine.count(), 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let testGlobe;
|
||||||
|
test.beforeEach(() => { testGlobe = createFakeEngine(); });
|
||||||
|
test.afterEach(() => { renderGovernorFor(testGlobe).destroy(); testGlobe.destroy(); });
|
||||||
@@ -6,7 +6,6 @@ import assert from 'node:assert/strict';
|
|||||||
import { createApplicationTools } from './tools.js';
|
import { createApplicationTools } from './tools.js';
|
||||||
import { createSceneHost } from './sceneHost.js';
|
import { createSceneHost } from './sceneHost.js';
|
||||||
import { createFakeEngine } from '../globe/testing/fakeEngine.js';
|
import { createFakeEngine } from '../globe/testing/fakeEngine.js';
|
||||||
import { _resetRenderGovernorForTest } from '../renderGovernor.js';
|
|
||||||
|
|
||||||
test('the debug handle leaves window between engines and comes back the same', async (t) => {
|
test('the debug handle leaves window between engines and comes back the same', async (t) => {
|
||||||
const saved = {
|
const saved = {
|
||||||
@@ -32,7 +31,6 @@ test('the debug handle leaves window between engines and comes back the same', a
|
|||||||
removeEventListener() {},
|
removeEventListener() {},
|
||||||
};
|
};
|
||||||
t.after(() => {
|
t.after(() => {
|
||||||
_resetRenderGovernorForTest();
|
|
||||||
for (const [name, value] of Object.entries(saved)) {
|
for (const [name, value] of Object.entries(saved)) {
|
||||||
if (value === undefined) delete globalThis[name];
|
if (value === undefined) delete globalThis[name];
|
||||||
else globalThis[name] = value;
|
else globalThis[name] = value;
|
||||||
|
|||||||
@@ -45,7 +45,9 @@ test('the scene engine defaults to the Cesium engine factory', () => {
|
|||||||
|
|
||||||
test('the scene constructs the engine with the viewer and returns it beside the viewer', () => {
|
test('the scene constructs the engine with the viewer and returns it beside the viewer', () => {
|
||||||
const scene = read('src/app/scene.js');
|
const scene = read('src/app/scene.js');
|
||||||
const viewerTeardown = scene.indexOf('uninstallRenderGovernor(viewer);');
|
const viewerTeardown = scene.indexOf(
|
||||||
|
'if (!viewer.isDestroyed()) viewer.destroy();',
|
||||||
|
);
|
||||||
const engineAt = scene.indexOf('createSceneEngine(viewer, {');
|
const engineAt = scene.indexOf('createSceneEngine(viewer, {');
|
||||||
assert.ok(viewerTeardown >= 0 && engineAt >= 0);
|
assert.ok(viewerTeardown >= 0 && engineAt >= 0);
|
||||||
// Cleanups run last-in first-out: the engine goes before the viewer.
|
// Cleanups run last-in first-out: the engine goes before the viewer.
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export class LayerPresentation {
|
|||||||
manager,
|
manager,
|
||||||
{
|
{
|
||||||
weatherClock,
|
weatherClock,
|
||||||
requestRender = governorRequestRender,
|
requestRender = (reason) => governorRequestRender(reason, manager.engine),
|
||||||
invalidateDetection = markDetectionSourcesChanged,
|
invalidateDetection = markDetectionSourcesChanged,
|
||||||
} = {},
|
} = {},
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { showOsmCredit, hideOsmCredit } from '../data/dataCredits.js';
|
import { showOsmCredit, hideOsmCredit } from '../data/dataCredits.js';
|
||||||
import { governorRequestRender } from '../renderGovernor.js';
|
|
||||||
import {
|
import {
|
||||||
clearSelectedEntityContextForLayer,
|
clearSelectedEntityContextForLayer,
|
||||||
registerEntityContext,
|
registerEntityContext,
|
||||||
@@ -24,7 +23,6 @@ export const localGeoJsonServices = Object.freeze({
|
|||||||
removeEntityContextsForLayer,
|
removeEntityContextsForLayer,
|
||||||
selectEntityContext,
|
selectEntityContext,
|
||||||
getSelectedEntityContext,
|
getSelectedEntityContext,
|
||||||
governorRequestRender,
|
|
||||||
showOsmCredit,
|
showOsmCredit,
|
||||||
hideOsmCredit,
|
hideOsmCredit,
|
||||||
});
|
});
|
||||||
@@ -1,13 +1,8 @@
|
|||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { MapStackController } from '../mapStackController.js';
|
import { MapStackController } from '../mapStackController.js';
|
||||||
import {
|
import { createFakeEngine } from '../globe/testing/fakeEngine.js';
|
||||||
installRenderGovernor,
|
import { createSceneServices } from '../data/sceneServices.js';
|
||||||
uninstallRenderGovernor,
|
|
||||||
holdContinuousRender,
|
|
||||||
governorRequestRender,
|
|
||||||
getRenderGovernorDiagnostics,
|
|
||||||
} from '../renderGovernor.js';
|
|
||||||
|
|
||||||
test('destroy invalidates a pending imagery provider before it can touch the viewer', async () => {
|
test('destroy invalidates a pending imagery provider before it can touch the viewer', async () => {
|
||||||
let resolveProvider;
|
let resolveProvider;
|
||||||
@@ -35,23 +30,18 @@ test('destroy invalidates a pending imagery provider before it can touch the vie
|
|||||||
assert.equal(removed, 1);
|
assert.equal(removed, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('uninstall only releases the current viewer and makes later render requests inactive', () => {
|
test('scene teardown makes its later render requests inactive', () => {
|
||||||
let renders = 0;
|
const engine = createFakeEngine();
|
||||||
const viewer = {
|
const services = createSceneServices(engine);
|
||||||
scene: {
|
const governor = services.render;
|
||||||
requestRender: () => {
|
governor.install();
|
||||||
renders++;
|
governor.holdContinuousRender('fixture');
|
||||||
},
|
assert.equal(governor.getRenderGovernorDiagnostics().installed, true);
|
||||||
},
|
services.destroy();
|
||||||
};
|
engine.fake.resetCalls();
|
||||||
installRenderGovernor(viewer);
|
governor.governorRequestRender('late');
|
||||||
holdContinuousRender('fixture');
|
assert.equal(engine.fake.callsOf('render.request').length, 0);
|
||||||
uninstallRenderGovernor({});
|
assert.equal(governor.getRenderGovernorDiagnostics().installed, false);
|
||||||
assert.equal(getRenderGovernorDiagnostics().installed, true);
|
assert.deepEqual(governor.getRenderGovernorDiagnostics().holds, []);
|
||||||
uninstallRenderGovernor(viewer);
|
engine.destroy();
|
||||||
const before = renders;
|
|
||||||
governorRequestRender('late');
|
|
||||||
assert.equal(renders, before);
|
|
||||||
assert.equal(getRenderGovernorDiagnostics().installed, false);
|
|
||||||
assert.deepEqual(getRenderGovernorDiagnostics().holds, []);
|
|
||||||
});
|
});
|
||||||
+1
-12
@@ -7,11 +7,6 @@ import { t } from '../i18n/index.js';
|
|||||||
|
|
||||||
import { DATA_CREDITS, registerDataCredits } from '../data/dataCredits.js';
|
import { DATA_CREDITS, registerDataCredits } from '../data/dataCredits.js';
|
||||||
import { createLayerAvailability } from './layers/renderers.js';
|
import { createLayerAvailability } from './layers/renderers.js';
|
||||||
import {
|
|
||||||
governorRequestRender,
|
|
||||||
installEngineRenderGovernor,
|
|
||||||
uninstallRenderGovernor,
|
|
||||||
} from '../renderGovernor.js';
|
|
||||||
import { createNavaraAnnotationRenderer } from '../annotations/navaraAnnotationRenderer.js';
|
import { createNavaraAnnotationRenderer } from '../annotations/navaraAnnotationRenderer.js';
|
||||||
|
|
||||||
/** The Navara globe module, loaded on demand. */
|
/** The Navara globe module, loaded on demand. */
|
||||||
@@ -93,12 +88,10 @@ export async function createNavaraScene({
|
|||||||
});
|
});
|
||||||
defer(() => globe.destroy());
|
defer(() => globe.destroy());
|
||||||
const { engine } = globe;
|
const { engine } = globe;
|
||||||
// The tools install the governor on this engine; it goes before the engine.
|
|
||||||
defer(() => uninstallRenderGovernor(engine));
|
|
||||||
signal.throwIfAborted();
|
signal.throwIfAborted();
|
||||||
loaderStatus.textContent = t('Initializing systems...');
|
loaderStatus.textContent = t('Initializing systems...');
|
||||||
const mapStackController = new MapController(null, {
|
const mapStackController = new MapController(null, {
|
||||||
requestRender: governorRequestRender,
|
requestRender: (reason) => engine.render.request(reason),
|
||||||
engine,
|
engine,
|
||||||
...mapOptions,
|
...mapOptions,
|
||||||
cesiumToken,
|
cesiumToken,
|
||||||
@@ -125,7 +118,6 @@ export async function createNavaraScene({
|
|||||||
engine,
|
engine,
|
||||||
tileset: null,
|
tileset: null,
|
||||||
suspend() {
|
suspend() {
|
||||||
uninstallRenderGovernor(engine);
|
|
||||||
engine.camera.cancelFlight();
|
engine.camera.cancelFlight();
|
||||||
engine.render.setContinuous(false);
|
engine.render.setContinuous(false);
|
||||||
globe.setRenderLoop(false);
|
globe.setRenderLoop(false);
|
||||||
@@ -156,9 +148,6 @@ export function navaraToolBackend({ engine }) {
|
|||||||
// The session's tool on the engine's input and preview.
|
// The session's tool on the engine's input and preview.
|
||||||
draw: () => ({ engine }),
|
draw: () => ({ engine }),
|
||||||
recentImagery: () => ({ engine }),
|
recentImagery: () => ({ engine }),
|
||||||
// Holds drive engine.render.setContinuous; requests go to the engine. A
|
|
||||||
// parked engine uninstalls it (createNavaraScene suspend).
|
|
||||||
installGovernor: () => installEngineRenderGovernor(engine),
|
|
||||||
attach({ data, styleManager }, { signal, defer }) {
|
attach({ data, styleManager }, { signal, defer }) {
|
||||||
void styleManager.initialRestorePromise?.then(() => {
|
void styleManager.initialRestorePromise?.then(() => {
|
||||||
if (!signal.aborted) views.alignDaylightToRestoredView();
|
if (!signal.aborted) views.alignDaylightToRestoredView();
|
||||||
|
|||||||
@@ -3,10 +3,7 @@ import assert from 'node:assert/strict';
|
|||||||
import { createApplicationScene } from './scene.js';
|
import { createApplicationScene } from './scene.js';
|
||||||
import { sceneServicesFor } from '../data/sceneServices.js';
|
import { sceneServicesFor } from '../data/sceneServices.js';
|
||||||
import { createFakeEngine } from '../globe/testing/fakeEngine.js';
|
import { createFakeEngine } from '../globe/testing/fakeEngine.js';
|
||||||
import {
|
import { renderGovernorFor } from '../renderGovernor.js';
|
||||||
installEngineRenderGovernor,
|
|
||||||
isRenderGovernorInstalled,
|
|
||||||
} from '../renderGovernor.js';
|
|
||||||
|
|
||||||
// The scene's Navara branch in Node: the globe loader is a seam, so the fake
|
// The scene's Navara branch in Node: the globe loader is a seam, so the fake
|
||||||
// engine stands in for Navara and nothing Cesium is built.
|
// engine stands in for Navara and nothing Cesium is built.
|
||||||
@@ -165,12 +162,15 @@ test('the Navara scene tears down the controller, then the globe', async (t) =>
|
|||||||
const run = await startScene();
|
const run = await startScene();
|
||||||
// The tools install the governor on the engine (src/app/tools.js); the
|
// The tools install the governor on the engine (src/app/tools.js); the
|
||||||
// scene removes it before the engine goes.
|
// scene removes it before the engine goes.
|
||||||
installEngineRenderGovernor(run.engine);
|
renderGovernorFor(run.engine).install();
|
||||||
const view = globalThis.window;
|
const view = globalThis.window;
|
||||||
await run.teardown();
|
await run.teardown();
|
||||||
assert.equal(run.globeDestroyed(), 1);
|
assert.equal(run.globeDestroyed(), 1);
|
||||||
assert.equal(run.engine.isDestroyed(), true);
|
assert.equal(run.engine.isDestroyed(), true);
|
||||||
assert.equal(isRenderGovernorInstalled(run.engine), false);
|
assert.equal(
|
||||||
|
renderGovernorFor(run.engine).getRenderGovernorDiagnostics().installed,
|
||||||
|
false,
|
||||||
|
);
|
||||||
assert.equal(view.__GOOGLE_MAPS_API_KEY__, undefined);
|
assert.equal(view.__GOOGLE_MAPS_API_KEY__, undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -262,16 +262,21 @@ test('parking or tearing down the Navara scene releases a running camera verb',
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('the application scene owns and releases its pick services', async () => {
|
test('the application scene owns and releases its pick and render services', async () => {
|
||||||
const run = await startScene({ googleApiKey: '' });
|
const run = await startScene({ googleApiKey: '' });
|
||||||
const { engine, services } = run.scene;
|
const { engine, services } = run.scene;
|
||||||
try {
|
try {
|
||||||
assert.equal(sceneServicesFor(engine), services);
|
assert.equal(sceneServicesFor(engine), services);
|
||||||
|
services.render.install();
|
||||||
|
services.render.holdContinuousRender('flights');
|
||||||
|
assert.equal(engine.render.isContinuous(), true);
|
||||||
services.selection.register('flights', { owns: (id) => id === 'contact' });
|
services.selection.register('flights', { owns: (id) => id === 'contact' });
|
||||||
assert.equal(services.picking.findPickOwner('contact'), 'flights');
|
assert.equal(services.picking.findPickOwner('contact'), 'flights');
|
||||||
} finally {
|
} finally {
|
||||||
await run.teardown();
|
await run.teardown();
|
||||||
}
|
}
|
||||||
|
assert.equal(services.render.getRenderGovernorDiagnostics().installed, false);
|
||||||
|
assert.deepEqual(services.render.getRenderGovernorDiagnostics().holds, []);
|
||||||
assert.equal(services.picking.findPickOwner('contact'), null);
|
assert.equal(services.picking.findPickOwner('contact'), null);
|
||||||
services.picking.registerPickOwner('late', () => true);
|
services.picking.registerPickOwner('late', () => true);
|
||||||
assert.equal(services.picking.findPickOwner('contact'), null);
|
assert.equal(services.picking.findPickOwner('contact'), null);
|
||||||
|
|||||||
+1
-7
@@ -11,10 +11,6 @@ import { registerDataCredits } from '../data/dataCredits.js';
|
|||||||
import { configureCreditKeyboardAccess } from '../creditKeyboard.js';
|
import { configureCreditKeyboardAccess } from '../creditKeyboard.js';
|
||||||
import { MapStackController } from '../mapStackController.js';
|
import { MapStackController } from '../mapStackController.js';
|
||||||
import { initLogoGaze } from '../logoGaze.js';
|
import { initLogoGaze } from '../logoGaze.js';
|
||||||
import {
|
|
||||||
uninstallRenderGovernor,
|
|
||||||
governorRequestRender,
|
|
||||||
} from '../renderGovernor.js';
|
|
||||||
import { describeError } from './errors.js';
|
import { describeError } from './errors.js';
|
||||||
import { createSceneEngine } from './globeEngine.js';
|
import { createSceneEngine } from './globeEngine.js';
|
||||||
import { onLocaleChange, t } from '../i18n/index.js';
|
import { onLocaleChange, t } from '../i18n/index.js';
|
||||||
@@ -157,7 +153,6 @@ async function createRenderer({
|
|||||||
creditContainer,
|
creditContainer,
|
||||||
});
|
});
|
||||||
defer(() => {
|
defer(() => {
|
||||||
uninstallRenderGovernor(viewer);
|
|
||||||
if (!viewer.isDestroyed()) viewer.destroy();
|
if (!viewer.isDestroyed()) viewer.destroy();
|
||||||
});
|
});
|
||||||
// The globe engine over this viewer; Google's startup tileset is named
|
// The globe engine over this viewer; Google's startup tileset is named
|
||||||
@@ -216,7 +211,7 @@ async function createRenderer({
|
|||||||
loaderStatus.textContent = t('Initializing systems...');
|
loaderStatus.textContent = t('Initializing systems...');
|
||||||
|
|
||||||
const mapStackController = new MapController(viewer, {
|
const mapStackController = new MapController(viewer, {
|
||||||
requestRender: governorRequestRender,
|
requestRender: (reason) => engine.render.request(reason),
|
||||||
// Map sources reach the scene through the globe engine's maps.
|
// Map sources reach the scene through the globe engine's maps.
|
||||||
engine,
|
engine,
|
||||||
...mapOptions,
|
...mapOptions,
|
||||||
@@ -247,7 +242,6 @@ async function createRenderer({
|
|||||||
tileset,
|
tileset,
|
||||||
mapStackController,
|
mapStackController,
|
||||||
suspend() {
|
suspend() {
|
||||||
uninstallRenderGovernor(viewer);
|
|
||||||
engine.camera.cancelFlight();
|
engine.camera.cancelFlight();
|
||||||
releaseCameraVerbs(engine);
|
releaseCameraVerbs(engine);
|
||||||
viewer.useDefaultRenderLoop = false;
|
viewer.useDefaultRenderLoop = false;
|
||||||
|
|||||||
+14
-16
@@ -22,13 +22,7 @@ import {
|
|||||||
import { KEYHOLE_OUTER_RADIUS } from '../celestialRing.js';
|
import { KEYHOLE_OUTER_RADIUS } from '../celestialRing.js';
|
||||||
import { installGlobeInViewRecovery } from '../globe/cesium/viewer.js';
|
import { installGlobeInViewRecovery } from '../globe/cesium/viewer.js';
|
||||||
import { installLayerReleaseTrim } from '../sceneCommandRelease.js';
|
import { installLayerReleaseTrim } from '../sceneCommandRelease.js';
|
||||||
import {
|
import { renderGovernorFor } from '../renderGovernor.js';
|
||||||
installRenderGovernor,
|
|
||||||
getRenderGovernorDiagnostics,
|
|
||||||
governorRequestRender,
|
|
||||||
holdContinuousRender,
|
|
||||||
releaseContinuousRender,
|
|
||||||
} from '../renderGovernor.js';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attach scene tools, rendering listeners and the application debug handle.
|
* Attach scene tools, rendering listeners and the application debug handle.
|
||||||
@@ -148,9 +142,11 @@ export async function createApplicationTools({
|
|||||||
weatherEffects: controls.weatherEffects ?? null,
|
weatherEffects: controls.weatherEffects ?? null,
|
||||||
cockpitCloudEffects: null,
|
cockpitCloudEffects: null,
|
||||||
voiceCommands: null,
|
voiceCommands: null,
|
||||||
getRenderGovernorDiagnostics,
|
getRenderGovernorDiagnostics: () =>
|
||||||
|
renderGovernorFor(debug.engine)?.getRenderGovernorDiagnostics(),
|
||||||
surfaceServices: operations.surface,
|
surfaceServices: operations.surface,
|
||||||
requestRender: governorRequestRender,
|
requestRender: (reason) =>
|
||||||
|
renderGovernorFor(debug.engine)?.governorRequestRender(reason),
|
||||||
};
|
};
|
||||||
defer(() => {
|
defer(() => {
|
||||||
if (window.__godsEyeView === debug) delete window.__godsEyeView;
|
if (window.__godsEyeView === debug) delete window.__godsEyeView;
|
||||||
@@ -266,6 +262,11 @@ const TOOL_BACKENDS = Object.freeze({
|
|||||||
/** The Cesium tool backend: the shared tools and its own on the viewer. */
|
/** The Cesium tool backend: the shared tools and its own on the viewer. */
|
||||||
function cesiumToolBackend({ engine, tileset }) {
|
function cesiumToolBackend({ engine, tileset }) {
|
||||||
const viewer = cesiumViewerOf(engine);
|
const viewer = cesiumViewerOf(engine);
|
||||||
|
const {
|
||||||
|
governorRequestRender,
|
||||||
|
holdContinuousRender,
|
||||||
|
releaseContinuousRender,
|
||||||
|
} = renderGovernorFor(engine);
|
||||||
return {
|
return {
|
||||||
viewer,
|
viewer,
|
||||||
startupChrome: {},
|
startupChrome: {},
|
||||||
@@ -276,10 +277,6 @@ function cesiumToolBackend({ engine, tileset }) {
|
|||||||
layer.attachTileset(tileset);
|
layer.attachTileset(tileset);
|
||||||
return { viewer };
|
return { viewer };
|
||||||
},
|
},
|
||||||
// Idle render governor: flips the scene into requestRenderMode whenever
|
|
||||||
// nothing animates per frame. (perf wave 2) A parked viewer uninstalls
|
|
||||||
// it (src/app/scene.js suspend).
|
|
||||||
installGovernor: () => installRenderGovernor(viewer),
|
|
||||||
attach({ dataManager }, { defer }) {
|
attach({ dataManager }, { defer }) {
|
||||||
// Cesium keeps a turned-off layer's draw commands (and so all its
|
// Cesium keeps a turned-off layer's draw commands (and so all its
|
||||||
// entities) alive in reused per-pass arrays; trim them after each
|
// entities) alive in reused per-pass arrays; trim them after each
|
||||||
@@ -336,7 +333,7 @@ function cesiumToolBackend({ engine, tileset }) {
|
|||||||
* engine kind's backend (TOOL_BACKENDS) says where the shared tools draw:
|
* engine kind's backend (TOOL_BACKENDS) says where the shared tools draw:
|
||||||
* `board()` and `draw()` give their attachScene arguments (null: not on
|
* `board()` and `draw()` give their attachScene arguments (null: not on
|
||||||
* this engine), `recentImagery(layer)` the panel's globe,
|
* this engine), `recentImagery(layer)` the panel's globe,
|
||||||
* `installGovernor()` the governor's; `attach(context, attachment)` adds
|
* `attach(context, attachment)` adds
|
||||||
* the engine's own tools, `suspend(hidden, controls)` its hidden-tab
|
* the engine's own tools, `suspend(hidden, controls)` its hidden-tab
|
||||||
* handling and `attachViews(context, attachment)` its embedded views. The
|
* handling and `attachViews(context, attachment)` its embedded views. The
|
||||||
* shared tools keep their session state across engines. Returns the
|
* shared tools keep their session state across engines. Returns the
|
||||||
@@ -358,6 +355,7 @@ async function attachEngineTools(
|
|||||||
{ signal, defer },
|
{ signal, defer },
|
||||||
) {
|
) {
|
||||||
const { engine, mapStackController } = scene;
|
const { engine, mapStackController } = scene;
|
||||||
|
const governor = scene.services?.render ?? renderGovernorFor(engine);
|
||||||
const backend = (await TOOL_BACKENDS[engine.kind]())(scene);
|
const backend = (await TOOL_BACKENDS[engine.kind]())(scene);
|
||||||
// The page session's board, Draw and Import (src/app/sessionTools.js):
|
// The page session's board, Draw and Import (src/app/sessionTools.js):
|
||||||
// an engine switch only takes their drawing off this globe.
|
// an engine switch only takes their drawing off this globe.
|
||||||
@@ -412,7 +410,7 @@ async function attachEngineTools(
|
|||||||
}
|
}
|
||||||
// Installed AFTER every module above has had its chance to register
|
// Installed AFTER every module above has had its chance to register
|
||||||
// pre-install holds.
|
// pre-install holds.
|
||||||
backend.installGovernor();
|
governor.install();
|
||||||
backend.attach?.({ data, styleManager, dataManager }, { signal, defer });
|
backend.attach?.({ data, styleManager, dataManager }, { signal, defer });
|
||||||
|
|
||||||
// Install the explicit scope mask used by the DISPLAY controls: a canvas
|
// Install the explicit scope mask used by the DISPLAY controls: a canvas
|
||||||
@@ -430,7 +428,7 @@ async function attachEngineTools(
|
|||||||
backend.suspend?.(hidden, controls);
|
backend.suspend?.(hidden, controls);
|
||||||
if (!hidden) {
|
if (!hidden) {
|
||||||
data.presentation.flushVisible();
|
data.presentation.flushVisible();
|
||||||
governorRequestRender('visibility-restore');
|
governor.governorRequestRender('visibility-restore');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
document.addEventListener('visibilitychange', syncVisibilitySuspension);
|
document.addEventListener('visibilitychange', syncVisibilitySuspension);
|
||||||
|
|||||||
@@ -5,9 +5,8 @@ import { navaraModeNotice } from './navaraMode.js';
|
|||||||
import { LAYER_MANIFEST, createLayerAvailability } from './layers/renderers.js';
|
import { LAYER_MANIFEST, createLayerAvailability } from './layers/renderers.js';
|
||||||
import { createFakeEngine } from '../globe/testing/fakeEngine.js';
|
import { createFakeEngine } from '../globe/testing/fakeEngine.js';
|
||||||
import {
|
import {
|
||||||
_resetRenderGovernorForTest,
|
renderGovernorFor,
|
||||||
holdContinuousRender,
|
holdContinuousRender,
|
||||||
isRenderGovernorInstalled,
|
|
||||||
releaseContinuousRender,
|
releaseContinuousRender,
|
||||||
} from '../renderGovernor.js';
|
} from '../renderGovernor.js';
|
||||||
|
|
||||||
@@ -47,7 +46,6 @@ test('without a viewer the tools run the governor on the engine, show the notice
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
t.after(() => {
|
t.after(() => {
|
||||||
_resetRenderGovernorForTest();
|
|
||||||
for (const [name, value] of Object.entries(saved)) {
|
for (const [name, value] of Object.entries(saved)) {
|
||||||
if (value === undefined) delete globalThis[name];
|
if (value === undefined) delete globalThis[name];
|
||||||
else globalThis[name] = value;
|
else globalThis[name] = value;
|
||||||
@@ -146,10 +144,13 @@ test('without a viewer the tools run the governor on the engine, show the notice
|
|||||||
'Data layers: Live Flights, Satellites',
|
'Data layers: Live Flights, Satellites',
|
||||||
);
|
);
|
||||||
// The governor drives the engine's continuous rendering.
|
// The governor drives the engine's continuous rendering.
|
||||||
assert.equal(isRenderGovernorInstalled(engine), true);
|
assert.equal(
|
||||||
holdContinuousRender('qa');
|
renderGovernorFor(engine).getRenderGovernorDiagnostics().installed,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
holdContinuousRender('qa', engine);
|
||||||
assert.equal(engine.render.isContinuous(), true);
|
assert.equal(engine.render.isContinuous(), true);
|
||||||
releaseContinuousRender('qa');
|
releaseContinuousRender('qa', engine);
|
||||||
assert.equal(engine.render.isContinuous(), false);
|
assert.equal(engine.render.isContinuous(), false);
|
||||||
const debug = window.__godsEyeView;
|
const debug = window.__godsEyeView;
|
||||||
assert.equal(debug.viewer, null);
|
assert.equal(debug.viewer, null);
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ const clone = (value) => Cesium.Cartesian3.clone(value);
|
|||||||
const longitude = (camera) =>
|
const longitude = (camera) =>
|
||||||
Cesium.Cartographic.fromCartesian(camera.positionWC).longitude;
|
Cesium.Cartographic.fromCartesian(camera.positionWC).longitude;
|
||||||
const wrappedAngle = Cesium.Math.negativePiToPi;
|
const wrappedAngle = Cesium.Math.negativePiToPi;
|
||||||
const hasRenderHold = () =>
|
const hasRenderHold = (engine) =>
|
||||||
getRenderGovernorDiagnostics().holds.includes('camera-verb');
|
getRenderGovernorDiagnostics(engine).holds.includes('camera-verb');
|
||||||
// The frame measure judges a pose; these tests read the Cesium camera's.
|
// The frame measure judges a pose; these tests read the Cesium camera's.
|
||||||
const globeFrameShare = (camera, options) =>
|
const globeFrameShare = (camera, options) =>
|
||||||
poseFrameShare(cesiumFramePose(camera), options);
|
poseFrameShare(cesiumFramePose(camera), options);
|
||||||
@@ -160,7 +160,7 @@ function harness(
|
|||||||
}
|
}
|
||||||
|
|
||||||
test('globe rotation yields to tracking acquired and cleared before the next tick', (t) => {
|
test('globe rotation yields to tracking acquired and cleared before the next tick', (t) => {
|
||||||
const { viewer, camera, tick } = harness(t);
|
const { viewer, camera, tick, engine } = harness(t);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
moveCamera({ motion: 'rotate_globe', mode: 'continuous' }).ok,
|
moveCamera({ motion: 'rotate_globe', mode: 'continuous' }).ok,
|
||||||
true,
|
true,
|
||||||
@@ -176,7 +176,7 @@ test('globe rotation yields to tracking acquired and cleared before the next tic
|
|||||||
viewer.trackedEntity = undefined;
|
viewer.trackedEntity = undefined;
|
||||||
tick();
|
tick();
|
||||||
samePose(camera, before);
|
samePose(camera, before);
|
||||||
assert.equal(hasRenderHold(), false);
|
assert.equal(hasRenderHold(engine), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('tracking event keeps legacy yaw and an untracked globe rotation active', (t) => {
|
test('tracking event keeps legacy yaw and an untracked globe rotation active', (t) => {
|
||||||
@@ -214,13 +214,14 @@ test('a replaced engine keeps no listener and its tracking stays out of the new
|
|||||||
assert.equal(activeCameraMotionId(), id);
|
assert.equal(activeCameraMotionId(), id);
|
||||||
next.viewer.trackedEntity = { id: 'new-aircraft' };
|
next.viewer.trackedEntity = { id: 'new-aircraft' };
|
||||||
assert.equal(getActiveCameraMotion(), null);
|
assert.equal(getActiveCameraMotion(), null);
|
||||||
assert.equal(hasRenderHold(), false);
|
assert.equal(hasRenderHold(first.engine), false);
|
||||||
|
assert.equal(hasRenderHold(next.engine), false);
|
||||||
assert.equal(next.scene.preRender.numberOfListeners, 0);
|
assert.equal(next.scene.preRender.numberOfListeners, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const latitude of [0, 30, -60, 89.5]) {
|
for (const latitude of [0, 30, -60, 89.5]) {
|
||||||
test(`globe rotation preserves Earth framing and radius from latitude ${latitude} degrees`, (t) => {
|
test(`globe rotation preserves Earth framing and radius from latitude ${latitude} degrees`, (t) => {
|
||||||
const { camera, tick, finishOnce } = harness(t, { latitude });
|
const { camera, tick, finishOnce, engine } = harness(t, { latitude });
|
||||||
const before = projection(camera);
|
const before = projection(camera);
|
||||||
const radius = Cesium.Cartesian3.magnitude(camera.positionWC);
|
const radius = Cesium.Cartesian3.magnitude(camera.positionWC);
|
||||||
const direction = clone(camera.directionWC);
|
const direction = clone(camera.directionWC);
|
||||||
@@ -242,7 +243,7 @@ for (const latitude of [0, 30, -60, 89.5]) {
|
|||||||
Cesium.Cartesian3.angleBetween(camera.directionWC, direction),
|
Cesium.Cartesian3.angleBetween(camera.directionWC, direction),
|
||||||
radians(15),
|
radians(15),
|
||||||
);
|
);
|
||||||
assert.equal(hasRenderHold(), false);
|
assert.equal(hasRenderHold(engine), false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -377,14 +378,14 @@ for (const [speed, degreesPerSecond] of [
|
|||||||
['fast', 20],
|
['fast', 20],
|
||||||
]) {
|
]) {
|
||||||
test(`continuous globe rotation honors ${speed} speed and explicit stop`, (t) => {
|
test(`continuous globe rotation honors ${speed} speed and explicit stop`, (t) => {
|
||||||
const { camera, tick } = harness(t);
|
const { camera, tick, engine } = harness(t);
|
||||||
const result = moveCamera({
|
const result = moveCamera({
|
||||||
motion: 'rotate_globe',
|
motion: 'rotate_globe',
|
||||||
mode: 'continuous',
|
mode: 'continuous',
|
||||||
speed,
|
speed,
|
||||||
});
|
});
|
||||||
assert.equal(result.ok, true);
|
assert.equal(result.ok, true);
|
||||||
assert.equal(hasRenderHold(), true);
|
assert.equal(hasRenderHold(engine), true);
|
||||||
tick(); // First tick initializes the motion's wall-clock anchor.
|
tick(); // First tick initializes the motion's wall-clock anchor.
|
||||||
const before = clone(camera.directionWC);
|
const before = clone(camera.directionWC);
|
||||||
tick(0.1);
|
tick(0.1);
|
||||||
@@ -395,7 +396,7 @@ for (const [speed, degreesPerSecond] of [
|
|||||||
assert.equal(getActiveCameraMotion()?.kind, 'rotate_globe');
|
assert.equal(getActiveCameraMotion()?.kind, 'rotate_globe');
|
||||||
const stopped = moveCamera({ motion: 'stop' });
|
const stopped = moveCamera({ motion: 'stop' });
|
||||||
assert.equal(stopped.stopped, true);
|
assert.equal(stopped.stopped, true);
|
||||||
assert.equal(hasRenderHold(), false);
|
assert.equal(hasRenderHold(engine), false);
|
||||||
const after = pose(camera);
|
const after = pose(camera);
|
||||||
tick();
|
tick();
|
||||||
samePose(camera, after);
|
samePose(camera, after);
|
||||||
@@ -405,21 +406,21 @@ for (const [speed, degreesPerSecond] of [
|
|||||||
|
|
||||||
for (const event of ['pointerdown', 'touchstart', 'wheel']) {
|
for (const event of ['pointerdown', 'touchstart', 'wheel']) {
|
||||||
test(`${event} immediately cancels globe rotation and releases the render hold`, (t) => {
|
test(`${event} immediately cancels globe rotation and releases the render hold`, (t) => {
|
||||||
const { camera, canvas, tick } = harness(t);
|
const { camera, canvas, tick, engine } = harness(t);
|
||||||
moveCamera({ motion: 'rotate_globe', mode: 'continuous' });
|
moveCamera({ motion: 'rotate_globe', mode: 'continuous' });
|
||||||
tick();
|
tick();
|
||||||
tick();
|
tick();
|
||||||
const before = pose(camera);
|
const before = pose(camera);
|
||||||
canvas.dispatchEvent(new Event(event));
|
canvas.dispatchEvent(new Event(event));
|
||||||
assert.equal(getActiveCameraMotion(), null);
|
assert.equal(getActiveCameraMotion(), null);
|
||||||
assert.equal(hasRenderHold(), false);
|
assert.equal(hasRenderHold(engine), false);
|
||||||
tick();
|
tick();
|
||||||
samePose(camera, before);
|
samePose(camera, before);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
test('new navigation can cancel globe rotation before setting a new destination', (t) => {
|
test('new navigation can cancel globe rotation before setting a new destination', (t) => {
|
||||||
const { camera, tick } = harness(t);
|
const { camera, tick, engine } = harness(t);
|
||||||
moveCamera({ motion: 'rotate_globe', mode: 'continuous' });
|
moveCamera({ motion: 'rotate_globe', mode: 'continuous' });
|
||||||
tick();
|
tick();
|
||||||
assert.equal(interruptCameraMotion('explicit-navigation').wasActive, true);
|
assert.equal(interruptCameraMotion('explicit-navigation').wasActive, true);
|
||||||
@@ -429,11 +430,11 @@ test('new navigation can cancel globe rotation before setting a new destination'
|
|||||||
const destination = pose(camera);
|
const destination = pose(camera);
|
||||||
tick();
|
tick();
|
||||||
samePose(camera, destination);
|
samePose(camera, destination);
|
||||||
assert.equal(hasRenderHold(), false);
|
assert.equal(hasRenderHold(engine), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('replacement motion rejects a stale globe owner cancellation', (t) => {
|
test('replacement motion rejects a stale globe owner cancellation', (t) => {
|
||||||
const { camera, tick } = harness(t);
|
const { camera, tick, engine } = harness(t);
|
||||||
moveCamera({ motion: 'rotate_globe', mode: 'continuous' });
|
moveCamera({ motion: 'rotate_globe', mode: 'continuous' });
|
||||||
const globeId = activeCameraMotionId();
|
const globeId = activeCameraMotionId();
|
||||||
moveCamera({ motion: 'rotate', direction: 'left', mode: 'continuous' });
|
moveCamera({ motion: 'rotate', direction: 'left', mode: 'continuous' });
|
||||||
@@ -446,11 +447,11 @@ test('replacement motion rejects a stale globe owner cancellation', (t) => {
|
|||||||
tick();
|
tick();
|
||||||
near(Cesium.Cartesian3.distance(camera.positionWC, position), 0, 1e-6);
|
near(Cesium.Cartesian3.distance(camera.positionWC, position), 0, 1e-6);
|
||||||
assert.equal(interruptCameraMotionIfActive(yawId).wasActive, true);
|
assert.equal(interruptCameraMotionIfActive(yawId).wasActive, true);
|
||||||
assert.equal(hasRenderHold(), false);
|
assert.equal(hasRenderHold(engine), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('replacing the engine retires globe motion and its frame listener', (t) => {
|
test('replacing the engine retires globe motion and its frame listener', (t) => {
|
||||||
const { viewer, scene, camera, tick } = harness(t);
|
const { viewer, scene, camera, tick, engine } = harness(t);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
moveCamera({ motion: 'rotate_globe', mode: 'continuous' }).ok,
|
moveCamera({ motion: 'rotate_globe', mode: 'continuous' }).ok,
|
||||||
true,
|
true,
|
||||||
@@ -462,7 +463,7 @@ test('replacing the engine retires globe motion and its frame listener', (t) =>
|
|||||||
t.after(() => nextEngine.destroy());
|
t.after(() => nextEngine.destroy());
|
||||||
initCameraVerbs(nextEngine);
|
initCameraVerbs(nextEngine);
|
||||||
assert.equal(getActiveCameraMotion(), null);
|
assert.equal(getActiveCameraMotion(), null);
|
||||||
assert.equal(hasRenderHold(), false);
|
assert.equal(hasRenderHold(engine), false);
|
||||||
assert.equal(scene.preRender.numberOfListeners, 0);
|
assert.equal(scene.preRender.numberOfListeners, 0);
|
||||||
tick();
|
tick();
|
||||||
samePose(camera, before);
|
samePose(camera, before);
|
||||||
@@ -470,7 +471,7 @@ test('replacing the engine retires globe motion and its frame listener', (t) =>
|
|||||||
|
|
||||||
for (const direction of ['up', 'down', 'diagonal']) {
|
for (const direction of ['up', 'down', 'diagonal']) {
|
||||||
test(`invalid globe direction ${direction} is refused before camera authority handoff`, (t) => {
|
test(`invalid globe direction ${direction} is refused before camera authority handoff`, (t) => {
|
||||||
const { camera } = harness(t);
|
const { camera, engine } = harness(t);
|
||||||
const before = pose(camera);
|
const before = pose(camera);
|
||||||
let handoffs = 0;
|
let handoffs = 0;
|
||||||
const result = moveCamera({ motion: 'rotate_globe', direction }, () => {
|
const result = moveCamera({ motion: 'rotate_globe', direction }, () => {
|
||||||
@@ -480,7 +481,7 @@ for (const direction of ['up', 'down', 'diagonal']) {
|
|||||||
assert.match(result.error, /left or right/);
|
assert.match(result.error, /left or right/);
|
||||||
assert.equal(handoffs, 0);
|
assert.equal(handoffs, 0);
|
||||||
assert.equal(getActiveCameraMotion(), null);
|
assert.equal(getActiveCameraMotion(), null);
|
||||||
assert.equal(hasRenderHold(), false);
|
assert.equal(hasRenderHold(engine), false);
|
||||||
samePose(camera, before);
|
samePose(camera, before);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -488,11 +489,11 @@ for (const direction of ['up', 'down', 'diagonal']) {
|
|||||||
for (const mode of ['once', 'continuous']) {
|
for (const mode of ['once', 'continuous']) {
|
||||||
for (const acquisition of ['tracking', 'reference frame']) {
|
for (const acquisition of ['tracking', 'reference frame']) {
|
||||||
test(`globe ${mode} yields to new ${acquisition} without moving the new owner`, (t) => {
|
test(`globe ${mode} yields to new ${acquisition} without moving the new owner`, (t) => {
|
||||||
const { viewer, camera, target, tick } = harness(t);
|
const { viewer, camera, target, tick, engine } = harness(t);
|
||||||
assert.equal(moveCamera({ motion: 'rotate_globe', mode }).ok, true);
|
assert.equal(moveCamera({ motion: 'rotate_globe', mode }).ok, true);
|
||||||
tick();
|
tick();
|
||||||
tick();
|
tick();
|
||||||
assert.equal(hasRenderHold(), true);
|
assert.equal(hasRenderHold(engine), true);
|
||||||
const tracked = { id: 'new-tracked-aircraft' };
|
const tracked = { id: 'new-tracked-aircraft' };
|
||||||
if (acquisition === 'tracking') viewer.trackedEntity = tracked;
|
if (acquisition === 'tracking') viewer.trackedEntity = tracked;
|
||||||
else
|
else
|
||||||
@@ -504,7 +505,7 @@ for (const mode of ['once', 'continuous']) {
|
|||||||
const transform = Cesium.Matrix4.clone(camera.transform);
|
const transform = Cesium.Matrix4.clone(camera.transform);
|
||||||
tick();
|
tick();
|
||||||
assert.equal(getActiveCameraMotion(), null);
|
assert.equal(getActiveCameraMotion(), null);
|
||||||
assert.equal(hasRenderHold(), false);
|
assert.equal(hasRenderHold(engine), false);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
viewer.trackedEntity,
|
viewer.trackedEntity,
|
||||||
acquisition === 'tracking' ? tracked : undefined,
|
acquisition === 'tracking' ? tracked : undefined,
|
||||||
@@ -518,7 +519,7 @@ for (const mode of ['once', 'continuous']) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
test('direct globe rotation refuses a tracked camera without disturbing its reference frame', (t) => {
|
test('direct globe rotation refuses a tracked camera without disturbing its reference frame', (t) => {
|
||||||
const { viewer, camera, target } = harness(t);
|
const { viewer, camera, target, engine } = harness(t);
|
||||||
viewer.trackedEntity = { id: 'tracked-aircraft' };
|
viewer.trackedEntity = { id: 'tracked-aircraft' };
|
||||||
camera.lookAt(target, new Cesium.HeadingPitchRange(0, radians(-40), 1_000));
|
camera.lookAt(target, new Cesium.HeadingPitchRange(0, radians(-40), 1_000));
|
||||||
const before = pose(camera);
|
const before = pose(camera);
|
||||||
@@ -529,7 +530,7 @@ test('direct globe rotation refuses a tracked camera without disturbing its refe
|
|||||||
assert.equal(Cesium.Matrix4.equals(camera.transform, transform), true);
|
assert.equal(Cesium.Matrix4.equals(camera.transform, transform), true);
|
||||||
samePose(camera, before);
|
samePose(camera, before);
|
||||||
assert.equal(getActiveCameraMotion(), null);
|
assert.equal(getActiveCameraMotion(), null);
|
||||||
assert.equal(hasRenderHold(), false);
|
assert.equal(hasRenderHold(engine), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('the existing Globe View preset remains unchanged', () => {
|
test('the existing Globe View preset remains unchanged', () => {
|
||||||
@@ -553,7 +554,7 @@ for (const latitude of [0, 90, -90])
|
|||||||
near(Cesium.Cartesian3.magnitude(camera.positionWC), radius, 1e-6);
|
near(Cesium.Cartesian3.magnitude(camera.positionWC), radius, 1e-6);
|
||||||
});
|
});
|
||||||
test('polar preset admission keeps centre-distance although geodetic altitude varies', (t) => {
|
test('polar preset admission keeps centre-distance although geodetic altitude varies', (t) => {
|
||||||
const { camera, tick } = harness(t, { latitude: 90 });
|
const { camera, tick, engine } = harness(t, { latitude: 90 });
|
||||||
const radius = Cesium.Cartesian3.magnitude(camera.positionWC);
|
const radius = Cesium.Cartesian3.magnitude(camera.positionWC);
|
||||||
assert.ok(Math.min(...arcHeights(camera, camera.upWC, Math.PI * 2)) > 0);
|
assert.ok(Math.min(...arcHeights(camera, camera.upWC, Math.PI * 2)) > 0);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
@@ -569,7 +570,7 @@ test('polar preset admission keeps centre-distance although geodetic altitude va
|
|||||||
assert.equal(getActiveCameraMotion().kind, 'rotate_globe');
|
assert.equal(getActiveCameraMotion().kind, 'rotate_globe');
|
||||||
near(Cesium.Cartesian3.magnitude(camera.positionWC), radius, 1e-6);
|
near(Cesium.Cartesian3.magnitude(camera.positionWC), radius, 1e-6);
|
||||||
moveCamera({ motion: 'stop' });
|
moveCamera({ motion: 'stop' });
|
||||||
assert.equal(hasRenderHold(), false);
|
assert.equal(hasRenderHold(engine), false);
|
||||||
});
|
});
|
||||||
test('close-up refusal preserves an inherited target frame', (t) => {
|
test('close-up refusal preserves an inherited target frame', (t) => {
|
||||||
const { camera, target } = harness(t);
|
const { camera, target } = harness(t);
|
||||||
@@ -617,7 +618,7 @@ test('direct globe request refuses Cockpit before navigation mutation', (t) => {
|
|||||||
);
|
);
|
||||||
assert.equal(calls, 0);
|
assert.equal(calls, 0);
|
||||||
samePose(camera, before);
|
samePose(camera, before);
|
||||||
assert.equal(hasRenderHold(), false);
|
assert.equal(hasRenderHold(engine), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('navigation handoff preserves the original world pose even when it levels a rolled camera', (t) => {
|
test('navigation handoff preserves the original world pose even when it levels a rolled camera', (t) => {
|
||||||
@@ -699,7 +700,7 @@ test('legacy orbit still revolves around the selected ground target', (t) => {
|
|||||||
// Actual UI/tool authority routes, not a direct call to the interrupt helper.
|
// Actual UI/tool authority routes, not a direct call to the interrupt helper.
|
||||||
test('new UI navigation intent stops globe motion before its destination and stale ticks', async (t) => {
|
test('new UI navigation intent stops globe motion before its destination and stale ticks', async (t) => {
|
||||||
const { NavigationController } = await import('./ui/navigationController.js');
|
const { NavigationController } = await import('./ui/navigationController.js');
|
||||||
const { viewer, camera, tick } = harness(t);
|
const { viewer, camera, tick, engine } = harness(t);
|
||||||
const navigation = new NavigationController({
|
const navigation = new NavigationController({
|
||||||
viewer,
|
viewer,
|
||||||
tracking: {
|
tracking: {
|
||||||
@@ -730,7 +731,7 @@ test('new UI navigation intent stops globe motion before its destination and sta
|
|||||||
);
|
);
|
||||||
navigation._runExplicitNavigation('camera', () => {
|
navigation._runExplicitNavigation('camera', () => {
|
||||||
assert.equal(getActiveCameraMotion(), null);
|
assert.equal(getActiveCameraMotion(), null);
|
||||||
assert.equal(hasRenderHold(), false);
|
assert.equal(hasRenderHold(engine), false);
|
||||||
});
|
});
|
||||||
const after = pose(camera);
|
const after = pose(camera);
|
||||||
tick();
|
tick();
|
||||||
@@ -738,7 +739,7 @@ test('new UI navigation intent stops globe motion before its destination and sta
|
|||||||
});
|
});
|
||||||
test('orbit-toggle controller stops globe motion before toggling', async (t) => {
|
test('orbit-toggle controller stops globe motion before toggling', async (t) => {
|
||||||
const { LocationNavigation } = await import('./ui/locationNavigation.js');
|
const { LocationNavigation } = await import('./ui/locationNavigation.js');
|
||||||
const { target } = harness(t);
|
const { target, engine } = harness(t);
|
||||||
let toggled = false;
|
let toggled = false;
|
||||||
moveCamera({ motion: 'rotate_globe', mode: 'continuous' });
|
moveCamera({ motion: 'rotate_globe', mode: 'continuous' });
|
||||||
LocationNavigation.prototype._toggleOrbit.call({
|
LocationNavigation.prototype._toggleOrbit.call({
|
||||||
@@ -748,7 +749,7 @@ test('orbit-toggle controller stops globe motion before toggling', async (t) =>
|
|||||||
orbitController: {
|
orbitController: {
|
||||||
toggle() {
|
toggle() {
|
||||||
assert.equal(getActiveCameraMotion(), null);
|
assert.equal(getActiveCameraMotion(), null);
|
||||||
assert.equal(hasRenderHold(), false);
|
assert.equal(hasRenderHold(engine), false);
|
||||||
toggled = true;
|
toggled = true;
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
@@ -842,13 +843,13 @@ for (const owner of ['tracking', 'cockpit', 'close-up'])
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('an external radius change retires globe rotation without another write', (t) => {
|
test('an external radius change retires globe rotation without another write', (t) => {
|
||||||
const { camera, tick } = harness(t);
|
const { camera, tick, engine } = harness(t);
|
||||||
moveCamera({ motion: 'rotate_globe', mode: 'continuous' });
|
moveCamera({ motion: 'rotate_globe', mode: 'continuous' });
|
||||||
camera.moveForward(1000);
|
camera.moveForward(1000);
|
||||||
const changed = pose(camera);
|
const changed = pose(camera);
|
||||||
tick();
|
tick();
|
||||||
assert.equal(getActiveCameraMotion(), null);
|
assert.equal(getActiveCameraMotion(), null);
|
||||||
assert.equal(hasRenderHold(), false);
|
assert.equal(hasRenderHold(engine), false);
|
||||||
samePose(camera, changed);
|
samePose(camera, changed);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1083,7 +1084,7 @@ test('keyhole overlap after resize stops rotation before another pose write', (t
|
|||||||
const before = pose(h.camera);
|
const before = pose(h.camera);
|
||||||
h.tick();
|
h.tick();
|
||||||
assert.equal(getActiveCameraMotion(), null);
|
assert.equal(getActiveCameraMotion(), null);
|
||||||
assert.equal(hasRenderHold(), false);
|
assert.equal(hasRenderHold(h.engine), false);
|
||||||
samePose(h.camera, before);
|
samePose(h.camera, before);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1097,7 +1098,7 @@ test('a depth-near globe is unsupported without a reset offer', (t) => {
|
|||||||
assert.equal(result.ok, false);
|
assert.equal(result.ok, false);
|
||||||
assert.equal(result.reason, 'unsupported-view');
|
assert.equal(result.reason, 'unsupported-view');
|
||||||
samePose(h.camera, before);
|
samePose(h.camera, before);
|
||||||
assert.equal(hasRenderHold(), false);
|
assert.equal(hasRenderHold(h.engine), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('fully depth-contained globe remains eligible at actual near/far limits', (t) => {
|
test('fully depth-contained globe remains eligible at actual near/far limits', (t) => {
|
||||||
@@ -1125,7 +1126,7 @@ for (const [near, far] of [
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('bare globe rotation defaults right and continues beyond a nudge', (t) => {
|
test('bare globe rotation defaults right and continues beyond a nudge', (t) => {
|
||||||
const { camera, tick } = harness(t, { latitude: 0 });
|
const { camera, tick, engine } = harness(t, { latitude: 0 });
|
||||||
const before = projection(camera);
|
const before = projection(camera);
|
||||||
const result = moveCamera({ motion: 'rotate_globe' });
|
const result = moveCamera({ motion: 'rotate_globe' });
|
||||||
assert.equal(result.ok, true);
|
assert.equal(result.ok, true);
|
||||||
@@ -1137,7 +1138,7 @@ test('bare globe rotation defaults right and continues beyond a nudge', (t) => {
|
|||||||
near(projection(camera).x, before.x);
|
near(projection(camera).x, before.x);
|
||||||
near(projection(camera).y, before.y);
|
near(projection(camera).y, before.y);
|
||||||
assert.equal(moveCamera({ motion: 'stop' }).stopped, true);
|
assert.equal(moveCamera({ motion: 'stop' }).stopped, true);
|
||||||
assert.equal(hasRenderHold(), false);
|
assert.equal(hasRenderHold(engine), false);
|
||||||
});
|
});
|
||||||
for (const [args, direction, mode, speed] of [
|
for (const [args, direction, mode, speed] of [
|
||||||
[{ mode: 'once' }, 'right', 'once', 'normal'],
|
[{ mode: 'once' }, 'right', 'once', 'normal'],
|
||||||
@@ -1195,7 +1196,7 @@ test('canvas-only clipping after resize preserves a keyhole-fitting rotation', (
|
|||||||
h.tick();
|
h.tick();
|
||||||
assert.notDeepEqual(pose(h.camera).position, before.position);
|
assert.notDeepEqual(pose(h.camera).position, before.position);
|
||||||
assert.equal(getActiveCameraMotion()?.kind, 'rotate_globe');
|
assert.equal(getActiveCameraMotion()?.kind, 'rotate_globe');
|
||||||
assert.equal(hasRenderHold(), true);
|
assert.equal(hasRenderHold(h.engine), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('an invalid camera request leaves the active globe rotation untouched', (t) => {
|
test('an invalid camera request leaves the active globe rotation untouched', (t) => {
|
||||||
@@ -1206,7 +1207,7 @@ test('an invalid camera request leaves the active globe rotation untouched', (t)
|
|||||||
assert.equal(moveCamera({ motion: 'pan' }).ok, false);
|
assert.equal(moveCamera({ motion: 'pan' }).ok, false);
|
||||||
assert.deepEqual(getActiveCameraMotion(), active);
|
assert.deepEqual(getActiveCameraMotion(), active);
|
||||||
samePose(h.camera, before);
|
samePose(h.camera, before);
|
||||||
assert.equal(hasRenderHold(), true);
|
assert.equal(hasRenderHold(h.engine), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('close-up ultrawide refusal does not offer an unfit reset or move the camera', (t) => {
|
test('close-up ultrawide refusal does not offer an unfit reset or move the camera', (t) => {
|
||||||
@@ -1223,7 +1224,7 @@ test('close-up ultrawide refusal does not offer an unfit reset or move the camer
|
|||||||
assert.equal(result.reason, 'globe-reset-unavailable');
|
assert.equal(result.reason, 'globe-reset-unavailable');
|
||||||
assert.equal(handoffs, 0);
|
assert.equal(handoffs, 0);
|
||||||
samePose(h.camera, before);
|
samePose(h.camera, before);
|
||||||
assert.equal(hasRenderHold(), false);
|
assert.equal(hasRenderHold(h.engine), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+9
-10
@@ -34,10 +34,7 @@ import {
|
|||||||
KEYHOLE_OUTER_RADIUS,
|
KEYHOLE_OUTER_RADIUS,
|
||||||
} from './celestialRing.js';
|
} from './celestialRing.js';
|
||||||
import { isScopeMaskEnabled } from './scopeMask.js';
|
import { isScopeMaskEnabled } from './scopeMask.js';
|
||||||
import {
|
import { renderGovernorFor } from './renderGovernor.js';
|
||||||
holdContinuousRender,
|
|
||||||
releaseContinuousRender,
|
|
||||||
} from './renderGovernor.js';
|
|
||||||
|
|
||||||
let _isCockpitActive = null;
|
let _isCockpitActive = null;
|
||||||
/** °/s by speed word — orbit; pan uses fractions of view height/s. */
|
/** °/s by speed word — orbit; pan uses fractions of view height/s. */
|
||||||
@@ -1372,7 +1369,7 @@ export function suspendRouteFlight(id) {
|
|||||||
const state = _active;
|
const state = _active;
|
||||||
_active = null;
|
_active = null;
|
||||||
unbindEngineMotion();
|
unbindEngineMotion();
|
||||||
releaseContinuousRender('camera-verb');
|
state.renderGovernor?.releaseContinuousRender('camera-verb');
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1383,9 +1380,10 @@ export function suspendRouteFlight(id) {
|
|||||||
export function resumeRouteFlight(state, engine) {
|
export function resumeRouteFlight(state, engine) {
|
||||||
if (!state || !engine || _active) return false;
|
if (!state || !engine || _active) return false;
|
||||||
state.rig = engineRigFor(engine);
|
state.rig = engineRigFor(engine);
|
||||||
|
state.renderGovernor = renderGovernorFor(engine);
|
||||||
_active = state;
|
_active = state;
|
||||||
bindEngineMotion(state.rig);
|
bindEngineMotion(state.rig);
|
||||||
holdContinuousRender('camera-verb');
|
_active?.renderGovernor?.holdContinuousRender('camera-verb');
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
/** Monotonic id for the motion currently running; 0 means "no motion". */
|
/** Monotonic id for the motion currently running; 0 means "no motion". */
|
||||||
@@ -1447,6 +1445,7 @@ function countManualInput(engine) {
|
|||||||
function setActiveMotion(state) {
|
function setActiveMotion(state) {
|
||||||
_motionSeq += 1;
|
_motionSeq += 1;
|
||||||
state.motionId = _motionSeq;
|
state.motionId = _motionSeq;
|
||||||
|
state.renderGovernor = renderGovernorFor(state.rig.engine);
|
||||||
_active = state;
|
_active = state;
|
||||||
bindEngineMotion(state.rig);
|
bindEngineMotion(state.rig);
|
||||||
return state;
|
return state;
|
||||||
@@ -1694,7 +1693,7 @@ export function interruptCameraMotion(reason = 'interrupt') {
|
|||||||
}
|
}
|
||||||
unbindEngineMotion();
|
unbindEngineMotion();
|
||||||
_active = null;
|
_active = null;
|
||||||
releaseContinuousRender('camera-verb');
|
motion?.renderGovernor?.releaseContinuousRender('camera-verb');
|
||||||
return { wasActive, reason, leveled };
|
return { wasActive, reason, leveled };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2604,7 +2603,7 @@ export function moveCamera(args = {}, runNavigation = null) {
|
|||||||
mode,
|
mode,
|
||||||
pending: true,
|
pending: true,
|
||||||
});
|
});
|
||||||
holdContinuousRender('camera-verb');
|
_active?.renderGovernor?.holdContinuousRender('camera-verb');
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
action: 'move_camera',
|
action: 'move_camera',
|
||||||
@@ -2623,7 +2622,7 @@ export function moveCamera(args = {}, runNavigation = null) {
|
|||||||
setActiveMotion(state);
|
setActiveMotion(state);
|
||||||
// Verb motion drives the camera per frame — hold until interrupted;
|
// Verb motion drives the camera per frame — hold until interrupted;
|
||||||
// interruptCameraMotion is the single release path. (perf wave 2)
|
// interruptCameraMotion is the single release path. (perf wave 2)
|
||||||
holdContinuousRender('camera-verb');
|
_active?.renderGovernor?.holdContinuousRender('camera-verb');
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
action: 'move_camera',
|
action: 'move_camera',
|
||||||
@@ -2860,7 +2859,7 @@ export function flyRoute(
|
|||||||
});
|
});
|
||||||
flight.rig = rig;
|
flight.rig = rig;
|
||||||
setActiveMotion(flight);
|
setActiveMotion(flight);
|
||||||
holdContinuousRender('camera-verb');
|
_active?.renderGovernor?.holdContinuousRender('camera-verb');
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
action: 'fly_route',
|
action: 'fly_route',
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ import { getRenderGovernorDiagnostics } from './renderGovernor.js';
|
|||||||
const WIDTH = 1280;
|
const WIDTH = 1280;
|
||||||
const HEIGHT = 800;
|
const HEIGHT = 800;
|
||||||
const radians = geo.math.toRadians;
|
const radians = geo.math.toRadians;
|
||||||
const hasRenderHold = () =>
|
const hasRenderHold = (engine) =>
|
||||||
getRenderGovernorDiagnostics().holds.includes('camera-verb');
|
getRenderGovernorDiagnostics(engine).holds.includes('camera-verb');
|
||||||
|
|
||||||
/** Wall clock the verbs read (performance.now), moved only by the tests. */
|
/** Wall clock the verbs read (performance.now), moved only by the tests. */
|
||||||
function installClock(t) {
|
function installClock(t) {
|
||||||
@@ -279,12 +279,12 @@ test('a once nudge spends its budget in 0.9 s and then lets go', (t) => {
|
|||||||
initCameraVerbs(engine);
|
initCameraVerbs(engine);
|
||||||
const before = camera();
|
const before = camera();
|
||||||
assert.equal(moveCamera({ motion: 'tilt', direction: 'up' }).ok, true);
|
assert.equal(moveCamera({ motion: 'tilt', direction: 'up' }).ok, true);
|
||||||
assert.equal(hasRenderHold(), true);
|
assert.equal(hasRenderHold(engine), true);
|
||||||
run(0.8);
|
run(0.8);
|
||||||
assert.equal(getActiveCameraMotion()?.kind, 'tilt', 'still easing');
|
assert.equal(getActiveCameraMotion()?.kind, 'tilt', 'still easing');
|
||||||
run(0.2);
|
run(0.2);
|
||||||
assert.equal(getActiveCameraMotion(), null, 'finished after 0.9 s');
|
assert.equal(getActiveCameraMotion(), null, 'finished after 0.9 s');
|
||||||
assert.equal(hasRenderHold(), false);
|
assert.equal(hasRenderHold(engine), false);
|
||||||
near(
|
near(
|
||||||
geo.math.toDegrees(camera().pitch - before.pitch),
|
geo.math.toDegrees(camera().pitch - before.pitch),
|
||||||
15,
|
15,
|
||||||
@@ -337,7 +337,7 @@ test('manual input on the canvas stops the motion and releases the hold', (t) =>
|
|||||||
run(0.3);
|
run(0.3);
|
||||||
engine.host.canvas.dispatchEvent(new Event(event));
|
engine.host.canvas.dispatchEvent(new Event(event));
|
||||||
assert.equal(getActiveCameraMotion(), null, event);
|
assert.equal(getActiveCameraMotion(), null, event);
|
||||||
assert.equal(hasRenderHold(), false, event);
|
assert.equal(hasRenderHold(engine), false, event);
|
||||||
assert.deepEqual(engine.camera.transform(), [...geo.matrix4.IDENTITY]);
|
assert.deepEqual(engine.camera.transform(), [...geo.matrix4.IDENTITY]);
|
||||||
engine.fake.resetCalls();
|
engine.fake.resetCalls();
|
||||||
run(0.3);
|
run(0.3);
|
||||||
@@ -367,7 +367,7 @@ test('a new verb supersedes the running one and stop ends it', (t) => {
|
|||||||
motion: 'stop',
|
motion: 'stop',
|
||||||
stopped: true,
|
stopped: true,
|
||||||
});
|
});
|
||||||
assert.equal(hasRenderHold(), false);
|
assert.equal(hasRenderHold(engine), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('one frame listener and three input listeners exist only while a motion runs', (t) => {
|
test('one frame listener and three input listeners exist only while a motion runs', (t) => {
|
||||||
@@ -407,7 +407,7 @@ test('an engine switch releases the verb: the parked engine is never driven agai
|
|||||||
// What the Navara scene's suspend does (src/app/scene.js).
|
// What the Navara scene's suspend does (src/app/scene.js).
|
||||||
releaseCameraVerbs(first.engine);
|
releaseCameraVerbs(first.engine);
|
||||||
assert.equal(getActiveCameraMotion(), null);
|
assert.equal(getActiveCameraMotion(), null);
|
||||||
assert.equal(hasRenderHold(), false);
|
assert.equal(hasRenderHold(first.engine), false);
|
||||||
first.engine.fake.resetCalls();
|
first.engine.fake.resetCalls();
|
||||||
first.run(0.5);
|
first.run(0.5);
|
||||||
assert.equal(first.engine.fake.callsOf('camera.setView').length, 0);
|
assert.equal(first.engine.fake.callsOf('camera.setView').length, 0);
|
||||||
@@ -454,7 +454,7 @@ test('globe rotation yields when a follow takes the engine camera', (t) => {
|
|||||||
viewFrom: geo.cartesian3.create(0, -2000, 1000),
|
viewFrom: geo.cartesian3.create(0, -2000, 1000),
|
||||||
});
|
});
|
||||||
assert.equal(getActiveCameraMotion(), null);
|
assert.equal(getActiveCameraMotion(), null);
|
||||||
assert.equal(hasRenderHold(), false);
|
assert.equal(hasRenderHold(engine), false);
|
||||||
// And a request while following is refused before any handoff.
|
// And a request while following is refused before any handoff.
|
||||||
let handoffs = 0;
|
let handoffs = 0;
|
||||||
const refused = moveCamera({ motion: 'rotate_globe' }, () => {
|
const refused = moveCamera({ motion: 'rotate_globe' }, () => {
|
||||||
@@ -514,7 +514,7 @@ test('fly_route flies the installed engine camera and lands level', (t) => {
|
|||||||
run(result.durationS + 2);
|
run(result.durationS + 2);
|
||||||
assert.equal(getActiveCameraMotion(), null, 'landed');
|
assert.equal(getActiveCameraMotion(), null, 'landed');
|
||||||
near(geo.math.negativePiToPi(engine.camera.pose().roll), 0, 1e-5, 'roll');
|
near(geo.math.negativePiToPi(engine.camera.pose().roll), 0, 1e-5, 'roll');
|
||||||
assert.equal(hasRenderHold(), false);
|
assert.equal(hasRenderHold(engine), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('a parked route flight resumes on another engine without jumping', (t) => {
|
test('a parked route flight resumes on another engine without jumping', (t) => {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
} from './globe/cesium/celestial.js';
|
} from './globe/cesium/celestial.js';
|
||||||
import { createCelestialEphemeris } from './celestialRing/ephemeris.js';
|
import { createCelestialEphemeris } from './celestialRing/ephemeris.js';
|
||||||
import { appClock } from './time/clock.js';
|
import { appClock } from './time/clock.js';
|
||||||
import { governorRequestRender } from './renderGovernor.js';
|
import { renderGovernorFor } from './renderGovernor.js';
|
||||||
|
|
||||||
/** Outer edge of the existing NVG/FLIR keyhole in normalized shader space. */
|
/** Outer edge of the existing NVG/FLIR keyhole in normalized shader space. */
|
||||||
export const KEYHOLE_OUTER_RADIUS = 1.05;
|
export const KEYHOLE_OUTER_RADIUS = 1.05;
|
||||||
@@ -413,6 +413,9 @@ export class CelestialRing {
|
|||||||
this._engineView = viewer?.kind ? createCelestialView(viewer) : null;
|
this._engineView = viewer?.kind ? createCelestialView(viewer) : null;
|
||||||
this.viewer = this._engineView?.view ?? viewer;
|
this.viewer = this._engineView?.view ?? viewer;
|
||||||
this._projector = engine?.projector ?? null;
|
this._projector = engine?.projector ?? null;
|
||||||
|
this._renderGovernor = renderGovernorFor(
|
||||||
|
engine ?? (viewer?.kind ? viewer : null),
|
||||||
|
);
|
||||||
this.enabled = !!enabled;
|
this.enabled = !!enabled;
|
||||||
this.visible = false;
|
this.visible = false;
|
||||||
this._onAutoDisable =
|
this._onAutoDisable =
|
||||||
@@ -460,7 +463,7 @@ export class CelestialRing {
|
|||||||
// dirty flag up immediately. (review finding)
|
// dirty flag up immediately. (review finding)
|
||||||
this._ephemerisDirty = true;
|
this._ephemerisDirty = true;
|
||||||
if (typeof document !== 'undefined' && document.hidden) return;
|
if (typeof document !== 'undefined' && document.hidden) return;
|
||||||
governorRequestRender('celestial-ephemeris');
|
this._renderGovernor?.governorRequestRender('celestial-ephemeris');
|
||||||
}, 60_000);
|
}, 60_000);
|
||||||
this.setEnabled(this.enabled);
|
this.setEnabled(this.enabled);
|
||||||
}
|
}
|
||||||
@@ -526,7 +529,8 @@ export class CelestialRing {
|
|||||||
this._moonCtx = ctx;
|
this._moonCtx = ctx;
|
||||||
this._moonRenderKey = '';
|
this._moonRenderKey = '';
|
||||||
} else return;
|
} else return;
|
||||||
if (canvas && this.enabled) governorRequestRender('celestial-ring');
|
if (canvas && this.enabled)
|
||||||
|
this._renderGovernor?.governorRequestRender('celestial-ring');
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Enable or disable the user preference for the effect. */
|
/** Enable or disable the user preference for the effect. */
|
||||||
@@ -539,7 +543,8 @@ export class CelestialRing {
|
|||||||
// clearing disable) must request its frame or the ring never draws at
|
// clearing disable) must request its frame or the ring never draws at
|
||||||
// all. Camera motion covers every later repaint; the 60 s ephemeris
|
// all. Camera motion covers every later repaint; the 60 s ephemeris
|
||||||
// timer requests its own. (perf wave 2 fix — owner playtest finding)
|
// timer requests its own. (perf wave 2 fix — owner playtest finding)
|
||||||
if (this.enabled !== wasEnabled) governorRequestRender('celestial-ring');
|
if (this.enabled !== wasEnabled)
|
||||||
|
this._renderGovernor?.governorRequestRender('celestial-ring');
|
||||||
if (!this.enabled) {
|
if (!this.enabled) {
|
||||||
this.visible = false;
|
this.visible = false;
|
||||||
this._clear();
|
this._clear();
|
||||||
|
|||||||
+28
-24
@@ -1136,7 +1136,7 @@ export function createBhoteKoshiEventLayer({
|
|||||||
if (!_enabled || generation !== _comparisonSurfaceGeneration) return;
|
if (!_enabled || generation !== _comparisonSurfaceGeneration) return;
|
||||||
syncFloodSurfaceMode();
|
syncFloodSurfaceMode();
|
||||||
updateProgress(_progress);
|
updateProgress(_progress);
|
||||||
renderHost.request('bhote-koshi-comparison-ready');
|
renderHost.request('bhote-koshi-comparison-ready', _engine);
|
||||||
}
|
}
|
||||||
|
|
||||||
function attachDataManager(dataManager) {
|
function attachDataManager(dataManager) {
|
||||||
@@ -1421,7 +1421,8 @@ export function createBhoteKoshiEventLayer({
|
|||||||
}
|
}
|
||||||
_sceneMediaPlaybackBeatId = null;
|
_sceneMediaPlaybackBeatId = null;
|
||||||
if (pauseMedia) _embeddedMedia?.pause?.();
|
if (pauseMedia) _embeddedMedia?.pause?.();
|
||||||
if (hadPlayback) renderHost.request('bhote-koshi-scene-media-hold');
|
if (hadPlayback)
|
||||||
|
renderHost.request('bhote-koshi-scene-media-hold', _engine);
|
||||||
}
|
}
|
||||||
|
|
||||||
function completeSceneMediaPlayback() {
|
function completeSceneMediaPlayback() {
|
||||||
@@ -1439,7 +1440,7 @@ export function createBhoteKoshiEventLayer({
|
|||||||
if (_sceneMediaPlaybackBeatId !== beatId) return;
|
if (_sceneMediaPlaybackBeatId !== beatId) return;
|
||||||
_sceneMediaPlaybackTimer = null;
|
_sceneMediaPlaybackTimer = null;
|
||||||
_sceneMediaPlaybackBeatId = null;
|
_sceneMediaPlaybackBeatId = null;
|
||||||
renderHost.request('bhote-koshi-scene-media-exit');
|
renderHost.request('bhote-koshi-scene-media-exit', _engine);
|
||||||
}, exitDurationSec * 1000) ?? null;
|
}, exitDurationSec * 1000) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1762,7 +1763,8 @@ export function createBhoteKoshiEventLayer({
|
|||||||
if (_sceneRevealAnimationFrame != null)
|
if (_sceneRevealAnimationFrame != null)
|
||||||
cancelAnimationFrame(_sceneRevealAnimationFrame);
|
cancelAnimationFrame(_sceneRevealAnimationFrame);
|
||||||
_sceneRevealAnimationFrame = null;
|
_sceneRevealAnimationFrame = null;
|
||||||
if (_sceneRevealHoldActive) renderHost.release(SCENE_REVEAL_RENDER_HOLD);
|
if (_sceneRevealHoldActive)
|
||||||
|
renderHost.release(SCENE_REVEAL_RENDER_HOLD, _engine);
|
||||||
_sceneRevealHoldActive = false;
|
_sceneRevealHoldActive = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1773,7 +1775,8 @@ export function createBhoteKoshiEventLayer({
|
|||||||
if (_scenePathDeadlineTimer != null)
|
if (_scenePathDeadlineTimer != null)
|
||||||
globalThis.window?.clearTimeout?.(_scenePathDeadlineTimer);
|
globalThis.window?.clearTimeout?.(_scenePathDeadlineTimer);
|
||||||
_scenePathDeadlineTimer = null;
|
_scenePathDeadlineTimer = null;
|
||||||
if (_scenePathHoldActive) renderHost.release(SCENE_PATH_RENDER_HOLD);
|
if (_scenePathHoldActive)
|
||||||
|
renderHost.release(SCENE_PATH_RENDER_HOLD, _engine);
|
||||||
_scenePathHoldActive = false;
|
_scenePathHoldActive = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1787,8 +1790,9 @@ export function createBhoteKoshiEventLayer({
|
|||||||
_scenePathRevealProgress = targetProgress;
|
_scenePathRevealProgress = targetProgress;
|
||||||
updateFloodTrace(_scenePathRevealProgress);
|
updateFloodTrace(_scenePathRevealProgress);
|
||||||
updateSurgeFront(_scenePathRevealProgress);
|
updateSurgeFront(_scenePathRevealProgress);
|
||||||
renderHost.request(requestReason);
|
renderHost.request(requestReason, _engine);
|
||||||
if (_scenePathHoldActive) renderHost.release(SCENE_PATH_RENDER_HOLD);
|
if (_scenePathHoldActive)
|
||||||
|
renderHost.release(SCENE_PATH_RENDER_HOLD, _engine);
|
||||||
_scenePathHoldActive = false;
|
_scenePathHoldActive = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1959,7 +1963,7 @@ export function createBhoteKoshiEventLayer({
|
|||||||
lastTickAt = tickAt;
|
lastTickAt = tickAt;
|
||||||
return activeElapsedMs;
|
return activeElapsedMs;
|
||||||
};
|
};
|
||||||
renderHost.hold(SCENE_PATH_RENDER_HOLD);
|
renderHost.hold(SCENE_PATH_RENDER_HOLD, _engine);
|
||||||
_scenePathHoldActive = true;
|
_scenePathHoldActive = true;
|
||||||
// RAF normally paints every intermediate frame. This independent deadline
|
// RAF normally paints every intermediate frame. This independent deadline
|
||||||
// is registered before Cesium starts the flight, so a throttled or briefly
|
// is registered before Cesium starts the flight, so a throttled or briefly
|
||||||
@@ -2008,7 +2012,7 @@ export function createBhoteKoshiEventLayer({
|
|||||||
startProgress + (targetProgress - startProgress) * eased;
|
startProgress + (targetProgress - startProgress) * eased;
|
||||||
updateFloodTrace(_scenePathRevealProgress);
|
updateFloodTrace(_scenePathRevealProgress);
|
||||||
updateSurgeFront(_scenePathRevealProgress);
|
updateSurgeFront(_scenePathRevealProgress);
|
||||||
renderHost.request('bhote-koshi-scene-path-travel');
|
renderHost.request('bhote-koshi-scene-path-travel', _engine);
|
||||||
if (linear < 1) {
|
if (linear < 1) {
|
||||||
_scenePathAnimationFrame = requestAnimationFrame(step);
|
_scenePathAnimationFrame = requestAnimationFrame(step);
|
||||||
} else {
|
} else {
|
||||||
@@ -2066,7 +2070,7 @@ export function createBhoteKoshiEventLayer({
|
|||||||
if (sequenceWindow) updateProgress(sequenceWindow.startProgress);
|
if (sequenceWindow) updateProgress(sequenceWindow.startProgress);
|
||||||
let lastTickAt = performance.now();
|
let lastTickAt = performance.now();
|
||||||
let activeElapsedMs = 0;
|
let activeElapsedMs = 0;
|
||||||
renderHost.hold(SCENE_REVEAL_RENDER_HOLD);
|
renderHost.hold(SCENE_REVEAL_RENDER_HOLD, _engine);
|
||||||
_sceneRevealHoldActive = true;
|
_sceneRevealHoldActive = true;
|
||||||
const step = (now) => {
|
const step = (now) => {
|
||||||
if (
|
if (
|
||||||
@@ -2097,12 +2101,12 @@ export function createBhoteKoshiEventLayer({
|
|||||||
updateFloodTrace(floodProgress);
|
updateFloodTrace(floodProgress);
|
||||||
updateSurgeFront(floodProgress);
|
updateSurgeFront(floodProgress);
|
||||||
}
|
}
|
||||||
renderHost.request('bhote-koshi-scene-beat-reveal');
|
renderHost.request('bhote-koshi-scene-beat-reveal', _engine);
|
||||||
if (linear < 1) {
|
if (linear < 1) {
|
||||||
_sceneRevealAnimationFrame = requestAnimationFrame(step);
|
_sceneRevealAnimationFrame = requestAnimationFrame(step);
|
||||||
} else {
|
} else {
|
||||||
_sceneRevealAnimationFrame = null;
|
_sceneRevealAnimationFrame = null;
|
||||||
renderHost.release(SCENE_REVEAL_RENDER_HOLD);
|
renderHost.release(SCENE_REVEAL_RENDER_HOLD, _engine);
|
||||||
_sceneRevealHoldActive = false;
|
_sceneRevealHoldActive = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -2147,7 +2151,7 @@ export function createBhoteKoshiEventLayer({
|
|||||||
),
|
),
|
||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
renderHost.request('bhote-koshi-evidence-video-frame');
|
renderHost.request('bhote-koshi-evidence-video-frame', _engine);
|
||||||
}
|
}
|
||||||
_evidenceVideoFrameHandle = video.requestVideoFrameCallback(drawFrame);
|
_evidenceVideoFrameHandle = video.requestVideoFrameCallback(drawFrame);
|
||||||
};
|
};
|
||||||
@@ -2176,7 +2180,7 @@ export function createBhoteKoshiEventLayer({
|
|||||||
previousIndex >= 0 &&
|
previousIndex >= 0 &&
|
||||||
redrawEvidenceSlot(previousIndex)
|
redrawEvidenceSlot(previousIndex)
|
||||||
) {
|
) {
|
||||||
renderHost.request('bhote-koshi-evidence-poster-restore');
|
renderHost.request('bhote-koshi-evidence-poster-restore', _engine);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2204,7 +2208,7 @@ export function createBhoteKoshiEventLayer({
|
|||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
_evidenceVideoReady = true;
|
_evidenceVideoReady = true;
|
||||||
renderHost.request('bhote-koshi-evidence-video-ready');
|
renderHost.request('bhote-koshi-evidence-video-ready', _engine);
|
||||||
syncEvidenceMedia();
|
syncEvidenceMedia();
|
||||||
};
|
};
|
||||||
video.addEventListener?.('loadeddata', markReady, { once: true });
|
video.addEventListener?.('loadeddata', markReady, { once: true });
|
||||||
@@ -2215,7 +2219,7 @@ export function createBhoteKoshiEventLayer({
|
|||||||
video !== _evidenceVideo
|
video !== _evidenceVideo
|
||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
renderHost.request('bhote-koshi-evidence-video-seek');
|
renderHost.request('bhote-koshi-evidence-video-seek', _engine);
|
||||||
});
|
});
|
||||||
video.src = eventAsset(videoPath);
|
video.src = eventAsset(videoPath);
|
||||||
video.load?.();
|
video.load?.();
|
||||||
@@ -2363,7 +2367,7 @@ export function createBhoteKoshiEventLayer({
|
|||||||
mediaProgress: presentation.localProgress,
|
mediaProgress: presentation.localProgress,
|
||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
renderHost.request('bhote-koshi-evidence-video-frame');
|
renderHost.request('bhote-koshi-evidence-video-frame', _engine);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2509,7 +2513,7 @@ export function createBhoteKoshiEventLayer({
|
|||||||
slot.poster = poster;
|
slot.poster = poster;
|
||||||
poster = null;
|
poster = null;
|
||||||
if (!redrawEvidenceSlot(index)) return;
|
if (!redrawEvidenceSlot(index)) return;
|
||||||
renderHost.request('bhote-koshi-evidence-poster');
|
renderHost.request('bhote-koshi-evidence-poster', _engine);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
closeFrame(poster);
|
closeFrame(poster);
|
||||||
if (
|
if (
|
||||||
@@ -2785,7 +2789,7 @@ export function createBhoteKoshiEventLayer({
|
|||||||
if (releaseCamera)
|
if (releaseCamera)
|
||||||
cameraForScene()?.lookAtTransform?.(Cesium.Matrix4.IDENTITY);
|
cameraForScene()?.lookAtTransform?.(Cesium.Matrix4.IDENTITY);
|
||||||
syncCinematicButton();
|
syncCinematicButton();
|
||||||
renderHost.request('bhote-koshi-cinematic-stop');
|
renderHost.request('bhote-koshi-cinematic-stop', _engine);
|
||||||
}
|
}
|
||||||
|
|
||||||
function startCinematic({ play = true } = {}) {
|
function startCinematic({ play = true } = {}) {
|
||||||
@@ -3446,7 +3450,7 @@ export function createBhoteKoshiEventLayer({
|
|||||||
applyCinematicCamera();
|
applyCinematicCamera();
|
||||||
syncEvidenceMedia();
|
syncEvidenceMedia();
|
||||||
syncPanel();
|
syncPanel();
|
||||||
renderHost.request('bhote-koshi-progress');
|
renderHost.request('bhote-koshi-progress', _engine);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setSplit(split) {
|
function setSplit(split) {
|
||||||
@@ -3454,7 +3458,7 @@ export function createBhoteKoshiEventLayer({
|
|||||||
_splitControl?.setValue(_split);
|
_splitControl?.setValue(_split);
|
||||||
_native?.setSplit(_split);
|
_native?.setSplit(_split);
|
||||||
syncPanel();
|
syncPanel();
|
||||||
renderHost.request('bhote-koshi-split');
|
renderHost.request('bhote-koshi-split', _engine);
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopPlayback() {
|
function stopPlayback() {
|
||||||
@@ -3462,7 +3466,7 @@ export function createBhoteKoshiEventLayer({
|
|||||||
_playing = false;
|
_playing = false;
|
||||||
if (_animationFrame != null) cancelAnimationFrame(_animationFrame);
|
if (_animationFrame != null) cancelAnimationFrame(_animationFrame);
|
||||||
_animationFrame = null;
|
_animationFrame = null;
|
||||||
renderHost.release(FLOOD_RENDER_HOLD);
|
renderHost.release(FLOOD_RENDER_HOLD, _engine);
|
||||||
syncEvidenceMedia();
|
syncEvidenceMedia();
|
||||||
syncPanel();
|
syncPanel();
|
||||||
}
|
}
|
||||||
@@ -3486,7 +3490,7 @@ export function createBhoteKoshiEventLayer({
|
|||||||
_playing = true;
|
_playing = true;
|
||||||
_playStartProgress = _progress;
|
_playStartProgress = _progress;
|
||||||
_playStartedAt = performance.now();
|
_playStartedAt = performance.now();
|
||||||
renderHost.hold(FLOOD_RENDER_HOLD);
|
renderHost.hold(FLOOD_RENDER_HOLD, _engine);
|
||||||
syncEvidenceMedia();
|
syncEvidenceMedia();
|
||||||
syncPanel();
|
syncPanel();
|
||||||
_animationFrame = requestAnimationFrame(animationTick);
|
_animationFrame = requestAnimationFrame(animationTick);
|
||||||
@@ -3832,7 +3836,7 @@ export function createBhoteKoshiEventLayer({
|
|||||||
try {
|
try {
|
||||||
await comparisonLease?.release();
|
await comparisonLease?.release();
|
||||||
} finally {
|
} finally {
|
||||||
renderHost.request('bhote-koshi-disable');
|
renderHost.request('bhote-koshi-disable', _engine);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -291,10 +291,7 @@ export function createBhoteKoshiLocatorLayer({
|
|||||||
let _native = null;
|
let _native = null;
|
||||||
let _pausedApproach = null;
|
let _pausedApproach = null;
|
||||||
const cameraForScene = () => _engine?.camera ?? _viewer?.camera;
|
const cameraForScene = () => _engine?.camera ?? _viewer?.camera;
|
||||||
const requestRender = (reason) =>
|
const requestRender = (reason) => requestSceneRender(reason, _engine);
|
||||||
_engine?.kind === 'navara'
|
|
||||||
? _engine.render.request(reason)
|
|
||||||
: requestSceneRender(reason);
|
|
||||||
let _dataSource = null;
|
let _dataSource = null;
|
||||||
let _enabled = false;
|
let _enabled = false;
|
||||||
let _boundaryReady = false;
|
let _boundaryReady = false;
|
||||||
|
|||||||
@@ -400,7 +400,7 @@ function _scheduleCyberSonarRender(nowMs) {
|
|||||||
_cyberSonarRenderTimer = globalThis.setTimeout(() => {
|
_cyberSonarRenderTimer = globalThis.setTimeout(() => {
|
||||||
_cyberSonarRenderTimer = null;
|
_cyberSonarRenderTimer = null;
|
||||||
if (isCyberSonarActive()) {
|
if (isCyberSonarActive()) {
|
||||||
governorRequestRender('cyber-sonar-cadence');
|
governorRequestRender('cyber-sonar-cadence', _engine);
|
||||||
}
|
}
|
||||||
}, delay);
|
}, delay);
|
||||||
}
|
}
|
||||||
@@ -655,7 +655,7 @@ function _syncSurfaceVisibility() {
|
|||||||
// owes itself follow-up frames only while a bounded animation is in flight
|
// owes itself follow-up frames only while a bounded animation is in flight
|
||||||
// (see `detectionRenderDemand.js` for why a hold was wrong here, and why
|
// (see `detectionRenderDemand.js` for why a hold was wrong here, and why
|
||||||
// removing it is safe).
|
// removing it is safe).
|
||||||
governorRequestRender('detection-visibility');
|
governorRequestRender('detection-visibility', _engine);
|
||||||
if (!_hostSurface) return;
|
if (!_hostSurface) return;
|
||||||
_hostSurface.style.display = _mode === MODE_OFF ? 'none' : 'block';
|
_hostSurface.style.display = _mode === MODE_OFF ? 'none' : 'block';
|
||||||
_hostSurface.style.opacity = _suspended ? '0' : '1';
|
_hostSurface.style.opacity = _suspended ? '0' : '1';
|
||||||
@@ -750,7 +750,7 @@ function _shouldPaintDetectionLane(frame) {
|
|||||||
if (_lastDiagnostics)
|
if (_lastDiagnostics)
|
||||||
_lastDiagnostics.throttleSkipCount = _throttleSkipCount;
|
_lastDiagnostics.throttleSkipCount = _throttleSkipCount;
|
||||||
if (decision.requestFollowUp)
|
if (decision.requestFollowUp)
|
||||||
governorRequestRender('detection-paint-skipped');
|
governorRequestRender('detection-paint-skipped', _engine);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -815,7 +815,7 @@ function _paintDetectionLane(frame) {
|
|||||||
solvePending: result.solvePending === true,
|
solvePending: result.solvePending === true,
|
||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
governorRequestRender('detection-animation');
|
governorRequestRender('detection-animation', _engine);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -275,9 +275,9 @@ test('detection holds nothing, and asks for its own frames instead', async () =>
|
|||||||
assert.doesNotMatch(source, /releaseContinuousRender/,
|
assert.doesNotMatch(source, /releaseContinuousRender/,
|
||||||
'and with no hold there is nothing to release');
|
'and with no hold there is nothing to release');
|
||||||
|
|
||||||
assert.match(source, /governorRequestRender\('detection-visibility'\)/,
|
assert.match(source, /governorRequestRender\('detection-visibility', _engine\)/,
|
||||||
'mode/suspend transitions request their own repaint');
|
'mode/suspend transitions request their own repaint');
|
||||||
assert.match(source, /if \(\s*detectionNeedsFollowUpFrame\(\{[\s\S]*?\}\)\s*\) \{\s*\n\s*governorRequestRender\('detection-animation'\);/,
|
assert.match(source, /if \(\s*detectionNeedsFollowUpFrame\(\{[\s\S]*?\}\)\s*\) \{\s*\n\s*governorRequestRender\('detection-animation', _engine\);/,
|
||||||
'the follow-up frame is gated on the policy, never unconditional');
|
'the follow-up frame is gated on the policy, never unconditional');
|
||||||
|
|
||||||
// The demand call must read the FRAME's timestamp, not a fresh sample.
|
// The demand call must read the FRAME's timestamp, not a fresh sample.
|
||||||
@@ -339,7 +339,7 @@ test('detection holds nothing, and asks for its own frames instead', async () =>
|
|||||||
'the valve asks the shared policy instead of inlining its own threshold');
|
'the valve asks the shared policy instead of inlining its own threshold');
|
||||||
assert.doesNotMatch(shouldPaint, /_lastPaintMs > 22/,
|
assert.doesNotMatch(shouldPaint, /_lastPaintMs > 22/,
|
||||||
'the old inline threshold must be gone, not shadowing the policy');
|
'the old inline threshold must be gone, not shadowing the policy');
|
||||||
assert.match(shouldPaint, /if \(decision\.requestFollowUp\)\s*governorRequestRender\('detection-paint-skipped'\)/,
|
assert.match(shouldPaint, /if \(decision\.requestFollowUp\)\s*governorRequestRender\('detection-paint-skipped', _engine\)/,
|
||||||
'skipping a paint must hand the request forward, not swallow it');
|
'skipping a paint must hand the request forward, not swallow it');
|
||||||
|
|
||||||
// The scanline must not go back to the frame counter: that is what made a
|
// The scanline must not go back to the frame counter: that is what made a
|
||||||
@@ -478,13 +478,13 @@ test('aircraft brackets stay prompt because the aircraft layers hold the render
|
|||||||
assert.ok(enable, `${file}: enable() is still identifiable`);
|
assert.ok(enable, `${file}: enable() is still identifiable`);
|
||||||
assert.match(
|
assert.match(
|
||||||
enable,
|
enable,
|
||||||
/holdContinuousRender\('(flights|military)'\)/,
|
/holdContinuousRender\(\s*'(flights|military)',\s*flightState\._renderer\?\.engine,?\s*\)/,
|
||||||
`${file}: enabling the layer must hold continuous render — detection no longer ` +
|
`${file}: enabling the layer must hold continuous render — detection no longer ` +
|
||||||
'holds one, so this is what keeps its AIR brackets repainting on a parked scene',
|
'holds one, so this is what keeps its AIR brackets repainting on a parked scene',
|
||||||
);
|
);
|
||||||
assert.match(
|
assert.match(
|
||||||
source,
|
source,
|
||||||
/releaseContinuousRender\('(flights|military)'\)/,
|
/releaseContinuousRender\(\s*'(flights|military)',\s*flightState\._renderer\?\.engine,?\s*\)/,
|
||||||
`${file}: and the hold must be released, or the governor can never idle`,
|
`${file}: and the hold must be released, or the governor can never idle`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,8 +17,7 @@ import {
|
|||||||
import { DataLayerManager } from './manager.js';
|
import { DataLayerManager } from './manager.js';
|
||||||
import {
|
import {
|
||||||
getRenderGovernorDiagnostics,
|
getRenderGovernorDiagnostics,
|
||||||
installRenderGovernor,
|
renderGovernorFor,
|
||||||
_resetRenderGovernorForTest,
|
|
||||||
} from '../renderGovernor.js';
|
} from '../renderGovernor.js';
|
||||||
import { loadLayerRenderers } from '../app/layers/renderers.js';
|
import { loadLayerRenderers } from '../app/layers/renderers.js';
|
||||||
|
|
||||||
@@ -214,6 +213,7 @@ test('a quake poll still reaches the screen with the render loop idle', async ()
|
|||||||
const originalFetch = globalThis.fetch;
|
const originalFetch = globalThis.fetch;
|
||||||
const renderRequests = [];
|
const renderRequests = [];
|
||||||
const ground = createFakeGroundHarness();
|
const ground = createFakeGroundHarness();
|
||||||
|
ground.engine.render.request = () => renderRequests.push(Date.now());
|
||||||
const viewer = {
|
const viewer = {
|
||||||
scene: { requestRenderMode: false, requestRender: () => renderRequests.push(Date.now()) },
|
scene: { requestRenderMode: false, requestRender: () => renderRequests.push(Date.now()) },
|
||||||
};
|
};
|
||||||
@@ -231,8 +231,7 @@ test('a quake poll still reaches the screen with the render loop idle', async ()
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
_resetRenderGovernorForTest();
|
renderGovernorFor(ground.engine).install();
|
||||||
installRenderGovernor(viewer);
|
|
||||||
// Installing the governor enters idle mode, which itself paints one settling
|
// Installing the governor enters idle mode, which itself paints one settling
|
||||||
// frame. Drop that from the baseline or the enable assertion below can pass on
|
// frame. Drop that from the baseline or the enable assertion below can pass on
|
||||||
// the install alone, without the manager ever requesting anything.
|
// the install alone, without the manager ever requesting anything.
|
||||||
@@ -244,9 +243,9 @@ test('a quake poll still reaches the screen with the render loop idle', async ()
|
|||||||
await manager.setEnabled('earthquakes', true, { origin: 'test' });
|
await manager.setEnabled('earthquakes', true, { origin: 'test' });
|
||||||
|
|
||||||
// The whole point of the change: enabling quakes must not pin the loop on.
|
// The whole point of the change: enabling quakes must not pin the loop on.
|
||||||
assert.equal(getRenderGovernorDiagnostics().mode, 'idle', 'quakes must not force continuous render');
|
assert.equal(getRenderGovernorDiagnostics(ground.engine).mode, 'idle', 'quakes must not force continuous render');
|
||||||
assert.deepEqual(getRenderGovernorDiagnostics().holds, []);
|
assert.deepEqual(getRenderGovernorDiagnostics(ground.engine).holds, []);
|
||||||
assert.equal(viewer.scene.requestRenderMode, true, 'the governor really is in idle mode');
|
assert.equal(ground.engine.render.isContinuous(), false, 'the governor really is in idle mode');
|
||||||
|
|
||||||
// ...and the poll that populated the discs must still have asked for a frame,
|
// ...and the poll that populated the discs must still have asked for a frame,
|
||||||
// because in idle mode nothing repaints on its own.
|
// because in idle mode nothing repaints on its own.
|
||||||
@@ -255,7 +254,7 @@ test('a quake poll still reaches the screen with the render loop idle', async ()
|
|||||||
// Named, not merely counted: the frame has to come from the manager's
|
// Named, not merely counted: the frame has to come from the manager's
|
||||||
// visibility request, not from some incidental repaint.
|
// visibility request, not from some incidental repaint.
|
||||||
assert.ok(
|
assert.ok(
|
||||||
getRenderGovernorDiagnostics().recentRequests.some(({ reason }) => reason === 'layer-visibility'),
|
getRenderGovernorDiagnostics(ground.engine).recentRequests.some(({ reason }) => reason === 'layer-visibility'),
|
||||||
'the enable frame must be the manager\'s layer-visibility request',
|
'the enable frame must be the manager\'s layer-visibility request',
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -267,7 +266,7 @@ test('a quake poll still reaches the screen with the render loop idle', async ()
|
|||||||
renderRequests.length > beforeRefresh,
|
renderRequests.length > beforeRefresh,
|
||||||
'each refresh tick must request its own render while the loop is idle',
|
'each refresh tick must request its own render while the loop is idle',
|
||||||
);
|
);
|
||||||
const reasons = getRenderGovernorDiagnostics().recentRequests.map(({ reason }) => reason);
|
const reasons = getRenderGovernorDiagnostics(ground.engine).recentRequests.map(({ reason }) => reason);
|
||||||
assert.ok(
|
assert.ok(
|
||||||
reasons.includes('layer-tick:earthquakes'),
|
reasons.includes('layer-tick:earthquakes'),
|
||||||
`the tick request must be attributed to the layer, got ${JSON.stringify(reasons)}`,
|
`the tick request must be attributed to the layer, got ${JSON.stringify(reasons)}`,
|
||||||
@@ -275,7 +274,6 @@ test('a quake poll still reaches the screen with the render loop idle', async ()
|
|||||||
} finally {
|
} finally {
|
||||||
globalThis.fetch = originalFetch;
|
globalThis.fetch = originalFetch;
|
||||||
await manager.setEnabled('earthquakes', false, { origin: 'test' }).catch(() => {});
|
await manager.setEnabled('earthquakes', false, { origin: 'test' }).catch(() => {});
|
||||||
_resetRenderGovernorForTest();
|
|
||||||
ground.destroy();
|
ground.destroy();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -32,9 +32,8 @@ import aisLiveVesselsLayer, {
|
|||||||
} from './aisLiveVessels.js';
|
} from './aisLiveVessels.js';
|
||||||
import militaryInstallationsLayer from './militaryInstallations.js';
|
import militaryInstallationsLayer from './militaryInstallations.js';
|
||||||
import {
|
import {
|
||||||
_resetRenderGovernorForTest,
|
|
||||||
getRenderGovernorDiagnostics,
|
getRenderGovernorDiagnostics,
|
||||||
installRenderGovernor,
|
renderGovernorFor,
|
||||||
} from '../renderGovernor.js';
|
} from '../renderGovernor.js';
|
||||||
import {
|
import {
|
||||||
AWARENESS_RADIUS_M,
|
AWARENESS_RADIUS_M,
|
||||||
@@ -54,7 +53,10 @@ import { registerLayerRenderers } from '../app/layers/renderers.js';
|
|||||||
// the layer through the renderer registry, under an engine kind of its own.
|
// the layer through the renderer registry, under an engine kind of its own.
|
||||||
const VIEWER_ENGINE = 'awareness-viewer-test';
|
const VIEWER_ENGINE = 'awareness-viewer-test';
|
||||||
registerLayerRenderers(VIEWER_ENGINE, {
|
registerLayerRenderers(VIEWER_ENGINE, {
|
||||||
'military-awareness': ({ viewer }) => ({
|
'military-awareness': (engine) => {
|
||||||
|
const { viewer } = engine;
|
||||||
|
return {
|
||||||
|
engine,
|
||||||
container: viewer.container,
|
container: viewer.container,
|
||||||
canvas: viewer.canvas,
|
canvas: viewer.canvas,
|
||||||
get camera() {
|
get camera() {
|
||||||
@@ -70,9 +72,21 @@ registerLayerRenderers(VIEWER_ENGINE, {
|
|||||||
viewer.camera.flyToBoundingSphere({ center, radius }, { duration }),
|
viewer.camera.flyToBoundingSphere({ center, radius }, { duration }),
|
||||||
createRim: () => ({ move() {}, setAlpha() {}, destroy() {} }),
|
createRim: () => ({ move() {}, setAlpha() {}, destroy() {} }),
|
||||||
createPulses: () => null,
|
createPulses: () => null,
|
||||||
}),
|
};
|
||||||
|
},
|
||||||
});
|
});
|
||||||
const viewerEngine = (viewer) => ({ kind: VIEWER_ENGINE, viewer });
|
const viewerEngines = new WeakMap();
|
||||||
|
const viewerEngine = (viewer) => {
|
||||||
|
if (!viewerEngines.has(viewer)) viewerEngines.set(viewer, {
|
||||||
|
kind: VIEWER_ENGINE, viewer,
|
||||||
|
render: {
|
||||||
|
request: () => viewer.scene.requestRender?.(),
|
||||||
|
setContinuous: (enabled) => { viewer.scene.requestRenderMode = !enabled; },
|
||||||
|
isContinuous: () => viewer.scene.requestRenderMode === false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return viewerEngines.get(viewer);
|
||||||
|
};
|
||||||
|
|
||||||
const militaryAwarenessSource = readLayerSource(
|
const militaryAwarenessSource = readLayerSource(
|
||||||
new URL('./militaryAwareness.js', import.meta.url),
|
new URL('./militaryAwareness.js', import.meta.url),
|
||||||
@@ -3922,13 +3936,12 @@ test('parked Contacts takes no continuous-render hold and requests at most the p
|
|||||||
{ icao24: 'neighbour-flight', position: neighbour, distanceM: 4000 },
|
{ icao24: 'neighbour-flight', position: neighbour, distanceM: 4000 },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
_resetRenderGovernorForTest();
|
|
||||||
const runtime = installAwarenessRuntime();
|
const runtime = installAwarenessRuntime();
|
||||||
const holds = () => getRenderGovernorDiagnostics().holds;
|
const holds = () => getRenderGovernorDiagnostics(viewerEngine(runtime.viewer)).holds;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Date.now = () => clockMs;
|
Date.now = () => clockMs;
|
||||||
installRenderGovernor(runtime.viewer);
|
renderGovernorFor(viewerEngine(runtime.viewer)).install();
|
||||||
runtime.viewer.camera = {
|
runtime.viewer.camera = {
|
||||||
positionWC: new Cesium.Cartesian3(1_000_000, 200_000, 300_000),
|
positionWC: new Cesium.Cartesian3(1_000_000, 200_000, 300_000),
|
||||||
heading: 0,
|
heading: 0,
|
||||||
@@ -4003,7 +4016,6 @@ test('parked Contacts takes no continuous-render hold and requests at most the p
|
|||||||
Date.now = realNow;
|
Date.now = realNow;
|
||||||
runtime.restore();
|
runtime.restore();
|
||||||
restoreCollections();
|
restoreCollections();
|
||||||
_resetRenderGovernorForTest();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -4058,13 +4070,12 @@ test('a moving view over failed feeds takes no continuous-render hold', () => {
|
|||||||
// Every dependency reports unavailable, so every cohort summarizes to count
|
// Every dependency reports unavailable, so every cohort summarizes to count
|
||||||
// null — the exact state the old Boolean(state.results) gate still held for.
|
// null — the exact state the old Boolean(state.results) gate still held for.
|
||||||
const restoreCollections = stubAwarenessCollections({});
|
const restoreCollections = stubAwarenessCollections({});
|
||||||
_resetRenderGovernorForTest();
|
|
||||||
const runtime = installAwarenessRuntime({ isEnabled: () => false });
|
const runtime = installAwarenessRuntime({ isEnabled: () => false });
|
||||||
const holds = () => getRenderGovernorDiagnostics().holds;
|
const holds = () => getRenderGovernorDiagnostics(viewerEngine(runtime.viewer)).holds;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Date.now = () => clockMs;
|
Date.now = () => clockMs;
|
||||||
installRenderGovernor(runtime.viewer);
|
renderGovernorFor(viewerEngine(runtime.viewer)).install();
|
||||||
runtime.viewer.camera = {
|
runtime.viewer.camera = {
|
||||||
positionWC: new Cesium.Cartesian3(1_000_000, 200_000, 300_000),
|
positionWC: new Cesium.Cartesian3(1_000_000, 200_000, 300_000),
|
||||||
heading: 0,
|
heading: 0,
|
||||||
@@ -4096,7 +4107,6 @@ test('a moving view over failed feeds takes no continuous-render hold', () => {
|
|||||||
Date.now = realNow;
|
Date.now = realNow;
|
||||||
runtime.restore();
|
runtime.restore();
|
||||||
restoreCollections();
|
restoreCollections();
|
||||||
_resetRenderGovernorForTest();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -19,9 +19,8 @@ import {
|
|||||||
} from './groundFloor.js';
|
} from './groundFloor.js';
|
||||||
import { _resetFireAnchorsForTest } from './fireAnchors.js';
|
import { _resetFireAnchorsForTest } from './fireAnchors.js';
|
||||||
import {
|
import {
|
||||||
_resetRenderGovernorForTest,
|
|
||||||
getRenderGovernorDiagnostics,
|
getRenderGovernorDiagnostics,
|
||||||
installRenderGovernor,
|
renderGovernorFor,
|
||||||
} from '../renderGovernor.js';
|
} from '../renderGovernor.js';
|
||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
import { getContextStore, registerEntityContext, selectEntityContext, getSelectedEntityContext, clearSelectedEntityContextForLayer } from './contextStore.js';
|
import { getContextStore, registerEntityContext, selectEntityContext, getSelectedEntityContext, clearSelectedEntityContextForLayer } from './contextStore.js';
|
||||||
@@ -283,7 +282,6 @@ async function runInstallationLoad({
|
|||||||
const originalFetch = globalThis.fetch;
|
const originalFetch = globalThis.fetch;
|
||||||
const requests = [];
|
const requests = [];
|
||||||
const contextEvents = [];
|
const contextEvents = [];
|
||||||
_resetRenderGovernorForTest();
|
|
||||||
globalThis.document = { addEventListener() {}, removeEventListener() {} };
|
globalThis.document = { addEventListener() {}, removeEventListener() {} };
|
||||||
globalThis.window = {
|
globalThis.window = {
|
||||||
dispatchEvent(event) {
|
dispatchEvent(event) {
|
||||||
@@ -312,17 +310,8 @@ async function runInstallationLoad({
|
|||||||
return { ok: true, status: 200, json: async () => payload };
|
return { ok: true, status: 200, json: async () => payload };
|
||||||
};
|
};
|
||||||
const engine = engineOver(VIEWPORT);
|
const engine = engineOver(VIEWPORT);
|
||||||
// Enough of a viewer for the real render governor, so one-shot render
|
|
||||||
// requests are observable.
|
|
||||||
const viewer = {
|
|
||||||
scene: {
|
|
||||||
requestRenderMode: false,
|
|
||||||
maximumRenderTimeChange: 0,
|
|
||||||
requestRender() {},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
militaryInstallationsLayer.init(null, { engine });
|
militaryInstallationsLayer.init(null, { engine });
|
||||||
installRenderGovernor(viewer);
|
renderGovernorFor(engine).install();
|
||||||
militaryInstallationsLayer.enable();
|
militaryInstallationsLayer.enable();
|
||||||
await militaryInstallationsLayer.update();
|
await militaryInstallationsLayer.update();
|
||||||
|
|
||||||
@@ -338,11 +327,11 @@ async function runInstallationLoad({
|
|||||||
engine.fake.input('click', { position: { x: 0, y: 0 } });
|
engine.fake.input('click', { position: { x: 0, y: 0 } });
|
||||||
engine.fake.unscript('pick');
|
engine.fake.unscript('pick');
|
||||||
},
|
},
|
||||||
renderRequests: () => getRenderGovernorDiagnostics().recentRequests.map((item) => item.reason),
|
renderRequests: () => getRenderGovernorDiagnostics(engine).recentRequests.map((item) => item.reason),
|
||||||
restore() {
|
restore() {
|
||||||
militaryInstallationsLayer.destroy();
|
militaryInstallationsLayer.destroy();
|
||||||
|
renderGovernorFor(engine).destroy();
|
||||||
engine.destroy();
|
engine.destroy();
|
||||||
_resetRenderGovernorForTest();
|
|
||||||
globalThis.fetch = originalFetch;
|
globalThis.fetch = originalFetch;
|
||||||
if (originalDocument === undefined) delete globalThis.document;
|
if (originalDocument === undefined) delete globalThis.document;
|
||||||
else globalThis.document = originalDocument;
|
else globalThis.document = originalDocument;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { createRenderGovernor } from '../renderGovernor.js';
|
||||||
import { createPickRegistry } from './pickRegistry.js';
|
import { createPickRegistry } from './pickRegistry.js';
|
||||||
import { createSelectionDispatcher } from './selectionDispatcher.js';
|
import { createSelectionDispatcher } from './selectionDispatcher.js';
|
||||||
|
|
||||||
@@ -8,17 +9,20 @@ const scenes = new WeakMap();
|
|||||||
/** Create the services for one attachment, including reuse of a parked engine. */
|
/** Create the services for one attachment, including reuse of a parked engine. */
|
||||||
export function createSceneServices(engine, options = {}) {
|
export function createSceneServices(engine, options = {}) {
|
||||||
scenes.get(engine)?.destroy();
|
scenes.get(engine)?.destroy();
|
||||||
|
const render = createRenderGovernor(engine);
|
||||||
const picking = createPickRegistry();
|
const picking = createPickRegistry();
|
||||||
const selection = createSelectionDispatcher(engine, { ...options, picking });
|
const selection = createSelectionDispatcher(engine, { ...options, picking });
|
||||||
let destroyed = false;
|
let destroyed = false;
|
||||||
const services = {
|
const services = {
|
||||||
picking,
|
picking,
|
||||||
selection,
|
selection,
|
||||||
|
render,
|
||||||
destroy() {
|
destroy() {
|
||||||
if (destroyed) return;
|
if (destroyed) return;
|
||||||
destroyed = true;
|
destroyed = true;
|
||||||
selection.destroy();
|
selection.destroy();
|
||||||
picking.destroy();
|
picking.destroy();
|
||||||
|
render.destroy();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
scenes.set(engine, services);
|
scenes.set(engine, services);
|
||||||
|
|||||||
@@ -45,9 +45,7 @@ import { createCesiumSurface } from './surface.js';
|
|||||||
* imagery.js) are test seams, `createStage` and `createSonar` too
|
* imagery.js) are test seams, `createStage` and `createSonar` too
|
||||||
* (render.js).
|
* (render.js).
|
||||||
*
|
*
|
||||||
* `render.setContinuous` takes a hold on the app's singleton renderGovernor
|
* `render` owns this scene's requests and continuous-render mode.
|
||||||
* (src/renderGovernor.js), which governs the one viewer it was installed on;
|
|
||||||
* so one Cesium engine per page, on that viewer.
|
|
||||||
* @returns {import('../contract.js').GlobeEngine}
|
* @returns {import('../contract.js').GlobeEngine}
|
||||||
*/
|
*/
|
||||||
export function createCesiumEngine(viewer, options = {}) {
|
export function createCesiumEngine(viewer, options = {}) {
|
||||||
|
|||||||
@@ -2,11 +2,6 @@ import test from 'node:test';
|
|||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
import { assertGlobeEngine, engineCapabilities } from '../contract.js';
|
import { assertGlobeEngine, engineCapabilities } from '../contract.js';
|
||||||
import {
|
|
||||||
_resetRenderGovernorForTest,
|
|
||||||
getRenderGovernorDiagnostics,
|
|
||||||
installRenderGovernor,
|
|
||||||
} from '../../renderGovernor.js';
|
|
||||||
import { createContractSuite } from '../testing/contractSuite.mjs';
|
import { createContractSuite } from '../testing/contractSuite.mjs';
|
||||||
import { createCesiumEngine } from './createCesiumEngine.js';
|
import { createCesiumEngine } from './createCesiumEngine.js';
|
||||||
import { createCameraViewer } from './testing/cameraViewer.mjs';
|
import { createCameraViewer } from './testing/cameraViewer.mjs';
|
||||||
@@ -15,8 +10,6 @@ import {
|
|||||||
createFakeViewer,
|
createFakeViewer,
|
||||||
} from './testing/fakeViewer.mjs';
|
} from './testing/fakeViewer.mjs';
|
||||||
|
|
||||||
test.afterEach(() => _resetRenderGovernorForTest());
|
|
||||||
|
|
||||||
function snapshot(viewer) {
|
function snapshot(viewer) {
|
||||||
const { scene } = viewer;
|
const { scene } = viewer;
|
||||||
return {
|
return {
|
||||||
@@ -85,7 +78,6 @@ test('the render option reaches render.request', () => {
|
|||||||
|
|
||||||
test('destroy releases holds, listeners, handlers and engine-owned map state', async () => {
|
test('destroy releases holds, listeners, handlers and engine-owned map state', async () => {
|
||||||
const viewer = createFakeViewer();
|
const viewer = createFakeViewer();
|
||||||
installRenderGovernor(viewer);
|
|
||||||
const handlers = createFakeEventHandlers();
|
const handlers = createFakeEventHandlers();
|
||||||
const engine = createCesiumEngine(viewer, {
|
const engine = createCesiumEngine(viewer, {
|
||||||
createEventHandler: handlers.create,
|
createEventHandler: handlers.create,
|
||||||
@@ -104,7 +96,7 @@ test('destroy releases holds, listeners, handlers and engine-owned map state', a
|
|||||||
assert.equal(viewer.scene.camera.changed.numberOfListeners, 0);
|
assert.equal(viewer.scene.camera.changed.numberOfListeners, 0);
|
||||||
assert.equal(viewer.scene.postRender.numberOfListeners, 0);
|
assert.equal(viewer.scene.postRender.numberOfListeners, 0);
|
||||||
assert.equal(handlers.handlers[0].destroyed, true);
|
assert.equal(handlers.handlers[0].destroyed, true);
|
||||||
assert.deepEqual(getRenderGovernorDiagnostics().holds, []);
|
assert.equal(viewer.scene.requestRenderMode, true);
|
||||||
assert.deepEqual(viewer.staticCredits, []);
|
assert.deepEqual(viewer.staticCredits, []);
|
||||||
assert.deepEqual(viewer.imageryList, []);
|
assert.deepEqual(viewer.imageryList, []);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,15 +1,5 @@
|
|||||||
// GlobeRender on the app's render governor, which owns Cesium's
|
// Cesium owns its render mode and scene effects. Scene services drive this API.
|
||||||
// requestRenderMode and maximumRenderTimeChange; the engine only takes a
|
|
||||||
// hold under its own owner id. The full-frame effects (visual styles,
|
|
||||||
// bloom, sharpening) are the scene's post stages (postStages.js), the
|
|
||||||
// contact sonar the scene's native draw commands (sonar.js).
|
|
||||||
|
|
||||||
import {
|
|
||||||
governorRequestRender,
|
|
||||||
holdContinuousRender,
|
|
||||||
isRenderGovernorInstalled,
|
|
||||||
releaseContinuousRender,
|
|
||||||
} from '../../renderGovernor.js';
|
|
||||||
import { CONTACT_SONAR_INITIAL, mergeContactSonar } from '../contract.js';
|
import { CONTACT_SONAR_INITIAL, mergeContactSonar } from '../contract.js';
|
||||||
import { createCesiumPostStages } from './postStages.js';
|
import { createCesiumPostStages } from './postStages.js';
|
||||||
import { createCesiumSonar } from './sonar.js';
|
import { createCesiumSonar } from './sonar.js';
|
||||||
@@ -21,8 +11,7 @@ export const RENDER_OWNER = 'globe-engine';
|
|||||||
* @param {{ requestRender?: (reason: string) => void,
|
* @param {{ requestRender?: (reason: string) => void,
|
||||||
* createStage?: (options: object) => object,
|
* createStage?: (options: object) => object,
|
||||||
* createSonar?: typeof createCesiumSonar }} [options] `requestRender`
|
* createSonar?: typeof createCesiumSonar }} [options] `requestRender`
|
||||||
* defaults to the render governor when it governs this viewer, else the
|
* defaults to the scene itself;
|
||||||
* scene itself (before the governor is installed, or on another viewer);
|
|
||||||
* `createStage` (postStages.js) and `createSonar` are test seams.
|
* `createStage` (postStages.js) and `createSonar` are test seams.
|
||||||
* @returns {{ render: import('../contract.js').GlobeRender,
|
* @returns {{ render: import('../contract.js').GlobeRender,
|
||||||
* sonarDiagnostics: () => object, destroy: () => void }}
|
* sonarDiagnostics: () => object, destroy: () => void }}
|
||||||
@@ -30,15 +19,12 @@ export const RENDER_OWNER = 'globe-engine';
|
|||||||
export function createCesiumRender(
|
export function createCesiumRender(
|
||||||
viewer,
|
viewer,
|
||||||
{
|
{
|
||||||
requestRender = (reason) => {
|
requestRender = () => viewer.scene.requestRender?.(),
|
||||||
if (isRenderGovernorInstalled(viewer)) governorRequestRender(reason);
|
|
||||||
else viewer.scene.requestRender?.();
|
|
||||||
},
|
|
||||||
createStage,
|
createStage,
|
||||||
createSonar = createCesiumSonar,
|
createSonar = createCesiumSonar,
|
||||||
} = {},
|
} = {},
|
||||||
) {
|
) {
|
||||||
let held = false;
|
viewer.scene.maximumRenderTimeChange = Infinity;
|
||||||
const effects = createCesiumPostStages(viewer, {
|
const effects = createCesiumPostStages(viewer, {
|
||||||
requestRender,
|
requestRender,
|
||||||
...(createStage ? { createStage } : {}),
|
...(createStage ? { createStage } : {}),
|
||||||
@@ -69,11 +55,8 @@ export function createCesiumRender(
|
|||||||
requestRender(reason);
|
requestRender(reason);
|
||||||
},
|
},
|
||||||
setContinuous(enabled) {
|
setContinuous(enabled) {
|
||||||
held = Boolean(enabled);
|
viewer.scene.requestRenderMode = !enabled;
|
||||||
if (held) holdContinuousRender(RENDER_OWNER);
|
|
||||||
else releaseContinuousRender(RENDER_OWNER);
|
|
||||||
},
|
},
|
||||||
// The scene's real mode: another owner's hold keeps it continuous.
|
|
||||||
isContinuous: () => viewer.scene.requestRenderMode === false,
|
isContinuous: () => viewer.scene.requestRenderMode === false,
|
||||||
...effects.render,
|
...effects.render,
|
||||||
setContactSonar(options) {
|
setContactSonar(options) {
|
||||||
@@ -93,7 +76,7 @@ export function createCesiumRender(
|
|||||||
render,
|
render,
|
||||||
sonarDiagnostics: () => sonar.diagnostics(),
|
sonarDiagnostics: () => sonar.diagnostics(),
|
||||||
destroy() {
|
destroy() {
|
||||||
if (held) render.setContinuous(false);
|
render.setContinuous(false);
|
||||||
removeFrameWatch?.();
|
removeFrameWatch?.();
|
||||||
sonar.destroy();
|
sonar.destroy();
|
||||||
effects.destroy();
|
effects.destroy();
|
||||||
|
|||||||
@@ -1,17 +1,8 @@
|
|||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import {
|
|
||||||
_resetRenderGovernorForTest,
|
|
||||||
getRenderGovernorDiagnostics,
|
|
||||||
holdContinuousRender,
|
|
||||||
installRenderGovernor,
|
|
||||||
} from '../../renderGovernor.js';
|
|
||||||
import { createCesiumRender, RENDER_OWNER } from './render.js';
|
import { createCesiumRender, RENDER_OWNER } from './render.js';
|
||||||
import { createFakeViewer } from './testing/fakeViewer.mjs';
|
import { createFakeViewer } from './testing/fakeViewer.mjs';
|
||||||
|
|
||||||
test.beforeEach(() => _resetRenderGovernorForTest());
|
|
||||||
test.after(() => _resetRenderGovernorForTest());
|
|
||||||
|
|
||||||
test('request goes through the injected requestRender with a reason', () => {
|
test('request goes through the injected requestRender with a reason', () => {
|
||||||
const viewer = createFakeViewer();
|
const viewer = createFakeViewer();
|
||||||
const reasons = [];
|
const reasons = [];
|
||||||
@@ -19,84 +10,37 @@ test('request goes through the injected requestRender with a reason', () => {
|
|||||||
requestRender: (reason) => reasons.push(reason),
|
requestRender: (reason) => reasons.push(reason),
|
||||||
});
|
});
|
||||||
render.request();
|
render.request();
|
||||||
assert.deepEqual(reasons, [RENDER_OWNER]);
|
render.request('world-overlay');
|
||||||
|
assert.deepEqual(reasons, [RENDER_OWNER, 'world-overlay']);
|
||||||
assert.equal(viewer.callsOf('scene.requestRender').length, 0);
|
assert.equal(viewer.callsOf('scene.requestRender').length, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('request defaults to the render governor', () => {
|
test('creation disables time-driven renders and preserves the current mode', () => {
|
||||||
const viewer = createFakeViewer();
|
|
||||||
installRenderGovernor(viewer);
|
|
||||||
const { render } = createCesiumRender(viewer);
|
|
||||||
render.request();
|
|
||||||
assert.equal(viewer.callsOf('scene.requestRender').length, 1);
|
|
||||||
assert.deepEqual(
|
|
||||||
getRenderGovernorDiagnostics().recentRequests.map(({ reason }) => reason),
|
|
||||||
[RENDER_OWNER],
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('creation leaves the scene and the governor alone', () => {
|
|
||||||
const viewer = createFakeViewer();
|
const viewer = createFakeViewer();
|
||||||
viewer.scene.maximumRenderTimeChange = 0.5;
|
viewer.scene.maximumRenderTimeChange = 0.5;
|
||||||
|
viewer.scene.requestRenderMode = false;
|
||||||
createCesiumRender(viewer);
|
createCesiumRender(viewer);
|
||||||
assert.equal(viewer.scene.requestRenderMode, true);
|
|
||||||
assert.equal(viewer.scene.maximumRenderTimeChange, 0.5);
|
|
||||||
assert.equal(getRenderGovernorDiagnostics().installed, false);
|
|
||||||
assert.deepEqual(getRenderGovernorDiagnostics().holds, []);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('setContinuous holds and releases the governor under its own owner', () => {
|
|
||||||
const viewer = createFakeViewer();
|
|
||||||
installRenderGovernor(viewer);
|
|
||||||
const { render } = createCesiumRender(viewer);
|
|
||||||
assert.equal(render.isContinuous(), false);
|
|
||||||
render.setContinuous(true);
|
|
||||||
assert.deepEqual(getRenderGovernorDiagnostics().holds, [RENDER_OWNER]);
|
|
||||||
assert.equal(viewer.scene.requestRenderMode, false);
|
assert.equal(viewer.scene.requestRenderMode, false);
|
||||||
assert.equal(render.isContinuous(), true);
|
assert.equal(viewer.scene.maximumRenderTimeChange, Infinity);
|
||||||
render.setContinuous(false);
|
|
||||||
assert.deepEqual(getRenderGovernorDiagnostics().holds, []);
|
|
||||||
assert.equal(render.isContinuous(), false);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("isContinuous reports the scene's mode, including other owners' holds", () => {
|
test('render mode, requests and destruction affect only the owning scene', () => {
|
||||||
const viewer = createFakeViewer();
|
const first = createFakeViewer();
|
||||||
installRenderGovernor(viewer);
|
const second = createFakeViewer();
|
||||||
const { render } = createCesiumRender(viewer);
|
const a = createCesiumRender(first);
|
||||||
holdContinuousRender('flights');
|
const b = createCesiumRender(second);
|
||||||
render.setContinuous(false);
|
a.render.setContinuous(true);
|
||||||
assert.equal(render.isContinuous(), true);
|
b.render.setContinuous(true);
|
||||||
});
|
assert.equal(a.render.isContinuous(), true);
|
||||||
|
assert.equal(b.render.isContinuous(), true);
|
||||||
test('destroy releases the engine hold', () => {
|
a.render.setContinuous(false);
|
||||||
const viewer = createFakeViewer();
|
assert.equal(a.render.isContinuous(), false);
|
||||||
installRenderGovernor(viewer);
|
assert.equal(b.render.isContinuous(), true);
|
||||||
const { render, destroy } = createCesiumRender(viewer);
|
a.render.request('world-overlay');
|
||||||
render.setContinuous(true);
|
assert.equal(first.callsOf('scene.requestRender').length, 1);
|
||||||
destroy();
|
assert.equal(second.callsOf('scene.requestRender').length, 0);
|
||||||
assert.deepEqual(getRenderGovernorDiagnostics().holds, []);
|
a.destroy();
|
||||||
});
|
assert.equal(b.render.isContinuous(), true);
|
||||||
|
b.destroy();
|
||||||
test('request passes a caller reason through to the governor', () => {
|
assert.equal(b.render.isContinuous(), false);
|
||||||
const viewer = createFakeViewer();
|
|
||||||
installRenderGovernor(viewer);
|
|
||||||
const { render } = createCesiumRender(viewer);
|
|
||||||
render.request('world-overlay');
|
|
||||||
assert.deepEqual(
|
|
||||||
getRenderGovernorDiagnostics().recentRequests.map(({ reason }) => reason),
|
|
||||||
['world-overlay'],
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('without a governor on its viewer, request renders the scene directly', () => {
|
|
||||||
const viewer = createFakeViewer();
|
|
||||||
const { render } = createCesiumRender(viewer);
|
|
||||||
render.request();
|
|
||||||
assert.equal(viewer.callsOf('scene.requestRender').length, 1);
|
|
||||||
const governed = createFakeViewer();
|
|
||||||
installRenderGovernor(governed);
|
|
||||||
render.request();
|
|
||||||
assert.equal(viewer.callsOf('scene.requestRender').length, 2);
|
|
||||||
assert.equal(governed.callsOf('scene.requestRender').length, 0);
|
|
||||||
assert.deepEqual(getRenderGovernorDiagnostics().recentRequests, []);
|
|
||||||
});
|
});
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
import { loadBundledJson } from '../../data/bundledJson.js';
|
import { loadBundledJson } from '../../data/bundledJson.js';
|
||||||
import { governorRequestRender } from '../../renderGovernor.js';
|
|
||||||
import { buildStarVertices, STAR_VERTEX_FLOATS } from '../../sky/stars.js';
|
import { buildStarVertices, STAR_VERTEX_FLOATS } from '../../sky/stars.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -143,9 +142,7 @@ export class StarrySky {
|
|||||||
* @param {() => void} [options.requestRender] renders one frame when an
|
* @param {() => void} [options.requestRender] renders one frame when an
|
||||||
* asset or the precise rotation arrives; the idle governor by default.
|
* asset or the precise rotation arrives; the idle governor by default.
|
||||||
*/
|
*/
|
||||||
constructor({
|
constructor({ requestRender = () => {} } = {}) {
|
||||||
requestRender = () => governorRequestRender('sky-assets'),
|
|
||||||
} = {}) {
|
|
||||||
/** Matches `SkyBox#show`. */
|
/** Matches `SkyBox#show`. */
|
||||||
this.show = true;
|
this.show = true;
|
||||||
this.milkyWayIntensity = MILKY_WAY_INTENSITY;
|
this.milkyWayIntensity = MILKY_WAY_INTENSITY;
|
||||||
|
|||||||
@@ -387,7 +387,7 @@ export const loadCesiumPhotorealTileset = (keys) =>
|
|||||||
loadPhotorealisticTileset(Cesium, keys);
|
loadPhotorealisticTileset(Cesium, keys);
|
||||||
|
|
||||||
/** Cesium.Viewer options of the standard globe viewer. */
|
/** Cesium.Viewer options of the standard globe viewer. */
|
||||||
export function applicationViewerOptions(creditContainer) {
|
export function applicationViewerOptions(creditContainer, requestRender) {
|
||||||
return {
|
return {
|
||||||
timeline: false,
|
timeline: false,
|
||||||
animation: false,
|
animation: false,
|
||||||
@@ -403,7 +403,7 @@ export function applicationViewerOptions(creditContainer) {
|
|||||||
baseLayer: false,
|
baseLayer: false,
|
||||||
// Replaces the blocky Tycho-2 default. An option, not `skyBox: false`,
|
// Replaces the blocky Tycho-2 default. An option, not `skyBox: false`,
|
||||||
// which would also stop CesiumWidget from creating the Sun and Moon.
|
// which would also stop CesiumWidget from creating the Sun and Moon.
|
||||||
skyBox: new StarrySky(),
|
skyBox: new StarrySky({ requestRender }),
|
||||||
creditContainer,
|
creditContainer,
|
||||||
msaaSamples: 4,
|
msaaSamples: 4,
|
||||||
contextOptions: { webgl: { preserveDrawingBuffer: true } },
|
contextOptions: { webgl: { preserveDrawingBuffer: true } },
|
||||||
@@ -416,7 +416,9 @@ export function createApplicationViewer({ container, creditContainer }) {
|
|||||||
throw new TypeError('Viewer and credit containers are required');
|
throw new TypeError('Viewer and credit containers are required');
|
||||||
const viewer = new Cesium.Viewer(
|
const viewer = new Cesium.Viewer(
|
||||||
container,
|
container,
|
||||||
applicationViewerOptions(creditContainer),
|
applicationViewerOptions(creditContainer, () =>
|
||||||
|
viewer.scene.requestRender(),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
viewer.targetFrameRate = 60;
|
viewer.targetFrameRate = 60;
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
import { governorRequestRender } from '../../renderGovernor.js';
|
|
||||||
|
|
||||||
/** Whole-Earth input limit; explicit camera destinations can go farther. */
|
/** Whole-Earth input limit; explicit camera destinations can go farther. */
|
||||||
export const MAXIMUM_ZOOM_DISTANCE_M = 40_000_000;
|
export const MAXIMUM_ZOOM_DISTANCE_M = 40_000_000;
|
||||||
@@ -84,7 +83,7 @@ function readInput(aggregator, bindings) {
|
|||||||
export function installZoomOutGuard(
|
export function installZoomOutGuard(
|
||||||
viewer,
|
viewer,
|
||||||
{
|
{
|
||||||
requestRender = governorRequestRender,
|
requestRender = () => viewer.scene.requestRender(),
|
||||||
subscribeCameraHandoff,
|
subscribeCameraHandoff,
|
||||||
createAggregator = (canvas) => new Cesium.CameraEventAggregator(canvas),
|
createAggregator = (canvas) => new Cesium.CameraEventAggregator(canvas),
|
||||||
} = {},
|
} = {},
|
||||||
|
|||||||
@@ -83,7 +83,10 @@ export function createAircraftSceneBinding({
|
|||||||
const renderer = flightState._renderer;
|
const renderer = flightState._renderer;
|
||||||
if (!renderer) return;
|
if (!renderer) return;
|
||||||
removeSceneListeners();
|
removeSceneListeners();
|
||||||
services.render.releaseContinuousRender(id);
|
services.render.releaseContinuousRender(
|
||||||
|
id,
|
||||||
|
flightState._renderer?.engine,
|
||||||
|
);
|
||||||
flightState._trackedCameraFrameStop?.();
|
flightState._trackedCameraFrameStop?.();
|
||||||
flightState._trackedCameraFrameStop = null;
|
flightState._trackedCameraFrameStop = null;
|
||||||
if (flightState._trackedEntity) {
|
if (flightState._trackedEntity) {
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export function createAlprCamerasLayer({
|
|||||||
if (state.status === status && state.error === error) return;
|
if (state.status === status && state.error === error) return;
|
||||||
state.status = status;
|
state.status = status;
|
||||||
state.error = error;
|
state.error = error;
|
||||||
governorRequestRender('alpr-status');
|
governorRequestRender('alpr-status', sceneEngine);
|
||||||
}
|
}
|
||||||
|
|
||||||
function scheduleUnavailableRetry() {
|
function scheduleUnavailableRetry() {
|
||||||
@@ -260,7 +260,7 @@ export function createAlprCamerasLayer({
|
|||||||
state.pendingQueryBox = null;
|
state.pendingQueryBox = null;
|
||||||
state.loading = false;
|
state.loading = false;
|
||||||
state.retrying = false;
|
state.retrying = false;
|
||||||
governorRequestRender('alpr-status');
|
governorRequestRender('alpr-status', sceneEngine);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -296,7 +296,7 @@ export function createAlprDisplay(engine, { state, services, source }) {
|
|||||||
}
|
}
|
||||||
resolveFloors();
|
resolveFloors();
|
||||||
flushSprites();
|
flushSprites();
|
||||||
governorRequestRender('alpr-surface');
|
governorRequestRender('alpr-surface', engine);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -352,7 +352,7 @@ export function createAlprDisplay(engine, { state, services, source }) {
|
|||||||
}
|
}
|
||||||
if (!moved) return;
|
if (!moved) return;
|
||||||
flushSprites();
|
flushSprites();
|
||||||
governorRequestRender('alpr-floor');
|
governorRequestRender('alpr-floor', engine);
|
||||||
});
|
});
|
||||||
floorRequest = request;
|
floorRequest = request;
|
||||||
}
|
}
|
||||||
@@ -399,7 +399,7 @@ export function createAlprDisplay(engine, { state, services, source }) {
|
|||||||
creditTimer = null;
|
creditTimer = null;
|
||||||
removeCredit?.();
|
removeCredit?.();
|
||||||
removeCredit = null;
|
removeCredit = null;
|
||||||
governorRequestRender('alpr-credit');
|
governorRequestRender('alpr-credit', engine);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -419,7 +419,7 @@ export function createAlprDisplay(engine, { state, services, source }) {
|
|||||||
creditPresented = true;
|
creditPresented = true;
|
||||||
removeCredit = engine.credits.addStatic(creditMarkup, { onScreen: true });
|
removeCredit = engine.credits.addStatic(creditMarkup, { onScreen: true });
|
||||||
creditTimer = setTimeout(hideOnMapCredit, CREDIT_DISPLAY_MS);
|
creditTimer = setTimeout(hideOnMapCredit, CREDIT_DISPLAY_MS);
|
||||||
governorRequestRender('alpr-credit');
|
governorRequestRender('alpr-credit', engine);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderRecords() {
|
function renderRecords() {
|
||||||
@@ -442,7 +442,7 @@ export function createAlprDisplay(engine, { state, services, source }) {
|
|||||||
dropSelectedSprite();
|
dropSelectedSprite();
|
||||||
clearSelectedEntityContextForLayer(LAYER_ID);
|
clearSelectedEntityContextForLayer(LAYER_ID);
|
||||||
}
|
}
|
||||||
governorRequestRender('alpr-render');
|
governorRequestRender('alpr-render', engine);
|
||||||
const ids = new Set(shown.map((record) => record.id));
|
const ids = new Set(shown.map((record) => record.id));
|
||||||
let changed = false;
|
let changed = false;
|
||||||
for (const id of [...markers.keys()])
|
for (const id of [...markers.keys()])
|
||||||
@@ -553,7 +553,7 @@ export function createAlprDisplay(engine, { state, services, source }) {
|
|||||||
updateSelectedAnchor();
|
updateSelectedAnchor();
|
||||||
overlay.sync(visibleRecords);
|
overlay.sync(visibleRecords);
|
||||||
flushSprites();
|
flushSprites();
|
||||||
governorRequestRender('alpr-selection');
|
governorRequestRender('alpr-selection', engine);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -566,7 +566,7 @@ export function createAlprDisplay(engine, { state, services, source }) {
|
|||||||
overlay.sync(visibleRecords);
|
overlay.sync(visibleRecords);
|
||||||
flushSprites();
|
flushSprites();
|
||||||
clearSelectedEntityContextForLayer(LAYER_ID);
|
clearSelectedEntityContextForLayer(LAYER_ID);
|
||||||
governorRequestRender('alpr-selection');
|
governorRequestRender('alpr-selection', engine);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -600,7 +600,7 @@ export function createAlprDisplay(engine, { state, services, source }) {
|
|||||||
geo.cartesian3.distance(next, marker.displayPosition) > 0.1
|
geo.cartesian3.distance(next, marker.displayPosition) > 0.1
|
||||||
) {
|
) {
|
||||||
marker.displayPosition = next;
|
marker.displayPosition = next;
|
||||||
governorRequestRender('alpr-anchor');
|
governorRequestRender('alpr-anchor', engine);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -637,7 +637,7 @@ export function createAlprDisplay(engine, { state, services, source }) {
|
|||||||
range: 800,
|
range: 800,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
governorRequestRender('alpr-focus');
|
governorRequestRender('alpr-focus', engine);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -111,8 +111,9 @@ export function createLifecycle({
|
|||||||
|
|
||||||
function syncAwarenessRenderHold() {
|
function syncAwarenessRenderHold() {
|
||||||
if (layerState.enabled && parts.model.awarenessNeedsContinuousRender())
|
if (layerState.enabled && parts.model.awarenessNeedsContinuousRender())
|
||||||
holdContinuousRender('military-awareness');
|
holdContinuousRender('military-awareness', layerState.display?.engine);
|
||||||
else releaseContinuousRender('military-awareness');
|
else
|
||||||
|
releaseContinuousRender('military-awareness', layerState.display?.engine);
|
||||||
}
|
}
|
||||||
|
|
||||||
function attachRuntimeListeners() {
|
function attachRuntimeListeners() {
|
||||||
@@ -218,7 +219,7 @@ export function createLifecycle({
|
|||||||
layerState.cameraMoving = false;
|
layerState.cameraMoving = false;
|
||||||
// No frames will arrive to run the per-frame release once the listener is
|
// No frames will arrive to run the per-frame release once the listener is
|
||||||
// gone, so drop the hold here.
|
// gone, so drop the hold here.
|
||||||
releaseContinuousRender('military-awareness');
|
releaseContinuousRender('military-awareness', layerState.display?.engine);
|
||||||
parts.rendering.cancelDirectionOverlayUpdate();
|
parts.rendering.cancelDirectionOverlayUpdate();
|
||||||
layerState.runtimeListenersAttached = false;
|
layerState.runtimeListenersAttached = false;
|
||||||
}
|
}
|
||||||
@@ -320,7 +321,7 @@ export function createLifecycle({
|
|||||||
|
|
||||||
disable() {
|
disable() {
|
||||||
layerState.enabled = false;
|
layerState.enabled = false;
|
||||||
releaseContinuousRender('military-awareness');
|
releaseContinuousRender('military-awareness', layerState.display?.engine);
|
||||||
detachRuntimeListeners();
|
detachRuntimeListeners();
|
||||||
parts.panel.stopAwarenessPageRotation();
|
parts.panel.stopAwarenessPageRotation();
|
||||||
layerState.cohortPages.clear();
|
layerState.cohortPages.clear();
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ export function createAwarenessDisplay(engine) {
|
|||||||
});
|
});
|
||||||
const style = AWARENESS_RANGE_RING_STYLE;
|
const style = AWARENESS_RANGE_RING_STYLE;
|
||||||
return {
|
return {
|
||||||
|
engine,
|
||||||
container: engine.host.container,
|
container: engine.host.container,
|
||||||
canvas: engine.host.canvas,
|
canvas: engine.host.canvas,
|
||||||
// Camera-shaped: positionWC, heading, rightWC, upWC, refreshed per frame.
|
// Camera-shaped: positionWC, heading, rightWC, upWC, refreshed per frame.
|
||||||
|
|||||||
@@ -457,7 +457,7 @@ export function createRendering({
|
|||||||
visual.rim.destroy();
|
visual.rim.destroy();
|
||||||
visual.disc?.destroy();
|
visual.disc?.destroy();
|
||||||
// Idle mode renders only on request; a removed ring must not linger.
|
// Idle mode renders only on request; a removed ring must not linger.
|
||||||
governorRequestRender('awareness-visual');
|
governorRequestRender('awareness-visual', layerState.display?.engine);
|
||||||
}
|
}
|
||||||
layerState.visual = null;
|
layerState.visual = null;
|
||||||
}
|
}
|
||||||
@@ -529,7 +529,7 @@ export function createRendering({
|
|||||||
if (performance.now() < wake.untilMs && !belowRim())
|
if (performance.now() < wake.untilMs && !belowRim())
|
||||||
timer = globalThis.setTimeout(() => {
|
timer = globalThis.setTimeout(() => {
|
||||||
timer = null;
|
timer = null;
|
||||||
governorRequestRender('awareness-pulse');
|
governorRequestRender('awareness-pulse', layerState.display?.engine);
|
||||||
}, CYBER_SONAR_RENDER_INTERVAL_MS);
|
}, CYBER_SONAR_RENDER_INTERVAL_MS);
|
||||||
});
|
});
|
||||||
setAwarenessEchoField({
|
setAwarenessEchoField({
|
||||||
@@ -612,7 +612,7 @@ export function createRendering({
|
|||||||
stopPulses,
|
stopPulses,
|
||||||
removeMotionListener: watchReducedMotion(),
|
removeMotionListener: watchReducedMotion(),
|
||||||
};
|
};
|
||||||
governorRequestRender('awareness-visual');
|
governorRequestRender('awareness-visual', layerState.display?.engine);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderVisual(subject) {
|
function renderVisual(subject) {
|
||||||
@@ -642,7 +642,7 @@ export function createRendering({
|
|||||||
if (performance.now() < visual.wake?.untilMs)
|
if (performance.now() < visual.wake?.untilMs)
|
||||||
visual.disc?.move(groundCenter);
|
visual.disc?.move(groundCenter);
|
||||||
// Content actually changed — buy exactly one frame instead of holding.
|
// Content actually changed — buy exactly one frame instead of holding.
|
||||||
governorRequestRender('awareness-visual');
|
governorRequestRender('awareness-visual', layerState.display?.engine);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export function createStationPoints({ state, services, parts }, points) {
|
|||||||
function ensureCityPoints(cityId, stationMap) {
|
function ensureCityPoints(cityId, stationMap) {
|
||||||
// Deferred debounce/fetch completions change points after the camera
|
// Deferred debounce/fetch completions change points after the camera
|
||||||
// settled: each commit needs one frame in idle mode.
|
// settled: each commit needs one frame in idle mode.
|
||||||
services.render.governorRequestRender('bikeshare-points');
|
services.render.governorRequestRender('bikeshare-points', state._engine);
|
||||||
const runtime = ensureCityRuntime(cityId);
|
const runtime = ensureCityRuntime(cityId);
|
||||||
|
|
||||||
for (const station of stationMap.values()) {
|
for (const station of stationMap.values()) {
|
||||||
@@ -120,7 +120,7 @@ export function createStationPoints({ state, services, parts }, points) {
|
|||||||
* @param {Map<string, Object>} statusMap - Parsed station status map.
|
* @param {Map<string, Object>} statusMap - Parsed station status map.
|
||||||
*/
|
*/
|
||||||
function applyStatusToPoints(cityId, statusMap) {
|
function applyStatusToPoints(cityId, statusMap) {
|
||||||
services.render.governorRequestRender('bikeshare-status');
|
services.render.governorRequestRender('bikeshare-status', state._engine);
|
||||||
const runtime = state._cityRuntime.get(cityId);
|
const runtime = state._cityRuntime.get(cityId);
|
||||||
if (!runtime) return;
|
if (!runtime) return;
|
||||||
|
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export function createViewport({ state: layerState, services, parts, camera }) {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
function deactivateCity(cityId) {
|
function deactivateCity(cityId) {
|
||||||
governorRequestRender('bikeshare-deactivate');
|
governorRequestRender('bikeshare-deactivate', layerState._engine);
|
||||||
parts.ingestion.abortInFlight(layerState._inFlightInfo, cityId);
|
parts.ingestion.abortInFlight(layerState._inFlightInfo, cityId);
|
||||||
parts.ingestion.abortInFlight(layerState._inFlightStatus, cityId);
|
parts.ingestion.abortInFlight(layerState._inFlightStatus, cityId);
|
||||||
parts.rendering?.removeCityPoints(cityId);
|
parts.rendering?.removeCityPoints(cityId);
|
||||||
|
|||||||
Vendored
+2
-2
@@ -153,9 +153,9 @@ export function createControls({ state: layerState, services, parts, source }) {
|
|||||||
layerState._gizmo?.setEnabled(true);
|
layerState._gizmo?.setEnabled(true);
|
||||||
// ADJUST mode: gizmo drags mutate entity geometry from pointer events,
|
// ADJUST mode: gizmo drags mutate entity geometry from pointer events,
|
||||||
// which don't trigger renders in requestRenderMode. (perf wave 2)
|
// which don't trigger renders in requestRenderMode. (perf wave 2)
|
||||||
holdContinuousRender('cctv-adjust');
|
holdContinuousRender('cctv-adjust', parts.renderer?.engine);
|
||||||
} else {
|
} else {
|
||||||
releaseContinuousRender('cctv-adjust');
|
releaseContinuousRender('cctv-adjust', parts.renderer?.engine);
|
||||||
layerState._gizmo?.setEnabled(false);
|
layerState._gizmo?.setEnabled(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -453,7 +453,10 @@ export function createLifecycle({
|
|||||||
if (layerState._calibrationMode) {
|
if (layerState._calibrationMode) {
|
||||||
parts.calibration.ensureGizmo();
|
parts.calibration.ensureGizmo();
|
||||||
layerState._gizmo?.setEnabled(true);
|
layerState._gizmo?.setEnabled(true);
|
||||||
services.render.holdContinuousRender('cctv-adjust');
|
services.render.holdContinuousRender(
|
||||||
|
'cctv-adjust',
|
||||||
|
parts.renderer?.engine,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
settleView();
|
settleView();
|
||||||
},
|
},
|
||||||
@@ -481,7 +484,7 @@ export function createLifecycle({
|
|||||||
null;
|
null;
|
||||||
layerState._gizmo?.destroy();
|
layerState._gizmo?.destroy();
|
||||||
layerState._gizmo = null;
|
layerState._gizmo = null;
|
||||||
releaseContinuousRender('cctv-adjust');
|
releaseContinuousRender('cctv-adjust', parts.renderer?.engine);
|
||||||
layerState._removeClick?.();
|
layerState._removeClick?.();
|
||||||
layerState._removeClick = null;
|
layerState._removeClick = null;
|
||||||
parts.projection.stopProjectionLoop();
|
parts.projection.stopProjectionLoop();
|
||||||
@@ -580,7 +583,7 @@ export function createLifecycle({
|
|||||||
unregisterPickOwner('cctv', parts.renderer?.engine);
|
unregisterPickOwner('cctv', parts.renderer?.engine);
|
||||||
// ADJUST mode does not survive a layer toggle — predictable re-entry.
|
// ADJUST mode does not survive a layer toggle — predictable re-entry.
|
||||||
layerState._calibrationMode = false;
|
layerState._calibrationMode = false;
|
||||||
releaseContinuousRender('cctv-adjust');
|
releaseContinuousRender('cctv-adjust', parts.renderer?.engine);
|
||||||
layerState._gizmo?.setEnabled(false);
|
layerState._gizmo?.setEnabled(false);
|
||||||
layerState._removeFocusAppearListener?.();
|
layerState._removeFocusAppearListener?.();
|
||||||
layerState._removeFocusAppearListener = null;
|
layerState._removeFocusAppearListener = null;
|
||||||
@@ -631,7 +634,7 @@ export function createLifecycle({
|
|||||||
layerState._gizmo = null;
|
layerState._gizmo = null;
|
||||||
}
|
}
|
||||||
layerState._calibrationMode = false;
|
layerState._calibrationMode = false;
|
||||||
releaseContinuousRender('cctv-adjust');
|
releaseContinuousRender('cctv-adjust', parts.renderer?.engine);
|
||||||
layerState._removeClick?.();
|
layerState._removeClick?.();
|
||||||
layerState._removeClick = null;
|
layerState._removeClick = null;
|
||||||
parts.projection.stopProjectionLoop();
|
parts.projection.stopProjectionLoop();
|
||||||
|
|||||||
@@ -389,7 +389,7 @@ export function createProjection({
|
|||||||
layerState._projectionRaf = 0;
|
layerState._projectionRaf = 0;
|
||||||
layerState._projectionTimer = 0;
|
layerState._projectionTimer = 0;
|
||||||
if (!parts.renderer?.ready || !parts.model.projectionLoopIsNeeded()) {
|
if (!parts.renderer?.ready || !parts.model.projectionLoopIsNeeded()) {
|
||||||
releaseContinuousRender('cctv-projection');
|
releaseContinuousRender('cctv-projection', parts.renderer?.engine);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -401,7 +401,10 @@ export function createProjection({
|
|||||||
if (active.projection && !active.projection.video) {
|
if (active.projection && !active.projection.video) {
|
||||||
parts.frames.drawProjectionFrame(active);
|
parts.frames.drawProjectionFrame(active);
|
||||||
if (parts.frames.refreshProjectionTextures(active))
|
if (parts.frames.refreshProjectionTextures(active))
|
||||||
governorRequestRender('cctv-projection-image');
|
governorRequestRender(
|
||||||
|
'cctv-projection-image',
|
||||||
|
parts.renderer?.engine,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -412,12 +415,12 @@ export function createProjection({
|
|||||||
layerState._activeFocusStyleCount,
|
layerState._activeFocusStyleCount,
|
||||||
);
|
);
|
||||||
if (continuous) {
|
if (continuous) {
|
||||||
holdContinuousRender('cctv-projection');
|
holdContinuousRender('cctv-projection', parts.renderer?.engine);
|
||||||
layerState._projectionRaf = requestAnimationFrame(tick);
|
layerState._projectionRaf = requestAnimationFrame(tick);
|
||||||
} else {
|
} else {
|
||||||
// Stills retain the existing fetch/texture cadence without rendering
|
// Stills retain the existing fetch/texture cadence without rendering
|
||||||
// unchanged pixels at display refresh rate.
|
// unchanged pixels at display refresh rate.
|
||||||
releaseContinuousRender('cctv-projection');
|
releaseContinuousRender('cctv-projection', parts.renderer?.engine);
|
||||||
layerState._projectionTimer = setTimeout(
|
layerState._projectionTimer = setTimeout(
|
||||||
tick,
|
tick,
|
||||||
PROJECTION_TEXTURE_SWAP_MS,
|
PROJECTION_TEXTURE_SWAP_MS,
|
||||||
@@ -438,7 +441,7 @@ export function createProjection({
|
|||||||
cancelAnimationFrame(layerState._projectionRaf);
|
cancelAnimationFrame(layerState._projectionRaf);
|
||||||
layerState._projectionRaf = 0;
|
layerState._projectionRaf = 0;
|
||||||
}
|
}
|
||||||
releaseContinuousRender('cctv-projection');
|
releaseContinuousRender('cctv-projection', parts.renderer?.engine);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -999,8 +999,9 @@ export function createDirectionsLayer({
|
|||||||
const shouldHold = _enabled && _routeDrawn;
|
const shouldHold = _enabled && _routeDrawn;
|
||||||
if (shouldHold === _renderHeld) return;
|
if (shouldHold === _renderHeld) return;
|
||||||
_renderHeld = shouldHold;
|
_renderHeld = shouldHold;
|
||||||
if (shouldHold) services.render.holdContinuousRender?.('directions');
|
if (shouldHold)
|
||||||
else services.render.releaseContinuousRender?.('directions');
|
services.render.holdContinuousRender?.('directions', _engine);
|
||||||
|
else services.render.releaseContinuousRender?.('directions', _engine);
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearRouteGraphics() {
|
function clearRouteGraphics() {
|
||||||
@@ -1053,7 +1054,7 @@ export function createDirectionsLayer({
|
|||||||
pushSteps();
|
pushSteps();
|
||||||
// A card already open on a step that has just been anchored moves with it.
|
// A card already open on a step that has just been anchored moves with it.
|
||||||
if (_selectedStep !== null) refreshStepCard(_selectedStep);
|
if (_selectedStep !== null) refreshStepCard(_selectedStep);
|
||||||
services.render.governorRequestRender('directions-anchor');
|
services.render.governorRequestRender('directions-anchor', _engine);
|
||||||
if (!unresolved) {
|
if (!unresolved) {
|
||||||
_anchorAttempts = 0;
|
_anchorAttempts = 0;
|
||||||
_anchorStartedAt = 0;
|
_anchorStartedAt = 0;
|
||||||
@@ -1089,7 +1090,7 @@ export function createDirectionsLayer({
|
|||||||
_anchorAttempts = 0;
|
_anchorAttempts = 0;
|
||||||
_anchorStartedAt = Date.now();
|
_anchorStartedAt = Date.now();
|
||||||
anchorStepPoints();
|
anchorStepPoints();
|
||||||
services.render.governorRequestRender('directions-route');
|
services.render.governorRequestRender('directions-route', _engine);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function requestRoute() {
|
async function requestRoute() {
|
||||||
@@ -1149,7 +1150,7 @@ export function createDirectionsLayer({
|
|||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
if (_routeAbort === controller) _routeAbort = null;
|
if (_routeAbort === controller) _routeAbort = null;
|
||||||
if (seq === _routeSeq) notifyRow();
|
if (seq === _routeSeq) notifyRow();
|
||||||
services.render.governorRequestRender('directions-route');
|
services.render.governorRequestRender('directions-route', _engine);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1214,7 +1215,7 @@ export function createDirectionsLayer({
|
|||||||
placeMarker(which, point);
|
placeMarker(which, point);
|
||||||
if (_a && _b) void requestRoute();
|
if (_a && _b) void requestRoute();
|
||||||
else notifyRow();
|
else notifyRow();
|
||||||
services.render.governorRequestRender('directions-place');
|
services.render.governorRequestRender('directions-place', _engine);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1277,7 +1278,7 @@ export function createDirectionsLayer({
|
|||||||
placeMarker('a', null);
|
placeMarker('a', null);
|
||||||
placeMarker('b', null);
|
placeMarker('b', null);
|
||||||
clearRouteGraphics();
|
clearRouteGraphics();
|
||||||
services.render.governorRequestRender('directions-clear');
|
services.render.governorRequestRender('directions-clear', _engine);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1434,7 +1435,7 @@ export function createDirectionsLayer({
|
|||||||
}
|
}
|
||||||
_selectedStep = index;
|
_selectedStep = index;
|
||||||
_display?.selectStep(index);
|
_display?.selectStep(index);
|
||||||
services.render.governorRequestRender('directions-select');
|
services.render.governorRequestRender('directions-select', _engine);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1601,7 +1602,7 @@ export function createDirectionsLayer({
|
|||||||
}
|
}
|
||||||
if (patch.fly) flyCurrentRoute();
|
if (patch.fly) flyCurrentRoute();
|
||||||
notifyRow();
|
notifyRow();
|
||||||
services.render.governorRequestRender('directions-params');
|
services.render.governorRequestRender('directions-params', _engine);
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -193,7 +193,7 @@ for (const makeHarness of GROUND_HARNESSES) {
|
|||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
assert.ok(
|
assert.ok(
|
||||||
getRenderGovernorDiagnostics().holds.includes('directions'),
|
getRenderGovernorDiagnostics(f.engine).holds.includes('directions'),
|
||||||
'a drawn route holds continuous render',
|
'a drawn route holds continuous render',
|
||||||
);
|
);
|
||||||
if (f.cesium) {
|
if (f.cesium) {
|
||||||
@@ -215,7 +215,7 @@ for (const makeHarness of GROUND_HARNESSES) {
|
|||||||
f.layer.setParams({ clear: true });
|
f.layer.setParams({ clear: true });
|
||||||
assert.equal(f.set(IDS.route), undefined);
|
assert.equal(f.set(IDS.route), undefined);
|
||||||
assert.ok(
|
assert.ok(
|
||||||
!getRenderGovernorDiagnostics().holds.includes('directions'),
|
!getRenderGovernorDiagnostics(f.engine).holds.includes('directions'),
|
||||||
'a cleared route releases it',
|
'a cleared route releases it',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -150,7 +150,10 @@ export function createViewport({
|
|||||||
settleTimer = setTimeout(() => {
|
settleTimer = setTimeout(() => {
|
||||||
settleTimer = null;
|
settleTimer = null;
|
||||||
if (layerState._enabled && !layerState._destroyed)
|
if (layerState._enabled && !layerState._destroyed)
|
||||||
governorRequestRender('firms-lod-settle');
|
governorRequestRender(
|
||||||
|
'firms-lod-settle',
|
||||||
|
layerState._renderer?.engine,
|
||||||
|
);
|
||||||
}, LOD_CHECK_MS + 40);
|
}, LOD_CHECK_MS + 40);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,7 +115,8 @@ export function createLifecycle({
|
|||||||
const renderer = flightState._renderer;
|
const renderer = flightState._renderer;
|
||||||
flightState.enabled = true;
|
flightState.enabled = true;
|
||||||
renderer?.fleet.setVisible(true);
|
renderer?.fleet.setVisible(true);
|
||||||
if (renderer) holdContinuousRender('flights'); // per-frame animator
|
if (renderer)
|
||||||
|
holdContinuousRender('flights', flightState._renderer?.engine); // per-frame animator
|
||||||
renderer?.models.setVisible(true);
|
renderer?.models.setVisible(true);
|
||||||
parts.tracking._setCockpitContactMode(
|
parts.tracking._setCockpitContactMode(
|
||||||
globalThis.document?.body.classList.contains('cockpit-mode') === true,
|
globalThis.document?.body.classList.contains('cockpit-mode') === true,
|
||||||
@@ -181,7 +182,7 @@ export function createLifecycle({
|
|||||||
parts.controller._abortActiveUpdates();
|
parts.controller._abortActiveUpdates();
|
||||||
parts.tracking._cancelPendingTrackingRestore();
|
parts.tracking._cancelPendingTrackingRestore();
|
||||||
flightState._renderer?.fleet.setVisible(false);
|
flightState._renderer?.fleet.setVisible(false);
|
||||||
releaseContinuousRender('flights');
|
releaseContinuousRender('flights', flightState._renderer?.engine);
|
||||||
parts.rendering._releaseModels();
|
parts.rendering._releaseModels();
|
||||||
flightState._renderer?.models.setVisible(false);
|
flightState._renderer?.models.setVisible(false);
|
||||||
parts.tracking._clearTracking();
|
parts.tracking._clearTracking();
|
||||||
@@ -201,7 +202,7 @@ export function createLifecycle({
|
|||||||
flightState._enrichActive = 0;
|
flightState._enrichActive = 0;
|
||||||
flightState._enrichLastDispatchMs = 0;
|
flightState._enrichLastDispatchMs = 0;
|
||||||
parts.controller._abortActiveUpdates();
|
parts.controller._abortActiveUpdates();
|
||||||
releaseContinuousRender('flights'); // direct-destroy path (perf wave 2 fix)
|
releaseContinuousRender('flights', flightState._renderer?.engine); // direct-destroy path (perf wave 2 fix)
|
||||||
parts.tracking._clearTracking();
|
parts.tracking._clearTracking();
|
||||||
parts.tracking._destroyTrail();
|
parts.tracking._destroyTrail();
|
||||||
parts.tracking._cancelPendingTrackingRestore();
|
parts.tracking._cancelPendingTrackingRestore();
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export function createIngestion({
|
|||||||
if (layerState.status === status && layerState.error === error) return;
|
if (layerState.status === status && layerState.error === error) return;
|
||||||
layerState.status = status;
|
layerState.status = status;
|
||||||
layerState.error = error;
|
layerState.error = error;
|
||||||
governorRequestRender('installations-status');
|
governorRequestRender('installations-status', layerState.engine);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadInstallations() {
|
async function loadInstallations() {
|
||||||
|
|||||||
@@ -327,7 +327,7 @@ export function createInstallationsDisplay(
|
|||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
fill.commit();
|
fill.commit();
|
||||||
governorRequestRender('installation-selection-fill');
|
governorRequestRender('installation-selection-fill', engine);
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearRendered() {
|
function clearRendered() {
|
||||||
@@ -433,7 +433,7 @@ export function createInstallationsDisplay(
|
|||||||
}
|
}
|
||||||
// Post-moveEnd debounced fetches commit after the camera settles; the
|
// Post-moveEnd debounced fetches commit after the camera settles; the
|
||||||
// rebuilt sets need one frame in idle mode.
|
// rebuilt sets need one frame in idle mode.
|
||||||
governorRequestRender('installations-render');
|
governorRequestRender('installations-render', engine);
|
||||||
syncOsmCredit(state.records);
|
syncOsmCredit(state.records);
|
||||||
syncPlacesCredit(state.records);
|
syncPlacesCredit(state.records);
|
||||||
const records = renderableRecords();
|
const records = renderableRecords();
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ export function createNamedMarkers(
|
|||||||
|
|
||||||
function cameraChanged() {
|
function cameraChanged() {
|
||||||
refresh();
|
refresh();
|
||||||
services.render.governorRequestRender('installation-names');
|
services.render.governorRequestRender('installation-names', engine);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Small pose changes raise no camera event: test the discs before the
|
// Small pose changes raise no camera event: test the discs before the
|
||||||
@@ -232,7 +232,7 @@ export function createNamedMarkers(
|
|||||||
points.commit();
|
points.commit();
|
||||||
if (state.enabled) publishLabels();
|
if (state.enabled) publishLabels();
|
||||||
refresh();
|
refresh();
|
||||||
services.render.governorRequestRender('installation-names');
|
services.render.governorRequestRender('installation-names', engine);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ export function createLifecycle({
|
|||||||
);
|
);
|
||||||
parts.launchPad.initLaunchPadZonePrimitive();
|
parts.launchPad.initLaunchPadZonePrimitive();
|
||||||
if (layerState._enabled) {
|
if (layerState._enabled) {
|
||||||
holdContinuousRender('rocket-launches');
|
holdContinuousRender('rocket-launches', layerState._engine);
|
||||||
getRenderer().setVisible(true);
|
getRenderer().setVisible(true);
|
||||||
layerState._orbitMatches = 0;
|
layerState._orbitMatches = 0;
|
||||||
for (const launch of layerState._launches)
|
for (const launch of layerState._launches)
|
||||||
@@ -144,16 +144,17 @@ export function createLifecycle({
|
|||||||
layerState._satelliteTelemetry.clear();
|
layerState._satelliteTelemetry.clear();
|
||||||
layerState._replayTracks.clear();
|
layerState._replayTracks.clear();
|
||||||
parts.rendering.removeMissionElements();
|
parts.rendering.removeMissionElements();
|
||||||
|
if (layerState._enabled)
|
||||||
|
releaseContinuousRender('rocket-launches', layerState._engine);
|
||||||
layerState._renderer.destroy();
|
layerState._renderer.destroy();
|
||||||
layerState._renderer = layerState._engine = null;
|
layerState._renderer = layerState._engine = null;
|
||||||
if (layerState._enabled) releaseContinuousRender('rocket-launches');
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async enable() {
|
async enable() {
|
||||||
layerState._sourceController.abort();
|
layerState._sourceController.abort();
|
||||||
layerState._sourceController = new AbortController();
|
layerState._sourceController = new AbortController();
|
||||||
layerState._enabled = true;
|
layerState._enabled = true;
|
||||||
holdContinuousRender('rocket-launches'); // per-frame animator (perf wave 2)
|
holdContinuousRender('rocket-launches', layerState._engine); // per-frame animator (perf wave 2)
|
||||||
layerState._lifecycleToken++;
|
layerState._lifecycleToken++;
|
||||||
layerState._postTleRetryCount = 0;
|
layerState._postTleRetryCount = 0;
|
||||||
try {
|
try {
|
||||||
@@ -218,7 +219,7 @@ export function createLifecycle({
|
|||||||
entryView,
|
entryView,
|
||||||
layerState._entryLandedView,
|
layerState._entryLandedView,
|
||||||
);
|
);
|
||||||
releaseContinuousRender('rocket-launches');
|
releaseContinuousRender('rocket-launches', layerState._engine);
|
||||||
layerState._lifecycleToken++;
|
layerState._lifecycleToken++;
|
||||||
layerState._updateDirty = false;
|
layerState._updateDirty = false;
|
||||||
parts.ingestion.clearPostTleRetry();
|
parts.ingestion.clearPostTleRetry();
|
||||||
@@ -244,7 +245,7 @@ export function createLifecycle({
|
|||||||
layerState._resumeRenderer?.();
|
layerState._resumeRenderer?.();
|
||||||
layerState._waitForRenderer = layerState._resumeRenderer = null;
|
layerState._waitForRenderer = layerState._resumeRenderer = null;
|
||||||
layerState._sourceController.abort();
|
layerState._sourceController.abort();
|
||||||
releaseContinuousRender('rocket-launches'); // direct-destroy path (perf wave 2 fix)
|
releaseContinuousRender('rocket-launches', layerState._engine); // direct-destroy path (perf wave 2 fix)
|
||||||
layerState._enabled = false;
|
layerState._enabled = false;
|
||||||
layerState._lifecycleToken++;
|
layerState._lifecycleToken++;
|
||||||
layerState._updateDirty = false;
|
layerState._updateDirty = false;
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ export function createLocalAdsbLayer({
|
|||||||
onChange: () => {
|
onChange: () => {
|
||||||
for (const marker of markers.values()) refreshClass(marker);
|
for (const marker of markers.values()) refreshClass(marker);
|
||||||
refreshSelectedCard();
|
refreshSelectedCard();
|
||||||
governorRequestRender('local-adsb-enrichment');
|
governorRequestRender('local-adsb-enrichment', renderer?.engine);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -441,8 +441,8 @@ export function createLocalAdsbLayer({
|
|||||||
const want = Boolean(renderer) && enabled && markers.size > 0;
|
const want = Boolean(renderer) && enabled && markers.size > 0;
|
||||||
if (want === renderHeld) return;
|
if (want === renderHeld) return;
|
||||||
renderHeld = want;
|
renderHeld = want;
|
||||||
if (want) holdContinuousRender(RENDER_HOLD_ID);
|
if (want) holdContinuousRender(RENDER_HOLD_ID, renderer?.engine);
|
||||||
else releaseContinuousRender(RENDER_HOLD_ID);
|
else releaseContinuousRender(RENDER_HOLD_ID, renderer?.engine);
|
||||||
}
|
}
|
||||||
|
|
||||||
function requestEnrichment() {
|
function requestEnrichment() {
|
||||||
@@ -488,7 +488,8 @@ export function createLocalAdsbLayer({
|
|||||||
}
|
}
|
||||||
requestEnrichment();
|
requestEnrichment();
|
||||||
updateRenderHold();
|
updateRenderHold();
|
||||||
if (live.size || removed) governorRequestRender('local-adsb-update');
|
if (live.size || removed)
|
||||||
|
governorRequestRender('local-adsb-update', renderer?.engine);
|
||||||
// Detection re-solves labels only when the set of contacts changes, not
|
// Detection re-solves labels only when the set of contacts changes, not
|
||||||
// on every position report.
|
// on every position report.
|
||||||
if (added || removed) markSourcesChanged('local-adsb-update');
|
if (added || removed) markSourcesChanged('local-adsb-update');
|
||||||
@@ -521,7 +522,7 @@ export function createLocalAdsbLayer({
|
|||||||
marker.entity.gevLabelModel = cardModel(marker, now());
|
marker.entity.gevLabelModel = cardModel(marker, now());
|
||||||
selectEntityContext(marker.entity);
|
selectEntityContext(marker.entity);
|
||||||
if (!reselect) startTrail(marker);
|
if (!reselect) startTrail(marker);
|
||||||
governorRequestRender('local-adsb-selection');
|
governorRequestRender('local-adsb-selection', renderer?.engine);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -560,7 +561,7 @@ export function createLocalAdsbLayer({
|
|||||||
onEmptyClick() {
|
onEmptyClick() {
|
||||||
if (!enabled || !selectedId) return;
|
if (!enabled || !selectedId) return;
|
||||||
clearSelection();
|
clearSelection();
|
||||||
governorRequestRender('local-adsb-selection');
|
governorRequestRender('local-adsb-selection', renderer?.engine);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -667,6 +668,8 @@ export function createLocalAdsbLayer({
|
|||||||
releaseTrail();
|
releaseTrail();
|
||||||
models?.destroy();
|
models?.destroy();
|
||||||
models = null;
|
models = null;
|
||||||
|
releaseContinuousRender(RENDER_HOLD_ID, renderer.engine);
|
||||||
|
renderHeld = false;
|
||||||
renderer.detach();
|
renderer.detach();
|
||||||
renderer.destroy();
|
renderer.destroy();
|
||||||
renderer = null;
|
renderer = null;
|
||||||
|
|||||||
@@ -98,7 +98,8 @@ export function createLifecycle({
|
|||||||
const renderer = flightState._renderer;
|
const renderer = flightState._renderer;
|
||||||
flightState.enabled = true;
|
flightState.enabled = true;
|
||||||
renderer?.fleet.setVisible(true);
|
renderer?.fleet.setVisible(true);
|
||||||
if (renderer) holdContinuousRender('military'); // per-frame animator
|
if (renderer)
|
||||||
|
holdContinuousRender('military', flightState._renderer?.engine); // per-frame animator
|
||||||
renderer?.models.setVisible(true);
|
renderer?.models.setVisible(true);
|
||||||
parts.tracking._setCockpitContactMode(
|
parts.tracking._setCockpitContactMode(
|
||||||
globalThis.document?.body.classList.contains('cockpit-mode') === true,
|
globalThis.document?.body.classList.contains('cockpit-mode') === true,
|
||||||
@@ -161,7 +162,7 @@ export function createLifecycle({
|
|||||||
parts.controller._abortActiveUpdates();
|
parts.controller._abortActiveUpdates();
|
||||||
parts.tracking._cancelPendingTrackingRestore();
|
parts.tracking._cancelPendingTrackingRestore();
|
||||||
flightState._renderer?.fleet.setVisible(false);
|
flightState._renderer?.fleet.setVisible(false);
|
||||||
releaseContinuousRender('military');
|
releaseContinuousRender('military', flightState._renderer?.engine);
|
||||||
parts.rendering._releaseModels();
|
parts.rendering._releaseModels();
|
||||||
flightState._renderer?.models.setVisible(false);
|
flightState._renderer?.models.setVisible(false);
|
||||||
parts.tracking._clearTracking();
|
parts.tracking._clearTracking();
|
||||||
@@ -181,7 +182,7 @@ export function createLifecycle({
|
|||||||
setMilitaryLayerActive(false);
|
setMilitaryLayerActive(false);
|
||||||
flightState.lifetime.abort();
|
flightState.lifetime.abort();
|
||||||
parts.controller._abortActiveUpdates();
|
parts.controller._abortActiveUpdates();
|
||||||
releaseContinuousRender('military'); // direct-destroy path (perf wave 2 fix)
|
releaseContinuousRender('military', flightState._renderer?.engine); // direct-destroy path (perf wave 2 fix)
|
||||||
parts.tracking._clearTracking();
|
parts.tracking._clearTracking();
|
||||||
parts.tracking._destroyTrail();
|
parts.tracking._destroyTrail();
|
||||||
parts.tracking._cancelPendingTrackingRestore();
|
parts.tracking._cancelPendingTrackingRestore();
|
||||||
|
|||||||
@@ -227,7 +227,8 @@ export function createRendering({
|
|||||||
scheduleRadioOverlayPublish();
|
scheduleRadioOverlayPublish();
|
||||||
// The horizon timer can commit AFTER the camera settles and the governor
|
// The horizon timer can commit AFTER the camera settles and the governor
|
||||||
// parks the scene — a changed show flag needs one frame. (perf wave 2 fix)
|
// parks the scene — a changed show flag needs one frame. (perf wave 2 fix)
|
||||||
if (visibilityChanged) governorRequestRender('radio-horizon');
|
if (visibilityChanged)
|
||||||
|
governorRequestRender('radio-horizon', parts.renderer?.engine);
|
||||||
}
|
}
|
||||||
|
|
||||||
function reconcileStations(stations) {
|
function reconcileStations(stations) {
|
||||||
|
|||||||
@@ -126,13 +126,14 @@ export function createLifecycle({
|
|||||||
}
|
}
|
||||||
layerState._detectionObjects.clear();
|
layerState._detectionObjects.clear();
|
||||||
renderer.removePoints(layerState._pointCollection);
|
renderer.removePoints(layerState._pointCollection);
|
||||||
|
if (layerState._enabled)
|
||||||
|
releaseContinuousRender('satellites', layerState._engine);
|
||||||
renderer.destroy();
|
renderer.destroy();
|
||||||
layerState._engine =
|
layerState._engine =
|
||||||
layerState._renderer =
|
layerState._renderer =
|
||||||
layerState._pointCollection =
|
layerState._pointCollection =
|
||||||
null;
|
null;
|
||||||
layerState._overlayHost.clearSource(ISS_OVERLAY_SOURCE_ID);
|
layerState._overlayHost.clearSource(ISS_OVERLAY_SOURCE_ID);
|
||||||
if (layerState._enabled) releaseContinuousRender('satellites');
|
|
||||||
},
|
},
|
||||||
|
|
||||||
attachRenderer(
|
attachRenderer(
|
||||||
@@ -187,7 +188,7 @@ export function createLifecycle({
|
|||||||
|
|
||||||
enable() {
|
enable() {
|
||||||
layerState._enabled = true;
|
layerState._enabled = true;
|
||||||
holdContinuousRender('satellites'); // per-frame animator (perf wave 2)
|
holdContinuousRender('satellites', layerState._engine); // per-frame animator (perf wave 2)
|
||||||
if (layerState._pointCollection)
|
if (layerState._pointCollection)
|
||||||
layerState._pointCollection.show =
|
layerState._pointCollection.show =
|
||||||
parts.controls.satelliteVisualsVisible(
|
parts.controls.satelliteVisualsVisible(
|
||||||
@@ -225,7 +226,7 @@ export function createLifecycle({
|
|||||||
parts.catalog._abortActiveUpdates();
|
parts.catalog._abortActiveUpdates();
|
||||||
parts.tracking._cancelPendingTrackingRestore();
|
parts.tracking._cancelPendingTrackingRestore();
|
||||||
layerState._enabled = false;
|
layerState._enabled = false;
|
||||||
releaseContinuousRender('satellites');
|
releaseContinuousRender('satellites', layerState._engine);
|
||||||
if (layerState._pointCollection) layerState._pointCollection.show = false;
|
if (layerState._pointCollection) layerState._pointCollection.show = false;
|
||||||
for (const path of layerState._orbitPaths.values())
|
for (const path of layerState._orbitPaths.values())
|
||||||
path.primitive.show = false;
|
path.primitive.show = false;
|
||||||
@@ -252,7 +253,7 @@ export function createLifecycle({
|
|||||||
layerState._resumeRenderer?.();
|
layerState._resumeRenderer?.();
|
||||||
layerState._waitForRenderer = layerState._resumeRenderer = null;
|
layerState._waitForRenderer = layerState._resumeRenderer = null;
|
||||||
parts.catalog._abortActiveUpdates();
|
parts.catalog._abortActiveUpdates();
|
||||||
releaseContinuousRender('satellites'); // direct-destroy path (perf wave 2 fix)
|
releaseContinuousRender('satellites', layerState._engine); // direct-destroy path (perf wave 2 fix)
|
||||||
layerState._enabled = false;
|
layerState._enabled = false;
|
||||||
parts.tracking._clearTracking();
|
parts.tracking._clearTracking();
|
||||||
parts.tracking._cancelPendingTrackingRestore();
|
parts.tracking._cancelPendingTrackingRestore();
|
||||||
|
|||||||
@@ -95,7 +95,16 @@ export function createStreetLevelLayer({
|
|||||||
|
|
||||||
function providerContext(entry) {
|
function providerContext(entry) {
|
||||||
return Object.freeze({
|
return Object.freeze({
|
||||||
services: state.services,
|
services: {
|
||||||
|
...state.services,
|
||||||
|
render: {
|
||||||
|
governorRequestRender: (reason) =>
|
||||||
|
state.services.render?.governorRequestRender?.(
|
||||||
|
reason,
|
||||||
|
state.map?.engine,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
/** Resolved against now, so "since N days" keeps up with the clock. */
|
/** Resolved against now, so "since N days" keeps up with the clock. */
|
||||||
getFilter: () => resolveFilter(state.filter),
|
getFilter: () => resolveFilter(state.filter),
|
||||||
/** The layer and this provider's switch are both on. */
|
/** The layer and this provider's switch are both on. */
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ export function createLineCoverage({
|
|||||||
: Infinity;
|
: Infinity;
|
||||||
|
|
||||||
function requestRender() {
|
function requestRender() {
|
||||||
render?.governorRequestRender?.(renderKey);
|
render?.governorRequestRender?.(renderKey, state.map?.engine);
|
||||||
}
|
}
|
||||||
|
|
||||||
function tileKey({ x, y, z }) {
|
function tileKey({ x, y, z }) {
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export function createMarker({ state }) {
|
|||||||
const { render } = state.services;
|
const { render } = state.services;
|
||||||
|
|
||||||
function requestRender() {
|
function requestRender() {
|
||||||
render?.governorRequestRender?.('street-level-marker');
|
render?.governorRequestRender?.('street-level-marker', state.map?.engine);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensure(view) {
|
function ensure(view) {
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export function createSequenceCones({
|
|||||||
const { render } = state.services;
|
const { render } = state.services;
|
||||||
|
|
||||||
function requestRender() {
|
function requestRender() {
|
||||||
render?.governorRequestRender?.(renderKey);
|
render?.governorRequestRender?.(renderKey, state.map?.engine);
|
||||||
}
|
}
|
||||||
|
|
||||||
function notify() {
|
function notify() {
|
||||||
|
|||||||
@@ -29,9 +29,10 @@ export function createAnimation({
|
|||||||
layerState._renderHeld = needed;
|
layerState._renderHeld = needed;
|
||||||
if (needed) {
|
if (needed) {
|
||||||
layerState._lastAnimTime = 0;
|
layerState._lastAnimTime = 0;
|
||||||
services.render.holdContinuousRender('traffic');
|
services.render.holdContinuousRender('traffic', layerState._engine);
|
||||||
layerState._engine?.render.request('traffic');
|
layerState._engine?.render.request('traffic');
|
||||||
} else services.render.releaseContinuousRender('traffic');
|
} else
|
||||||
|
services.render.releaseContinuousRender('traffic', layerState._engine);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Writes the dots to the engine's display (once a frame, and on a clear). */
|
/** Writes the dots to the engine's display (once a frame, and on a clear). */
|
||||||
|
|||||||
@@ -199,7 +199,7 @@ export function createLifecycle({
|
|||||||
// The dots keep their look (`dot.point`) for the next engine.
|
// The dots keep their look (`dot.point`) for the next engine.
|
||||||
layerState._display?.destroy();
|
layerState._display?.destroy();
|
||||||
if (layerState._renderHeld)
|
if (layerState._renderHeld)
|
||||||
services.render.releaseContinuousRender('traffic');
|
services.render.releaseContinuousRender('traffic', layerState._engine);
|
||||||
layerState._renderHeld = false;
|
layerState._renderHeld = false;
|
||||||
layerState._engine = layerState._display = null;
|
layerState._engine = layerState._display = null;
|
||||||
layerState._waitForScene ??= new Promise((resolve) => {
|
layerState._waitForScene ??= new Promise((resolve) => {
|
||||||
|
|||||||
@@ -196,7 +196,7 @@ export function createHeight({ state, services, parts }) {
|
|||||||
// adopted by the next bounded reread.
|
// adopted by the next bounded reread.
|
||||||
for (const entry of entries) parts.trails.prepareEntry(entry);
|
for (const entry of entries) parts.trails.prepareEntry(entry);
|
||||||
parts.rendering.requestVisibility();
|
parts.rendering.requestVisibility();
|
||||||
governorRequestRender('transit-floor');
|
governorRequestRender('transit-floor', parts.renderer?.engine);
|
||||||
if (state._floorAttempts++ < FLOOR_REREAD_ATTEMPTS)
|
if (state._floorAttempts++ < FLOOR_REREAD_ATTEMPTS)
|
||||||
state._floorTimer = setTimeout(rereadFloors, FLOOR_REREAD_MS);
|
state._floorTimer = setTimeout(rereadFloors, FLOOR_REREAD_MS);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ export function createIngestion({ state, services, parts, source }) {
|
|||||||
// Nothing else would notice: the per-frame pass returns early on an empty
|
// Nothing else would notice: the per-frame pass returns early on an empty
|
||||||
// fleet, so the hold would sit there held by a fleet that no longer exists.
|
// fleet, so the hold would sit there held by a fleet that no longer exists.
|
||||||
parts.rendering.syncRenderHold();
|
parts.rendering.syncRenderHold();
|
||||||
governorRequestRender('transit-evict');
|
governorRequestRender('transit-evict', parts.renderer?.engine);
|
||||||
state._dataManager?.refreshLayerStats?.();
|
state._dataManager?.refreshLayerStats?.();
|
||||||
}
|
}
|
||||||
return removed;
|
return removed;
|
||||||
@@ -190,7 +190,7 @@ export function createIngestion({ state, services, parts, source }) {
|
|||||||
removeFeedVehicles(feed.id);
|
removeFeedVehicles(feed.id);
|
||||||
status.count = 0;
|
status.count = 0;
|
||||||
parts.rendering.syncRenderHold();
|
parts.rendering.syncRenderHold();
|
||||||
governorRequestRender('transit-stale-refused');
|
governorRequestRender('transit-stale-refused', parts.renderer?.engine);
|
||||||
state._dataManager?.refreshLayerStats?.();
|
state._dataManager?.refreshLayerStats?.();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -414,7 +414,7 @@ export function createIngestion({ state, services, parts, source }) {
|
|||||||
state._detectRevision++;
|
state._detectRevision++;
|
||||||
parts.rendering.requestVisibility();
|
parts.rendering.requestVisibility();
|
||||||
parts.rendering.syncRenderHold();
|
parts.rendering.syncRenderHold();
|
||||||
governorRequestRender('transit-poll');
|
governorRequestRender('transit-poll', parts.renderer?.engine);
|
||||||
state._dataManager?.refreshLayerStats?.();
|
state._dataManager?.refreshLayerStats?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -246,7 +246,8 @@ export function createLifecycle({
|
|||||||
state._detectBuiltAt = -Infinity;
|
state._detectBuiltAt = -Infinity;
|
||||||
state._visible.clear();
|
state._visible.clear();
|
||||||
state._moving.clear();
|
state._moving.clear();
|
||||||
if (state._renderHeld) releaseContinuousRender('transit');
|
if (state._renderHeld)
|
||||||
|
releaseContinuousRender('transit', parts.renderer?.engine);
|
||||||
state._renderHeld = false;
|
state._renderHeld = false;
|
||||||
parts.renderer.destroy();
|
parts.renderer.destroy();
|
||||||
parts.renderer =
|
parts.renderer =
|
||||||
@@ -309,7 +310,7 @@ export function createLifecycle({
|
|||||||
state._rotationPose = null;
|
state._rotationPose = null;
|
||||||
state._rotationDirty = false;
|
state._rotationDirty = false;
|
||||||
if (state._renderHeld) {
|
if (state._renderHeld) {
|
||||||
releaseContinuousRender('transit');
|
releaseContinuousRender('transit', parts.renderer?.engine);
|
||||||
state._renderHeld = false;
|
state._renderHeld = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -37,13 +37,11 @@ import {
|
|||||||
releaseCameraSensitivity,
|
releaseCameraSensitivity,
|
||||||
} from '../../data/cameraSensitivity.js';
|
} from '../../data/cameraSensitivity.js';
|
||||||
import {
|
import {
|
||||||
_resetRenderGovernorForTest,
|
|
||||||
getRenderGovernorDiagnostics,
|
getRenderGovernorDiagnostics,
|
||||||
governorRequestRender,
|
governorRequestRender,
|
||||||
holdContinuousRender,
|
holdContinuousRender,
|
||||||
installRenderGovernor,
|
renderGovernorFor,
|
||||||
releaseContinuousRender,
|
releaseContinuousRender,
|
||||||
uninstallRenderGovernor,
|
|
||||||
} from '../../renderGovernor.js';
|
} from '../../renderGovernor.js';
|
||||||
|
|
||||||
const BOSTON = { lat: 42.3601, lon: -71.0589 };
|
const BOSTON = { lat: 42.3601, lon: -71.0589 };
|
||||||
@@ -283,7 +281,7 @@ function harness(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
installRenderGovernor(viewer);
|
renderGovernorFor(engine).install();
|
||||||
layer.init(viewer, { engine });
|
layer.init(viewer, { engine });
|
||||||
|
|
||||||
t.after(() => {
|
t.after(() => {
|
||||||
@@ -293,8 +291,8 @@ function harness(
|
|||||||
/* a test may already have destroyed it */
|
/* a test may already have destroyed it */
|
||||||
}
|
}
|
||||||
engine.destroy();
|
engine.destroy();
|
||||||
uninstallRenderGovernor(viewer);
|
renderGovernorFor(engine).destroy();
|
||||||
_resetRenderGovernorForTest();
|
|
||||||
globalThis.fetch = originalFetch;
|
globalThis.fetch = originalFetch;
|
||||||
if (originalDocument === undefined) delete globalThis.document;
|
if (originalDocument === undefined) delete globalThis.document;
|
||||||
else globalThis.document = originalDocument;
|
else globalThis.document = originalDocument;
|
||||||
@@ -361,7 +359,7 @@ function harness(
|
|||||||
frame();
|
frame();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
holds: () => getRenderGovernorDiagnostics().holds,
|
holds: () => getRenderGovernorDiagnostics(engine).holds,
|
||||||
ground,
|
ground,
|
||||||
meshCalls,
|
meshCalls,
|
||||||
/**
|
/**
|
||||||
@@ -839,7 +837,7 @@ test('the render hold follows real motion, and is released when everything settl
|
|||||||
);
|
);
|
||||||
app.frame();
|
app.frame();
|
||||||
assert.equal(app.holds().includes('transit'), false);
|
assert.equal(app.holds().includes('transit'), false);
|
||||||
assert.equal(app.viewer.scene.requestRenderMode, true);
|
assert.equal(!app.engine.render.isContinuous(), true);
|
||||||
|
|
||||||
// A second fix IS motion to come: the vehicle is armed and the loop held.
|
// A second fix IS motion to come: the vehicle is armed and the loop held.
|
||||||
app.advance(TRANSIT_POLL_MS);
|
app.advance(TRANSIT_POLL_MS);
|
||||||
@@ -861,7 +859,7 @@ test('the render hold follows real motion, and is released when everything settl
|
|||||||
app.holds().includes('transit'),
|
app.holds().includes('transit'),
|
||||||
'a moving vehicle keeps the loop awake',
|
'a moving vehicle keeps the loop awake',
|
||||||
);
|
);
|
||||||
assert.equal(app.viewer.scene.requestRenderMode, false);
|
assert.equal(!app.engine.render.isContinuous(), false);
|
||||||
|
|
||||||
// Past the second fix it holds and the loop is released again.
|
// Past the second fix it holds and the loop is released again.
|
||||||
app.run(TRANSIT_POLL_MS);
|
app.run(TRANSIT_POLL_MS);
|
||||||
@@ -870,7 +868,7 @@ test('the render hold follows real motion, and is released when everything settl
|
|||||||
false,
|
false,
|
||||||
'a settled fleet does not hold the GPU',
|
'a settled fleet does not hold the GPU',
|
||||||
);
|
);
|
||||||
assert.equal(app.viewer.scene.requestRenderMode, true);
|
assert.equal(!app.engine.render.isContinuous(), true);
|
||||||
});
|
});
|
||||||
test('disable hands back the camera sensitivity it borrowed', async (t) => {
|
test('disable hands back the camera sensitivity it borrowed', async (t) => {
|
||||||
const app = harness(t);
|
const app = harness(t);
|
||||||
@@ -1112,7 +1110,7 @@ test('a parked fleet that keeps reporting the same position stops asking for fra
|
|||||||
);
|
);
|
||||||
assert.equal(app.state()._moving.size, 0);
|
assert.equal(app.state()._moving.size, 0);
|
||||||
}
|
}
|
||||||
assert.equal(app.viewer.scene.requestRenderMode, true);
|
assert.equal(!app.engine.render.isContinuous(), true);
|
||||||
assert.equal(app.vehicles().length, 2, 'and the fleet is still on screen');
|
assert.equal(app.vehicles().length, 2, 'and the fleet is still on screen');
|
||||||
|
|
||||||
// One vehicle finally moves: that IS work, and the hold comes back.
|
// One vehicle finally moves: that IS work, and the hold comes back.
|
||||||
@@ -1198,7 +1196,7 @@ test('the last vehicle leaving mid-segment takes the render hold with it', async
|
|||||||
false,
|
false,
|
||||||
'no hold may outlive the fleet that justified it',
|
'no hold may outlive the fleet that justified it',
|
||||||
);
|
);
|
||||||
assert.equal(app.viewer.scene.requestRenderMode, true);
|
assert.equal(!app.engine.render.isContinuous(), true);
|
||||||
assert.ok(Date.now() - fetchedAt > FEED_EVICT_AFTER_MS);
|
assert.ok(Date.now() - fetchedAt > FEED_EVICT_AFTER_MS);
|
||||||
});
|
});
|
||||||
test('only the vehicles near the view are animated frame by frame', async (t) => {
|
test('only the vehicles near the view are animated frame by frame', async (t) => {
|
||||||
@@ -3110,7 +3108,7 @@ for (const bounds of ['null', 'stale']) {
|
|||||||
parts.rendering.refreshVisibility();
|
parts.rendering.refreshVisibility();
|
||||||
assert.equal(state._visible.size, loaded.count);
|
assert.equal(state._visible.size, loaded.count);
|
||||||
assert.equal(state._moving.size, loaded.count);
|
assert.equal(state._moving.size, loaded.count);
|
||||||
assert.equal(app.viewer.scene.requestRenderMode, false);
|
assert.equal(!app.engine.render.isContinuous(), false);
|
||||||
const entry = app.vehicles()[0],
|
const entry = app.vehicles()[0],
|
||||||
before = Cesium.Cartesian3.clone(entry.marker.position);
|
before = Cesium.Cartesian3.clone(entry.marker.position);
|
||||||
app.run(1000, 16);
|
app.run(1000, 16);
|
||||||
@@ -3182,7 +3180,7 @@ for (const count of [800, 3000]) {
|
|||||||
before = Cesium.Cartesian3.clone(entry.marker.position);
|
before = Cesium.Cartesian3.clone(entry.marker.position);
|
||||||
app.run(1000, 16);
|
app.run(1000, 16);
|
||||||
assert.ok(Cesium.Cartesian3.distance(before, entry.marker.position) > 0.9);
|
assert.ok(Cesium.Cartesian3.distance(before, entry.marker.position) > 0.9);
|
||||||
assert.equal(app.viewer.scene.requestRenderMode, false);
|
assert.equal(!app.engine.render.isContinuous(), false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3235,7 +3233,7 @@ test('intercepted scripted reports resolve their explicit floor and wake without
|
|||||||
assert.equal(entry.surfaceReady, true);
|
assert.equal(entry.surfaceReady, true);
|
||||||
assert.equal(entry.marker.show, true);
|
assert.equal(entry.marker.show, true);
|
||||||
assert.ok(app.state()._moving.has(entry));
|
assert.ok(app.state()._moving.has(entry));
|
||||||
assert.equal(app.viewer.scene.requestRenderMode, false);
|
assert.equal(!app.engine.render.isContinuous(), false);
|
||||||
const before = Cesium.Cartesian3.clone(entry.marker.position);
|
const before = Cesium.Cartesian3.clone(entry.marker.position);
|
||||||
app.run(1000, 16);
|
app.run(1000, 16);
|
||||||
assert.ok(Cesium.Cartesian3.distance(before, entry.marker.position) > 1);
|
assert.ok(Cesium.Cartesian3.distance(before, entry.marker.position) > 1);
|
||||||
|
|||||||
@@ -168,7 +168,10 @@ export function createRendering({ state, services, parts }) {
|
|||||||
placeSample(entry);
|
placeSample(entry);
|
||||||
schedulePlayback(entry);
|
schedulePlayback(entry);
|
||||||
syncRenderHold();
|
syncRenderHold();
|
||||||
governorRequestRender('transit-motion-boundary');
|
governorRequestRender(
|
||||||
|
'transit-motion-boundary',
|
||||||
|
parts.renderer?.engine,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
Math.max(1, sample.nextWakeMonoMs - performance.now()),
|
Math.max(1, sample.nextWakeMonoMs - performance.now()),
|
||||||
);
|
);
|
||||||
@@ -280,7 +283,7 @@ export function createRendering({ state, services, parts }) {
|
|||||||
state._stylePreset = next;
|
state._stylePreset = next;
|
||||||
for (const entry of state._vehicles.values()) applySpriteStyle(entry);
|
for (const entry of state._vehicles.values()) applySpriteStyle(entry);
|
||||||
parts.trails.style();
|
parts.trails.style();
|
||||||
governorRequestRender('transit-style');
|
governorRequestRender('transit-style', parts.renderer?.engine);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -358,12 +361,12 @@ export function createRendering({ state, services, parts }) {
|
|||||||
// frames on — it is not moving.
|
// frames on — it is not moving.
|
||||||
const working = state._enabled && state._moving.size > 0;
|
const working = state._enabled && state._moving.size > 0;
|
||||||
if (working && !state._renderHeld) {
|
if (working && !state._renderHeld) {
|
||||||
holdContinuousRender('transit');
|
holdContinuousRender('transit', parts.renderer?.engine);
|
||||||
state._renderHeld = true;
|
state._renderHeld = true;
|
||||||
} else if (!working && state._renderHeld) {
|
} else if (!working && state._renderHeld) {
|
||||||
releaseContinuousRender('transit');
|
releaseContinuousRender('transit', parts.renderer?.engine);
|
||||||
state._renderHeld = false;
|
state._renderHeld = false;
|
||||||
governorRequestRender('transit-settled');
|
governorRequestRender('transit-settled', parts.renderer?.engine);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -421,7 +424,8 @@ export function createRendering({ state, services, parts }) {
|
|||||||
)
|
)
|
||||||
: 0;
|
: 0;
|
||||||
}
|
}
|
||||||
if (state._heightDirty.size) governorRequestRender('transit-height-batch');
|
if (state._heightDirty.size)
|
||||||
|
governorRequestRender('transit-height-batch', parts.renderer?.engine);
|
||||||
syncRenderHold();
|
syncRenderHold();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -508,7 +512,7 @@ export function createRendering({ state, services, parts }) {
|
|||||||
state._shownCount = state._visible.size;
|
state._shownCount = state._visible.size;
|
||||||
if (changed) state._detectRevision++;
|
if (changed) state._detectRevision++;
|
||||||
syncRenderHold();
|
syncRenderHold();
|
||||||
governorRequestRender('transit-visibility');
|
governorRequestRender('transit-visibility', parts.renderer?.engine);
|
||||||
return state._shownCount;
|
return state._shownCount;
|
||||||
}
|
}
|
||||||
function resumePlayback() {
|
function resumePlayback() {
|
||||||
@@ -529,7 +533,7 @@ export function createRendering({ state, services, parts }) {
|
|||||||
parts.selection.refreshSelectedCard(true);
|
parts.selection.refreshSelectedCard(true);
|
||||||
state._detectRevision++;
|
state._detectRevision++;
|
||||||
syncRenderHold();
|
syncRenderHold();
|
||||||
governorRequestRender('transit-re-entry');
|
governorRequestRender('transit-re-entry', parts.renderer?.engine);
|
||||||
}
|
}
|
||||||
// Text and membership refreshes are bounded timer work, never paint work.
|
// Text and membership refreshes are bounded timer work, never paint work.
|
||||||
function maintainPresentation() {
|
function maintainPresentation() {
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ export function createSelection({ state, services, parts }) {
|
|||||||
parts.rendering.paintSelected(entry, true);
|
parts.rendering.paintSelected(entry, true);
|
||||||
parts.trails.select(entry);
|
parts.trails.select(entry);
|
||||||
refreshSelectedCard(true);
|
refreshSelectedCard(true);
|
||||||
governorRequestRender('transit-select');
|
governorRequestRender('transit-select', parts.renderer?.engine);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onKeyDown(event) {
|
function onKeyDown(event) {
|
||||||
|
|||||||
@@ -189,7 +189,10 @@ export function createTrails({ state, services, parts, source }) {
|
|||||||
});
|
});
|
||||||
entry.trailVertices = vertices;
|
entry.trailVertices = vertices;
|
||||||
entry.trailSegments = all;
|
entry.trailSegments = all;
|
||||||
services.render.governorRequestRender('transit-trail-history');
|
services.render.governorRequestRender(
|
||||||
|
'transit-trail-history',
|
||||||
|
parts.renderer?.engine,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return changed;
|
return changed;
|
||||||
@@ -300,7 +303,10 @@ export function createTrails({ state, services, parts, source }) {
|
|||||||
parts.height.anchorFloors();
|
parts.height.anchorFloors();
|
||||||
prepareEntry(entry);
|
prepareEntry(entry);
|
||||||
update();
|
update();
|
||||||
services.render.governorRequestRender('transit-trail-backfill');
|
services.render.governorRequestRender(
|
||||||
|
'transit-trail-backfill',
|
||||||
|
parts.renderer?.engine,
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
// Session history remains usable if the optional retained read fails.
|
// Session history remains usable if the optional retained read fails.
|
||||||
@@ -313,7 +319,10 @@ export function createTrails({ state, services, parts, source }) {
|
|||||||
if (!state._enabled || selected !== entry) return;
|
if (!state._enabled || selected !== entry) return;
|
||||||
parts.rendering.sampleIdle(entry);
|
parts.rendering.sampleIdle(entry);
|
||||||
update();
|
update();
|
||||||
services.render.governorRequestRender('transit-trail-age');
|
services.render.governorRequestRender(
|
||||||
|
'transit-trail-age',
|
||||||
|
parts.renderer?.engine,
|
||||||
|
);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
function detachRenderer() {
|
function detachRenderer() {
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ export function createViewport({ state, services, parts }) {
|
|||||||
}
|
}
|
||||||
parts.ingestion.sweepAgedVehicles(Date.now());
|
parts.ingestion.sweepAgedVehicles(Date.now());
|
||||||
parts.rendering.syncRenderHold();
|
parts.rendering.syncRenderHold();
|
||||||
governorRequestRender('transit-proximity');
|
governorRequestRender('transit-proximity', parts.renderer?.engine);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onCameraChanged() {
|
function onCameraChanged() {
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ export function createLifecycle({
|
|||||||
state.preRenderRemover?.();
|
state.preRenderRemover?.();
|
||||||
state.preRenderRemover = null;
|
state.preRenderRemover = null;
|
||||||
components.rendering.cancelTrailingVisibilityPass();
|
components.rendering.cancelTrailingVisibilityPass();
|
||||||
releaseContinuousRender('ais-vessels');
|
releaseContinuousRender('ais-vessels', state.renderer?.engine);
|
||||||
unregisterPickOwner('ais-live-vessels', state.renderer?.engine);
|
unregisterPickOwner('ais-live-vessels', state.renderer?.engine);
|
||||||
components.tracking.detachSelectedVesselTrail();
|
components.tracking.detachSelectedVesselTrail();
|
||||||
for (const record of state.records.all)
|
for (const record of state.records.all)
|
||||||
@@ -242,7 +242,7 @@ export function createLifecycle({
|
|||||||
removeViewListener();
|
removeViewListener();
|
||||||
state.feed.enabled = false;
|
state.feed.enabled = false;
|
||||||
invalidateAisSession();
|
invalidateAisSession();
|
||||||
releaseContinuousRender('ais-vessels');
|
releaseContinuousRender('ais-vessels', state.renderer?.engine);
|
||||||
components.rendering.cancelTrailingVisibilityPass();
|
components.rendering.cancelTrailingVisibilityPass();
|
||||||
unregisterPickOwner('ais-live-vessels', state.renderer?.engine);
|
unregisterPickOwner('ais-live-vessels', state.renderer?.engine);
|
||||||
components.rendering.setVisible(false);
|
components.rendering.setVisible(false);
|
||||||
@@ -261,7 +261,7 @@ export function createLifecycle({
|
|||||||
destroy() {
|
destroy() {
|
||||||
removeViewListener();
|
removeViewListener();
|
||||||
invalidateAisSession();
|
invalidateAisSession();
|
||||||
releaseContinuousRender('ais-vessels'); // direct-destroy path (perf wave 2 fix)
|
releaseContinuousRender('ais-vessels', state.renderer?.engine); // direct-destroy path (perf wave 2 fix)
|
||||||
components.rendering.cancelTrailingVisibilityPass();
|
components.rendering.cancelTrailingVisibilityPass();
|
||||||
if (state.feed.abort) state.feed.abort.abort();
|
if (state.feed.abort) state.feed.abort.abort();
|
||||||
unregisterPickOwner('ais-live-vessels', state.renderer?.engine);
|
unregisterPickOwner('ais-live-vessels', state.renderer?.engine);
|
||||||
|
|||||||
@@ -215,7 +215,8 @@ export function createRendering({
|
|||||||
);
|
);
|
||||||
state.trailingVisibilityTimer = vesselState._aisRuntime.setTimeout(() => {
|
state.trailingVisibilityTimer = vesselState._aisRuntime.setTimeout(() => {
|
||||||
state.trailingVisibilityTimer = null;
|
state.trailingVisibilityTimer = null;
|
||||||
if (state.feed.enabled) governorRequestRender('ais-vessels-visibility');
|
if (state.feed.enabled)
|
||||||
|
governorRequestRender('ais-vessels-visibility', state.renderer?.engine);
|
||||||
}, waitMs);
|
}, waitMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,7 +233,8 @@ export function createRendering({
|
|||||||
|
|
||||||
function updateVisibility(force = false) {
|
function updateVisibility(force = false) {
|
||||||
if (!state.feed.enabled || (!state.renderer && !state.viewer)) return;
|
if (!state.feed.enabled || (!state.renderer && !state.viewer)) return;
|
||||||
if (force) governorRequestRender('ais-vessels-update');
|
if (force)
|
||||||
|
governorRequestRender('ais-vessels-update', state.renderer?.engine);
|
||||||
const now = focusNowMs(performance.now());
|
const now = focusNowMs(performance.now());
|
||||||
const focusTarget = getFocusTarget();
|
const focusTarget = getFocusTarget();
|
||||||
const regularPass =
|
const regularPass =
|
||||||
@@ -261,7 +263,7 @@ export function createRendering({
|
|||||||
}
|
}
|
||||||
if (focusPass) {
|
if (focusPass) {
|
||||||
state.activeFocusCount = 0;
|
state.activeFocusCount = 0;
|
||||||
releaseContinuousRender('ais-vessels');
|
releaseContinuousRender('ais-vessels', state.renderer?.engine);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -314,8 +316,8 @@ export function createRendering({
|
|||||||
});
|
});
|
||||||
state.activeFocusCount = result.activeCount;
|
state.activeFocusCount = result.activeCount;
|
||||||
if (result.activeCount > 0 || result.transitioning)
|
if (result.activeCount > 0 || result.transitioning)
|
||||||
holdContinuousRender('ais-vessels');
|
holdContinuousRender('ais-vessels', state.renderer?.engine);
|
||||||
else releaseContinuousRender('ais-vessels');
|
else releaseContinuousRender('ais-vessels', state.renderer?.engine);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -104,7 +104,10 @@ export function createRendering({ state, services, parts }) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
commit();
|
commit();
|
||||||
services.render?.governorRequestRender?.(`wildlife-sightings-${reason}`);
|
services.render?.governorRequestRender?.(
|
||||||
|
`wildlife-sightings-${reason}`,
|
||||||
|
state.engine,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
function beginBatch() {
|
function beginBatch() {
|
||||||
batchDepth += 1;
|
batchDepth += 1;
|
||||||
|
|||||||
@@ -22,7 +22,9 @@ export class MapStackController extends MapSourceController {
|
|||||||
? options.initialStack || 'photoreal'
|
? options.initialStack || 'photoreal'
|
||||||
: registry.defaultId,
|
: registry.defaultId,
|
||||||
...options,
|
...options,
|
||||||
requestRender: governorRequestRender,
|
requestRender:
|
||||||
|
options.requestRender ??
|
||||||
|
((reason) => governorRequestRender(reason, options.engine)),
|
||||||
});
|
});
|
||||||
this.googleTileset = options.googleTileset || null;
|
this.googleTileset = options.googleTileset || null;
|
||||||
this.cesiumToken = String(options.cesiumToken || '').trim();
|
this.cesiumToken = String(options.cesiumToken || '').trim();
|
||||||
|
|||||||
+5
-7
@@ -1,8 +1,5 @@
|
|||||||
import * as geo from './geo/index.js';
|
import * as geo from './geo/index.js';
|
||||||
import {
|
import { renderGovernorFor } from './renderGovernor.js';
|
||||||
holdContinuousRender,
|
|
||||||
releaseContinuousRender,
|
|
||||||
} from './renderGovernor.js';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* OrbitController — smooth orbit around a target point.
|
* OrbitController — smooth orbit around a target point.
|
||||||
@@ -43,7 +40,8 @@ export class OrbitController {
|
|||||||
this.active = true;
|
this.active = true;
|
||||||
// Orbit mutates the camera from preRender — without a hold the loop
|
// Orbit mutates the camera from preRender — without a hold the loop
|
||||||
// starves after its first idle frame. (perf wave 2)
|
// starves after its first idle frame. (perf wave 2)
|
||||||
holdContinuousRender('camera-orbit');
|
this._renderGovernor = renderGovernorFor(this.engine);
|
||||||
|
this._renderGovernor?.holdContinuousRender('camera-orbit');
|
||||||
|
|
||||||
// Start from the camera's current heading for seamless transition
|
// Start from the camera's current heading for seamless transition
|
||||||
this.angle = camera.pose().heading;
|
this.angle = camera.pose().heading;
|
||||||
@@ -71,7 +69,7 @@ export class OrbitController {
|
|||||||
*/
|
*/
|
||||||
stop() {
|
stop() {
|
||||||
this.active = false;
|
this.active = false;
|
||||||
releaseContinuousRender('camera-orbit');
|
this._renderGovernor?.releaseContinuousRender('camera-orbit');
|
||||||
if (this._removeListener) {
|
if (this._removeListener) {
|
||||||
this._removeListener();
|
this._removeListener();
|
||||||
this._removeListener = null;
|
this._removeListener = null;
|
||||||
@@ -87,7 +85,7 @@ export class OrbitController {
|
|||||||
attachEngine(engine) {
|
attachEngine(engine) {
|
||||||
if (this.active || this._removeListener) {
|
if (this.active || this._removeListener) {
|
||||||
this.active = false;
|
this.active = false;
|
||||||
releaseContinuousRender('camera-orbit');
|
this._renderGovernor?.releaseContinuousRender('camera-orbit');
|
||||||
this._removeListener?.();
|
this._removeListener?.();
|
||||||
this._removeListener = null;
|
this._removeListener = null;
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-8
@@ -3,12 +3,7 @@ import assert from 'node:assert/strict';
|
|||||||
import * as geo from './geo/index.js';
|
import * as geo from './geo/index.js';
|
||||||
import { createFakeEngine } from './globe/testing/fakeEngine.js';
|
import { createFakeEngine } from './globe/testing/fakeEngine.js';
|
||||||
import { OrbitController } from './orbit.js';
|
import { OrbitController } from './orbit.js';
|
||||||
import {
|
import { getRenderGovernorDiagnostics } from './renderGovernor.js';
|
||||||
_resetRenderGovernorForTest,
|
|
||||||
getRenderGovernorDiagnostics,
|
|
||||||
} from './renderGovernor.js';
|
|
||||||
|
|
||||||
test.afterEach(() => _resetRenderGovernorForTest());
|
|
||||||
|
|
||||||
test('orbit locks onto the target every frame from the current heading, and stop releases it', (t) => {
|
test('orbit locks onto the target every frame from the current heading, and stop releases it', (t) => {
|
||||||
t.mock.timers.enable({ apis: ['Date'], now: 1000 });
|
t.mock.timers.enable({ apis: ['Date'], now: 1000 });
|
||||||
@@ -25,7 +20,9 @@ test('orbit locks onto the target every frame from the current heading, and stop
|
|||||||
orbit.toggle(target, { radius: 800, pitch: -20, speed: 10 }),
|
orbit.toggle(target, { radius: 800, pitch: -20, speed: 10 }),
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
assert.ok(getRenderGovernorDiagnostics().holds.includes('camera-orbit'));
|
assert.ok(
|
||||||
|
getRenderGovernorDiagnostics(engine).holds.includes('camera-orbit'),
|
||||||
|
);
|
||||||
t.mock.timers.tick(500);
|
t.mock.timers.tick(500);
|
||||||
engine.fake.frame();
|
engine.fake.frame();
|
||||||
const [first] = engine.fake.callsOf('camera.lookAt');
|
const [first] = engine.fake.callsOf('camera.lookAt');
|
||||||
@@ -37,7 +34,9 @@ test('orbit locks onto the target every frame from the current heading, and stop
|
|||||||
assert.equal(first.args[1].range, 800);
|
assert.equal(first.args[1].range, 800);
|
||||||
assert.equal(orbit.toggle(), false);
|
assert.equal(orbit.toggle(), false);
|
||||||
assert.equal(engine.fake.callsOf('camera.release').length, 1);
|
assert.equal(engine.fake.callsOf('camera.release').length, 1);
|
||||||
assert.ok(!getRenderGovernorDiagnostics().holds.includes('camera-orbit'));
|
assert.ok(
|
||||||
|
!getRenderGovernorDiagnostics(engine).holds.includes('camera-orbit'),
|
||||||
|
);
|
||||||
engine.fake.frame();
|
engine.fake.frame();
|
||||||
assert.equal(
|
assert.equal(
|
||||||
engine.fake.callsOf('camera.lookAt').length,
|
engine.fake.callsOf('camera.lookAt').length,
|
||||||
|
|||||||
+74
-186
@@ -1,197 +1,85 @@
|
|||||||
/**
|
import { sceneServicesFor } from './data/sceneServices.js';
|
||||||
* Idle render governor — the wave-2 flagship of the 2026-08-05 perf
|
|
||||||
* investigation.
|
|
||||||
*
|
|
||||||
* The problem: Cesium's default render loop repaints every vsync forever, so
|
|
||||||
* the app burned ~60% GPU + ~54% of a core with ZERO layers enabled and a
|
|
||||||
* parked camera. The fix: flip the scene into Cesium's `requestRenderMode`
|
|
||||||
* whenever nothing animates per frame, and return to the continuous loop the
|
|
||||||
* moment something does.
|
|
||||||
*
|
|
||||||
* Architecture — a binary mode driven by ref-counted holds:
|
|
||||||
*
|
|
||||||
* - **Continuous mode** (`requestRenderMode = false`, today's behavior)
|
|
||||||
* while ANY hold is registered. Every per-frame animator — fleet
|
|
||||||
* interpolation, traffic sim, satellite motion, tracked-entity follow,
|
|
||||||
* style crossfades, CCTV projection — registers a hold for exactly the
|
|
||||||
* lifetime of its scene-loop listener or animation. While one is active,
|
|
||||||
* behavior is byte-identical to pre-governor main: the locked
|
|
||||||
* interpolation/tracking invariants are preserved by construction.
|
|
||||||
* - **Idle mode** (`requestRenderMode = true`) when zero holds. Cesium
|
|
||||||
* auto-renders on camera input and tile loads; every other scene mutation
|
|
||||||
* must call `governorRequestRender()` for its one frame. Discrete
|
|
||||||
* mutators (layer poll ticks, slider writes, annotation changes) route
|
|
||||||
* through that.
|
|
||||||
*
|
|
||||||
* Holds are identity-keyed (a Set of owner ids), NOT a counter — a module
|
|
||||||
* that double-holds or double-releases cannot corrupt the mode. Owners are
|
|
||||||
* short stable strings ('flights', 'traffic', 'style-anim', …) so the
|
|
||||||
* diagnostics read like a story.
|
|
||||||
*
|
|
||||||
* The governor is O(1) passive: no per-frame work of its own, ever.
|
|
||||||
*
|
|
||||||
* Globe engine: the governor is the Cesium engine's render backend.
|
|
||||||
* `engine.render.request()/setContinuous()` (src/globe/cesium/render.js)
|
|
||||||
* delegate here, so the governor never uses `engine.render` (that would be a
|
|
||||||
* cycle) and keeps the viewer for what the contract leaves to the engine:
|
|
||||||
* `scene.requestRenderMode`, `scene.maximumRenderTimeChange` and
|
|
||||||
* `scene.requestRender()`. It needs no frame events (`engine.frames`).
|
|
||||||
*
|
|
||||||
* Any other globe engine (Navara) is the governor's backend instead: install
|
|
||||||
* it on the engine (installEngineRenderGovernor) and the owner-counted holds
|
|
||||||
* drive `engine.render.setContinuous`, requests go to `engine.render.request`.
|
|
||||||
*/
|
|
||||||
|
|
||||||
let _viewer = null;
|
/** Owner-keyed render demand for one scene attachment, on any globe engine. */
|
||||||
/** The non-Cesium engine the governor drives, or null (Cesium: `_viewer`). */
|
export function createRenderGovernor(engine) {
|
||||||
let _engine = null;
|
const holds = new Set();
|
||||||
let _installed = false;
|
const recentRequests = [];
|
||||||
const _holds = new Set();
|
let installed = false;
|
||||||
|
let destroyed = false;
|
||||||
/** Debug trail of the most recent one-shot render requests (idle mode only). */
|
function applyMode() {
|
||||||
const _recentRequests = [];
|
if (!installed || destroyed) return;
|
||||||
const RECENT_REQUEST_CAP = 16;
|
const continuous = holds.size > 0;
|
||||||
|
if (engine.render.isContinuous() === continuous) return;
|
||||||
function applyMode() {
|
engine.render.setContinuous(continuous);
|
||||||
if (!_installed) return;
|
if (!continuous) engine.render.request('governor-idle');
|
||||||
const continuous = _holds.size > 0;
|
|
||||||
if (_engine) {
|
|
||||||
if (_engine.render.isContinuous() === continuous) return;
|
|
||||||
_engine.render.setContinuous(continuous);
|
|
||||||
if (!continuous) _engine.render.request('governor-idle');
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
if (!_viewer?.scene) return;
|
|
||||||
const scene = _viewer.scene;
|
|
||||||
if (scene.requestRenderMode === !continuous) return;
|
|
||||||
scene.requestRenderMode = !continuous;
|
|
||||||
if (!continuous) {
|
|
||||||
// Entering idle: render one settling frame so anything the last
|
|
||||||
// continuous frame mutated is on screen before the loop stops.
|
|
||||||
scene.requestRender?.();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Install the governor on a Cesium viewer, as the Cesium engine's render
|
|
||||||
* backend. Idempotent. Before install, hold/release still record into the
|
|
||||||
* holds set (and apply at install time); requests are safe no-ops — so
|
|
||||||
* modules can call all three unconditionally in tests without a viewer.
|
|
||||||
* @param {Cesium.Viewer} viewer
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
export function installRenderGovernor(viewer) {
|
|
||||||
if (!viewer?.scene)
|
|
||||||
throw new TypeError('installRenderGovernor requires a Cesium viewer');
|
|
||||||
_engine = null;
|
|
||||||
_viewer = viewer;
|
|
||||||
_installed = true;
|
|
||||||
// Never let Cesium re-render on simulation-time deltas behind our back —
|
|
||||||
// idle means idle. All re-renders are camera/tiles (Cesium-native) or
|
|
||||||
// explicit requests.
|
|
||||||
viewer.scene.maximumRenderTimeChange = Infinity;
|
|
||||||
applyMode();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Install the governor on a globe engine that renders by itself (Navara):
|
|
||||||
* the holds drive `engine.render.setContinuous`, requests go to
|
|
||||||
* `engine.render.request`. Idempotent, like installRenderGovernor.
|
|
||||||
* @param {import('./globe/contract.js').GlobeEngine} engine
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
export function installEngineRenderGovernor(engine) {
|
|
||||||
if (typeof engine?.render?.setContinuous !== 'function')
|
|
||||||
throw new TypeError('installEngineRenderGovernor requires a globe engine');
|
|
||||||
_engine = engine;
|
|
||||||
_viewer = engine;
|
|
||||||
_installed = true;
|
|
||||||
applyMode();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register a continuous-render hold. Idempotent per owner.
|
|
||||||
* Call where the owner's per-frame work BEGINS (scene listener installed,
|
|
||||||
* animation starts, tracking begins).
|
|
||||||
* @param {string} ownerId Short stable id, e.g. 'flights', 'traffic'.
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
export function holdContinuousRender(ownerId) {
|
|
||||||
if (!ownerId) return;
|
|
||||||
_holds.add(ownerId);
|
|
||||||
applyMode();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Release a hold. Safe when never held.
|
|
||||||
* Call where the owner's per-frame work ENDS (listener removed, animation
|
|
||||||
* settled, tracking stopped, layer disabled).
|
|
||||||
* @param {string} ownerId
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
export function releaseContinuousRender(ownerId) {
|
|
||||||
if (!ownerId) return;
|
|
||||||
_holds.delete(ownerId);
|
|
||||||
applyMode();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* One-shot render request for a discrete scene mutation (layer tick, slider
|
|
||||||
* write, annotation change). Always forwards to scene.requestRender() — in
|
|
||||||
* continuous mode that is a harmless flag set (and forwarding closes the
|
|
||||||
* request-then-last-release race); only idle-mode requests are recorded in
|
|
||||||
* diagnostics. Cheap enough to call unconditionally after any mutation.
|
|
||||||
* @param {string} [reason] For diagnostics only.
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
export function governorRequestRender(reason = 'unspecified') {
|
|
||||||
if (!_installed || (!_engine && !_viewer?.scene)) return;
|
|
||||||
if (_holds.size === 0) {
|
|
||||||
_recentRequests.push({ reason, at: Date.now() });
|
|
||||||
if (_recentRequests.length > RECENT_REQUEST_CAP) _recentRequests.shift();
|
|
||||||
}
|
|
||||||
if (_engine) _engine.render.request(reason);
|
|
||||||
else _viewer.scene.requestRender?.();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns {{installed: boolean, mode: 'continuous'|'idle', holds: string[],
|
|
||||||
* recentRequests: Array<{reason: string, at: number}>}}
|
|
||||||
*/
|
|
||||||
export function getRenderGovernorDiagnostics() {
|
|
||||||
return {
|
return {
|
||||||
installed: _installed,
|
install() {
|
||||||
mode: _holds.size > 0 ? 'continuous' : 'idle',
|
if (destroyed) return;
|
||||||
holds: [..._holds].sort(),
|
installed = true;
|
||||||
recentRequests: [..._recentRequests],
|
applyMode();
|
||||||
|
},
|
||||||
|
holdContinuousRender(ownerId) {
|
||||||
|
if (destroyed || !ownerId) return;
|
||||||
|
holds.add(ownerId);
|
||||||
|
applyMode();
|
||||||
|
},
|
||||||
|
releaseContinuousRender(ownerId) {
|
||||||
|
if (destroyed || !ownerId) return;
|
||||||
|
holds.delete(ownerId);
|
||||||
|
applyMode();
|
||||||
|
},
|
||||||
|
governorRequestRender(reason = 'unspecified') {
|
||||||
|
if (!installed || destroyed) return;
|
||||||
|
if (holds.size === 0) {
|
||||||
|
recentRequests.push({ reason, at: Date.now() });
|
||||||
|
if (recentRequests.length > 16) recentRequests.shift();
|
||||||
|
}
|
||||||
|
engine.render.request(reason);
|
||||||
|
},
|
||||||
|
getRenderGovernorDiagnostics() {
|
||||||
|
return {
|
||||||
|
installed,
|
||||||
|
mode: holds.size > 0 ? 'continuous' : 'idle',
|
||||||
|
holds: [...holds].sort(),
|
||||||
|
recentRequests: [...recentRequests],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
destroy() {
|
||||||
|
if (destroyed) return;
|
||||||
|
holds.clear();
|
||||||
|
applyMode();
|
||||||
|
installed = false;
|
||||||
|
destroyed = true;
|
||||||
|
recentRequests.length = 0;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Portable callers resolve the same governor exposed as scene.services.render. */
|
||||||
* Whether the governor is installed on this viewer (its render requests then
|
export function renderGovernorFor(engine) {
|
||||||
* go through `governorRequestRender`).
|
return sceneServicesFor(engine)?.render ?? null;
|
||||||
* @param {object} viewer
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
export function isRenderGovernorInstalled(viewer) {
|
|
||||||
return _installed && viewer != null && _viewer === viewer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Release the installed viewer after its animation owners have stopped. */
|
export function holdContinuousRender(ownerId, engine) {
|
||||||
export function uninstallRenderGovernor(viewer) {
|
renderGovernorFor(engine)?.holdContinuousRender(ownerId);
|
||||||
if (_viewer !== viewer) return;
|
|
||||||
_viewer = null;
|
|
||||||
_engine = null;
|
|
||||||
_installed = false;
|
|
||||||
_holds.clear();
|
|
||||||
_recentRequests.length = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Test seam: reset module state between unit tests. */
|
export function releaseContinuousRender(ownerId, engine) {
|
||||||
export function _resetRenderGovernorForTest() {
|
renderGovernorFor(engine)?.releaseContinuousRender(ownerId);
|
||||||
_viewer = null;
|
}
|
||||||
_engine = null;
|
|
||||||
_installed = false;
|
export function governorRequestRender(reason, engine) {
|
||||||
_holds.clear();
|
renderGovernorFor(engine)?.governorRequestRender(reason);
|
||||||
_recentRequests.length = 0;
|
}
|
||||||
|
|
||||||
|
export function getRenderGovernorDiagnostics(engine) {
|
||||||
|
return (
|
||||||
|
renderGovernorFor(engine)?.getRenderGovernorDiagnostics() ?? {
|
||||||
|
installed: false,
|
||||||
|
mode: 'idle',
|
||||||
|
holds: [],
|
||||||
|
recentRequests: [],
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
+104
-152
@@ -1,156 +1,108 @@
|
|||||||
import { test, beforeEach } from 'node:test';
|
import test from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import {
|
import { OrbitController } from './orbit.js';
|
||||||
installRenderGovernor,
|
import { GROUND_HARNESSES } from './testSupport/groundDisplayEngines.mjs';
|
||||||
installEngineRenderGovernor,
|
import { createSceneServices, sceneServicesFor } from './data/sceneServices.js';
|
||||||
holdContinuousRender,
|
import { renderGovernorFor, governorRequestRender } from './renderGovernor.js';
|
||||||
releaseContinuousRender,
|
|
||||||
governorRequestRender,
|
|
||||||
getRenderGovernorDiagnostics,
|
|
||||||
_resetRenderGovernorForTest,
|
|
||||||
isRenderGovernorInstalled,
|
|
||||||
uninstallRenderGovernor,
|
|
||||||
} from './renderGovernor.js';
|
|
||||||
|
|
||||||
function makeViewer() {
|
for (const makeHarness of GROUND_HARNESSES) {
|
||||||
const calls = { requestRender: 0 };
|
test(`${makeHarness.name}: owner holds, installation and idle requests`, (t) => {
|
||||||
const scene = {
|
const harness = makeHarness();
|
||||||
requestRenderMode: false,
|
const { engine } = harness;
|
||||||
maximumRenderTimeChange: 0,
|
const services = createSceneServices(engine);
|
||||||
requestRender() { calls.requestRender += 1; },
|
const governor = services.render;
|
||||||
};
|
t.after(() => { services.destroy(); harness.destroy(); });
|
||||||
return { viewer: { scene }, scene, calls };
|
const requests = [];
|
||||||
|
const request = engine.render.request;
|
||||||
|
engine.render.request = (reason) => { requests.push(reason); request(reason); };
|
||||||
|
assert.equal(renderGovernorFor(engine), governor);
|
||||||
|
governor.holdContinuousRender('flights');
|
||||||
|
governor.governorRequestRender('before-install');
|
||||||
|
assert.equal(requests.length, 0);
|
||||||
|
governor.install();
|
||||||
|
assert.equal(engine.render.isContinuous(), true);
|
||||||
|
governor.holdContinuousRender('flights');
|
||||||
|
governor.holdContinuousRender('satellites');
|
||||||
|
governor.governorRequestRender('while-held');
|
||||||
|
assert.equal(requests.at(-1), 'while-held', 'requests forward even while held');
|
||||||
|
assert.deepEqual(governor.getRenderGovernorDiagnostics().recentRequests, []);
|
||||||
|
governor.releaseContinuousRender('flights');
|
||||||
|
assert.equal(engine.render.isContinuous(), true);
|
||||||
|
assert.deepEqual(governor.getRenderGovernorDiagnostics().holds, ['satellites']);
|
||||||
|
governor.releaseContinuousRender('satellites');
|
||||||
|
assert.equal(engine.render.isContinuous(), false);
|
||||||
|
assert.equal(requests.at(-1), 'governor-idle', 'last release paints a settling frame');
|
||||||
|
const settled = requests.length;
|
||||||
|
governor.releaseContinuousRender('satellites');
|
||||||
|
governor.releaseContinuousRender('absent');
|
||||||
|
assert.equal(requests.length, settled, 'absent releases do not wake an idle engine');
|
||||||
|
for (let i = 0; i < 20; i++) governorRequestRender(`tick-${i}`, engine);
|
||||||
|
const diagnostics = governor.getRenderGovernorDiagnostics();
|
||||||
|
assert.equal(diagnostics.recentRequests.length, 16);
|
||||||
|
assert.equal(diagnostics.recentRequests[0].reason, 'tick-4');
|
||||||
|
diagnostics.holds.push('external');
|
||||||
|
assert.deepEqual(governor.getRenderGovernorDiagnostics().holds, []);
|
||||||
|
if (harness.viewer) assert.equal(harness.viewer.scene.maximumRenderTimeChange, Infinity);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(`${makeHarness.name}: identical owners and teardown stay in their scene`, (t) => {
|
||||||
|
const first = makeHarness();
|
||||||
|
const second = makeHarness();
|
||||||
|
const a = createSceneServices(first.engine);
|
||||||
|
const b = createSceneServices(second.engine);
|
||||||
|
t.after(() => { sceneServicesFor(first.engine).destroy(); b.destroy(); first.destroy(); second.destroy(); });
|
||||||
|
a.render.install();
|
||||||
|
b.render.install();
|
||||||
|
a.render.holdContinuousRender('flights');
|
||||||
|
b.render.holdContinuousRender('flights');
|
||||||
|
a.render.releaseContinuousRender('flights');
|
||||||
|
assert.equal(first.engine.render.isContinuous(), false);
|
||||||
|
assert.equal(second.engine.render.isContinuous(), true);
|
||||||
|
a.destroy();
|
||||||
|
const replacement = createSceneServices(first.engine);
|
||||||
|
replacement.render.install();
|
||||||
|
replacement.render.holdContinuousRender('flights');
|
||||||
|
const oldRelease = a.render.releaseContinuousRender;
|
||||||
|
oldRelease('flights');
|
||||||
|
a.render.install();
|
||||||
|
a.render.holdContinuousRender('late');
|
||||||
|
a.render.governorRequestRender('late');
|
||||||
|
a.destroy();
|
||||||
|
assert.deepEqual(a.render.getRenderGovernorDiagnostics(), {
|
||||||
|
installed: false, mode: 'idle', holds: [], recentRequests: [],
|
||||||
|
});
|
||||||
|
assert.equal(first.engine.render.isContinuous(), true);
|
||||||
|
assert.equal(second.engine.render.isContinuous(), true);
|
||||||
|
b.destroy();
|
||||||
|
assert.equal(second.engine.render.isContinuous(), false);
|
||||||
|
assert.equal(first.engine.render.isContinuous(), true);
|
||||||
|
replacement.destroy();
|
||||||
|
assert.equal(first.engine.render.isContinuous(), false);
|
||||||
|
assert.equal(sceneServicesFor(first.engine), replacement, 'late lookups cannot revive a detached scene');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(() => _resetRenderGovernorForTest());
|
for (const makeHarness of GROUND_HARNESSES) {
|
||||||
|
test(`${makeHarness.name}: an old orbit release cannot idle a replacement attachment`, (t) => {
|
||||||
test('install with zero holds enters idle mode and pins maximumRenderTimeChange', () => {
|
const harness = makeHarness();
|
||||||
const { viewer, scene } = makeViewer();
|
const { engine } = harness;
|
||||||
installRenderGovernor(viewer);
|
const first = createSceneServices(engine);
|
||||||
assert.equal(scene.requestRenderMode, true);
|
first.render.install();
|
||||||
assert.equal(scene.maximumRenderTimeChange, Infinity);
|
const oldOrbit = new OrbitController(engine);
|
||||||
assert.equal(getRenderGovernorDiagnostics().mode, 'idle');
|
const newOrbit = new OrbitController(engine);
|
||||||
});
|
t.after(() => {
|
||||||
|
oldOrbit.stop(); newOrbit.stop(); sceneServicesFor(engine).destroy(); harness.destroy();
|
||||||
test('a hold flips to continuous; releasing the last hold returns to idle with a settling frame', () => {
|
});
|
||||||
const { viewer, scene, calls } = makeViewer();
|
oldOrbit.start({ x: 6378137, y: 0, z: 0 });
|
||||||
installRenderGovernor(viewer);
|
assert.equal(engine.render.isContinuous(), true);
|
||||||
const settleBaseline = calls.requestRender;
|
first.destroy();
|
||||||
holdContinuousRender('flights');
|
const next = createSceneServices(engine);
|
||||||
assert.equal(scene.requestRenderMode, false);
|
next.render.install();
|
||||||
assert.equal(getRenderGovernorDiagnostics().mode, 'continuous');
|
newOrbit.start({ x: 6378137, y: 0, z: 0 });
|
||||||
releaseContinuousRender('flights');
|
oldOrbit.stop();
|
||||||
assert.equal(scene.requestRenderMode, true);
|
assert.deepEqual(next.render.getRenderGovernorDiagnostics().holds, ['camera-orbit']);
|
||||||
// Entering idle renders one settling frame.
|
assert.equal(engine.render.isContinuous(), true);
|
||||||
assert.equal(calls.requestRender, settleBaseline + 1);
|
newOrbit.stop();
|
||||||
});
|
assert.equal(engine.render.isContinuous(), false);
|
||||||
|
});
|
||||||
test('holds are identity-keyed: double-hold cannot leak, double-release cannot corrupt', () => {
|
}
|
||||||
const { viewer, scene } = makeViewer();
|
|
||||||
installRenderGovernor(viewer);
|
|
||||||
holdContinuousRender('traffic');
|
|
||||||
holdContinuousRender('traffic');
|
|
||||||
releaseContinuousRender('traffic');
|
|
||||||
assert.equal(scene.requestRenderMode, true, 'single release clears an idempotent double-hold');
|
|
||||||
releaseContinuousRender('traffic');
|
|
||||||
releaseContinuousRender('never-held');
|
|
||||||
assert.equal(scene.requestRenderMode, true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('mode stays continuous until the LAST holder releases', () => {
|
|
||||||
const { viewer, scene } = makeViewer();
|
|
||||||
installRenderGovernor(viewer);
|
|
||||||
holdContinuousRender('flights');
|
|
||||||
holdContinuousRender('satellites');
|
|
||||||
releaseContinuousRender('flights');
|
|
||||||
assert.equal(scene.requestRenderMode, false);
|
|
||||||
assert.deepEqual(getRenderGovernorDiagnostics().holds, ['satellites']);
|
|
||||||
releaseContinuousRender('satellites');
|
|
||||||
assert.equal(scene.requestRenderMode, true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('governorRequestRender forwards to the scene and records reasons only in idle mode', () => {
|
|
||||||
const { viewer, calls } = makeViewer();
|
|
||||||
installRenderGovernor(viewer);
|
|
||||||
const baseline = calls.requestRender;
|
|
||||||
governorRequestRender('layer-tick:earthquakes');
|
|
||||||
assert.equal(calls.requestRender, baseline + 1);
|
|
||||||
assert.equal(getRenderGovernorDiagnostics().recentRequests.at(-1).reason, 'layer-tick:earthquakes');
|
|
||||||
holdContinuousRender('flights');
|
|
||||||
const idleRequests = getRenderGovernorDiagnostics().recentRequests.length;
|
|
||||||
governorRequestRender('slider');
|
|
||||||
assert.equal(
|
|
||||||
getRenderGovernorDiagnostics().recentRequests.length,
|
|
||||||
idleRequests,
|
|
||||||
'continuous-mode requests are not recorded as idle diagnostics',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('hold/release/request are safe no-ops before install (test environments without a viewer)', () => {
|
|
||||||
holdContinuousRender('flights');
|
|
||||||
releaseContinuousRender('flights');
|
|
||||||
governorRequestRender('noop');
|
|
||||||
assert.equal(getRenderGovernorDiagnostics().installed, false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('holds registered before install apply at install time', () => {
|
|
||||||
holdContinuousRender('flights');
|
|
||||||
const { viewer, scene } = makeViewer();
|
|
||||||
installRenderGovernor(viewer);
|
|
||||||
assert.equal(scene.requestRenderMode, false, 'pre-install hold keeps continuous mode');
|
|
||||||
releaseContinuousRender('flights');
|
|
||||||
assert.equal(scene.requestRenderMode, true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('isRenderGovernorInstalled names the one governed viewer', async () => {
|
|
||||||
const { isRenderGovernorInstalled, uninstallRenderGovernor } = await import('./renderGovernor.js');
|
|
||||||
const { viewer } = makeViewer();
|
|
||||||
const other = makeViewer().viewer;
|
|
||||||
assert.equal(isRenderGovernorInstalled(viewer), false);
|
|
||||||
installRenderGovernor(viewer);
|
|
||||||
assert.equal(isRenderGovernorInstalled(viewer), true);
|
|
||||||
assert.equal(isRenderGovernorInstalled(other), false);
|
|
||||||
uninstallRenderGovernor(viewer);
|
|
||||||
assert.equal(isRenderGovernorInstalled(viewer), false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('on a non-Cesium globe engine the governor drives engine.render with owner-counted holds', async () => {
|
|
||||||
const { createFakeEngine } = await import('./globe/testing/fakeEngine.js');
|
|
||||||
_resetRenderGovernorForTest();
|
|
||||||
const engine = createFakeEngine({ kind: 'navara' });
|
|
||||||
holdContinuousRender('before-install');
|
|
||||||
installEngineRenderGovernor(engine);
|
|
||||||
assert.equal(engine.render.isContinuous(), true, 'a hold taken before install applies');
|
|
||||||
releaseContinuousRender('before-install');
|
|
||||||
assert.equal(engine.render.isContinuous(), false);
|
|
||||||
holdContinuousRender('a');
|
|
||||||
holdContinuousRender('b');
|
|
||||||
assert.equal(engine.render.isContinuous(), true);
|
|
||||||
releaseContinuousRender('a');
|
|
||||||
assert.equal(engine.render.isContinuous(), true, 'b still holds');
|
|
||||||
releaseContinuousRender('b');
|
|
||||||
assert.equal(engine.render.isContinuous(), false);
|
|
||||||
engine.fake.resetCalls();
|
|
||||||
governorRequestRender('slider');
|
|
||||||
assert.deepEqual(engine.fake.callsOf('render.request').map((c) => c.args), [['slider']]);
|
|
||||||
assert.equal(isRenderGovernorInstalled(engine), true);
|
|
||||||
uninstallRenderGovernor(engine);
|
|
||||||
_resetRenderGovernorForTest();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('releasing an absent Navara hold does not wake an already idle engine', async () => {
|
|
||||||
const { createFakeEngine } = await import('./globe/testing/fakeEngine.js');
|
|
||||||
_resetRenderGovernorForTest();
|
|
||||||
const engine = createFakeEngine({ kind: 'navara' });
|
|
||||||
installEngineRenderGovernor(engine);
|
|
||||||
engine.fake.resetCalls();
|
|
||||||
for (let i = 0; i < 22; i++) releaseContinuousRender('cctv-projection');
|
|
||||||
assert.equal(engine.fake.callsOf('render.request').length, 0);
|
|
||||||
holdContinuousRender('video');
|
|
||||||
releaseContinuousRender('video');
|
|
||||||
assert.equal(engine.fake.callsOf('render.request').length, 1);
|
|
||||||
releaseContinuousRender('video');
|
|
||||||
assert.equal(engine.fake.callsOf('render.request').length, 1);
|
|
||||||
uninstallRenderGovernor(engine);
|
|
||||||
_resetRenderGovernorForTest();
|
|
||||||
});
|
|
||||||
@@ -46,11 +46,6 @@ import {
|
|||||||
setDetectionTuning,
|
setDetectionTuning,
|
||||||
} from '../data/detection.js';
|
} from '../data/detection.js';
|
||||||
import { isTr3b, toggleTr3b } from '../data/tr3bRegistry.js';
|
import { isTr3b, toggleTr3b } from '../data/tr3bRegistry.js';
|
||||||
import {
|
|
||||||
holdContinuousRender,
|
|
||||||
releaseContinuousRender,
|
|
||||||
governorRequestRender,
|
|
||||||
} from '../renderGovernor.js';
|
|
||||||
import {
|
import {
|
||||||
setScopeMaskEnabled,
|
setScopeMaskEnabled,
|
||||||
isScopeMaskEnabled,
|
isScopeMaskEnabled,
|
||||||
@@ -105,9 +100,6 @@ export class StyleManager extends ApplicationShell {
|
|||||||
setDetectionTuning,
|
setDetectionTuning,
|
||||||
isTr3b,
|
isTr3b,
|
||||||
toggleTr3b,
|
toggleTr3b,
|
||||||
holdContinuousRender,
|
|
||||||
releaseContinuousRender,
|
|
||||||
governorRequestRender,
|
|
||||||
setScopeMaskEnabled,
|
setScopeMaskEnabled,
|
||||||
isScopeMaskEnabled,
|
isScopeMaskEnabled,
|
||||||
setScopeMaskFeather,
|
setScopeMaskFeather,
|
||||||
|
|||||||
@@ -694,7 +694,7 @@ test('the Display selector, voice schema and final stylesheet expose Cyber', ()
|
|||||||
assert.match(detection, /sonarSampler\.label\(sonarFactor\)/);
|
assert.match(detection, /sonarSampler\.label\(sonarFactor\)/);
|
||||||
assert.match(detection, /createCyberSonarSampler\(width, height, now\)/);
|
assert.match(detection, /createCyberSonarSampler\(width, height, now\)/);
|
||||||
assert.match(cyberStyles, /data-cyber-sonar='off'/);
|
assert.match(cyberStyles, /data-cyber-sonar='off'/);
|
||||||
assert.match(detection, /governorRequestRender\('cyber-sonar-cadence'\)/);
|
assert.match(detection, /governorRequestRender\('cyber-sonar-cadence', _engine\)/);
|
||||||
assert.match(
|
assert.match(
|
||||||
detection,
|
detection,
|
||||||
/const animatingCount = countAnimatingRenderEntries\(renderEntries\);/,
|
/const animatingCount = countAnimatingRenderEntries\(renderEntries\);/,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { renderGovernorFor } from '../renderGovernor.js';
|
||||||
import { retainsLayerSession } from '../app/layerSession.js';
|
import { retainsLayerSession } from '../app/layerSession.js';
|
||||||
import { resolveImageryHost } from '../layers/weather/imageryHost.js';
|
import { resolveImageryHost } from '../layers/weather/imageryHost.js';
|
||||||
import { LayerBindings } from './layerBindings.js';
|
import { LayerBindings } from './layerBindings.js';
|
||||||
@@ -28,7 +29,10 @@ export class ShellScene extends ShellFacade {
|
|||||||
/** Share restoration, visual settings and layer bindings of this globe. */
|
/** Share restoration, visual settings and layer bindings of this globe. */
|
||||||
_buildSceneControls() {
|
_buildSceneControls() {
|
||||||
const { viewer, engine, mapStackController } = this._scene;
|
const { viewer, engine, mapStackController } = this._scene;
|
||||||
const services = this.services;
|
const services = {
|
||||||
|
...this.services,
|
||||||
|
...renderGovernorFor(this._scene.engine),
|
||||||
|
};
|
||||||
const layerSession = this._layerSession;
|
const layerSession = this._layerSession;
|
||||||
this._shareRestoration = new ShareRestoration({
|
this._shareRestoration = new ShareRestoration({
|
||||||
engine,
|
engine,
|
||||||
@@ -141,7 +145,10 @@ export class ShellScene extends ShellFacade {
|
|||||||
/** Cockpit and the celestial ring of this globe. */
|
/** Cockpit and the celestial ring of this globe. */
|
||||||
_buildSceneCockpit() {
|
_buildSceneCockpit() {
|
||||||
const { viewer, engine } = this._scene;
|
const { viewer, engine } = this._scene;
|
||||||
const services = this.services;
|
const services = {
|
||||||
|
...this.services,
|
||||||
|
...renderGovernorFor(this._scene.engine),
|
||||||
|
};
|
||||||
const { CelestialRing } = services;
|
const { CelestialRing } = services;
|
||||||
this._cockpitCoordinator = new CockpitCoordinator({
|
this._cockpitCoordinator = new CockpitCoordinator({
|
||||||
viewer,
|
viewer,
|
||||||
|
|||||||
Reference in new issue
Block a user