Share a scene's camera-input holds beyond the dispatcher

The take-over of the camera controller's enableInputs moves, unchanged,
to src/render/cesium/cameraInput.js. acquireCameraInput(scene) gives every
user one set of counted holds per scene, so the renderer's input and
camera ports can hold camera input alongside the dispatcher; the
controller gets its own property back when the last user releases. The
dispatcher acquires its share once its handler is bound. The input guard
names the one module allowed to write the flag, and the new render files
join the format scope.
This commit is contained in:
Sameh Khamis committed 2026-11-05 18:29:38 -08:00
1 parent 468590485b
commit 60a3a2808e
7 files changed
+237 -77

No files matched your search

+4
View File
@@ -49,6 +49,10 @@
"src/input/dispatcher.js", "src/input/dispatcher.js",
"src/queries/heights.js", "src/queries/heights.js",
"src/render/cesium/internals.js", "src/render/cesium/internals.js",
"src/render/cesium/cameraInput.js",
"src/render/cesium/cameraInput.test.mjs",
"src/render/input/router.js",
"src/render/input/router.test.mjs",
"src/render/cesium/internals.test.mjs", "src/render/cesium/internals.test.mjs",
"src/render/cesium/tileRequests.js", "src/render/cesium/tileRequests.js",
"src/render/cesium/tileRequests.test.mjs", "src/render/cesium/tileRequests.test.mjs",
+38 -13
View File
@@ -13,6 +13,7 @@
"src/data/pickRegistry.js", "src/data/pickRegistry.js",
"src/data/scenePick.js", "src/data/scenePick.js",
"src/input/dispatcher.js", "src/input/dispatcher.js",
"src/render/cesium/cameraInput.js",
"src/input/sensorPick.js", "src/input/sensorPick.js",
"src/data/infrastructureOverlayEntry.js", "src/data/infrastructureOverlayEntry.js",
"src/sources/infrastructureData.js", "src/sources/infrastructureData.js",
@@ -882,7 +883,8 @@
"src/data/scenePick.js", "src/data/scenePick.js",
"src/data/pickRegistry.js", "src/data/pickRegistry.js",
"src/input/sensorPick.js", "src/input/sensorPick.js",
"src/input/dispatcher.js" "src/input/dispatcher.js",
"src/render/cesium/cameraInput.js"
], ],
"external": ["cesium"] "external": ["cesium"]
}, },
@@ -1140,7 +1142,8 @@
"src/data/inputOwnership.js", "src/data/inputOwnership.js",
"src/data/pickRegistry.js", "src/data/pickRegistry.js",
"src/input/sensorPick.js", "src/input/sensorPick.js",
"src/input/dispatcher.js" "src/input/dispatcher.js",
"src/render/cesium/cameraInput.js"
], ],
"external": ["cesium", "vue", "pinia", "vue-i18n"] "external": ["cesium", "vue", "pinia", "vue-i18n"]
}, },
@@ -1278,7 +1281,8 @@
"src/input/sensorPick.js", "src/input/sensorPick.js",
"src/data/scenePick.js", "src/data/scenePick.js",
"src/data/pickRegistry.js", "src/data/pickRegistry.js",
"src/input/dispatcher.js" "src/input/dispatcher.js",
"src/render/cesium/cameraInput.js"
], ],
"external": ["cesium", "vue", "pinia", "vue-i18n"] "external": ["cesium", "vue", "pinia", "vue-i18n"]
}, },
@@ -1371,6 +1375,7 @@
"src/data/scenePick.js", "src/data/scenePick.js",
"src/data/pickRegistry.js", "src/data/pickRegistry.js",
"src/input/dispatcher.js", "src/input/dispatcher.js",
"src/render/cesium/cameraInput.js",
"src/render/cesium/internals.js" "src/render/cesium/internals.js"
], ],
"external": ["cesium"] "external": ["cesium"]
@@ -1442,7 +1447,8 @@
"src/data/scenePick.js", "src/data/scenePick.js",
"src/input/sensorPick.js", "src/input/sensorPick.js",
"src/data/pickRegistry.js", "src/data/pickRegistry.js",
"src/input/dispatcher.js" "src/input/dispatcher.js",
"src/render/cesium/cameraInput.js"
], ],
"external": ["cesium"] "external": ["cesium"]
}, },
@@ -1526,7 +1532,8 @@
"src/data/scenePick.js", "src/data/scenePick.js",
"src/data/pickRegistry.js", "src/data/pickRegistry.js",
"src/input/sensorPick.js", "src/input/sensorPick.js",
"src/input/dispatcher.js" "src/input/dispatcher.js",
"src/render/cesium/cameraInput.js"
], ],
"external": ["cesium"] "external": ["cesium"]
}, },
@@ -1612,7 +1619,8 @@
"src/input/sensorPick.js", "src/input/sensorPick.js",
"src/data/scenePick.js", "src/data/scenePick.js",
"src/data/pickRegistry.js", "src/data/pickRegistry.js",
"src/input/dispatcher.js" "src/input/dispatcher.js",
"src/render/cesium/cameraInput.js"
], ],
"external": ["cesium"] "external": ["cesium"]
}, },
@@ -1656,7 +1664,8 @@
"src/input/sensorPick.js", "src/input/sensorPick.js",
"src/data/scenePick.js", "src/data/scenePick.js",
"src/data/pickRegistry.js", "src/data/pickRegistry.js",
"src/input/dispatcher.js" "src/input/dispatcher.js",
"src/render/cesium/cameraInput.js"
], ],
"external": ["cesium", "satellite.js"] "external": ["cesium", "satellite.js"]
}, },
@@ -1700,7 +1709,8 @@
"src/data/scenePick.js", "src/data/scenePick.js",
"src/data/pickRegistry.js", "src/data/pickRegistry.js",
"src/input/sensorPick.js", "src/input/sensorPick.js",
"src/input/dispatcher.js" "src/input/dispatcher.js",
"src/render/cesium/cameraInput.js"
], ],
"external": ["cesium"] "external": ["cesium"]
}, },
@@ -1757,7 +1767,8 @@
"src/input/sensorPick.js", "src/input/sensorPick.js",
"src/data/scenePick.js", "src/data/scenePick.js",
"src/data/pickRegistry.js", "src/data/pickRegistry.js",
"src/input/dispatcher.js" "src/input/dispatcher.js",
"src/render/cesium/cameraInput.js"
], ],
"external": ["cesium", "@mapbox/vector-tile", "pbf"] "external": ["cesium", "@mapbox/vector-tile", "pbf"]
}, },
@@ -1904,6 +1915,7 @@
"src/data/scenePick.js", "src/data/scenePick.js",
"src/data/pickRegistry.js", "src/data/pickRegistry.js",
"src/input/dispatcher.js", "src/input/dispatcher.js",
"src/render/cesium/cameraInput.js",
"src/queries/heights.js" "src/queries/heights.js"
], ],
"external": ["cesium"] "external": ["cesium"]
@@ -1973,7 +1985,8 @@
"src/data/scenePick.js", "src/data/scenePick.js",
"src/data/pickRegistry.js", "src/data/pickRegistry.js",
"src/input/sensorPick.js", "src/input/sensorPick.js",
"src/input/dispatcher.js" "src/input/dispatcher.js",
"src/render/cesium/cameraInput.js"
], ],
"external": ["hls.js", "cesium"] "external": ["hls.js", "cesium"]
}, },
@@ -2051,6 +2064,7 @@
"src/input/sensorPick.js", "src/input/sensorPick.js",
"src/data/pickRegistry.js", "src/data/pickRegistry.js",
"src/input/dispatcher.js", "src/input/dispatcher.js",
"src/render/cesium/cameraInput.js",
"src/queries/heights.js" "src/queries/heights.js"
], ],
"external": ["cesium", "pbf", "@mapbox/vector-tile", "mapillary-js"] "external": ["cesium", "pbf", "@mapbox/vector-tile", "mapillary-js"]
@@ -2095,6 +2109,7 @@
"src/data/scenePick.js", "src/data/scenePick.js",
"src/data/pickRegistry.js", "src/data/pickRegistry.js",
"src/input/dispatcher.js", "src/input/dispatcher.js",
"src/render/cesium/cameraInput.js",
"src/queries/heights.js" "src/queries/heights.js"
], ],
"external": ["cesium", "@mapbox/vector-tile", "pbf"] "external": ["cesium", "@mapbox/vector-tile", "pbf"]
@@ -2148,6 +2163,7 @@
"src/input/sensorPick.js", "src/input/sensorPick.js",
"src/data/pickRegistry.js", "src/data/pickRegistry.js",
"src/input/dispatcher.js", "src/input/dispatcher.js",
"src/render/cesium/cameraInput.js",
"src/render/cesium/internals.js" "src/render/cesium/internals.js"
], ],
"external": ["cesium"] "external": ["cesium"]
@@ -2184,7 +2200,8 @@
"src/data/scenePick.js", "src/data/scenePick.js",
"src/input/sensorPick.js", "src/input/sensorPick.js",
"src/data/pickRegistry.js", "src/data/pickRegistry.js",
"src/input/dispatcher.js" "src/input/dispatcher.js",
"src/render/cesium/cameraInput.js"
], ],
"external": ["cesium"] "external": ["cesium"]
}, },
@@ -3344,6 +3361,7 @@
"src/data/entityRecords.js", "src/data/entityRecords.js",
"src/input/sensorPick.js", "src/input/sensorPick.js",
"src/input/dispatcher.js", "src/input/dispatcher.js",
"src/render/cesium/cameraInput.js",
"src/queries/heights.js", "src/queries/heights.js",
"src/render/cesium/internals.js", "src/render/cesium/internals.js",
"src/render/cesium/tileRequests.js" "src/render/cesium/tileRequests.js"
@@ -3972,6 +3990,7 @@
"src/data/entityRecords.js", "src/data/entityRecords.js",
"src/input/sensorPick.js", "src/input/sensorPick.js",
"src/input/dispatcher.js", "src/input/dispatcher.js",
"src/render/cesium/cameraInput.js",
"src/queries/heights.js", "src/queries/heights.js",
"src/render/cesium/tileRequests.js", "src/render/cesium/tileRequests.js",
"src/render/cesium/internals.js" "src/render/cesium/internals.js"
@@ -4083,6 +4102,7 @@
"exports": ["./input"], "exports": ["./input"],
"modules": [ "modules": [
"src/input/dispatcher.js", "src/input/dispatcher.js",
"src/render/cesium/cameraInput.js",
"src/input/sensorPick.js", "src/input/sensorPick.js",
"src/data/inputOwnership.js", "src/data/inputOwnership.js",
"src/data/pickRegistry.js", "src/data/pickRegistry.js",
@@ -4340,7 +4360,8 @@
"src/data/scenePick.js", "src/data/scenePick.js",
"src/input/sensorPick.js", "src/input/sensorPick.js",
"src/data/pickRegistry.js", "src/data/pickRegistry.js",
"src/input/dispatcher.js" "src/input/dispatcher.js",
"src/render/cesium/cameraInput.js"
], ],
"external": ["cesium"] "external": ["cesium"]
}, },
@@ -4672,6 +4693,7 @@
"src/i18n/locales/zh-Hant.json", "src/i18n/locales/zh-Hant.json",
"src/input/sensorPick.js", "src/input/sensorPick.js",
"src/input/dispatcher.js", "src/input/dispatcher.js",
"src/render/cesium/cameraInput.js",
"src/queries/heights.js", "src/queries/heights.js",
"src/render/cesium/internals.js" "src/render/cesium/internals.js"
], ],
@@ -4730,6 +4752,7 @@
"src/data/scenePick.js", "src/data/scenePick.js",
"src/data/pickRegistry.js", "src/data/pickRegistry.js",
"src/input/dispatcher.js", "src/input/dispatcher.js",
"src/render/cesium/cameraInput.js",
"src/queries/heights.js", "src/queries/heights.js",
"src/render/cesium/internals.js" "src/render/cesium/internals.js"
], ],
@@ -4797,7 +4820,8 @@
"src/input/sensorPick.js", "src/input/sensorPick.js",
"src/data/scenePick.js", "src/data/scenePick.js",
"src/data/pickRegistry.js", "src/data/pickRegistry.js",
"src/input/dispatcher.js" "src/input/dispatcher.js",
"src/render/cesium/cameraInput.js"
], ],
"external": ["cesium"] "external": ["cesium"]
}, },
@@ -4898,6 +4922,7 @@
"src/input/sensorPick.js", "src/input/sensorPick.js",
"src/data/pickRegistry.js", "src/data/pickRegistry.js",
"src/input/dispatcher.js", "src/input/dispatcher.js",
"src/render/cesium/cameraInput.js",
"src/queries/heights.js" "src/queries/heights.js"
], ],
"external": ["cesium"] "external": ["cesium"]
+1
View File
@@ -186,6 +186,7 @@ test('consumer build includes only infrastructure code and resolves assets under
const entry = output.find((item) => item.type === 'chunk' && item.isEntry); const entry = output.find((item) => item.type === 'chunk' && item.isEntry);
const sources = Object.keys(entry.modules).filter((id) => id.endsWith('.js')); const sources = Object.keys(entry.modules).filter((id) => id.endsWith('.js'));
assert.deepEqual(sources.map((id) => id.split('/').at(-1)).sort(), [ assert.deepEqual(sources.map((id) => id.split('/').at(-1)).sort(), [
'cameraInput.js',
'dispatcher.js', 'dispatcher.js',
'heights.js', 'heights.js',
'infrastructure.js', 'infrastructure.js',
+8 -2
View File
@@ -115,18 +115,23 @@ test('pointer input, camera holds and the stock clicks go through the one dispat
// edit each layer. A layer that binds its own handler, writes the camera // edit each layer. A layer that binds its own handler, writes the camera
// flag or takes the stock clicks itself steps around that contract. // flag or takes the stock clicks itself steps around that contract.
const DISPATCHER = 'src/input/dispatcher.js'; const DISPATCHER = 'src/input/dispatcher.js';
// Each rule names the one other module it belongs in, if any.
const rules = [ const rules = [
[ [
/new\s+[\w.]*ScreenSpaceEventHandler\(/, /new\s+[\w.]*ScreenSpaceEventHandler\(/,
'binds its own ScreenSpaceEventHandler', 'binds its own ScreenSpaceEventHandler',
[],
], ],
[ [
/\.enableInputs\s*=(?!=)/, /\.enableInputs\s*=(?!=)/,
'writes enableInputs instead of suspendCamera()', 'writes enableInputs instead of suspendCamera()',
// The scene's camera-input holds, which own the flag.
['src/render/cesium/cameraInput.js'],
], ],
[ [
/screenSpaceEventHandler\??\.removeInputAction\(/, /screenSpaceEventHandler\??\.removeInputAction\(/,
'removes stock clicks instead of suppressStockClicks()', 'removes stock clicks instead of suppressStockClicks()',
[],
], ],
]; ];
const walk = (dir) => const walk = (dir) =>
@@ -141,8 +146,9 @@ test('pointer input, camera holds and the stock clicks go through the one dispat
for (const file of walk('src')) { for (const file of walk('src')) {
if (file === DISPATCHER) continue; if (file === DISPATCHER) continue;
const source = read(file); const source = read(file);
for (const [pattern, what] of rules) for (const [pattern, what, allowed] of rules)
if (pattern.test(source)) offenders.push(`${file} ${what}`); if (!allowed.includes(file) && pattern.test(source))
offenders.push(`${file} ${what}`);
} }
assert.deepEqual(offenders, []); assert.deepEqual(offenders, []);
}); });
+11 -62
View File
@@ -22,18 +22,17 @@
* free still ends (and gives its camera hold back). * free still ends (and gives its camera hold back).
* - Tool subscribers pass their lease and run only while it is current. * - Tool subscribers pass their lease and run only while it is current.
* *
* Camera input is suspended by holds, not by a flag: every suspender gets its * Camera input is suspended by the scene's counted holds
* own release, and input comes back when the last one releases. The * (src/render/cesium/cameraInput.js), which own the controller's
* dispatcher owns the controller's `enableInputs`: what others write to it * `enableInputs`, so a camera flight that ends cannot re-enable input under
* (Cesium's own camera flights turn it off and back on) is kept, and it reads * a drag or the cockpit.
* true only while that is true and no hold is out, so a flight that ends can
* no longer re-enable input under a drag or the cockpit.
* *
* A dispatcher is made per scene on first use ({@link inputFor}) and lives * A dispatcher is made per scene on first use ({@link inputFor}) and lives
* until {@link InputDispatcher.destroy}; the application scene owns it. * until {@link InputDispatcher.destroy}; the application scene owns it.
*/ */
import * as Cesium from 'cesium'; import * as Cesium from 'cesium';
import { isLeaseCurrent, isPointerFree } from '../data/inputOwnership.js'; import { isLeaseCurrent, isPointerFree } from '../data/inputOwnership.js';
import { acquireCameraInput } from '../render/cesium/cameraInput.js';
import { import {
findPickOwner, findPickOwner,
pickContact, pickContact,
@@ -129,49 +128,6 @@ export function inputFor(viewer, options = {}) {
return dispatcher; return dispatcher;
} }
/**
* Take over the controller's `enableInputs`: what others write to it is kept
* (through an accessor already there, if any), and it reads true only while
* that is true and no hold is out.
* @param {object|undefined} controller
* @param {Set<object>} holds
* @returns {() => void} Gives the controller its own property back.
*/
function ownCameraInput(controller, holds) {
if (!controller) return () => {};
const own = Object.getOwnPropertyDescriptor(controller, 'enableInputs');
if (own && !own.configurable) {
console.warn('Camera input holds are unavailable: enableInputs is fixed');
return () => {};
}
// A class may define it on its prototype; the hold shadows that for a while.
let original = own;
for (let proto = Object.getPrototypeOf(controller); !original && proto;) {
original = Object.getOwnPropertyDescriptor(proto, 'enableInputs');
proto = Object.getPrototypeOf(proto);
}
original ??= { value: true, writable: true, enumerable: true };
let written = Boolean(original.get ? true : original.value);
const read = () =>
original.get ? Boolean(original.get.call(controller)) : written;
Object.defineProperty(controller, 'enableInputs', {
configurable: true,
enumerable: original.enumerable ?? true,
get: () => read() && holds.size === 0,
set: (value) => {
written = Boolean(value);
original.set?.call(controller, value);
},
});
return () => {
delete controller.enableInputs;
if (own?.get || own?.set)
Object.defineProperty(controller, 'enableInputs', own);
else if (own || !(original.get || original.set))
controller.enableInputs = written;
};
}
function createDispatcher( function createDispatcher(
viewer, viewer,
{ {
@@ -181,12 +137,6 @@ function createDispatcher(
}, },
) { ) {
const scene = viewer.scene; const scene = viewer.scene;
const cameraHolds = new Set();
// Before the handler, so a controller that cannot be owned leaks nothing.
const releaseCameraInput = ownCameraInput(
scene.screenSpaceCameraController,
cameraHolds,
);
const handler = createHandler(scene.canvas); const handler = createHandler(scene.canvas);
/** @type {Array<{fn: Function}>} */ /** @type {Array<{fn: Function}>} */
const clickSubscribers = []; const clickSubscribers = [];
@@ -276,6 +226,10 @@ function createDispatcher(
run(pointerSubscribers.dblclick, isPointerFree(), event); run(pointerSubscribers.dblclick, isPointerFree(), event);
}, eventTypes.LEFT_DOUBLE_CLICK); }, eventTypes.LEFT_DOUBLE_CLICK);
// Once the handler is bound, so a setup that throws holds no share of the
// scene's camera input.
const cameraInput = acquireCameraInput(scene);
const subscribe = (list, entry) => { const subscribe = (list, entry) => {
if (destroyed) return () => {}; if (destroyed) return () => {};
list.push(entry); list.push(entry);
@@ -314,11 +268,7 @@ function createDispatcher(
suspendCamera(reason) { suspendCamera(reason) {
if (destroyed) return () => {}; if (destroyed) return () => {};
const hold = { reason }; return cameraInput.suspend(reason);
cameraHolds.add(hold);
return () => {
cameraHolds.delete(hold);
};
}, },
suppressStockClicks() { suppressStockClicks() {
@@ -356,8 +306,7 @@ function createDispatcher(
dispatchers.delete(scene); dispatchers.delete(scene);
clickSubscribers.length = 0; clickSubscribers.length = 0;
for (const type of POINTER_TYPES) pointerSubscribers[type].length = 0; for (const type of POINTER_TYPES) pointerSubscribers[type].length = 0;
cameraHolds.clear(); cameraInput.release();
releaseCameraInput();
if (stockHolds.size) { if (stockHolds.size) {
stockHolds.clear(); stockHolds.clear();
restoreStock(); restoreStock();
+119
View File
@@ -0,0 +1,119 @@
/**
* @module render/cesium/cameraInput
* @description User camera input on a Cesium scene, suspended by counted
* holds. Every suspender gets its own release, and input comes back when the
* last one releases.
*
* The holds own the controller's `enableInputs`: what others write to it
* (Cesium's own camera flights turn it off and back on) is kept, and it reads
* true only while that is true and no hold is out, so a flight that ends can
* no longer re-enable input under a drag or the cockpit.
*
* One set of holds per scene, shared by everyone who acquires it (the input
* port, the camera port, the application's dispatcher); the controller gets
* its own property back when the last of them releases.
*/
/** @type {WeakMap<object, {holds: Set<object>, users: number, restore: () => void}>} */
const shared = new WeakMap();
/**
* Take over the controller's `enableInputs`: what others write to it is kept
* (through an accessor already there, if any), and it reads true only while
* that is true and no hold is out.
* @param {object|undefined} controller
* @param {Set<object>} holds
* @returns {() => void} Gives the controller its own property back.
*/
function ownCameraInput(controller, holds) {
if (!controller) return () => {};
const own = Object.getOwnPropertyDescriptor(controller, 'enableInputs');
if (own && !own.configurable) {
console.warn('Camera input holds are unavailable: enableInputs is fixed');
return () => {};
}
// A class may define it on its prototype; the hold shadows that for a while.
let original = own;
for (let proto = Object.getPrototypeOf(controller); !original && proto;) {
original = Object.getOwnPropertyDescriptor(proto, 'enableInputs');
proto = Object.getPrototypeOf(proto);
}
original ??= { value: true, writable: true, enumerable: true };
let written = Boolean(original.get ? true : original.value);
const read = () =>
original.get ? Boolean(original.get.call(controller)) : written;
Object.defineProperty(controller, 'enableInputs', {
configurable: true,
enumerable: original.enumerable ?? true,
get: () => read() && holds.size === 0,
set: (value) => {
written = Boolean(value);
original.set?.call(controller, value);
},
});
return () => {
delete controller.enableInputs;
if (own?.get || own?.set)
Object.defineProperty(controller, 'enableInputs', own);
else if (own || !(original.get || original.set))
controller.enableInputs = written;
};
}
/**
* A user of the scene's camera-input holds.
* @typedef {object} CameraInput
* @property {(reason: string) => () => void} suspend Hold camera input off;
* returns this hold's release (a second call does nothing).
* @property {boolean} enabled False while any hold, anyone's, is out.
* @property {() => string[]} reasons The reasons of every hold out.
* @property {() => void} release Give back this user's holds, and the
* controller's property once no user is left.
*/
/**
* @param {{screenSpaceCameraController?: object}} scene
* @returns {CameraInput}
*/
export function acquireCameraInput(scene) {
let state = shared.get(scene);
if (!state) {
const holds = new Set();
state = {
holds,
users: 0,
restore: ownCameraInput(scene.screenSpaceCameraController, holds),
};
shared.set(scene, state);
}
state.users += 1;
const s = state;
const mine = new Set();
let released = false;
return {
suspend(reason) {
if (released) return () => {};
const hold = { reason: String(reason) };
s.holds.add(hold);
mine.add(hold);
return () => {
if (!mine.delete(hold)) return;
s.holds.delete(hold);
};
},
get enabled() {
return s.holds.size === 0;
},
reasons: () => [...s.holds].map((hold) => hold.reason),
release() {
if (released) return;
released = true;
for (const hold of mine) s.holds.delete(hold);
mine.clear();
s.users -= 1;
if (s.users > 0) return;
s.restore();
if (shared.get(scene) === s) shared.delete(scene);
},
};
}
+56
View File
@@ -0,0 +1,56 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { acquireCameraInput } from './cameraInput.js';
test('users of one scene share its holds', () => {
const controller = { enableInputs: true };
const scene = { screenSpaceCameraController: controller };
const a = acquireCameraInput(scene);
const b = acquireCameraInput(scene);
const drag = a.suspend('drag');
assert.equal(controller.enableInputs, false);
assert.equal(b.enabled, false, 'b sees a’s hold');
assert.deepEqual(b.reasons(), ['drag']);
const flight = b.suspend(7);
assert.deepEqual(a.reasons(), ['drag', '7']);
drag();
assert.equal(controller.enableInputs, false, 'b’s hold is still out');
a.release();
assert.equal(controller.enableInputs, false, 'b still holds');
flight();
assert.equal(controller.enableInputs, true);
});
test('releasing a user gives back its holds, and the property with the last', () => {
const controller = { enableInputs: true };
const scene = { screenSpaceCameraController: controller };
const a = acquireCameraInput(scene);
const b = acquireCameraInput(scene);
a.suspend('drag');
b.suspend('cockpit');
a.release();
assert.deepEqual(b.reasons(), ['cockpit']);
assert.equal(
typeof Object.getOwnPropertyDescriptor(controller, 'enableInputs').get,
'function',
'still owned while b is a user',
);
b.release();
assert.deepEqual(
Object.getOwnPropertyDescriptor(controller, 'enableInputs'),
{
value: true,
writable: true,
enumerable: true,
configurable: true,
},
);
assert.deepEqual(
a.suspend('late')(),
undefined,
'a released user holds nothing',
);
const c = acquireCameraInput(scene);
assert.equal(c.enabled, true, 'a new user starts afresh');
c.release();
});