diff --git a/src/app/navaraScene.test.mjs b/src/app/navaraScene.test.mjs index cc23114e0..e971bdd4d 100644 --- a/src/app/navaraScene.test.mjs +++ b/src/app/navaraScene.test.mjs @@ -204,3 +204,59 @@ test('Navara keeps attribution for every available W1/W2 source and excludes una keys.map((key) => `Credit ${key}`), ); }); + +test('parking or tearing down the Navara scene releases a running camera verb', async (t) => { + const { initCameraVerbs, moveCamera, getActiveCameraMotion } = + await import('../cameraVerbs.js'); + const { createRendererCache } = + await import('../standalone/rendererCache.js'); + for (const end of ['park', 'teardown']) { + const dom = installDom(); + t.after(dom.restore); + // The renderer cache's own host element (parking needs one). + const element = () => ({ + className: '', + prepend() {}, + remove() {}, + }); + globalThis.document.createElement = element; + dom.container.prepend = () => {}; + const engine = createFakeEngine({ + kind: 'navara', + canvas: new EventTarget(), + }); + const cache = createRendererCache(); + const cleanups = []; + await createApplicationScene({ + engineKind: 'navara', + requestServices, + googleApiKey: '', + credits: [], + loaderStatus: { textContent: '' }, + signal: new AbortController().signal, + defer: (dispose) => cleanups.push(dispose), + rendererCache: cache, + loadNavaraGlobe: async () => async () => ({ + engine, + setRenderLoop() {}, + destroy: () => engine.destroy(), + }), + }); + initCameraVerbs(engine); + assert.equal( + moveCamera({ motion: 'pan', direction: 'left', mode: 'continuous' }).ok, + true, + ); + assert.equal(getActiveCameraMotion()?.rig, 'engine'); + // An engine switch parks the renderer (its scene's suspend); a page + // teardown disposes it. + if (end === 'park') cache.retain('navara'); + while (cleanups.length) await cleanups.pop()(); + if (end === 'teardown') cache.clear(); + assert.equal(getActiveCameraMotion(), null, end); + engine.fake.resetCalls(); + if (!engine.isDestroyed()) engine.fake.frames(3); + assert.equal(engine.fake.callsOf('camera.setView').length, 0, end); + if (end === 'park') cache.clear(); + } +}); diff --git a/src/app/scene.js b/src/app/scene.js index 5821ed23e..5609d7790 100644 --- a/src/app/scene.js +++ b/src/app/scene.js @@ -112,8 +112,10 @@ async function createRenderer({ }) { // The Navara mode's own code loads only in that mode, with Navara itself, // so the Cesium-mode bundle carries none of it. - if (engineKind === 'navara') - return (await import('./navaraMode.js')).createNavaraScene({ + if (engineKind === 'navara') { + const scene = await ( + await import('./navaraMode.js') + ).createNavaraScene({ container, cesiumToken, credits, @@ -124,6 +126,17 @@ async function createRenderer({ signal, defer, }); + // As the Cesium scene below: a parked or released engine keeps no camera + // verb running on it (a Directions flight is parked before this). + const { engine } = scene; + defer(() => releaseCameraVerbs(engine)); + const suspend = scene.suspend; + scene.suspend = () => { + releaseCameraVerbs(engine); + suspend(); + }; + return scene; + } if (engineKind !== 'cesium') throw new TypeError(`Unknown globe engine: ${engineKind}`); loaderStatus.textContent = t('Configuring viewer...'); @@ -228,6 +241,7 @@ async function createRenderer({ uninstallRenderGovernor(viewer); engine.camera.cancelFlight(); releaseCameraVerbs(viewer); + releaseCameraVerbs(engine); viewer.useDefaultRenderLoop = false; }, resume() {