feat(globe): render the tilt and north-up buttons from the globe actions store

The camera orientation controls publish whether the map is tilted and the
camera's whole-degree heading instead of writing the buttons' attributes,
and answer tilt and north-up through the globe actions store. The new
GlobeCompass renders the heading, so a rotating camera re-renders only the
compass. The store drops an unchanged camera state and keeps each clear
owner's busy state, so the buttons show the shell that answers them: a
shell replaced by a newer one, or disposed mid-clear, leaves the live
shell's state alone, and an older shell that answers again shows its own.
createOwners gains owns() and ownerOf().
This commit is contained in:
Sameh Khamis committed 2026-11-01 13:37:06 -08:00
1 parent 7edf8a577e
commit eccaeea0c4
15 files changed
+495 -180

No files matched your search

+122 -1
View File
@@ -160,7 +160,7 @@ test('a host hook that answers without a promise, or throws, still gets a toast'
}
});
test('the shell answers the globe bar’s share and clear until it is disposed', async () => {
test('the shell answers the globe bar until it is disposed', async () => {
const calls = [];
const owner = {
_disposed: false,
@@ -173,9 +173,130 @@ test('the shell answers the globe bar’s share and clear until it is disposed',
store.act('share');
store.act('clear');
assert.deepEqual(calls, ['share', 'clear']);
// A newer shell answers, and owns the busy state, until it goes.
const newer = { _disposed: false, clearSelectedLayers: async () => {} };
StyleManager.prototype._initClearSelectedLayersButton.call(newer);
assert.equal(store.owns(owner._clearLayersToken, 'clear'), false);
store.detach(newer._clearLayersToken);
assert.equal(store.owns(owner._clearLayersToken, 'clear'), true);
store.detach(owner._shareToken);
store.detach(owner._clearLayersToken);
store.act('share');
store.act('clear');
assert.deepEqual(calls, ['share', 'clear']);
});
test('the shell’s camera controls answer tilt and north-up and publish their state', () => {
const store = useGlobeActionsStore(applicationPinia());
const navigations = [];
const owner = {
viewer: { camera: { heading: 0 } },
_navigation: {
runOrientation: (noun, navigate) => {
navigations.push(noun);
return navigate();
},
},
_showToast() {},
};
StyleManager.prototype._initCameraOrientationControls.call(owner);
assert.deepEqual(store.camera, { tilted: false, heading: 0 });
store.act('tilt');
store.act('northUp');
assert.deepEqual(navigations, ['camera', 'camera']);
// Initialised again, the shell replaces its own owner.
const first = owner._orientationToken;
StyleManager.prototype._initCameraOrientationControls.call(owner);
assert.notEqual(owner._orientationToken, first);
assert.equal(store.owns(first, 'tilt'), false);
assert.equal(store.owns(owner._orientationToken, 'tilt'), true);
// A newer shell owns what the buttons show.
const newer = store.connect({ tilt() {} });
owner.viewer.camera.heading = Math.PI / 2;
owner._cameraOrientationControls.sync();
assert.deepEqual(store.camera, { tilted: false, heading: 0 });
store.detach(newer);
owner.viewer.camera.heading = Math.PI;
owner._cameraOrientationControls.sync();
assert.deepEqual(store.camera, { tilted: false, heading: 180 });
owner._cameraOrientationControls.destroy();
store.detach(owner._orientationToken);
});
test('a shell that goes after a newer one leaves the newer one’s bar state', () => {
const store = useGlobeActionsStore(applicationPinia());
const shellWith = () => {
const owner = {
_disposed: false,
viewer: { camera: { heading: 0 } },
_navigation: { runOrientation: (_noun, navigate) => navigate() },
_showToast() {},
clearSelectedLayers: async () => {},
};
StyleManager.prototype._initClearSelectedLayersButton.call(owner);
StyleManager.prototype._initCameraOrientationControls.call(owner);
return owner;
};
const older = shellWith();
const newer = shellWith();
newer.viewer.camera.heading = Math.PI / 2;
newer._cameraOrientationControls.sync();
StyleManager.prototype._publishClearBusy.call(newer, true);
StyleManager.prototype._publishClearBusy.call(older, false);
assert.equal(store.clearBusy, true);
older._cameraOrientationControls.destroy();
StyleManager.prototype._releaseGlobeActions.call(older);
assert.equal(store.clearBusy, true);
assert.deepEqual(store.camera, { tilted: false, heading: 90 });
// The shell that answers resets its state as it goes.
newer._cameraOrientationControls.destroy();
StyleManager.prototype._releaseGlobeActions.call(newer);
assert.equal(store.clearBusy, null);
assert.equal(store.camera, null);
// Its clear finishing after it went changes nothing.
newer._disposed = true;
StyleManager.prototype._publishClearBusy.call(newer, false);
assert.equal(store.clearBusy, null);
});
test('an older shell that answers again shows its clear and its unchanged camera', () => {
const store = useGlobeActionsStore(applicationPinia());
const shellWith = (heading) => {
const owner = {
_disposed: false,
viewer: { camera: { heading } },
_navigation: { runOrientation: (_noun, navigate) => navigate() },
_showToast() {},
clearSelectedLayers: async () => {},
};
StyleManager.prototype._initClearSelectedLayersButton.call(owner);
StyleManager.prototype._initCameraOrientationControls.call(owner);
return owner;
};
const older = shellWith(Math.PI / 2);
StyleManager.prototype._publishClearBusy.call(older, true);
const newer = shellWith(0);
// The newer shell's bar does not show the older one's clear.
assert.equal(store.clearBusy, null);
older._cameraOrientationControls.sync();
assert.deepEqual(store.camera, { tilted: false, heading: 0 });
// Initialised again mid-clear, a shell keeps its clear on the button.
StyleManager.prototype._publishClearBusy.call(newer, true);
StyleManager.prototype._initClearSelectedLayersButton.call(newer);
assert.equal(store.clearBusy, true);
StyleManager.prototype._publishClearBusy.call(newer, false);
newer._cameraOrientationControls.destroy();
StyleManager.prototype._releaseGlobeActions.call(newer);
assert.equal(store.camera, null);
// The older shell's clear is the button's again, and the next frame it
// draws puts its heading back.
assert.equal(store.clearBusy, true);
older._cameraOrientationControls.sync();
assert.deepEqual(store.camera, { tilted: false, heading: 90 });
older._cameraOrientationControls.destroy();
StyleManager.prototype._releaseGlobeActions.call(older);
});