feat(globe): answer both reset buttons through the globe actions store

The top-center and cockpit reset buttons call the location controls'
new reset() through the globe actions store, and render a reset under
way from it, instead of the location controls binding their clicks and
the navigation writing their labels. The store keeps each owner's reset
under way like its clear, and controls replaced mid-reset keep it on the
buttons. LocationControls no longer takes resetButtons.
This commit is contained in:
Sameh Khamis committed 2026-11-01 13:43:57 -08:00
1 parent eccaeea0c4
commit b4ee2321cf
17 files changed
+173 -84

No files matched your search

+6 -5
View File
@@ -72,9 +72,9 @@
are the kit's new `UiChip`. The selects are the kit's new `UiSelect`, the are the kit's new `UiChip`. The selects are the kit's new `UiSelect`, the
status badges its new `UiBadge` and the one-line empty states its new status badges its new `UiBadge` and the one-line empty states its new
`UiEmpty`. `UiEmpty`.
The top-center clear, share, tilt and north-up buttons call the shell The top-center clear, share, tilt, north-up and reset buttons, and the
through the new globe actions store, which also carries the camera's tilt cockpit's reset, call the shell through the new globe actions store,
and compass heading. `syncScopeControls` (`./ui/display`), which also carries the camera's tilt and compass heading. `syncScopeControls` (`./ui/display`),
`shouldHideCollapsedRightPanels` (`./ui/layout`) and `shouldHideCollapsedRightPanels` (`./ui/layout`) and
`bindClearLayersControl` (`./ui/layers`) are deprecated: the Display view, `bindClearLayersControl` (`./ui/layers`) are deprecated: the Display view,
the rail and the globe actions view now own what they did. `./ui/input` imports Vue the rail and the globe actions view now own what they did. `./ui/input` imports Vue
@@ -113,8 +113,9 @@
landmark pills or writes the mini status: it presents the tray, including landmark pills or writes the mini status: it presents the tray, including
the search error (`setSearchError()`), and hands it to its `publish` the search error (`setSearchError()`), and hands it to its `publish`
option, and Core's dock renders it and calls `chooseCity()` and option, and Core's dock renders it and calls `chooseCity()` and
`choosePoi()`. Its `elements` are now only `search`, `searchToggle` and `choosePoi()`. Its `elements` are now only `search` and `searchToggle`:
`resetButtons`. `createOrbitIndicator()` is replaced by `resetButtons` is gone, and the reset buttons call its new `reset()`.
`createOrbitIndicator()` is replaced by
`showOrbit(active)`, which adds the ORBIT indicator to the presented tray `showOrbit(active)`, which adds the ORBIT indicator to the presented tray
as `orbit: { active }` (null until offered) (samehkhamis). as `orbit: { active }` (null until offered) (samehkhamis).
+5 -4
View File
@@ -225,10 +225,11 @@ a feature supplying only its accent.
`splitFlap.js` plans, writing only the label's one text node; the toast `splitFlap.js` plans, writing only the label's one text node; the toast
renders what each owner's `createToast()` publishes there, on that renders what each owner's `createToast()` publishes there, on that
owner's own timer, and the title bar's FPS readout what owner's own timer, and the title bar's FPS readout what
`createFrameRateMonitor()` publishes), the globe actions (in progress: `createFrameRateMonitor()` publishes), the globe actions
`GlobeActions` renders the top-center bar; its clear, share, tilt and (`GlobeActions` renders the top-center bar; its clear, share, tilt,
north-up buttons call the shell through the `gev-globe-actions` store, north-up and reset buttons, and the cockpit's reset, call the shell
which also carries a clear under way and the camera's tilt and heading through the `gev-globe-actions` store, which also carries a clear or a
reset under way and the camera's tilt and heading
that `bindCameraOrientationControls` publishes each drawn frame it that `bindCameraOrientationControls` publishes each drawn frame it
changes, which only `GlobeCompass` renders; an owner publishes only changes, which only `GlobeCompass` renders; an owner publishes only
while it answers the button, so a shell replaced by a newer one leaves while it answers the button, so a shell replaced by a newer one leaves
+2 -2
View File
@@ -296,7 +296,7 @@ try {
'native reset returns through the existing camera handoff', 'native reset returns through the existing camera handoff',
await page.evaluate(async () => { await page.evaluate(async () => {
const ui = window.__godsEyeView.styleManager; const ui = window.__godsEyeView.styleManager;
ui._resetGlobeBtn.click(); document.getElementById('reset-globe-view').click();
const pending = ui._globeResetPromise; const pending = ui._globeResetPromise;
if (!pending) return false; if (!pending) return false;
await pending; await pending;
@@ -317,7 +317,7 @@ try {
.getElementById('location-pills') .getElementById('location-pills')
.querySelector('button') .querySelector('button')
?.click(); ?.click();
ui._resetGlobeBtn.click(); document.getElementById('reset-globe-view').click();
ui._locationSearch.dispatchEvent( ui._locationSearch.dispatchEvent(
new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }), new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }),
); );
+13 -19
View File
@@ -795,35 +795,29 @@ test('Reset releases Contact camera ownership through its selection-preserving r
assert.ok(resetStart >= 0); assert.ok(resetStart >= 0);
assert.ok(contextRelease > resetStart); assert.ok(contextRelease > resetStart);
assert.ok(satelliteRelease > contextRelease); assert.ok(satelliteRelease > contextRelease);
assert.match(
readShellElements.toString(),
/_cockpitResetGlobeBtn: document\.getElementById\('cockpit-reset-globe'\)/,
);
const controls = fs.readFileSync( const controls = fs.readFileSync(
path.join(ROOT, 'src', 'ui', 'locationControls.js'), path.join(ROOT, 'src', 'ui', 'locationControls.js'),
'utf8', 'utf8',
); );
assert.match(
ui,
/resetButtons: \[this\._resetGlobeBtn, this\._cockpitResetGlobeBtn\]/,
);
assert.match(ui, /onReset: \(\) => this\.resetToGlobeView\(\)/); assert.match(ui, /onReset: \(\) => this\.resetToGlobeView\(\)/);
assert.match( assert.match(ui, /this\._connectGlobeReset\?\.\(this\._locationControls\)/);
controls, assert.match(controls, /reset\(\) \{\s*if \(!this\.destroyed\) return this\.onReset\(\);/);
/for \(const button of elements\.resetButtons\)[\s\S]*?this\.bind\(button, 'click', onReset\)/, // Both reset buttons ask the one reset owner.
'both reset controls delegate to the same supplied reset action', const globeActions = fs.readFileSync(
path.join(ROOT, 'src', 'ui', 'views', 'panels', 'GlobeActions.vue'),
'utf8',
); );
assert.match(globeActions, /id="reset-globe-view"[^>]*@click="act\('reset'\)"/);
const cockpitView = readRadioSource(
new URL('./ui/views/CockpitView.vue', import.meta.url),
'utf8',
);
assert.match(cockpitView, /id="cockpit-reset-globe"[^>]*@click="globe\.act\('reset'\)"/);
assert.match( assert.match(
cockpitEnter.toString(), cockpitEnter.toString(),
/Object\.assign\(this\.presence, \{\s*active: true,/, /Object\.assign\(this\.presence, \{\s*active: true,/,
); );
assert.match( assert.match(cockpitView, /id="cockpit-reset-globe"[^>]*:hidden="!active"/);
readRadioSource(
new URL('./ui/views/CockpitView.vue', import.meta.url),
'utf8',
),
/id="cockpit-reset-globe"[^>]*:hidden="!active"/,
);
}); });
test('Location navigation releases immediate routes before flight and deferred routes after resolution', () => { test('Location navigation releases immediate routes before flight and deferred routes after resolution', () => {
+30 -2
View File
@@ -322,14 +322,14 @@ export class StyleManager extends ShellFacade {
elements: { elements: {
_locationSearch: this._locationSearch, _locationSearch: this._locationSearch,
_searchToggle: this._searchToggle, _searchToggle: this._searchToggle,
_resetGlobeBtn: this._resetGlobeBtn,
_cockpitResetGlobeBtn: this._cockpitResetGlobeBtn,
}, },
operations: { operations: {
_publishLocation: (view) => _publishLocation: (view) =>
useLocationStore(applicationPinia()).publish(view), useLocationStore(applicationPinia()).publish(view),
_connectLocation: (controls) => _connectLocation: (controls) =>
useLocationStore(applicationPinia()).connect(controls), useLocationStore(applicationPinia()).connect(controls),
_connectGlobeReset: (controls) => this._connectGlobeReset(controls),
_publishGlobeResetting: (busy) => this._publishGlobeResetting(busy),
_beginDeferredNavigation: (...args) => _beginDeferredNavigation: (...args) =>
this._beginDeferredNavigation(...args), this._beginDeferredNavigation(...args),
_reassertNavigationHandoff: (...args) => _reassertNavigationHandoff: (...args) =>
@@ -1649,6 +1649,32 @@ export class StyleManager extends ShellFacade {
); );
} }
/**
* Answer the globe and cockpit reset buttons through the location controls,
* or stop answering them (null).
*/
_connectGlobeReset(controls) {
const store = useGlobeActionsStore(applicationPinia());
if (this._resetToken) store.detach(this._resetToken);
this._resetToken = controls
? store.connect({ reset: () => controls.reset() })
: null;
// A reset under way as the controls are replaced stays on the buttons.
if (this._resetToken && this._globeResetting != null)
store.publishResetting(this._resetToken, this._globeResetting);
}
/** Publish a reset to the full globe view under way. */
_publishGlobeResetting(busy) {
if (this._disposed) return;
this._globeResetting = Boolean(busy);
if (this._resetToken)
useGlobeActionsStore(applicationPinia()).publishResetting(
this._resetToken,
busy,
);
}
/** /**
* Stop answering the globe actions. Its clear under way goes with its * Stop answering the globe actions. Its clear under way goes with its
* owner, and its camera state too; a newer shell's stays. * owner, and its camera state too; a newer shell's stays.
@@ -1659,7 +1685,9 @@ export class StyleManager extends ShellFacade {
if (this._clearLayersToken) store.detach(this._clearLayersToken); if (this._clearLayersToken) store.detach(this._clearLayersToken);
if (this._shareToken) store.detach(this._shareToken); if (this._shareToken) store.detach(this._shareToken);
if (this._orientationToken) store.detach(this._orientationToken); if (this._orientationToken) store.detach(this._orientationToken);
if (this._resetToken) store.detach(this._resetToken);
this._clearLayersToken = this._shareToken = this._orientationToken = null; this._clearLayersToken = this._shareToken = this._orientationToken = null;
this._resetToken = null;
} }
/** Wire Google Maps-style tilt and north-up camera actions. */ /** Wire Google Maps-style tilt and north-up camera actions. */
-1
View File
@@ -97,7 +97,6 @@ export class CockpitViewController {
this.tr3bToggle = document.getElementById('tr3b-toggle'); this.tr3bToggle = document.getElementById('tr3b-toggle');
this._tr3bSignature = null; this._tr3bSignature = null;
this.mapViewButton = document.getElementById('map-view-switch'); this.mapViewButton = document.getElementById('map-view-switch');
this.resetGlobeButton = document.getElementById('cockpit-reset-globe');
this.hud = document.getElementById('cockpit-hud'); this.hud = document.getElementById('cockpit-hud');
this.entryFocusOrigin = null; this.entryFocusOrigin = null;
this.visionPrevious = document.getElementById('cockpit-vision-previous'); this.visionPrevious = document.getElementById('cockpit-vision-previous');
-2
View File
@@ -169,7 +169,6 @@ test('the ORBIT indicator sits on the body while offered and lights while orbiti
elements: { elements: {
search: $('#location-search'), search: $('#location-search'),
searchToggle: $('#search-toggle'), searchToggle: $('#search-toggle'),
resetButtons: [],
}, },
cities: {}, cities: {},
getExpandedCity: () => null, getExpandedCity: () => null,
@@ -211,7 +210,6 @@ test('the location tray renders what LocationControls publishes', async (t) => {
elements: { elements: {
search: $('#location-search'), search: $('#location-search'),
searchToggle: $('#search-toggle'), searchToggle: $('#search-toggle'),
resetButtons: [],
}, },
cities: { cities: {
paris: { name: 'Paris', pois: [{ name: 'Louvre' }, { name: 'Eiffel' }] }, paris: { name: 'Paris', pois: [{ name: 'Louvre' }, { name: 'Eiffel' }] },
+44 -1
View File
@@ -8,6 +8,7 @@ import { createApp, nextTick } from 'vue';
import { createPinia } from 'pinia'; import { createPinia } from 'pinia';
import { useGlobeActionsStore } from './stores/globeActions.js'; import { useGlobeActionsStore } from './stores/globeActions.js';
import GlobeActions from './views/panels/GlobeActions.vue'; import GlobeActions from './views/panels/GlobeActions.vue';
import CockpitView from './views/CockpitView.vue';
function mount(t) { function mount(t) {
document.body.replaceChildren(); document.body.replaceChildren();
@@ -19,6 +20,7 @@ function mount(t) {
share: () => calls.push('share'), share: () => calls.push('share'),
tilt: () => calls.push('tilt'), tilt: () => calls.push('tilt'),
northUp: () => calls.push('northUp'), northUp: () => calls.push('northUp'),
reset: () => calls.push('reset'),
}); });
const app = createApp(GlobeActions).use(pinia); const app = createApp(GlobeActions).use(pinia);
app.mount(document.body.appendChild(document.createElement('div'))); app.mount(document.body.appendChild(document.createElement('div')));
@@ -33,7 +35,8 @@ test('every button calls its owner', (t) => {
$('share-btn').click(); $('share-btn').click();
$('tilt-map-view').click(); $('tilt-map-view').click();
$('north-up-view').click(); $('north-up-view').click();
assert.deepEqual(calls, ['clear', 'share', 'tilt', 'northUp']); $('reset-globe-view').click();
assert.deepEqual(calls, ['clear', 'share', 'tilt', 'northUp', 'reset']);
}); });
test('the tilt and compass buttons show the published camera', async (t) => { test('the tilt and compass buttons show the published camera', async (t) => {
@@ -130,3 +133,43 @@ test('the same camera state again changes nothing', () => {
assert.notEqual(store.camera, kept); assert.notEqual(store.camera, kept);
assert.deepEqual(store.camera, { tilted: true, heading: 13 }); assert.deepEqual(store.camera, { tilted: true, heading: 13 });
}); });
test('a reset under way relabels the reset button for its owner', async (t) => {
const { store, token } = mount(t);
const reset = $('reset-globe-view');
assert.equal(reset.getAttribute('aria-label'), 'Reset to full globe view');
store.publishResetting(token, true);
await nextTick();
assert.equal(
reset.getAttribute('aria-label'),
'Resetting to full globe view',
);
assert.equal(store.clearBusy, null, 'a reset is not a clear');
store.detach(token);
await nextTick();
assert.equal(reset.getAttribute('aria-label'), 'Reset to full globe view');
});
test('the cockpit reset asks the reset owner and shows its reset', async (t) => {
document.body.replaceChildren();
const pinia = createPinia();
const store = useGlobeActionsStore(pinia);
const calls = [];
const token = store.connect({ reset: () => calls.push('reset') });
const app = createApp(CockpitView).use(pinia);
app.mount(document.body.appendChild(document.createElement('div')));
t.after(() => app.unmount());
const reset = $('cockpit-reset-globe');
assert.equal(
reset.getAttribute('aria-label'),
'Reset cockpit to full globe view',
);
reset.click();
assert.deepEqual(calls, ['reset']);
store.publishResetting(token, true);
await nextTick();
assert.equal(
reset.getAttribute('aria-label'),
'Resetting cockpit to full globe view',
);
});
+6 -3
View File
@@ -19,7 +19,8 @@ const LOCATION_MINI_SEARCHED_PLACEHOLDER = Object.freeze(
* mini status, the search error and the ORBIT indicator are presented as a * mini status, the search error and the ORBIT indicator are presented as a
* plain view and handed to `publish`; Core's dock renders them and calls * plain view and handed to `publish`; Core's dock renders them and calls
* `chooseCity()` and `choosePoi()`, and the scene chrome shows the indicator. * `chooseCity()` and `choosePoi()`, and the scene chrome shows the indicator.
* The search field and reset buttons keep their listeners. * The globe actions and the cockpit call `reset()`. The search field keeps its
* listeners.
*/ */
export class LocationControls { export class LocationControls {
constructor({ constructor({
@@ -90,8 +91,6 @@ export class LocationControls {
this.bind(elements.search, 'blur', () => { this.bind(elements.search, 'blur', () => {
if (!elements.search.value) onSearchEdit?.(); if (!elements.search.value) onSearchEdit?.();
}); });
for (const button of elements.resetButtons)
this.bind(button, 'click', onReset);
this.present(); this.present();
} }
bind(element, event, handler, removers = this.removers) { bind(element, event, handler, removers = this.removers) {
@@ -132,6 +131,10 @@ export class LocationControls {
chooseCity(id) { chooseCity(id) {
if (!this.destroyed) this.onCity(id); if (!this.destroyed) this.onCity(id);
} }
/** A reset to the full globe view was asked for. */
reset() {
if (!this.destroyed) return this.onReset();
}
/** A landmark pill of the shown city was clicked. */ /** A landmark pill of the shown city was clicked. */
choosePoi(index) { choosePoi(index) {
if (!this.destroyed && this.poiCity !== null) if (!this.destroyed && this.poiCity !== null)
+2 -3
View File
@@ -61,7 +61,6 @@ function fixture() {
const elements = { const elements = {
search: node(), search: node(),
searchToggle: node(), searchToggle: node(),
resetButtons: [node(), node()],
}; };
const doc = node(); const doc = node();
doc.createElement = node; doc.createElement = node;
@@ -168,7 +167,7 @@ test('destruction revokes document, search, reset, city and POI actions and remo
f.elements.search.value = 'Place'; f.elements.search.value = 'Place';
f.elements.search.fire('keydown', { key: 'Enter' }); f.elements.search.fire('keydown', { key: 'Enter' });
f.doc.fire('keydown', { key: 'Q' }); f.doc.fire('keydown', { key: 'Q' });
f.elements.resetButtons[0].fire('click'); f.controls.reset();
f.frames.get(0)(); f.frames.get(0)();
assert.deepEqual(f.calls, []); assert.deepEqual(f.calls, []);
assert.equal(f.views.length, published); assert.equal(f.views.length, published);
@@ -187,7 +186,7 @@ test('location and POI keys route once while form controls retain typing', () =>
const f = fixture(); const f = fixture();
f.doc.fire('keydown', { key: 'W', target: { matches: () => false } }); f.doc.fire('keydown', { key: 'W', target: { matches: () => false } });
f.doc.fire('keydown', { key: 'Q', target: { matches: () => true } }); f.doc.fire('keydown', { key: 'Q', target: { matches: () => true } });
f.elements.resetButtons[1].fire('click'); f.controls.reset();
assert.deepEqual(f.calls, [['poi', 'a', 1], ['reset']]); assert.deepEqual(f.calls, [['poi', 'a', 1], ['reset']]);
}); });
test('typing clears a failed search error and closing the emptied field drops it too', () => { test('typing clears a failed search error and closing the emptied field drops it too', () => {
+4 -17
View File
@@ -144,7 +144,6 @@ export class LocationNavigation {
elements: { elements: {
search: this._locationSearch, search: this._locationSearch,
searchToggle: this._searchToggle, searchToggle: this._searchToggle,
resetButtons: [this._resetGlobeBtn, this._cockpitResetGlobeBtn],
}, },
publish: (view) => this._publishLocation?.(view), publish: (view) => this._publishLocation?.(view),
cities: CITY_POIS, cities: CITY_POIS,
@@ -156,6 +155,7 @@ export class LocationNavigation {
onReset: () => this.resetToGlobeView(), onReset: () => this.resetToGlobeView(),
}); });
this._connectLocation?.(this._locationControls); this._connectLocation?.(this._locationControls);
this._connectGlobeReset?.(this._locationControls);
} }
_beginWorldJumpTransition() { _beginWorldJumpTransition() {
@@ -390,14 +390,7 @@ export class LocationNavigation {
longitude: Number(Cesium.Math.toDegrees(carto.longitude).toFixed(2)), longitude: Number(Cesium.Math.toDegrees(carto.longitude).toFixed(2)),
}, },
}; };
this._resetGlobeBtn?.setAttribute( this._publishGlobeResetting?.(false);
'aria-label',
t('Reset to full globe view'),
);
this._cockpitResetGlobeBtn?.setAttribute(
'aria-label',
t('Reset cockpit to full globe view'),
);
this._globeResetPromise = null; this._globeResetPromise = null;
this._cancelGlobeReset = null; this._cancelGlobeReset = null;
resolveReset(result); resolveReset(result);
@@ -410,14 +403,7 @@ export class LocationNavigation {
Math.abs(height - GLOBE_VIEW.heightM) > 1000, Math.abs(height - GLOBE_VIEW.heightM) > 1000,
); );
}, 4200); }, 4200);
this._resetGlobeBtn?.setAttribute( this._publishGlobeResetting?.(true);
'aria-label',
t('Resetting to full globe view'),
);
this._cockpitResetGlobeBtn?.setAttribute(
'aria-label',
t('Resetting cockpit to full globe view'),
);
const target = flyToGlobeView(this.viewer, { const target = flyToGlobeView(this.viewer, {
onComplete: () => finish(false), onComplete: () => finish(false),
onCancel: () => finish(true), onCancel: () => finish(true),
@@ -436,6 +422,7 @@ export class LocationNavigation {
this._locationLookup?.destroy(); this._locationLookup?.destroy();
this._locationControls?.destroy(); this._locationControls?.destroy();
this._connectLocation?.(null); this._connectLocation?.(null);
this._connectGlobeReset?.(null);
this._publishLocation?.(null); this._publishLocation?.(null);
this._cancelGlobeReset?.(); this._cancelGlobeReset?.();
this._cancelGlobeReset = null; this._cancelGlobeReset = null;
-1
View File
@@ -54,7 +54,6 @@ test('ru: location pills and the mini-status show the translated city', () => {
const elements = { const elements = {
search: element(), search: element(),
searchToggle: element(), searchToggle: element(),
resetButtons: [],
}; };
const doc = { ...element(), createElement: element }; const doc = { ...element(), createElement: element };
const cities = { tokyo: CITY_POIS.tokyo }; const cities = { tokyo: CITY_POIS.tokyo };
+23
View File
@@ -300,3 +300,26 @@ test('an older shell that answers again shows its clear and its unchanged camera
older._cameraOrientationControls.destroy(); older._cameraOrientationControls.destroy();
StyleManager.prototype._releaseGlobeActions.call(older); StyleManager.prototype._releaseGlobeActions.call(older);
}); });
test('the location controls answer both reset buttons until they go', () => {
const store = useGlobeActionsStore(applicationPinia());
const calls = [];
const owner = { _disposed: false };
const controls = { reset: () => calls.push('reset') };
StyleManager.prototype._connectGlobeReset.call(owner, controls);
store.act('reset');
StyleManager.prototype._publishGlobeResetting.call(owner, true);
assert.equal(store.resetBusy, true);
// Controls replaced mid-reset keep the reset on the buttons.
StyleManager.prototype._connectGlobeReset.call(owner, controls);
assert.equal(store.resetBusy, true);
StyleManager.prototype._publishGlobeResetting.call(owner, false);
assert.equal(store.resetBusy, false);
StyleManager.prototype._connectGlobeReset.call(owner, null);
store.act('reset');
assert.deepEqual(calls, ['reset']);
assert.equal(store.resetBusy, null);
// A reset finishing after the controls went changes nothing.
StyleManager.prototype._publishGlobeResetting.call(owner, false);
assert.equal(store.resetBusy, null);
});
-2
View File
@@ -63,8 +63,6 @@ export function readShellElements(document = globalThis.document) {
_cctvSourceBadge: document.getElementById('cctv-source-badge'), _cctvSourceBadge: document.getElementById('cctv-source-badge'),
_cctvMeta: document.getElementById('cctv-meta'), _cctvMeta: document.getElementById('cctv-meta'),
_cctvSummary: document.getElementById('cctv-summary'), _cctvSummary: document.getElementById('cctv-summary'),
_resetGlobeBtn: document.getElementById('reset-globe-view'),
_cockpitResetGlobeBtn: document.getElementById('cockpit-reset-globe'),
_locationSearch: document.getElementById('location-search'), _locationSearch: document.getElementById('location-search'),
_searchToggle: document.getElementById('search-toggle'), _searchToggle: document.getElementById('search-toggle'),
_safeFrameOverlay: document.getElementById('safe-frame-overlay'), _safeFrameOverlay: document.getElementById('safe-frame-overlay'),
+32 -19
View File
@@ -3,33 +3,43 @@ import { computed, ref, shallowReactive } from 'vue';
import { createOwners } from './owners.js'; import { createOwners } from './owners.js';
/** /**
* The top-center globe actions as their view renders them. The shell * The top-center globe actions as their view renders them, and the cockpit's
* publishes whether a clear of the selected data layers is under way and the * reset. The shell publishes whether a clear of the selected data layers or a
* camera's tilt and heading, and answers the buttons through act(). * reset to the full globe view is under way and the camera's tilt and
* heading, and answers the buttons through act().
*/ */
export const useGlobeActionsStore = defineStore('gev-globe-actions', () => { export const useGlobeActionsStore = defineStore('gev-globe-actions', () => {
const owners = createOwners(); const owners = createOwners();
// Who answers which action, as the computed state below reads it. // Who answers which action, as the computed state below reads it.
const ownership = ref(0); const ownership = ref(0);
/** Each clear owner's clear under way, by its token. */
const busyBy = shallowReactive(new Map());
/** /**
* Whether the clear owner's clear is under way; null until it says. Each * Whether the work behind an action is under way, kept per owner so its
* owner keeps its own, so the button shows the one that answers it. * button shows the owner that answers it; null until that owner says.
*/ */
const clearBusy = computed(() => { function ownedBusy(name) {
void ownership.value; const byOwner = shallowReactive(new Map());
return busyBy.get(owners.ownerOf('clear')) ?? null; const busy = computed(() => {
}); void ownership.value;
return byOwner.get(owners.ownerOf(name)) ?? null;
/** });
* @param {object} token The clear owner's, from connect(). /**
* @param {boolean} busy * @param {object} token The owner's, from connect().
*/ * @param {boolean} value
function publishClearBusy(token, busy) { */
busyBy.set(token, Boolean(busy)); const publish = (token, value) => {
byOwner.set(token, Boolean(value));
};
return { byOwner, busy, publish };
} }
const clearing = ownedBusy('clear');
const resetting = ownedBusy('reset');
/** Whether a clear of the selected data layers is under way. */
const clearBusy = clearing.busy;
const publishClearBusy = clearing.publish;
/** Whether a reset to the full globe view is under way. */
const resetBusy = resetting.busy;
const publishResetting = resetting.publish;
/** /**
* Whether the map reads as tilted and the camera's whole-degree heading; * Whether the map reads as tilted and the camera's whole-degree heading;
@@ -63,7 +73,8 @@ export const useGlobeActionsStore = defineStore('gev-globe-actions', () => {
function detach(token) { function detach(token) {
owners.detach(token); owners.detach(token);
busyBy.delete(token); clearing.byOwner.delete(token);
resetting.byOwner.delete(token);
ownership.value += 1; ownership.value += 1;
} }
@@ -72,6 +83,8 @@ export const useGlobeActionsStore = defineStore('gev-globe-actions', () => {
return { return {
clearBusy, clearBusy,
publishClearBusy, publishClearBusy,
resetBusy,
publishResetting,
camera, camera,
publishCamera, publishCamera,
connect, connect,
+4 -1
View File
@@ -9,6 +9,7 @@ import {
} from '../cockpitInstrumentState.js'; } from '../cockpitInstrumentState.js';
import { vClassToggle } from '../rail/classToggle.js'; import { vClassToggle } from '../rail/classToggle.js';
import { useCockpitStore } from '../stores/cockpit.js'; import { useCockpitStore } from '../stores/cockpit.js';
import { useGlobeActionsStore } from '../stores/globeActions.js';
import CockpitBriefing from './panels/CockpitBriefing.vue'; import CockpitBriefing from './panels/CockpitBriefing.vue';
import CockpitContext from './panels/CockpitContext.vue'; import CockpitContext from './panels/CockpitContext.vue';
import CockpitRollingValue from './panels/CockpitRollingValue.vue'; import CockpitRollingValue from './panels/CockpitRollingValue.vue';
@@ -21,6 +22,8 @@ import CockpitUtilityControls from './panels/CockpitUtilityControls.vue';
// keep its listeners. // keep its listeners.
const { presence, instruments, route, vision } = storeToRefs(useCockpitStore()); const { presence, instruments, route, vision } = storeToRefs(useCockpitStore());
const active = computed(() => Boolean(presence.value?.active)); const active = computed(() => Boolean(presence.value?.active));
const globe = useGlobeActionsStore();
const { resetBusy } = storeToRefs(globe);
const initialInstruments = createInstrumentState(); const initialInstruments = createInstrumentState();
const initialRoute = createRouteState(); const initialRoute = createRouteState();
const initialVision = createVisionState(); const initialVision = createVisionState();
@@ -188,7 +191,7 @@ const tickStyle = (tick) =>
</section> </section>
<nav id="view-switcher" aria-label="View switcher" data-i18n-aria-label> <nav id="view-switcher" aria-label="View switcher" data-i18n-aria-label>
<button id="cockpit-reset-globe" type="button" aria-label="Reset cockpit to full globe view" title="Exit cockpit and return to full globe view" :hidden="!active" data-i18n-aria-label data-i18n-title> <button id="cockpit-reset-globe" type="button" :aria-label="resetBusy ? t('Resetting cockpit to full globe view') : t('Reset cockpit to full globe view')" title="Exit cockpit and return to full globe view" :hidden="!active" data-i18n-title @click="globe.act('reset')">
<span class="material-symbols-outlined" aria-hidden="true">public</span> <span class="material-symbols-outlined" aria-hidden="true">public</span>
<span data-i18n>RESET</span> <span data-i18n>RESET</span>
</button> </button>
+2 -2
View File
@@ -11,7 +11,7 @@ import GlobeCompass from './GlobeCompass.vue';
// changes on every frame a rotating camera draws, so only the compass reads // changes on every frame a rotating camera draws, so only the compass reads
// it; the bar renders again only when the tilt changes. // it; the bar renders again only when the tilt changes.
const globe = useGlobeActionsStore(); const globe = useGlobeActionsStore();
const { clearBusy, camera } = storeToRefs(globe); const { clearBusy, resetBusy, camera } = storeToRefs(globe);
const { act } = globe; const { act } = globe;
const tilted = computed(() => Boolean(camera.value?.tilted)); const tilted = computed(() => Boolean(camera.value?.tilted));
</script> </script>
@@ -28,7 +28,7 @@ const tilted = computed(() => Boolean(camera.value?.tilted));
<span class="material-symbols-outlined" aria-hidden="true">view_in_ar</span> <span class="material-symbols-outlined" aria-hidden="true">view_in_ar</span>
</button> </button>
<GlobeCompass @click="act('northUp')" /> <GlobeCompass @click="act('northUp')" />
<button id="reset-globe-view" type="button" aria-label="Reset to full globe view" title="Reset camera and return to full globe view" data-i18n-aria-label data-i18n-title> <button id="reset-globe-view" type="button" :aria-label="resetBusy ? t('Resetting to full globe view') : t('Reset to full globe view')" title="Reset camera and return to full globe view" data-i18n-title @click="act('reset')">
<span class="material-symbols-outlined" aria-hidden="true">public</span> <span class="material-symbols-outlined" aria-hidden="true">public</span>
</button> </button>
</nav> </nav>