mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 11:24:00 +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(
|
||||
(kind) =>
|
||||
window.__godsEyeView?.engine?.kind === kind &&
|
||||
document.querySelector('#globe-engine')?.value === kind &&
|
||||
!document.querySelector('#globe-engine').disabled &&
|
||||
document.getElementById('loading-screen')?.classList.contains('hidden'),
|
||||
{ timeout: 90000 },
|
||||
kind,
|
||||
@@ -65,19 +67,60 @@ try {
|
||||
await page.waitForSelector('#globe-engine', { visible: true });
|
||||
await page.evaluate(() => {
|
||||
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;
|
||||
});
|
||||
await page.select('#globe-engine', kind);
|
||||
await ready(kind);
|
||||
assert.ok(await page.evaluate(() => window.__switchDocument === document));
|
||||
assert.ok(
|
||||
await page.evaluate(
|
||||
() =>
|
||||
window.__outgoingEngine.isDestroyed() &&
|
||||
await page.evaluate(() => {
|
||||
window.__switchObserver.disconnect();
|
||||
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
|
||||
.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());
|
||||
assert.equal(url.searchParams.get('engine'), kind);
|
||||
@@ -155,7 +198,7 @@ try {
|
||||
assert.equal(await stored(), 'cesium');
|
||||
assert.deepEqual(errors, []);
|
||||
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 {
|
||||
await browser.close();
|
||||
|
||||
Vendored
+2
@@ -20,6 +20,7 @@ export function createApplicationControls({
|
||||
defer,
|
||||
initialShareCode = null,
|
||||
localePicker = true,
|
||||
engineSwitch = false,
|
||||
}) {
|
||||
// Initialize the style manager (post-processing, HUD, locations, share links)
|
||||
const styleManager = new Controls(viewer, {
|
||||
@@ -40,6 +41,7 @@ export function createApplicationControls({
|
||||
placeSearch,
|
||||
// A share code to open at start-up instead of the page's address.
|
||||
initialShareCode,
|
||||
engineSwitch,
|
||||
localePicker,
|
||||
});
|
||||
defer(() => styleManager.orbitController.stop());
|
||||
|
||||
@@ -177,7 +177,7 @@ export async function registerNavaraLayerRenderers() {
|
||||
* credits in its own attribution UI.
|
||||
*/
|
||||
export async function createNavaraScene({
|
||||
operations,
|
||||
container = document.getElementById('cesiumContainer'),
|
||||
cesiumToken,
|
||||
credits,
|
||||
MapController,
|
||||
@@ -191,7 +191,7 @@ export async function createNavaraScene({
|
||||
const createNavaraGlobe = await loadNavaraGlobe();
|
||||
signal.throwIfAborted();
|
||||
const globe = await createNavaraGlobe({
|
||||
container: document.getElementById('cesiumContainer'),
|
||||
container,
|
||||
// The cinematic look unless `?look=photoreal|minimal` asks otherwise.
|
||||
search: globalThis.location?.search ?? '',
|
||||
});
|
||||
@@ -241,8 +241,8 @@ export async function createNavaraScene({
|
||||
viewer: null,
|
||||
engine,
|
||||
tileset: null,
|
||||
setRenderLoop: globe.setRenderLoop,
|
||||
mapStackController,
|
||||
operations,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+33
-12
@@ -26,16 +26,10 @@ import { onLocaleChange, t } from '../i18n/index.js';
|
||||
export async function createApplicationScene({
|
||||
requestServices,
|
||||
googleApiKey,
|
||||
googleTokens = null,
|
||||
cesiumToken,
|
||||
credits,
|
||||
MapController = MapStackController,
|
||||
mapOptions = {},
|
||||
loaderStatus,
|
||||
engineKind = 'cesium',
|
||||
loadNavaraGlobe,
|
||||
signal,
|
||||
defer,
|
||||
rendererCache,
|
||||
...options
|
||||
}) {
|
||||
const operations = createApplicationOperations({
|
||||
requests: requestServices,
|
||||
@@ -51,11 +45,38 @@ export async function createApplicationScene({
|
||||
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,
|
||||
// so the Cesium-mode bundle carries none of it.
|
||||
if (engineKind === 'navara')
|
||||
return (await import('./navaraMode.js')).createNavaraScene({
|
||||
operations,
|
||||
container,
|
||||
cesiumToken,
|
||||
credits,
|
||||
MapController,
|
||||
@@ -71,10 +92,10 @@ export async function createApplicationScene({
|
||||
// Provider attribution stays visible, including clean-view and recording.
|
||||
const creditContainer = document.createElement('div');
|
||||
creditContainer.id = 'cesium-credits';
|
||||
document.body.appendChild(creditContainer);
|
||||
(container ?? document.body).appendChild(creditContainer);
|
||||
defer(() => creditContainer.remove());
|
||||
const viewer = createApplicationViewer({
|
||||
container: 'cesiumContainer',
|
||||
container: container ?? 'cesiumContainer',
|
||||
creditContainer,
|
||||
});
|
||||
defer(() => {
|
||||
@@ -160,5 +181,5 @@ export async function createApplicationScene({
|
||||
});
|
||||
|
||||
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) => {
|
||||
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 = () => {
|
||||
if (disposed || signal.aborted || firstRun) return;
|
||||
firstRun = initializeWelcome?.({ styleManager, dataManager });
|
||||
@@ -28,7 +30,7 @@ export function startApplicationChrome({
|
||||
/* Restoration reports its own outcome through the controls. */
|
||||
})
|
||||
.then(() => {
|
||||
if (disposed || signal.aborted) return;
|
||||
if (disposed || signal.aborted || switching) return;
|
||||
loadingScreen.classList.add('hidden');
|
||||
loadingScreen.addEventListener('transitionend', revealFirstRun, {
|
||||
once: true,
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
forEachLuminanceUniform,
|
||||
keepCascadedLightIds,
|
||||
navaraHooks,
|
||||
setRenderLoopEnabled,
|
||||
} from './internals.js';
|
||||
import { buildNavaraLook } from './look.js';
|
||||
import { createNavaraSharpen } from './sharpen.js';
|
||||
@@ -201,6 +202,7 @@ export async function createNavaraGlobe({
|
||||
NavaraEffect,
|
||||
DrapedMesh,
|
||||
setupMaterialForDrape,
|
||||
setRenderLoop: (enabled) => setRenderLoopEnabled(view, enabled),
|
||||
destroy() {
|
||||
if (destroyed) return;
|
||||
destroyed = true;
|
||||
|
||||
@@ -9,6 +9,14 @@ import { patchCascadedLightCopy } from './cascadedLightCopy.js';
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
|
||||
+58
-5
@@ -2,6 +2,7 @@ import {
|
||||
createStandaloneApplication,
|
||||
pageEngineKind,
|
||||
} from './standalone/application.js';
|
||||
import { createRendererCache } from './standalone/rendererCache.js';
|
||||
import { describeError } from './standalone/errors.js';
|
||||
import { ENGINE_STORAGE_KEY } from './app/engineChoice.js';
|
||||
import { useDisplayStore } from './ui/stores/display.js';
|
||||
@@ -13,16 +14,26 @@ const loading = document.getElementById('loading-screen');
|
||||
let kind = pageEngineKind();
|
||||
let application;
|
||||
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;
|
||||
application = createStandaloneApplication({
|
||||
googleApiKey: import.meta.env.GOOGLE_MAPS_API_KEY,
|
||||
cesiumToken: import.meta.env.CESIUM_ION_TOKEN,
|
||||
allowQaRegistration: import.meta.env.DEV,
|
||||
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() {
|
||||
@@ -48,6 +59,45 @@ function select(next) {
|
||||
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
|
||||
// after startup, read the actual address so the latest history entry wins.
|
||||
async function reconcile(remember = false) {
|
||||
@@ -55,11 +105,13 @@ async function reconcile(remember = false) {
|
||||
busy = true;
|
||||
present();
|
||||
const previous = kind;
|
||||
let cover;
|
||||
try {
|
||||
while (pageEngineKind() !== kind) {
|
||||
loading.classList.remove('hidden');
|
||||
cover ??= keepGlobeFrame(application.getComponents().scene.engine);
|
||||
renderers.retain(kind);
|
||||
await application.destroy();
|
||||
await start(pageEngineKind());
|
||||
await start(pageEngineKind(), true);
|
||||
}
|
||||
if (remember) {
|
||||
try {
|
||||
@@ -75,7 +127,7 @@ async function reconcile(remember = false) {
|
||||
history.replaceState(history.state, '', url);
|
||||
try {
|
||||
await application.destroy();
|
||||
await start(previous);
|
||||
await start(previous, true);
|
||||
application
|
||||
.getComponents()
|
||||
.controls.styleManager._showToast(describeError(error));
|
||||
@@ -83,6 +135,7 @@ async function reconcile(remember = false) {
|
||||
report(recoveryError);
|
||||
}
|
||||
} finally {
|
||||
cover?.remove();
|
||||
busy = false;
|
||||
present();
|
||||
}
|
||||
|
||||
+9
-2
@@ -333,7 +333,10 @@ export class ShareLinkManager {
|
||||
/**
|
||||
* 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)
|
||||
return { succeeded: false, reason: 'unavailable' };
|
||||
const view = {
|
||||
@@ -347,7 +350,11 @@ export class ShareLinkManager {
|
||||
let cameraPromise = Promise.resolve({
|
||||
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;
|
||||
let settleCamera;
|
||||
cameraPromise = new Promise((resolve) => {
|
||||
|
||||
@@ -41,6 +41,8 @@ export function createStandaloneApplication({
|
||||
streetLevelProviders,
|
||||
allowQaRegistration = false,
|
||||
engineKind = pageEngineKind(),
|
||||
rendererCache,
|
||||
engineSwitch = false,
|
||||
}) {
|
||||
if (owner)
|
||||
throw new Error('The standalone application already owns this page');
|
||||
@@ -86,6 +88,7 @@ export function createStandaloneApplication({
|
||||
cesiumToken,
|
||||
loaderStatus,
|
||||
engineKind,
|
||||
rendererCache,
|
||||
});
|
||||
// The catalog of this engine: on Navara only the layers with a Navara
|
||||
// renderer (src/app/layers/renderers.js; none yet), the others' state
|
||||
@@ -123,6 +126,7 @@ export function createStandaloneApplication({
|
||||
createControls: (context) =>
|
||||
createStandaloneControls({
|
||||
...context,
|
||||
engineSwitch,
|
||||
loaderStatus,
|
||||
placeSearch,
|
||||
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,
|
||||
requestServices,
|
||||
initialShareCode = null,
|
||||
engineSwitch = false,
|
||||
localePicker = true,
|
||||
} = {},
|
||||
) {
|
||||
@@ -620,7 +621,7 @@ export class StyleManager extends ShellFacade {
|
||||
this._publishActiveStyle();
|
||||
this._updateLocationMiniStatus();
|
||||
|
||||
this._shareRestoration.start();
|
||||
this._shareRestoration.start({ engineSwitch });
|
||||
|
||||
// Keep the parameter panel from overlapping toggle controls.
|
||||
this._layoutRightPanels();
|
||||
|
||||
@@ -153,7 +153,7 @@ export class ShareRestoration {
|
||||
this.shareLinkManager = shareLinkManager;
|
||||
this._initialShareState = shareLinkManager.parseInitialHash();
|
||||
}
|
||||
start() {
|
||||
start({ engineSwitch = false } = {}) {
|
||||
// Restore from URL hash if present
|
||||
const savedState = this._initialShareState;
|
||||
this._initialShareRestorePromise = savedState
|
||||
@@ -169,6 +169,7 @@ export class ShareRestoration {
|
||||
this.navigation._beginDeferredNavigation('shared view', {
|
||||
cancelPendingSelection: false,
|
||||
});
|
||||
const delay = engineSwitch ? 0 : 1500;
|
||||
this._initialShareRestoreTimeout = setTimeout(() => {
|
||||
this._initialShareRestoreTimeout = null;
|
||||
if (this._disposed) return;
|
||||
@@ -178,11 +179,12 @@ export class ShareRestoration {
|
||||
this.navigation._reassertNavigationHandoff(generation);
|
||||
void this._runShareRestore(savedState, {
|
||||
applyCamera,
|
||||
instant: engineSwitch,
|
||||
navigationToken: generation,
|
||||
layersPromise: this._layerStateRestorePromise,
|
||||
settle: (result) => this._settleInitialShareRestore(result),
|
||||
});
|
||||
}, 1500);
|
||||
}, delay);
|
||||
} else {
|
||||
this.syncShareState();
|
||||
if (this.shareLinkManager?.rejectedShareCode)
|
||||
@@ -236,7 +238,7 @@ export class ShareRestoration {
|
||||
*/
|
||||
async _runShareRestore(
|
||||
state,
|
||||
{ applyCamera, navigationToken, layersPromise, settle },
|
||||
{ applyCamera, instant = false, navigationToken, layersPromise, settle },
|
||||
) {
|
||||
const generation = this._shareRestoreGeneration;
|
||||
const current = () =>
|
||||
@@ -244,6 +246,7 @@ export class ShareRestoration {
|
||||
try {
|
||||
const share = await this.shareLinkManager.applyState(state, {
|
||||
applyCamera,
|
||||
instant,
|
||||
navigationToken,
|
||||
});
|
||||
const layers = await (layersPromise || Promise.resolve([]));
|
||||
|
||||
Vendored
+13
@@ -122,6 +122,19 @@ body {
|
||||
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
|
||||
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
|
||||
|
||||
Reference in new issue
Block a user