From 061ed1b6826a91c7893213f7525b91bc3aa5aa28 Mon Sep 17 00:00:00 2001 From: German Khokhlov Date: Tue, 3 Nov 2026 18:02:46 +0400 Subject: [PATCH] Attach the shell to another globe without disposing it StyleManager.attachScene/detachScene rebuild only the globe-bound parts (share restoration, visual settings, layer bindings, Cockpit, the celestial ring, the overlay host, detection, map-source and camera-orientation controls) and point navigation, location search, HUD, share link and the display bindings at the next globe. dispose() revokes input synchronously, detaches the scene and only then nulls the stores and unmounts the views. The radio tuner listens on the globe container, and an engine's persistent notice can be withdrawn. --- src/cameraHandoff.test.mjs | 18 +- src/loadingFeedback.test.mjs | 6 +- src/ui/applicationShell.js | 713 ++++++++++++++++++++------------- src/ui/displayBindings.js | 18 +- src/ui/locationNavigation.js | 14 +- src/ui/shellFacade.js | 4 + src/ui/shellFeedback.js | 11 + src/ui/shellSceneSwap.test.mjs | 197 +++++++++ 8 files changed, 693 insertions(+), 288 deletions(-) create mode 100644 src/ui/shellSceneSwap.test.mjs diff --git a/src/cameraHandoff.test.mjs b/src/cameraHandoff.test.mjs index d4f60e99f..87454e61a 100644 --- a/src/cameraHandoff.test.mjs +++ b/src/cameraHandoff.test.mjs @@ -302,10 +302,16 @@ test('newer navigation, reset, Cockpit, and teardown share one generation', () = const dispose = body(ui, /async dispose\(\) \{([\s\S]*?)\n \}/, 'dispose'); ordered(dispose, [ 'this._disposed = true;', - 'this._layerBindings.stop();', - 'this._navigation.destroy(', - 'await this._contextControls.restoreForDisposal();', + 'await this.detachScene(', + 'this._navigation.stop();', ], 'dispose invalidation'); + // An engine switch retires the outgoing globe's navigation the same way. + const detach = body(ui, /async detachScene\(\{[\s\S]*?\} = \{\}\) \{([\s\S]*?)\n \}/, 'detach'); + ordered(detach, [ + 'this._navigation.setScene(', + 'this._layerBindings.stop();', + 'await this._contextControls.restoreForDisposal();', + ], 'detach invalidation'); assert.match(shellMethod('destroy').toString(), /this\._stampNavigation\(\{\s*cancelPendingSelection: !retainScene\s*\}\)/); }); @@ -313,9 +319,13 @@ test('teardown synchronously closes immediate camera entry points', () => { const dispose = body(ui, /async dispose\(\) \{([\s\S]*?)\n \}/, 'dispose'); ordered(dispose, [ 'this._disposed = true;', + 'await this.detachScene(', + ], 'synchronous teardown barrier'); + const detach = body(ui, /async detachScene\(\{[\s\S]*?\} = \{\}\) \{([\s\S]*?)\n \}/, 'detach'); + ordered(detach, [ 'this._layerBindings.stop();', 'await this._contextControls.restoreForDisposal();', - ], 'synchronous teardown barrier'); + ], 'synchronous detach barrier'); ordered(LayerBindings.prototype.stop.toString(), [ 'this._disposed = true;', 'this._removeCctvRequestFocusListener?.();', diff --git a/src/loadingFeedback.test.mjs b/src/loadingFeedback.test.mjs index d892ddf1c..4740ac333 100644 --- a/src/loadingFeedback.test.mjs +++ b/src/loadingFeedback.test.mjs @@ -210,7 +210,11 @@ test('universal notice lifecycle clears on dispose and uses the one top-center l const dispose = ui.slice(disposeStart, disposeEnd); assert.match(dispose, /this\._feedback\.clearLoadingStatus\(\);/); - assert.match(dispose, /this\._shareRestoration\.destroy\(\)/); + // The globe-bound parts, share restoration included, go with the scene. + assert.match(dispose, /await this\.detachScene\(/); + const detachStart = ui.indexOf(' async detachScene({'); + const detach = ui.slice(detachStart, ui.indexOf('\n }\n', detachStart)); + assert.match(detach, /this\._shareRestoration\.destroy\(\)/); assert.match(ShareRestoration.prototype.destroy.toString(), /this\._shareTrackingNoticeGeneration \+= 1;/); assert.match(html, /