mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 11:24:00 +00:00
Hold the shell's inputs between two engines instead of reaching released controllers
A visual choice made while no globe is attached (a style key, a Display control, a voice command) is queued and applied to the next engine's settings after it restored the address's view, so the person's choice wins; reads answer from the released settings. The globe reset and a location lookup started without a globe do nothing, and the map-source chips and camera buttons are unbound until the next globe binds them.
This commit is contained in:
1 parent
df73c5b21a
commit
40a81c58e6
7 files changed
+193
-5
No files matched your search
@@ -1072,6 +1072,7 @@
|
||||
"src/layers/weather/imageryHost.js",
|
||||
"src/ui/layerBindings.js",
|
||||
"src/ui/displayBindings.js",
|
||||
"src/ui/pendingVisualSettings.js",
|
||||
"src/ui/shellFacade.js",
|
||||
"src/ui/streetLevelControls.js",
|
||||
"src/ui/streetLevelPresentation.js",
|
||||
@@ -3311,6 +3312,7 @@
|
||||
"src/ui/aircraftDisplay.js",
|
||||
"src/ui/layerBindings.js",
|
||||
"src/ui/displayBindings.js",
|
||||
"src/ui/pendingVisualSettings.js",
|
||||
"src/ui/shellFacade.js",
|
||||
"src/voice/realtimePreferences.js",
|
||||
"src/voice/realtimeProtocol.js",
|
||||
|
||||
@@ -86,3 +86,19 @@ test('navigation authority retires deferred flights when the globe changes', ()
|
||||
const next = navigation._beginDeferredNavigation('location');
|
||||
assert.equal(navigation._reassertNavigationHandoff(next), true);
|
||||
});
|
||||
|
||||
test('between engines the globe reset neither stamps navigation nor reads a camera', async () => {
|
||||
const { LocationNavigation } = await import('./ui/locationNavigation.js');
|
||||
const location = Object.create(LocationNavigation.prototype);
|
||||
let stamped = 0;
|
||||
Object.assign(location, {
|
||||
services: { interruptCameraMotion() {} },
|
||||
engine: null,
|
||||
_disposed: false,
|
||||
_stampNavigation: () => stamped++,
|
||||
});
|
||||
const result = await location.resetToGlobeView();
|
||||
assert.equal(result.ok, false);
|
||||
assert.equal(result.cancelled, true);
|
||||
assert.equal(stamped, 0);
|
||||
});
|
||||
@@ -9,6 +9,10 @@ import { VisualSettings } from './visualSettings.js';
|
||||
import { NavigationController } from './navigationController.js';
|
||||
import { ShareRestoration } from './shareRestoration.js';
|
||||
import { DisplayBindings } from './displayBindings.js';
|
||||
import {
|
||||
applyPendingVisualChoices,
|
||||
pendingVisualSettings,
|
||||
} from './pendingVisualSettings.js';
|
||||
import { createStateChannel } from '../app/stateChannel.js';
|
||||
import { UiLifetime } from './uiLifetime.js';
|
||||
import { RecordingControls } from './recordingControls.js';
|
||||
@@ -754,6 +758,17 @@ export class StyleManager extends ShellFacade {
|
||||
this._layerBindings.observeCamera();
|
||||
this._layoutRightPanels();
|
||||
this._syncCctvPanelViewport();
|
||||
// Choices made between the engines apply once this engine restored
|
||||
// the address's view: the person's choice wins over the link.
|
||||
const pending = this._pendingVisual?.queue;
|
||||
this._pendingVisual = null;
|
||||
if (pending?.length) {
|
||||
const settings = this._visualSettings;
|
||||
void this.initialRestorePromise.then(() => {
|
||||
if (!this._disposed && this._visualSettings === settings)
|
||||
applyPendingVisualChoices(settings, pending);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
void this.detachScene().catch((cleanupError) =>
|
||||
console.error(
|
||||
@@ -809,10 +824,18 @@ export class StyleManager extends ShellFacade {
|
||||
this.celestialRing?.destroy();
|
||||
this.celestialRing = null;
|
||||
this._visualSettings.destroy();
|
||||
// Until the next globe: choices wait for it (pendingVisualSettings.js),
|
||||
// and the map-source chips and camera buttons answer nothing.
|
||||
this._pendingVisual = pendingVisualSettings(this._visualSettings);
|
||||
this._visualSettings = this._pendingVisual.settings;
|
||||
this._mapSourceControls?.destroy();
|
||||
this._mapSourceControls = null;
|
||||
useDisplayStore(applicationPinia()).connectMapSource(null);
|
||||
this._cameraOrientationControls?.destroy();
|
||||
this._cameraOrientationControls = null;
|
||||
if (this._orientationToken)
|
||||
useGlobeActionsStore(applicationPinia()).detach(this._orientationToken);
|
||||
this._orientationToken = null;
|
||||
this._displayBindings.attachViewer(null);
|
||||
this._locationNavigation.detachScene();
|
||||
this.hud.attachEngine(null);
|
||||
|
||||
@@ -135,11 +135,16 @@ export class LocationNavigation {
|
||||
isCurrent: (generation) =>
|
||||
!this._disposed && generation === this._navigationGeneration,
|
||||
beforeFly: (generation) => this._reassertNavigationHandoff(generation),
|
||||
// The engine the lookup ends on: an answer that lands after a switch
|
||||
// flies the globe shown then; one started with none attached is
|
||||
// cancelled.
|
||||
search: (query, options) =>
|
||||
searchAndFlyTo(this.engine, query, {
|
||||
placeSearch: this.placeSearch,
|
||||
...options,
|
||||
}),
|
||||
this.engine
|
||||
? searchAndFlyTo(this.engine, query, {
|
||||
placeSearch: this.placeSearch,
|
||||
...options,
|
||||
})
|
||||
: Promise.resolve({ cancelled: true }),
|
||||
onResult: (destination) => {
|
||||
if (destination.outlineUnavailable)
|
||||
this._showToast(t('Detailed outline unavailable'));
|
||||
@@ -334,7 +339,9 @@ export class LocationNavigation {
|
||||
militaryAwarenessLayer,
|
||||
rocketLaunchesLayer,
|
||||
} = this.services;
|
||||
if (this._disposed)
|
||||
// Between two globe engines there is no camera to reset: nothing moves
|
||||
// and no navigation is stamped.
|
||||
if (this._disposed || !this.engine)
|
||||
return Promise.resolve({
|
||||
ok: false,
|
||||
action: 'zoom_to_globe',
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
// Between two globe engines the shell has no visual settings of its own:
|
||||
// the outgoing ones are released and the incoming ones are not built yet.
|
||||
// A choice the person makes meanwhile (a style key, a Display control, a
|
||||
// voice command) is kept and applied to the next engine's settings once
|
||||
// that engine has restored the address's view, so the person's choice wins.
|
||||
// Reads answer from the outgoing settings' last state.
|
||||
|
||||
/** The visual choices kept for the next engine, by method name. */
|
||||
export const PENDING_VISUAL_CHOICES = Object.freeze(
|
||||
new Set([
|
||||
'setStyle',
|
||||
'setBloom',
|
||||
'setSharpen',
|
||||
'setDetection',
|
||||
'setCelestialRingEnabled',
|
||||
'setStyleParameter',
|
||||
'applyVisualState',
|
||||
'applyCinematicPreset',
|
||||
'_setBloomEnabled',
|
||||
'_setBloomIntensity',
|
||||
'_setSharpenEnabled',
|
||||
'_setSharpenIntensity',
|
||||
'_setScope',
|
||||
'_setHudVariant',
|
||||
'_setDetectionDensity',
|
||||
'_setDetectionAllocation',
|
||||
'_setDetectionFade',
|
||||
'_setDetectionModeByLabel',
|
||||
'_setCyberSonarEnabled',
|
||||
'_setCyberSonarSetting',
|
||||
]),
|
||||
);
|
||||
|
||||
/**
|
||||
* Stand in for released visual settings: choices are queued (`queue`),
|
||||
* everything else reads the released settings.
|
||||
* @param {object} released The outgoing engine's VisualSettings.
|
||||
* @returns {{settings: object, queue: Array<[string, unknown[]]>}}
|
||||
*/
|
||||
export function pendingVisualSettings(released) {
|
||||
const queue = [];
|
||||
const settings = new Proxy(released, {
|
||||
get(target, name) {
|
||||
if (PENDING_VISUAL_CHOICES.has(name))
|
||||
return (...args) => {
|
||||
queue.push([name, args]);
|
||||
return { ok: true, pending: true };
|
||||
};
|
||||
const value = Reflect.get(target, name, target);
|
||||
return typeof value === 'function' ? value.bind(target) : value;
|
||||
},
|
||||
});
|
||||
return { settings, queue };
|
||||
}
|
||||
|
||||
/** Apply the queued choices, in order, to the next engine's settings. */
|
||||
export function applyPendingVisualChoices(settings, queue) {
|
||||
for (const [name, args] of queue.splice(0)) {
|
||||
try {
|
||||
settings[name]?.(...args);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`A visual choice made during the switch failed (${name}):`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
applyPendingVisualChoices,
|
||||
pendingVisualSettings,
|
||||
} from './pendingVisualSettings.js';
|
||||
|
||||
test('a style chosen between engines waits and then wins over the restored view', () => {
|
||||
const calls = [];
|
||||
const released = {
|
||||
activeStyle: 'nvg',
|
||||
setStyle(name) {
|
||||
calls.push(['released', name]);
|
||||
},
|
||||
_readShareState: () => ({ style: 'nvg' }),
|
||||
};
|
||||
const { settings, queue } = pendingVisualSettings(released);
|
||||
// Reads answer from the released settings; choices do not reach them.
|
||||
assert.equal(settings.activeStyle, 'nvg');
|
||||
assert.deepEqual(settings._readShareState(), { style: 'nvg' });
|
||||
assert.deepEqual(settings.setStyle('crt', { applyPreset: true }), {
|
||||
ok: true,
|
||||
pending: true,
|
||||
});
|
||||
settings._setBloomEnabled(true);
|
||||
assert.deepEqual(calls, []);
|
||||
// The next engine restored the link's 'normal'; the choices then apply.
|
||||
const next = {
|
||||
activeStyle: 'normal',
|
||||
setStyle(name) {
|
||||
calls.push(['next', name]);
|
||||
this.activeStyle = name;
|
||||
},
|
||||
_setBloomEnabled(on) {
|
||||
calls.push(['next bloom', on]);
|
||||
},
|
||||
};
|
||||
applyPendingVisualChoices(next, queue);
|
||||
assert.deepEqual(calls, [
|
||||
['next', 'crt'],
|
||||
['next bloom', true],
|
||||
]);
|
||||
assert.equal(next.activeStyle, 'crt');
|
||||
assert.equal(queue.length, 0);
|
||||
});
|
||||
@@ -216,3 +216,30 @@ test('an attachment that throws part-way leaves the shell detached and attachabl
|
||||
assert.equal(shell.engine, engine);
|
||||
assert.ok(log.includes('shareRestoration.start'));
|
||||
});
|
||||
|
||||
test('between engines a style key waits for the next engine instead of the released settings', async () => {
|
||||
const { shell } = fakeShell();
|
||||
const released = [];
|
||||
shell._visualSettings = {
|
||||
stop() {},
|
||||
releaseIrBoost() {},
|
||||
destroy() {},
|
||||
setStyle: (name) => released.push(name),
|
||||
};
|
||||
Object.defineProperty(shell, 'initialRestorePromise', {
|
||||
value: Promise.resolve(),
|
||||
});
|
||||
await shell.detachScene({ handoff: true });
|
||||
shell.setStyle('crt');
|
||||
assert.deepEqual(released, [], 'the released settings do not change');
|
||||
const applied = [];
|
||||
shell._buildSceneControls = () => {
|
||||
shell._shareRestoration = { attachLinks() {}, start() {}, destroy() {} };
|
||||
shell._layerBindings = { observeCamera() {}, stop() {}, disconnect() {} };
|
||||
shell._visualSettings = { setStyle: (name) => applied.push(name) };
|
||||
};
|
||||
shell.attachScene({ engine: { kind: 'cesium' } });
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
assert.deepEqual(applied, ['crt']);
|
||||
});
|
||||
Reference in new issue
Block a user