diff --git a/src/globe/navara/cameraModel.js b/src/globe/navara/cameraModel.js index ec6ad9601..f0991a4ed 100644 --- a/src/globe/navara/cameraModel.js +++ b/src/globe/navara/cameraModel.js @@ -389,6 +389,11 @@ export function createCameraModel({ camera, sync, afterFrame, + /** + * Ends a running flight (its `cancel` runs) without camera.commanded: + * for the user's own gestures, which are not a new camera owner. + */ + stopFlight: () => cancelCurrentFlight(), /** The current camera for projections: pose, vertical fov and CSS size. */ view, worldToWindow: (position, result) => diff --git a/src/globe/navara/createNavaraEngine.js b/src/globe/navara/createNavaraEngine.js index 22b9bcb8a..1d9ec5521 100644 --- a/src/globe/navara/createNavaraEngine.js +++ b/src/globe/navara/createNavaraEngine.js @@ -120,7 +120,8 @@ export function createNavaraEngine( view.on(name, handler); const input = createNavaraInput(view, { - onCameraInput: () => model.camera.cancelFlight(), + // A gesture is the user, not a new camera owner: no camera.commanded. + onCameraInput: () => model.stopFlight(), }); const credits = createNavaraCredits(view); const maps = createNavaraMaps(view, fetch ? { fetch } : {}); diff --git a/src/globe/navara/createNavaraEngine.test.mjs b/src/globe/navara/createNavaraEngine.test.mjs index 47adf2f6f..235cd1847 100644 --- a/src/globe/navara/createNavaraEngine.test.mjs +++ b/src/globe/navara/createNavaraEngine.test.mjs @@ -226,3 +226,20 @@ test('pickGlobe meets the terrain along the pick ray, or the ellipsoid', () => { near(math.toDegrees(place.longitude), 0, 1e-6, 'longitude'); assert.equal(engine.picker.pickGlobe(cartesian2.create(640, -1e7)), undefined); }); + +test('a user gesture cancels a flight without raising camera.commanded', () => { + const { engine, view } = setup(); + const outcome = []; + let commanded = 0; + engine.camera.flyTo({ + destination: cartesian3.fromDegrees(10, 10, 1e6), + duration: 5, + cancel: () => outcome.push('cancel'), + }); + engine.camera.commanded.addEventListener(() => commanded++); + view.canvas.dispatchEvent(new Event('pointerdown')); + view.canvas.dispatchEvent(new Event('wheel')); + assert.deepEqual(outcome, ['cancel']); + assert.equal(engine.camera.flightActive(), false); + assert.equal(commanded, 0, 'a gesture is the user, not a new camera owner'); +}); diff --git a/src/renderGovernor.js b/src/renderGovernor.js index f83da4803..4fd1c17a9 100644 --- a/src/renderGovernor.js +++ b/src/renderGovernor.js @@ -36,9 +36,17 @@ * 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 and the owner-counted holds drive + * `engine.render.setContinuous`, requests go to `engine.render.request`. */ +import { isGlobeEngine } from './globe/contract.js'; + let _viewer = null; +/** The non-Cesium engine the governor drives, or null (Cesium: `_viewer`). */ +let _engine = null; let _installed = false; const _holds = new Set(); @@ -47,8 +55,15 @@ const _recentRequests = []; const RECENT_REQUEST_CAP = 16; function applyMode() { - if (!_installed || !_viewer?.scene) return; + if (!_installed) return; const continuous = _holds.size > 0; + if (_engine) { + if (_engine.render.isContinuous() !== continuous) + _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; @@ -64,12 +79,21 @@ function applyMode() { * 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 + * @param {Cesium.Viewer | import('./globe/contract.js').GlobeEngine} viewer + * The Cesium viewer, or a non-Cesium globe engine. * @returns {void} */ export function installRenderGovernor(viewer) { + if (isGlobeEngine(viewer) && viewer.kind !== 'cesium') { + _engine = viewer; + _viewer = viewer; + _installed = true; + applyMode(); + return; + } 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 — @@ -115,12 +139,13 @@ export function releaseContinuousRender(ownerId) { * @returns {void} */ export function governorRequestRender(reason = 'unspecified') { - if (!_installed || !_viewer?.scene) return; + if (!_installed || (!_engine && !_viewer?.scene)) return; if (_holds.size === 0) { _recentRequests.push({ reason, at: Date.now() }); if (_recentRequests.length > RECENT_REQUEST_CAP) _recentRequests.shift(); } - _viewer.scene.requestRender?.(); + if (_engine) _engine.render.request(reason); + else _viewer.scene.requestRender?.(); } /** @@ -150,6 +175,7 @@ export function isRenderGovernorInstalled(viewer) { export function uninstallRenderGovernor(viewer) { if (_viewer !== viewer) return; _viewer = null; + _engine = null; _installed = false; _holds.clear(); _recentRequests.length = 0; @@ -158,6 +184,7 @@ export function uninstallRenderGovernor(viewer) { /** Test seam: reset module state between unit tests. */ export function _resetRenderGovernorForTest() { _viewer = null; + _engine = null; _installed = false; _holds.clear(); _recentRequests.length = 0; diff --git a/src/renderGovernor.test.mjs b/src/renderGovernor.test.mjs index d59151ebf..d9bdcc574 100644 --- a/src/renderGovernor.test.mjs +++ b/src/renderGovernor.test.mjs @@ -7,6 +7,8 @@ import { governorRequestRender, getRenderGovernorDiagnostics, _resetRenderGovernorForTest, + isRenderGovernorInstalled, + uninstallRenderGovernor, } from './renderGovernor.js'; function makeViewer() { @@ -110,3 +112,27 @@ test('isRenderGovernorInstalled names the one governed viewer', async () => { 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'); + installRenderGovernor(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(); +});