mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-15 15:52:21 +00:00
Keep globe renderers warm across live engine switches
This commit is contained in:
1 parent
f71c76c306
commit
5e658d8c9d
15 files changed
+343
-34
No files matched your search
@@ -40,6 +40,8 @@ try {
|
|||||||
await page.waitForFunction(
|
await page.waitForFunction(
|
||||||
(kind) =>
|
(kind) =>
|
||||||
window.__godsEyeView?.engine?.kind === kind &&
|
window.__godsEyeView?.engine?.kind === kind &&
|
||||||
|
document.querySelector('#globe-engine')?.value === kind &&
|
||||||
|
!document.querySelector('#globe-engine').disabled &&
|
||||||
document.getElementById('loading-screen')?.classList.contains('hidden'),
|
document.getElementById('loading-screen')?.classList.contains('hidden'),
|
||||||
{ timeout: 90000 },
|
{ timeout: 90000 },
|
||||||
kind,
|
kind,
|
||||||
@@ -65,19 +67,60 @@ try {
|
|||||||
await page.waitForSelector('#globe-engine', { visible: true });
|
await page.waitForSelector('#globe-engine', { visible: true });
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
window.__switchDocument = document;
|
window.__switchDocument = document;
|
||||||
|
window.__renderers ??= new Map();
|
||||||
|
window.__renderers.set(
|
||||||
|
window.__godsEyeView.engine.kind,
|
||||||
|
window.__godsEyeView.engine,
|
||||||
|
);
|
||||||
|
window.__switchSplash = false;
|
||||||
|
window.__switchObserver = new MutationObserver(() => {
|
||||||
|
if (!document.querySelector('#loading-screen.hidden'))
|
||||||
|
window.__switchSplash = true;
|
||||||
|
});
|
||||||
|
window.__switchObserver.observe(
|
||||||
|
document.querySelector('#loading-screen'),
|
||||||
|
{ attributes: true },
|
||||||
|
);
|
||||||
window.__outgoingEngine = window.__godsEyeView.engine;
|
window.__outgoingEngine = window.__godsEyeView.engine;
|
||||||
});
|
});
|
||||||
await page.select('#globe-engine', kind);
|
await page.select('#globe-engine', kind);
|
||||||
await ready(kind);
|
await ready(kind);
|
||||||
assert.ok(await page.evaluate(() => window.__switchDocument === document));
|
assert.ok(await page.evaluate(() => window.__switchDocument === document));
|
||||||
assert.ok(
|
assert.ok(
|
||||||
await page.evaluate(
|
await page.evaluate(() => {
|
||||||
() =>
|
window.__switchObserver.disconnect();
|
||||||
window.__outgoingEngine.isDestroyed() &&
|
const { engine } = window.__godsEyeView;
|
||||||
|
const previous = window.__renderers.get(engine.kind);
|
||||||
|
return (
|
||||||
|
!window.__switchSplash &&
|
||||||
|
(!previous || previous === engine) &&
|
||||||
|
!window.__outgoingEngine.isDestroyed() &&
|
||||||
|
!window.__outgoingEngine.host.canvas.isConnected &&
|
||||||
|
engine.host.canvas.isConnected &&
|
||||||
window.__godsEyeView.dataManager
|
window.__godsEyeView.dataManager
|
||||||
.getEnabledLayerIds()
|
.getEnabledLayerIds()
|
||||||
.has('earthquakes'),
|
.has('earthquakes')
|
||||||
),
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
await page.waitForFunction(
|
||||||
|
() => !document.querySelector('.engine-switch-frame'),
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
await page.evaluate(async () => {
|
||||||
|
const parked = window.__outgoingEngine;
|
||||||
|
let frames = 0;
|
||||||
|
const remove = parked.frames.postRender.addEventListener(() => {
|
||||||
|
frames++;
|
||||||
|
});
|
||||||
|
parked.render.request('qa-parked');
|
||||||
|
await new Promise((resolve) =>
|
||||||
|
requestAnimationFrame(() => requestAnimationFrame(resolve)),
|
||||||
|
);
|
||||||
|
remove();
|
||||||
|
return frames;
|
||||||
|
}),
|
||||||
|
0,
|
||||||
);
|
);
|
||||||
const url = new URL(page.url());
|
const url = new URL(page.url());
|
||||||
assert.equal(url.searchParams.get('engine'), kind);
|
assert.equal(url.searchParams.get('engine'), kind);
|
||||||
@@ -155,7 +198,7 @@ try {
|
|||||||
assert.equal(await stored(), 'cesium');
|
assert.equal(await stored(), 'cesium');
|
||||||
assert.deepEqual(errors, []);
|
assert.deepEqual(errors, []);
|
||||||
console.log(
|
console.log(
|
||||||
'PASS Cesium default, both switches, same document, outgoing engine destroyed, share pose/map/layers, Back/Forward, remembered engine, blocked preference storage',
|
'PASS Cesium default, both switches, same document, warm engine identity, parked canvas, no startup splash, share pose/map/layers, Back/Forward, remembered engine, blocked preference storage',
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
await browser.close();
|
await browser.close();
|
||||||
|
|||||||
Vendored
+2
@@ -20,6 +20,7 @@ export function createApplicationControls({
|
|||||||
defer,
|
defer,
|
||||||
initialShareCode = null,
|
initialShareCode = null,
|
||||||
localePicker = true,
|
localePicker = true,
|
||||||
|
engineSwitch = false,
|
||||||
}) {
|
}) {
|
||||||
// Initialize the style manager (post-processing, HUD, locations, share links)
|
// Initialize the style manager (post-processing, HUD, locations, share links)
|
||||||
const styleManager = new Controls(viewer, {
|
const styleManager = new Controls(viewer, {
|
||||||
@@ -40,6 +41,7 @@ export function createApplicationControls({
|
|||||||
placeSearch,
|
placeSearch,
|
||||||
// A share code to open at start-up instead of the page's address.
|
// A share code to open at start-up instead of the page's address.
|
||||||
initialShareCode,
|
initialShareCode,
|
||||||
|
engineSwitch,
|
||||||
localePicker,
|
localePicker,
|
||||||
});
|
});
|
||||||
defer(() => styleManager.orbitController.stop());
|
defer(() => styleManager.orbitController.stop());
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ export async function registerNavaraLayerRenderers() {
|
|||||||
* credits in its own attribution UI.
|
* credits in its own attribution UI.
|
||||||
*/
|
*/
|
||||||
export async function createNavaraScene({
|
export async function createNavaraScene({
|
||||||
operations,
|
container = document.getElementById('cesiumContainer'),
|
||||||
cesiumToken,
|
cesiumToken,
|
||||||
credits,
|
credits,
|
||||||
MapController,
|
MapController,
|
||||||
@@ -191,7 +191,7 @@ export async function createNavaraScene({
|
|||||||
const createNavaraGlobe = await loadNavaraGlobe();
|
const createNavaraGlobe = await loadNavaraGlobe();
|
||||||
signal.throwIfAborted();
|
signal.throwIfAborted();
|
||||||
const globe = await createNavaraGlobe({
|
const globe = await createNavaraGlobe({
|
||||||
container: document.getElementById('cesiumContainer'),
|
container,
|
||||||
// The cinematic look unless `?look=photoreal|minimal` asks otherwise.
|
// The cinematic look unless `?look=photoreal|minimal` asks otherwise.
|
||||||
search: globalThis.location?.search ?? '',
|
search: globalThis.location?.search ?? '',
|
||||||
});
|
});
|
||||||
@@ -241,8 +241,8 @@ export async function createNavaraScene({
|
|||||||
viewer: null,
|
viewer: null,
|
||||||
engine,
|
engine,
|
||||||
tileset: null,
|
tileset: null,
|
||||||
|
setRenderLoop: globe.setRenderLoop,
|
||||||
mapStackController,
|
mapStackController,
|
||||||
operations,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+33
-12
@@ -26,16 +26,10 @@ import { onLocaleChange, t } from '../i18n/index.js';
|
|||||||
export async function createApplicationScene({
|
export async function createApplicationScene({
|
||||||
requestServices,
|
requestServices,
|
||||||
googleApiKey,
|
googleApiKey,
|
||||||
googleTokens = null,
|
|
||||||
cesiumToken,
|
|
||||||
credits,
|
|
||||||
MapController = MapStackController,
|
|
||||||
mapOptions = {},
|
|
||||||
loaderStatus,
|
|
||||||
engineKind = 'cesium',
|
|
||||||
loadNavaraGlobe,
|
|
||||||
signal,
|
signal,
|
||||||
defer,
|
defer,
|
||||||
|
rendererCache,
|
||||||
|
...options
|
||||||
}) {
|
}) {
|
||||||
const operations = createApplicationOperations({
|
const operations = createApplicationOperations({
|
||||||
requests: requestServices,
|
requests: requestServices,
|
||||||
@@ -51,11 +45,38 @@ export async function createApplicationScene({
|
|||||||
else window.__GOOGLE_MAPS_API_KEY__ = previousKey;
|
else window.__GOOGLE_MAPS_API_KEY__ = previousKey;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
const build = (resources) =>
|
||||||
|
createRenderer({
|
||||||
|
...options,
|
||||||
|
googleApiKey,
|
||||||
|
signal,
|
||||||
|
...resources,
|
||||||
|
});
|
||||||
|
const { viewer, engine, tileset, mapStackController } = rendererCache
|
||||||
|
? await rendererCache.acquire(options.engineKind, build, defer)
|
||||||
|
: await build({ defer });
|
||||||
|
return { viewer, engine, tileset, mapStackController, operations };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createRenderer({
|
||||||
|
googleApiKey,
|
||||||
|
googleTokens = null,
|
||||||
|
cesiumToken,
|
||||||
|
credits,
|
||||||
|
MapController = MapStackController,
|
||||||
|
mapOptions = {},
|
||||||
|
loaderStatus,
|
||||||
|
engineKind = 'cesium',
|
||||||
|
loadNavaraGlobe,
|
||||||
|
signal,
|
||||||
|
defer,
|
||||||
|
container,
|
||||||
|
}) {
|
||||||
// The Navara mode's own code loads only in that mode, with Navara itself,
|
// The Navara mode's own code loads only in that mode, with Navara itself,
|
||||||
// so the Cesium-mode bundle carries none of it.
|
// so the Cesium-mode bundle carries none of it.
|
||||||
if (engineKind === 'navara')
|
if (engineKind === 'navara')
|
||||||
return (await import('./navaraMode.js')).createNavaraScene({
|
return (await import('./navaraMode.js')).createNavaraScene({
|
||||||
operations,
|
container,
|
||||||
cesiumToken,
|
cesiumToken,
|
||||||
credits,
|
credits,
|
||||||
MapController,
|
MapController,
|
||||||
@@ -71,10 +92,10 @@ export async function createApplicationScene({
|
|||||||
// Provider attribution stays visible, including clean-view and recording.
|
// Provider attribution stays visible, including clean-view and recording.
|
||||||
const creditContainer = document.createElement('div');
|
const creditContainer = document.createElement('div');
|
||||||
creditContainer.id = 'cesium-credits';
|
creditContainer.id = 'cesium-credits';
|
||||||
document.body.appendChild(creditContainer);
|
(container ?? document.body).appendChild(creditContainer);
|
||||||
defer(() => creditContainer.remove());
|
defer(() => creditContainer.remove());
|
||||||
const viewer = createApplicationViewer({
|
const viewer = createApplicationViewer({
|
||||||
container: 'cesiumContainer',
|
container: container ?? 'cesiumContainer',
|
||||||
creditContainer,
|
creditContainer,
|
||||||
});
|
});
|
||||||
defer(() => {
|
defer(() => {
|
||||||
@@ -160,5 +181,5 @@ export async function createApplicationScene({
|
|||||||
});
|
});
|
||||||
|
|
||||||
signal.throwIfAborted();
|
signal.throwIfAborted();
|
||||||
return { viewer, engine, tileset, mapStackController, operations };
|
return { viewer, engine, tileset, mapStackController };
|
||||||
}
|
}
|
||||||
@@ -16,7 +16,9 @@ export function startApplicationChrome({
|
|||||||
const minimumDelay = new Promise((resolve) => {
|
const minimumDelay = new Promise((resolve) => {
|
||||||
resolveDelay = resolve;
|
resolveDelay = resolve;
|
||||||
});
|
});
|
||||||
const delayTimer = setTimeout(resolveDelay, 1000);
|
const switching = loadingScreen.classList.contains?.('hidden');
|
||||||
|
const delayTimer = switching ? null : setTimeout(resolveDelay, 1000);
|
||||||
|
if (switching) resolveDelay();
|
||||||
const revealFirstRun = () => {
|
const revealFirstRun = () => {
|
||||||
if (disposed || signal.aborted || firstRun) return;
|
if (disposed || signal.aborted || firstRun) return;
|
||||||
firstRun = initializeWelcome?.({ styleManager, dataManager });
|
firstRun = initializeWelcome?.({ styleManager, dataManager });
|
||||||
@@ -28,7 +30,7 @@ export function startApplicationChrome({
|
|||||||
/* Restoration reports its own outcome through the controls. */
|
/* Restoration reports its own outcome through the controls. */
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if (disposed || signal.aborted) return;
|
if (disposed || signal.aborted || switching) return;
|
||||||
loadingScreen.classList.add('hidden');
|
loadingScreen.classList.add('hidden');
|
||||||
loadingScreen.addEventListener('transitionend', revealFirstRun, {
|
loadingScreen.addEventListener('transitionend', revealFirstRun, {
|
||||||
once: true,
|
once: true,
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
forEachLuminanceUniform,
|
forEachLuminanceUniform,
|
||||||
keepCascadedLightIds,
|
keepCascadedLightIds,
|
||||||
navaraHooks,
|
navaraHooks,
|
||||||
|
setRenderLoopEnabled,
|
||||||
} from './internals.js';
|
} from './internals.js';
|
||||||
import { buildNavaraLook } from './look.js';
|
import { buildNavaraLook } from './look.js';
|
||||||
import { createNavaraSharpen } from './sharpen.js';
|
import { createNavaraSharpen } from './sharpen.js';
|
||||||
@@ -201,6 +202,7 @@ export async function createNavaraGlobe({
|
|||||||
NavaraEffect,
|
NavaraEffect,
|
||||||
DrapedMesh,
|
DrapedMesh,
|
||||||
setupMaterialForDrape,
|
setupMaterialForDrape,
|
||||||
|
setRenderLoop: (enabled) => setRenderLoopEnabled(view, enabled),
|
||||||
destroy() {
|
destroy() {
|
||||||
if (destroyed) return;
|
if (destroyed) return;
|
||||||
destroyed = true;
|
destroyed = true;
|
||||||
|
|||||||
@@ -9,6 +9,14 @@ import { patchCascadedLightCopy } from './cascadedLightCopy.js';
|
|||||||
|
|
||||||
export const NAVARA_INTERNALS_VERSION = '0.2.1';
|
export const NAVARA_INTERNALS_VERSION = '0.2.1';
|
||||||
|
|
||||||
|
// Navara 0.2.1's animation=false still updates the tile tree every RAF.
|
||||||
|
// Until ThreeView exposes suspend/resume, stop its underlying Three loop
|
||||||
|
// while this view is parked and reinstall its normal loop when it returns.
|
||||||
|
export function setRenderLoopEnabled(view, enabled) {
|
||||||
|
if (enabled) view._startMainLoop();
|
||||||
|
else view._renderer.setAnimationLoop(null);
|
||||||
|
}
|
||||||
|
|
||||||
let restoreRelease = null;
|
let restoreRelease = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+58
-5
@@ -2,6 +2,7 @@ import {
|
|||||||
createStandaloneApplication,
|
createStandaloneApplication,
|
||||||
pageEngineKind,
|
pageEngineKind,
|
||||||
} from './standalone/application.js';
|
} from './standalone/application.js';
|
||||||
|
import { createRendererCache } from './standalone/rendererCache.js';
|
||||||
import { describeError } from './standalone/errors.js';
|
import { describeError } from './standalone/errors.js';
|
||||||
import { ENGINE_STORAGE_KEY } from './app/engineChoice.js';
|
import { ENGINE_STORAGE_KEY } from './app/engineChoice.js';
|
||||||
import { useDisplayStore } from './ui/stores/display.js';
|
import { useDisplayStore } from './ui/stores/display.js';
|
||||||
@@ -13,16 +14,26 @@ const loading = document.getElementById('loading-screen');
|
|||||||
let kind = pageEngineKind();
|
let kind = pageEngineKind();
|
||||||
let application;
|
let application;
|
||||||
let busy = false;
|
let busy = false;
|
||||||
|
const renderers = createRendererCache();
|
||||||
|
window.addEventListener('pagehide', (event) => {
|
||||||
|
if (!event.persisted) renderers.clear();
|
||||||
|
});
|
||||||
|
|
||||||
async function start(next) {
|
async function start(next, engineSwitch = false) {
|
||||||
kind = next;
|
kind = next;
|
||||||
application = createStandaloneApplication({
|
application = createStandaloneApplication({
|
||||||
googleApiKey: import.meta.env.GOOGLE_MAPS_API_KEY,
|
googleApiKey: import.meta.env.GOOGLE_MAPS_API_KEY,
|
||||||
cesiumToken: import.meta.env.CESIUM_ION_TOKEN,
|
cesiumToken: import.meta.env.CESIUM_ION_TOKEN,
|
||||||
allowQaRegistration: import.meta.env.DEV,
|
allowQaRegistration: import.meta.env.DEV,
|
||||||
engineKind: next,
|
engineKind: next,
|
||||||
|
rendererCache: renderers,
|
||||||
|
engineSwitch,
|
||||||
});
|
});
|
||||||
await application.start();
|
const { scene, controls } = await application.start();
|
||||||
|
if (engineSwitch) {
|
||||||
|
await controls.styleManager.initialRestorePromise;
|
||||||
|
await waitForGlobeFrame(scene.engine);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function present() {
|
function present() {
|
||||||
@@ -48,6 +59,45 @@ function select(next) {
|
|||||||
void reconcile(true);
|
void reconcile(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keep the last visible frame while the first visit initializes the other renderer.
|
||||||
|
function keepGlobeFrame(engine) {
|
||||||
|
const source = engine.host.canvas;
|
||||||
|
const frame = document.createElement('canvas');
|
||||||
|
frame.width = source.width;
|
||||||
|
frame.height = source.height;
|
||||||
|
if (!frame.width || !frame.height) return null;
|
||||||
|
const context = frame.getContext('2d');
|
||||||
|
if (!context) return null;
|
||||||
|
context.drawImage(source, 0, 0);
|
||||||
|
frame.className = 'engine-switch-frame';
|
||||||
|
frame.setAttribute('aria-hidden', 'true');
|
||||||
|
document.getElementById('cesiumContainer').append(frame);
|
||||||
|
return frame;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hand off the visible globe only after the new camera has actually drawn.
|
||||||
|
function waitForGlobeFrame(engine) {
|
||||||
|
if (document.hidden) return;
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const finish = () => {
|
||||||
|
remove();
|
||||||
|
clearTimeout(timer);
|
||||||
|
document.removeEventListener('visibilitychange', done);
|
||||||
|
};
|
||||||
|
const remove = engine.frames.postRender.addEventListener(done);
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
finish();
|
||||||
|
reject(new Error('The globe did not draw a frame'));
|
||||||
|
}, 8000);
|
||||||
|
function done() {
|
||||||
|
finish();
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
document.addEventListener('visibilitychange', done, { once: true });
|
||||||
|
engine.render.request('engine-switch');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// popstate can arrive again while an engine is starting. After teardown and
|
// popstate can arrive again while an engine is starting. After teardown and
|
||||||
// after startup, read the actual address so the latest history entry wins.
|
// after startup, read the actual address so the latest history entry wins.
|
||||||
async function reconcile(remember = false) {
|
async function reconcile(remember = false) {
|
||||||
@@ -55,11 +105,13 @@ async function reconcile(remember = false) {
|
|||||||
busy = true;
|
busy = true;
|
||||||
present();
|
present();
|
||||||
const previous = kind;
|
const previous = kind;
|
||||||
|
let cover;
|
||||||
try {
|
try {
|
||||||
while (pageEngineKind() !== kind) {
|
while (pageEngineKind() !== kind) {
|
||||||
loading.classList.remove('hidden');
|
cover ??= keepGlobeFrame(application.getComponents().scene.engine);
|
||||||
|
renderers.retain(kind);
|
||||||
await application.destroy();
|
await application.destroy();
|
||||||
await start(pageEngineKind());
|
await start(pageEngineKind(), true);
|
||||||
}
|
}
|
||||||
if (remember) {
|
if (remember) {
|
||||||
try {
|
try {
|
||||||
@@ -75,7 +127,7 @@ async function reconcile(remember = false) {
|
|||||||
history.replaceState(history.state, '', url);
|
history.replaceState(history.state, '', url);
|
||||||
try {
|
try {
|
||||||
await application.destroy();
|
await application.destroy();
|
||||||
await start(previous);
|
await start(previous, true);
|
||||||
application
|
application
|
||||||
.getComponents()
|
.getComponents()
|
||||||
.controls.styleManager._showToast(describeError(error));
|
.controls.styleManager._showToast(describeError(error));
|
||||||
@@ -83,6 +135,7 @@ async function reconcile(remember = false) {
|
|||||||
report(recoveryError);
|
report(recoveryError);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
cover?.remove();
|
||||||
busy = false;
|
busy = false;
|
||||||
present();
|
present();
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-2
@@ -333,7 +333,10 @@ export class ShareLinkManager {
|
|||||||
/**
|
/**
|
||||||
* Apply a parsed state to the camera + style manager.
|
* Apply a parsed state to the camera + style manager.
|
||||||
*/
|
*/
|
||||||
async applyState(state, { applyCamera = true, navigationToken = null } = {}) {
|
async applyState(
|
||||||
|
state,
|
||||||
|
{ applyCamera = true, instant = false, navigationToken = null } = {},
|
||||||
|
) {
|
||||||
if (this._destroyed || !state)
|
if (this._destroyed || !state)
|
||||||
return { succeeded: false, reason: 'unavailable' };
|
return { succeeded: false, reason: 'unavailable' };
|
||||||
const view = {
|
const view = {
|
||||||
@@ -347,7 +350,11 @@ export class ShareLinkManager {
|
|||||||
let cameraPromise = Promise.resolve({
|
let cameraPromise = Promise.resolve({
|
||||||
status: applyCamera ? 'superseded' : 'skipped',
|
status: applyCamera ? 'superseded' : 'skipped',
|
||||||
});
|
});
|
||||||
if (applyCamera && this._isNavigationCurrent(navigationToken)) {
|
if (applyCamera && instant && this._isNavigationCurrent(navigationToken)) {
|
||||||
|
this.engine.camera.setView(view);
|
||||||
|
this.engine.render.request('engine-switch');
|
||||||
|
cameraPromise = Promise.resolve({ status: 'applied' });
|
||||||
|
} else if (applyCamera && this._isNavigationCurrent(navigationToken)) {
|
||||||
const restoreGeneration = ++this._restoreGeneration;
|
const restoreGeneration = ++this._restoreGeneration;
|
||||||
let settleCamera;
|
let settleCamera;
|
||||||
cameraPromise = new Promise((resolve) => {
|
cameraPromise = new Promise((resolve) => {
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ export function createStandaloneApplication({
|
|||||||
streetLevelProviders,
|
streetLevelProviders,
|
||||||
allowQaRegistration = false,
|
allowQaRegistration = false,
|
||||||
engineKind = pageEngineKind(),
|
engineKind = pageEngineKind(),
|
||||||
|
rendererCache,
|
||||||
|
engineSwitch = false,
|
||||||
}) {
|
}) {
|
||||||
if (owner)
|
if (owner)
|
||||||
throw new Error('The standalone application already owns this page');
|
throw new Error('The standalone application already owns this page');
|
||||||
@@ -86,6 +88,7 @@ export function createStandaloneApplication({
|
|||||||
cesiumToken,
|
cesiumToken,
|
||||||
loaderStatus,
|
loaderStatus,
|
||||||
engineKind,
|
engineKind,
|
||||||
|
rendererCache,
|
||||||
});
|
});
|
||||||
// The catalog of this engine: on Navara only the layers with a Navara
|
// The catalog of this engine: on Navara only the layers with a Navara
|
||||||
// renderer (src/app/layers/renderers.js; none yet), the others' state
|
// renderer (src/app/layers/renderers.js; none yet), the others' state
|
||||||
@@ -123,6 +126,7 @@ export function createStandaloneApplication({
|
|||||||
createControls: (context) =>
|
createControls: (context) =>
|
||||||
createStandaloneControls({
|
createStandaloneControls({
|
||||||
...context,
|
...context,
|
||||||
|
engineSwitch,
|
||||||
loaderStatus,
|
loaderStatus,
|
||||||
placeSearch,
|
placeSearch,
|
||||||
catalog,
|
catalog,
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import { releaseCameraVerbs } from '../cameraVerbs.js';
|
||||||
|
import { uninstallRenderGovernor } from '../renderGovernor.js';
|
||||||
|
|
||||||
|
// Only the renderer and its map survive a switch. The outgoing UI, data feeds,
|
||||||
|
// tools and their subscriptions are still disposed with their application.
|
||||||
|
export function createRendererCache() {
|
||||||
|
const idle = new Map();
|
||||||
|
let retainKind;
|
||||||
|
|
||||||
|
async function acquire(kind, build, defer) {
|
||||||
|
const host = document.getElementById('cesiumContainer');
|
||||||
|
let entry = idle.get(kind);
|
||||||
|
idle.delete(kind);
|
||||||
|
if (entry?.lost) {
|
||||||
|
entry.dispose();
|
||||||
|
entry = null;
|
||||||
|
}
|
||||||
|
if (!entry) {
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.className = 'globe-renderer';
|
||||||
|
host.prepend(container);
|
||||||
|
const cleanups = [() => container.remove()];
|
||||||
|
const dispose = () => {
|
||||||
|
const errors = [];
|
||||||
|
while (cleanups.length) {
|
||||||
|
try {
|
||||||
|
cleanups.pop()();
|
||||||
|
} catch (error) {
|
||||||
|
errors.push(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (errors.length)
|
||||||
|
throw new AggregateError(errors, 'Renderer cleanup failed');
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const scene = await build({
|
||||||
|
container,
|
||||||
|
defer: (fn) => cleanups.push(fn),
|
||||||
|
});
|
||||||
|
entry = { container, scene, dispose, lost: false };
|
||||||
|
scene.engine.host.canvas.addEventListener(
|
||||||
|
'webglcontextlost',
|
||||||
|
() => {
|
||||||
|
entry.lost = true;
|
||||||
|
},
|
||||||
|
{ once: true },
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
dispose();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
host.prepend(entry.container);
|
||||||
|
if (entry.scene.viewer)
|
||||||
|
entry.scene.viewer.useDefaultRenderLoop = !document.hidden;
|
||||||
|
else entry.scene.setRenderLoop(true);
|
||||||
|
entry.scene.engine.render.request('engine-resume');
|
||||||
|
}
|
||||||
|
const { viewer, engine } = entry.scene;
|
||||||
|
defer(() => {
|
||||||
|
const keep = retainKind === kind && !entry.lost;
|
||||||
|
retainKind = null;
|
||||||
|
if (!keep) return entry.dispose();
|
||||||
|
uninstallRenderGovernor(viewer ?? engine);
|
||||||
|
engine.camera.cancelFlight();
|
||||||
|
if (viewer) {
|
||||||
|
releaseCameraVerbs(viewer);
|
||||||
|
viewer.useDefaultRenderLoop = false;
|
||||||
|
} else {
|
||||||
|
engine.render.setContinuous(false);
|
||||||
|
entry.scene.setRenderLoop(false);
|
||||||
|
}
|
||||||
|
entry.container.remove();
|
||||||
|
idle.set(kind, entry);
|
||||||
|
});
|
||||||
|
return entry.scene;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
acquire,
|
||||||
|
retain(kind) {
|
||||||
|
retainKind = kind;
|
||||||
|
},
|
||||||
|
clear() {
|
||||||
|
for (const entry of idle.values()) entry.dispose();
|
||||||
|
idle.clear();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
import { createRendererCache } from './rendererCache.js';
|
||||||
|
|
||||||
|
test('a parked renderer is reused, but a lost context is disposed before rebuilding', async (t) => {
|
||||||
|
const host = {
|
||||||
|
prepend: (el) => {
|
||||||
|
el.isConnected = true;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const saved = globalThis.document;
|
||||||
|
globalThis.document = {
|
||||||
|
getElementById: () => host,
|
||||||
|
createElement: () => ({
|
||||||
|
remove() {
|
||||||
|
this.isConnected = false;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
t.after(() => {
|
||||||
|
globalThis.document = saved;
|
||||||
|
});
|
||||||
|
const cache = createRendererCache();
|
||||||
|
let released = 0;
|
||||||
|
let release;
|
||||||
|
const build = ({ container, defer }) => {
|
||||||
|
defer(() => {
|
||||||
|
released += 1;
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
setRenderLoop(enabled) {
|
||||||
|
this.running = enabled;
|
||||||
|
},
|
||||||
|
engine: {
|
||||||
|
host: { container, canvas: new EventTarget() },
|
||||||
|
camera: { cancelFlight() {} },
|
||||||
|
render: { request() {}, setContinuous() {} },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const open = () =>
|
||||||
|
cache.acquire('navara', build, (stop) => {
|
||||||
|
release = stop;
|
||||||
|
});
|
||||||
|
const first = await open();
|
||||||
|
cache.retain('navara');
|
||||||
|
release();
|
||||||
|
assert.equal(first.running, false);
|
||||||
|
assert.equal(first.engine.host.container.isConnected, false);
|
||||||
|
assert.equal(await open(), first);
|
||||||
|
assert.equal(first.running, true);
|
||||||
|
assert.equal(first.engine.host.container.isConnected, true);
|
||||||
|
cache.retain('navara');
|
||||||
|
release();
|
||||||
|
first.engine.host.canvas.dispatchEvent(new Event('webglcontextlost'));
|
||||||
|
assert.notEqual(await open(), first);
|
||||||
|
assert.equal(released, 1);
|
||||||
|
release();
|
||||||
|
cache.clear();
|
||||||
|
assert.equal(released, 2);
|
||||||
|
});
|
||||||
@@ -104,6 +104,7 @@ export class StyleManager extends ShellFacade {
|
|||||||
services,
|
services,
|
||||||
requestServices,
|
requestServices,
|
||||||
initialShareCode = null,
|
initialShareCode = null,
|
||||||
|
engineSwitch = false,
|
||||||
localePicker = true,
|
localePicker = true,
|
||||||
} = {},
|
} = {},
|
||||||
) {
|
) {
|
||||||
@@ -620,7 +621,7 @@ export class StyleManager extends ShellFacade {
|
|||||||
this._publishActiveStyle();
|
this._publishActiveStyle();
|
||||||
this._updateLocationMiniStatus();
|
this._updateLocationMiniStatus();
|
||||||
|
|
||||||
this._shareRestoration.start();
|
this._shareRestoration.start({ engineSwitch });
|
||||||
|
|
||||||
// Keep the parameter panel from overlapping toggle controls.
|
// Keep the parameter panel from overlapping toggle controls.
|
||||||
this._layoutRightPanels();
|
this._layoutRightPanels();
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ export class ShareRestoration {
|
|||||||
this.shareLinkManager = shareLinkManager;
|
this.shareLinkManager = shareLinkManager;
|
||||||
this._initialShareState = shareLinkManager.parseInitialHash();
|
this._initialShareState = shareLinkManager.parseInitialHash();
|
||||||
}
|
}
|
||||||
start() {
|
start({ engineSwitch = false } = {}) {
|
||||||
// Restore from URL hash if present
|
// Restore from URL hash if present
|
||||||
const savedState = this._initialShareState;
|
const savedState = this._initialShareState;
|
||||||
this._initialShareRestorePromise = savedState
|
this._initialShareRestorePromise = savedState
|
||||||
@@ -169,6 +169,7 @@ export class ShareRestoration {
|
|||||||
this.navigation._beginDeferredNavigation('shared view', {
|
this.navigation._beginDeferredNavigation('shared view', {
|
||||||
cancelPendingSelection: false,
|
cancelPendingSelection: false,
|
||||||
});
|
});
|
||||||
|
const delay = engineSwitch ? 0 : 1500;
|
||||||
this._initialShareRestoreTimeout = setTimeout(() => {
|
this._initialShareRestoreTimeout = setTimeout(() => {
|
||||||
this._initialShareRestoreTimeout = null;
|
this._initialShareRestoreTimeout = null;
|
||||||
if (this._disposed) return;
|
if (this._disposed) return;
|
||||||
@@ -178,11 +179,12 @@ export class ShareRestoration {
|
|||||||
this.navigation._reassertNavigationHandoff(generation);
|
this.navigation._reassertNavigationHandoff(generation);
|
||||||
void this._runShareRestore(savedState, {
|
void this._runShareRestore(savedState, {
|
||||||
applyCamera,
|
applyCamera,
|
||||||
|
instant: engineSwitch,
|
||||||
navigationToken: generation,
|
navigationToken: generation,
|
||||||
layersPromise: this._layerStateRestorePromise,
|
layersPromise: this._layerStateRestorePromise,
|
||||||
settle: (result) => this._settleInitialShareRestore(result),
|
settle: (result) => this._settleInitialShareRestore(result),
|
||||||
});
|
});
|
||||||
}, 1500);
|
}, delay);
|
||||||
} else {
|
} else {
|
||||||
this.syncShareState();
|
this.syncShareState();
|
||||||
if (this.shareLinkManager?.rejectedShareCode)
|
if (this.shareLinkManager?.rejectedShareCode)
|
||||||
@@ -236,7 +238,7 @@ export class ShareRestoration {
|
|||||||
*/
|
*/
|
||||||
async _runShareRestore(
|
async _runShareRestore(
|
||||||
state,
|
state,
|
||||||
{ applyCamera, navigationToken, layersPromise, settle },
|
{ applyCamera, instant = false, navigationToken, layersPromise, settle },
|
||||||
) {
|
) {
|
||||||
const generation = this._shareRestoreGeneration;
|
const generation = this._shareRestoreGeneration;
|
||||||
const current = () =>
|
const current = () =>
|
||||||
@@ -244,6 +246,7 @@ export class ShareRestoration {
|
|||||||
try {
|
try {
|
||||||
const share = await this.shareLinkManager.applyState(state, {
|
const share = await this.shareLinkManager.applyState(state, {
|
||||||
applyCamera,
|
applyCamera,
|
||||||
|
instant,
|
||||||
navigationToken,
|
navigationToken,
|
||||||
});
|
});
|
||||||
const layers = await (layersPromise || Promise.resolve([]));
|
const layers = await (layersPromise || Promise.resolve([]));
|
||||||
|
|||||||
Vendored
+13
@@ -122,6 +122,19 @@ body {
|
|||||||
left: 0;
|
left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.globe-renderer,
|
||||||
|
.engine-switch-frame {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.engine-switch-frame {
|
||||||
|
z-index: 1;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* Unified world-overlay host: detection uses one host-owned blend surface
|
/* Unified world-overlay host: detection uses one host-owned blend surface
|
||||||
beneath the shared text/card canvas; both share one frame path. The root
|
beneath the shared text/card canvas; both share one frame path. The root
|
||||||
holds ONLY the card canvas — it is a stacking context (z-index below), and
|
holds ONLY the card canvas — it is a stacking context (z-index below), and
|
||||||
|
|||||||
Reference in new issue
Block a user