@aura3d/input 4.7.13 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/dist/ActionMap.d.ts.map +1 -1
  2. package/dist/ActionMap.js.map +1 -1
  3. package/dist/ComboDetector.d.ts.map +1 -1
  4. package/dist/ComboDetector.js.map +1 -1
  5. package/dist/GamepadDevice.d.ts.map +1 -1
  6. package/dist/GamepadDevice.js.map +1 -1
  7. package/dist/GamepadInput.d.ts.map +1 -1
  8. package/dist/GamepadInput.js.map +1 -1
  9. package/dist/GestureControls.d.ts.map +1 -1
  10. package/dist/GestureControls.js.map +1 -1
  11. package/dist/GestureRecognizer.d.ts +5 -1
  12. package/dist/GestureRecognizer.d.ts.map +1 -1
  13. package/dist/GestureRecognizer.js +74 -18
  14. package/dist/GestureRecognizer.js.map +1 -1
  15. package/dist/Haptics.d.ts.map +1 -1
  16. package/dist/Haptics.js.map +1 -1
  17. package/dist/InputReplay.d.ts.map +1 -1
  18. package/dist/InputReplay.js +42 -13
  19. package/dist/InputReplay.js.map +1 -1
  20. package/dist/InputSnapshot.d.ts +62 -7
  21. package/dist/InputSnapshot.d.ts.map +1 -1
  22. package/dist/InputSnapshot.js +36 -5
  23. package/dist/InputSnapshot.js.map +1 -1
  24. package/dist/InputSystem.d.ts +10 -1
  25. package/dist/InputSystem.d.ts.map +1 -1
  26. package/dist/InputSystem.js +175 -12
  27. package/dist/InputSystem.js.map +1 -1
  28. package/dist/InputValueProcessors.d.ts.map +1 -1
  29. package/dist/InputValueProcessors.js.map +1 -1
  30. package/dist/InteractionSystem.d.ts +9 -4
  31. package/dist/InteractionSystem.d.ts.map +1 -1
  32. package/dist/InteractionSystem.js +93 -18
  33. package/dist/InteractionSystem.js.map +1 -1
  34. package/dist/KeyboardDevice.d.ts.map +1 -1
  35. package/dist/KeyboardDevice.js.map +1 -1
  36. package/dist/PickingRay.d.ts.map +1 -1
  37. package/dist/PickingRay.js.map +1 -1
  38. package/dist/PointerDevice.d.ts +61 -11
  39. package/dist/PointerDevice.d.ts.map +1 -1
  40. package/dist/PointerDevice.js +306 -34
  41. package/dist/PointerDevice.js.map +1 -1
  42. package/dist/PointerLeases.d.ts +22 -0
  43. package/dist/PointerLeases.d.ts.map +1 -0
  44. package/dist/PointerLeases.js +432 -0
  45. package/dist/PointerLeases.js.map +1 -0
  46. package/dist/TouchLayouts.d.ts.map +1 -1
  47. package/dist/TouchLayouts.js.map +1 -1
  48. package/dist/VirtualTouchControls.d.ts.map +1 -1
  49. package/dist/VirtualTouchControls.js.map +1 -1
  50. package/dist/WebXRSessionController.d.ts +161 -51
  51. package/dist/WebXRSessionController.d.ts.map +1 -1
  52. package/dist/WebXRSessionController.js +430 -203
  53. package/dist/WebXRSessionController.js.map +1 -1
  54. package/dist/index.d.ts +5 -3
  55. package/dist/index.d.ts.map +1 -1
  56. package/dist/index.js +1 -0
  57. package/dist/index.js.map +1 -1
  58. package/package.json +4 -3
@@ -1,3 +1,8 @@
1
+ // A cancelled native request cannot itself be cancelled. Keep its owner busy
2
+ // until it rejects or its eventual session has finished closing. Weak keys keep
3
+ // this coordination scoped to the app without retaining disposed owners.
4
+ const sessionLeases = new WeakMap();
5
+ /** Owns one pending or active session. A cancelled start closes any session that arrives late. */
1
6
  export class WebXRSessionController {
2
7
  mode;
3
8
  requiredFeatures;
@@ -6,274 +11,496 @@ export class WebXRSessionController {
6
11
  explicitReferenceSpace;
7
12
  activeReferenceSpaceType;
8
13
  xr;
14
+ leaseOwner;
15
+ lease = null;
16
+ depthSensing;
9
17
  session = null;
10
18
  referenceSpace = null;
19
+ hitTestSource = null;
20
+ lightProbe = null;
21
+ pendingSession = null;
22
+ endedSessions = new WeakSet();
23
+ sessionClosePromises = new WeakMap();
24
+ sourceIds = new WeakMap();
25
+ nextSourceId = 1;
26
+ planeIds = new WeakMap();
27
+ nextPlaneId = 1;
28
+ generation = 0;
29
+ starting = false;
30
+ disposed = false;
31
+ endPromise = null;
32
+ endListener = null;
33
+ hapticCleanups = new Set();
34
+ warnings = [];
11
35
  constructor(options = {}) {
12
36
  this.xr = options.xr ?? readNavigatorXR();
37
+ this.leaseOwner = options.leaseOwner ?? this;
13
38
  this.mode = options.mode ?? "immersive-vr";
14
39
  this.requiredFeatures = options.requiredFeatures ?? [];
15
40
  this.optionalFeatures = options.optionalFeatures ?? [];
41
+ this.depthSensing = options.depthSensing;
16
42
  this.referenceSpaceType = options.referenceSpace ?? (this.mode === "immersive-ar" ? "viewer" : "local-floor");
17
43
  this.explicitReferenceSpace = options.referenceSpace !== undefined;
18
44
  this.activeReferenceSpaceType = this.referenceSpaceType;
19
45
  }
20
- get active() {
21
- return this.session !== null;
22
- }
23
- /** SC-04: the live session, for render-state and frame-loop wiring. Null when inactive. */
24
- get xrSession() {
25
- return this.session;
26
- }
27
- /** SC-04: the reference space requested at `start()`. Null when inactive. */
28
- get xrReferenceSpace() {
29
- return this.referenceSpace;
30
- }
46
+ get active() { return this.session !== null && this.referenceSpace !== null; }
47
+ get xrSession() { return this.session; }
48
+ get xrReferenceSpace() { return this.referenceSpace; }
49
+ get xrHitTestSource() { return this.hitTestSource; }
50
+ get hasLightProbe() { return this.lightProbe !== null; }
31
51
  async isSupported() {
32
- if (!this.xr)
52
+ if (this.disposed || !this.xr)
33
53
  return false;
34
- if (!this.xr.isSessionSupported)
35
- return true;
36
- return this.xr.isSessionSupported(this.mode);
54
+ return this.xr.isSessionSupported ? this.xr.isSessionSupported(this.mode) : true;
55
+ }
56
+ result(supported, started, busy = false) {
57
+ return { mode: this.mode, referenceSpace: this.activeReferenceSpaceType, supported, started, ...(busy ? { busy: true } : {}) };
58
+ }
59
+ releaseLease(lease) {
60
+ if (sessionLeases.get(this.leaseOwner) === lease)
61
+ sessionLeases.delete(this.leaseOwner);
62
+ if (this.lease === lease)
63
+ this.lease = null;
37
64
  }
38
65
  async start() {
39
- if (!this.xr) {
40
- return {
41
- mode: this.mode,
42
- referenceSpace: this.referenceSpaceType,
43
- supported: false,
44
- started: false
45
- };
46
- }
47
- const supported = await this.isSupported();
48
- if (!supported) {
49
- return {
50
- mode: this.mode,
51
- referenceSpace: this.referenceSpaceType,
52
- supported: false,
53
- started: false
54
- };
55
- }
56
- // WebXR: `viewer` is always available and `local` is implied for immersive sessions; every other
57
- // reference-space type (`local-floor`, `bounded-floor`, `unbounded`) must be requested as a session feature
58
- // or `requestReferenceSpace` rejects with NotSupportedError.
59
- const requestedType = this.referenceSpaceType;
60
- const implied = requestedType === "viewer" || requestedType === "local";
61
- const listed = this.requiredFeatures.includes(requestedType) || this.optionalFeatures.includes(requestedType);
62
- const optionalFeatures = implied || listed ? this.optionalFeatures : [...this.optionalFeatures, requestedType];
63
- const session = await this.xr.requestSession(this.mode, {
64
- requiredFeatures: this.requiredFeatures,
65
- optionalFeatures
66
- });
67
- this.session = session;
68
- this.activeReferenceSpaceType = requestedType;
66
+ if (this.disposed || !this.xr)
67
+ return this.result(false, false);
68
+ if (this.starting || this.session || this.endPromise || sessionLeases.has(this.leaseOwner))
69
+ return this.result(true, false, true);
70
+ const generation = ++this.generation;
71
+ this.starting = true;
72
+ this.warnings.length = 0;
73
+ let candidate = null;
74
+ let lease = null;
75
+ let source = null;
76
+ let pendingEndListener = null;
69
77
  try {
70
- this.referenceSpace = await session.requestReferenceSpace?.(requestedType) ?? { type: requestedType };
71
- }
72
- catch (error) {
73
- // Like three's VRButton, the default `local-floor` degrades to `local` on devices without a floor.
74
- if (this.explicitReferenceSpace || requestedType !== "local-floor") {
75
- await this.abandonSession(session);
76
- throw error;
78
+ const supported = await this.isSupported();
79
+ if (generation !== this.generation)
80
+ return this.result(supported, false);
81
+ if (!supported)
82
+ return this.result(false, false);
83
+ // Check again after support detection, which can run concurrently for
84
+ // different controllers with the same owner.
85
+ if (sessionLeases.has(this.leaseOwner))
86
+ return this.result(true, false, true);
87
+ lease = {};
88
+ sessionLeases.set(this.leaseOwner, lease);
89
+ this.lease = lease;
90
+ const requestedType = this.referenceSpaceType;
91
+ const implied = requestedType === "viewer" || requestedType === "local";
92
+ const listed = this.requiredFeatures.includes(requestedType) || this.optionalFeatures.includes(requestedType);
93
+ const optionalFeatures = implied || listed ? this.optionalFeatures : [...this.optionalFeatures, requestedType];
94
+ candidate = await this.xr.requestSession(this.mode, { requiredFeatures: this.requiredFeatures, optionalFeatures, ...(this.depthSensing ? { depthSensing: this.depthSensing } : {}) });
95
+ if (generation !== this.generation)
96
+ return this.result(true, false);
97
+ this.pendingSession = candidate;
98
+ const openingSession = candidate;
99
+ pendingEndListener = () => {
100
+ if (this.pendingSession === openingSession) {
101
+ this.pendingSession = null;
102
+ this.endedSessions.add(openingSession);
103
+ ++this.generation;
104
+ this.starting = false;
105
+ if (lease)
106
+ this.releaseLease(lease);
107
+ }
108
+ };
109
+ candidate.addEventListener?.("end", pendingEndListener);
110
+ if (generation !== this.generation)
111
+ return this.result(true, false);
112
+ if (typeof candidate.requestReferenceSpace !== "function") {
113
+ throw new Error("XR_REFERENCE_SPACE_UNAVAILABLE: the session does not expose requestReferenceSpace.");
77
114
  }
115
+ let space;
116
+ let actualType = requestedType;
78
117
  try {
79
- this.referenceSpace = await session.requestReferenceSpace?.("local") ?? { type: "local" };
80
- this.activeReferenceSpaceType = "local";
118
+ space = await candidate.requestReferenceSpace(requestedType);
119
+ }
120
+ catch (error) {
121
+ if (generation !== this.generation)
122
+ return this.result(true, false);
123
+ if (this.explicitReferenceSpace || requestedType !== "local-floor")
124
+ throw error;
125
+ actualType = "local";
126
+ try {
127
+ space = await candidate.requestReferenceSpace("local");
128
+ }
129
+ catch (fallbackError) {
130
+ if (generation !== this.generation)
131
+ return this.result(true, false);
132
+ throw fallbackError;
133
+ }
81
134
  }
82
- catch (fallbackError) {
83
- await this.abandonSession(session);
84
- throw fallbackError;
135
+ if (generation !== this.generation)
136
+ return this.result(true, false);
137
+ if (!space || typeof space !== "object") {
138
+ throw new Error(`XR_REFERENCE_SPACE_UNAVAILABLE: the session did not supply a usable ${actualType} reference space.`);
85
139
  }
140
+ const features = [...this.requiredFeatures, ...this.optionalFeatures];
141
+ if (features.includes("hit-test")) {
142
+ if (candidate.requestHitTestSource) {
143
+ try {
144
+ const viewer = actualType === "viewer" ? space : await candidate.requestReferenceSpace?.("viewer");
145
+ if (generation !== this.generation)
146
+ return this.result(true, false);
147
+ if (viewer) {
148
+ source = await candidate.requestHitTestSource({ space: viewer }) ?? null;
149
+ if (!source && generation === this.generation)
150
+ this.warnings.push("Hit testing unavailable: no hit-test source was supplied.");
151
+ }
152
+ else
153
+ this.warnings.push("Hit testing unavailable: viewer reference space was not supplied.");
154
+ }
155
+ catch (error) {
156
+ if (generation !== this.generation)
157
+ return this.result(true, false);
158
+ this.warnings.push(`Hit testing unavailable: ${message(error)}`);
159
+ }
160
+ }
161
+ else
162
+ this.warnings.push("Hit testing unavailable: session does not expose requestHitTestSource.");
163
+ }
164
+ if (generation !== this.generation)
165
+ return this.result(true, false);
166
+ let lightProbe = null;
167
+ if (features.includes("light-estimation")) {
168
+ if (candidate.requestLightProbe) {
169
+ try {
170
+ lightProbe = await candidate.requestLightProbe();
171
+ }
172
+ catch (error) {
173
+ if (generation === this.generation)
174
+ this.warnings.push(`Light estimation unavailable: ${message(error)}`);
175
+ }
176
+ }
177
+ else
178
+ this.warnings.push("Light estimation unavailable: session does not expose requestLightProbe.");
179
+ }
180
+ if (generation !== this.generation)
181
+ return this.result(true, false);
182
+ this.session = candidate;
183
+ this.referenceSpace = space;
184
+ this.activeReferenceSpaceType = actualType;
185
+ this.hitTestSource = source;
186
+ this.lightProbe = lightProbe;
187
+ const owned = candidate;
188
+ const ownedLease = lease;
189
+ this.endListener = () => {
190
+ if (this.session === owned) {
191
+ this.endedSessions.add(owned);
192
+ this.clearOwned();
193
+ if (ownedLease)
194
+ this.releaseLease(ownedLease);
195
+ }
196
+ };
197
+ candidate.addEventListener?.("end", this.endListener);
198
+ if (pendingEndListener)
199
+ candidate.removeEventListener?.("end", pendingEndListener);
200
+ pendingEndListener = null;
201
+ if (this.pendingSession === candidate)
202
+ this.pendingSession = null;
203
+ candidate = null;
204
+ source = null;
205
+ lease = null; // The active session now owns the lease.
206
+ return this.result(true, true);
207
+ }
208
+ finally {
209
+ if (generation === this.generation)
210
+ this.starting = false;
211
+ cancelSource(source);
212
+ if (candidate) {
213
+ if (pendingEndListener)
214
+ candidate.removeEventListener?.("end", pendingEndListener);
215
+ if (this.pendingSession === candidate)
216
+ this.pendingSession = null;
217
+ await this.closeSession(candidate);
218
+ }
219
+ if (lease)
220
+ this.releaseLease(lease);
86
221
  }
87
- return {
88
- mode: this.mode,
89
- referenceSpace: this.referenceSpace?.type ?? this.activeReferenceSpaceType,
90
- supported: true,
91
- started: true
92
- };
93
222
  }
94
- /** A session whose reference space could not be created is never left open. */
95
- async abandonSession(session) {
223
+ clearOwned() {
224
+ const session = this.session;
225
+ if (this.endListener)
226
+ session?.removeEventListener?.("end", this.endListener);
227
+ this.endListener = null;
228
+ for (const cleanup of this.hapticCleanups)
229
+ cleanup();
230
+ cancelSource(this.hitTestSource);
231
+ this.hitTestSource = null;
232
+ this.lightProbe = null;
96
233
  this.session = null;
97
234
  this.referenceSpace = null;
98
235
  this.activeReferenceSpaceType = this.referenceSpaceType;
99
- try {
100
- await session.end?.();
101
- }
102
- catch {
103
- // The session is already unusable; ending it is best effort.
104
- }
105
236
  }
106
- sampleFrame(frame, hitTestSource) {
237
+ sampleFrame(frame, hitTestSource, gpuDepth) {
107
238
  const referenceSpace = this.referenceSpace ?? { type: this.activeReferenceSpaceType };
108
239
  const sources = Array.from(this.session?.inputSources ?? []);
109
- const controllers = sources.map((source) => sampleInputSource(source, frame, referenceSpace));
110
- const hands = sources
111
- .filter((source) => source.hand !== undefined)
112
- .map((source) => sampleHand(source.handedness ?? "none", source.hand));
113
- const hitTests = frame?.getHitTestResults && hitTestSource
114
- ? frame.getHitTestResults(hitTestSource).map(sampleHitTest)
115
- : [];
116
- const camera = sampleViewerPose(frame, referenceSpace);
117
- const hitTestCapable = Boolean(frame?.getHitTestResults && hitTestSource);
240
+ const id = (source) => {
241
+ let value = this.sourceIds.get(source);
242
+ if (value === undefined) {
243
+ value = this.nextSourceId++;
244
+ this.sourceIds.set(source, value);
245
+ }
246
+ return value;
247
+ };
248
+ const controllers = sources.map((source) => sampleInputSource(id(source), source, frame, referenceSpace));
249
+ const hands = sources.filter((source) => source.hand != null).map((source) => sampleHand(id(source), source.handedness ?? "none", source.hand, frame, referenceSpace));
250
+ const source = hitTestSource ?? this.hitTestSource;
251
+ const hitTests = source && frame?.getHitTestResults ? safe(() => frame.getHitTestResults(source), []).flatMap((hit) => {
252
+ const sampled = sampleHitTest(hit, referenceSpace);
253
+ return sampled ? [sampled] : [];
254
+ }) : [];
255
+ const pose = safe(() => frame?.getViewerPose?.(referenceSpace), undefined);
256
+ const camera = sampleViewerPose(pose);
257
+ const planes = Array.from(frame?.detectedPlanes ?? [], (plane) => {
258
+ let planeId = this.planeIds.get(plane);
259
+ if (planeId === undefined) {
260
+ planeId = this.nextPlaneId++;
261
+ this.planeIds.set(plane, planeId);
262
+ }
263
+ return samplePlane(planeId, plane, frame, referenceSpace);
264
+ });
265
+ const depth = (pose?.views ?? []).flatMap((view) => {
266
+ const value = safe(() => frame?.getDepthInformation?.(view), undefined) ?? safe(() => gpuDepth?.(view), undefined);
267
+ return value ? [sampleDepth(view, value)] : [];
268
+ });
269
+ const lightEstimate = this.lightProbe ? safe(() => frame?.getLightEstimate?.(this.lightProbe), undefined) : undefined;
270
+ const light = lightEstimate ? sampleLight(lightEstimate) : null;
118
271
  const enabledFeatures = Array.from(this.session?.enabledFeatures ?? []);
119
272
  return {
120
- mode: this.mode,
121
- referenceSpace: this.referenceSpace?.type ?? this.referenceSpaceType,
122
- active: this.active,
123
- controllerCount: controllers.length,
124
- controllers,
125
- hitTestCount: hitTests.length,
126
- hitTests,
127
- hands,
128
- camera,
273
+ mode: this.mode, referenceSpace: this.activeReferenceSpaceType, active: this.active,
274
+ controllerCount: controllers.length, controllers, hitTestCount: hitTests.length, hitTests, hands, camera, planes, depth, light,
129
275
  capabilities: {
130
- handTracking: hands.some((hand) => hand.tracked),
131
- viewerPose: camera.tracked,
132
- hitTest: hitTestCapable,
133
- haptics: controllers.some((controller) => controller.hapticActuatorCount > 0),
134
- referenceSpace: this.referenceSpace?.type ?? this.referenceSpaceType,
135
- enabledFeatures
276
+ handTracking: hands.some((hand) => hand.tracked), viewerPose: camera.tracked,
277
+ hitTest: Boolean(frame?.getHitTestResults && source), haptics: controllers.some((controller) => controller.hapticActuatorCount > 0),
278
+ referenceSpace: this.activeReferenceSpaceType, enabledFeatures,
279
+ planeDetection: frame?.detectedPlanes !== undefined, depthSensing: depth.length > 0, lightEstimation: light !== null
136
280
  }
137
281
  };
138
282
  }
139
- async end() {
283
+ end() {
284
+ ++this.generation;
285
+ this.starting = false;
140
286
  const session = this.session;
141
- this.session = null;
142
- this.referenceSpace = null;
143
- await session?.end?.();
287
+ const pending = this.pendingSession;
288
+ const lease = pending || session ? this.lease : null;
289
+ this.pendingSession = null;
290
+ this.clearOwned();
291
+ const previous = this.endPromise;
292
+ // Publish the closing promise before invoking the native end method: an
293
+ // end listener can synchronously re-enter this controller or its runtime.
294
+ const closing = Promise.resolve().then(async () => {
295
+ if (previous)
296
+ await previous;
297
+ if (pending)
298
+ await this.closeSession(pending);
299
+ if (session)
300
+ await this.closeSession(session);
301
+ });
302
+ this.endPromise = closing;
303
+ return closing.finally(() => {
304
+ if (lease)
305
+ this.releaseLease(lease);
306
+ if (this.endPromise === closing)
307
+ this.endPromise = null;
308
+ });
309
+ }
310
+ async dispose() {
311
+ if (this.disposed) {
312
+ await this.endPromise;
313
+ return;
314
+ }
315
+ this.disposed = true;
316
+ await this.end();
317
+ }
318
+ closeSession(session) {
319
+ const previous = this.sessionClosePromises.get(session);
320
+ if (previous)
321
+ return previous;
322
+ if (this.endedSessions.has(session))
323
+ return Promise.resolve();
324
+ this.endedSessions.add(session);
325
+ const closing = Promise.resolve().then(async () => {
326
+ try {
327
+ await session.end?.();
328
+ }
329
+ catch { /* A native session can already be ended. */ }
330
+ });
331
+ this.sessionClosePromises.set(session, closing);
332
+ return closing;
144
333
  }
145
334
  async pulseHaptics(intensity = 0.5, duration = 35) {
146
335
  if (!Number.isFinite(intensity) || intensity < 0 || intensity > 1)
147
336
  throw new RangeError("WebXR haptic intensity must be in [0, 1].");
148
337
  if (!Number.isFinite(duration) || duration < 0)
149
338
  throw new RangeError("WebXR haptic duration must be finite and non-negative.");
150
- let pulses = 0;
151
- for (const source of this.session?.inputSources ?? []) {
152
- for (const actuator of source.gamepad?.hapticActuators ?? []) {
153
- const accepted = await actuator.pulse?.(intensity, duration);
154
- if (accepted !== false)
155
- pulses += 1;
339
+ const session = this.session;
340
+ if (!session || !this.referenceSpace || this.disposed)
341
+ return 0;
342
+ const generation = this.generation;
343
+ const isCurrent = () => !this.disposed && this.session === session && this.generation === generation;
344
+ const removed = new Set();
345
+ const onSourcesChanged = (event) => {
346
+ for (const source of event.removed ?? [])
347
+ removed.add(source);
348
+ };
349
+ const cleanup = () => {
350
+ session.removeEventListener?.("inputsourceschange", onSourcesChanged);
351
+ this.hapticCleanups.delete(cleanup);
352
+ };
353
+ if (session.addEventListener && session.removeEventListener) {
354
+ session.addEventListener("inputsourceschange", onSourcesChanged);
355
+ this.hapticCleanups.add(cleanup);
356
+ }
357
+ // Native lists are repeatable, while injected session sources may be a
358
+ // one-shot iterator. Preserve the latter's initial snapshot and use source
359
+ // removal events to invalidate it when available.
360
+ const oneShotSources = new Map();
361
+ const currentSources = () => {
362
+ const collection = session.inputSources;
363
+ if (!collection)
364
+ return [];
365
+ const previous = oneShotSources.get(collection);
366
+ if (previous)
367
+ return previous;
368
+ const iterator = collection[Symbol.iterator]();
369
+ const snapshot = Array.from({ [Symbol.iterator]: () => iterator });
370
+ if (iterator === collection)
371
+ oneShotSources.set(collection, snapshot);
372
+ return snapshot;
373
+ };
374
+ const sourceIsLive = (source) => !removed.has(source) && currentSources().includes(source);
375
+ try {
376
+ const sources = currentSources();
377
+ let pulses = 0;
378
+ for (const source of sources) {
379
+ if (!isCurrent())
380
+ break;
381
+ if (!sourceIsLive(source))
382
+ continue;
383
+ const actuators = Array.from(source.gamepad?.hapticActuators ?? []);
384
+ for (const actuator of actuators) {
385
+ if (!isCurrent())
386
+ return pulses;
387
+ if (!sourceIsLive(source))
388
+ break;
389
+ if (!source.gamepad?.hapticActuators?.includes(actuator))
390
+ continue;
391
+ if (!isCurrent())
392
+ return pulses;
393
+ if (actuator.pulse && await actuator.pulse(intensity, duration) !== false)
394
+ pulses += 1;
395
+ }
156
396
  }
397
+ return pulses;
398
+ }
399
+ finally {
400
+ cleanup();
157
401
  }
158
- return pulses;
159
402
  }
160
403
  }
161
- function sampleInputSource(source, frame, referenceSpace) {
162
- const buttons = source.gamepad?.buttons ?? [];
163
- const axes = source.gamepad?.axes ?? [];
164
- const trigger = buttons[0];
165
- const squeeze = buttons[1];
404
+ function sampleInputSource(id, source, frame, space) {
405
+ const trigger = source.gamepad?.buttons?.[0];
166
406
  return {
167
- handedness: source.handedness ?? "none",
168
- targetRayMode: source.targetRayMode ?? "tracked-pointer",
169
- profiles: source.profiles ?? [],
170
- triggerPressed: Boolean(trigger?.pressed),
171
- squeezePressed: Boolean(squeeze?.pressed),
172
- primaryValue: finiteOrZero(trigger?.value),
173
- axes: axes.map(finiteOrZero),
174
- targetRayMatrix: samplePoseMatrix(frame, source.targetRaySpace, referenceSpace),
175
- gripMatrix: samplePoseMatrix(frame, source.gripSpace, referenceSpace),
176
- hapticActuatorCount: source.gamepad?.hapticActuators?.length ?? 0
407
+ id, handedness: source.handedness ?? "none", targetRayMode: source.targetRayMode ?? "tracked-pointer", profiles: source.profiles ?? [],
408
+ triggerPressed: Boolean(trigger?.pressed), squeezePressed: Boolean(source.gamepad?.buttons?.[1]?.pressed), primaryValue: finiteOrZero(trigger?.value),
409
+ axes: (source.gamepad?.axes ?? []).map(finiteOrZero), targetRayMatrix: samplePoseMatrix(frame, source.targetRaySpace, space),
410
+ gripMatrix: samplePoseMatrix(frame, source.gripSpace, space), hapticActuatorCount: source.gamepad?.hapticActuators?.length ?? 0
177
411
  };
178
412
  }
179
- function samplePoseMatrix(frame, space, referenceSpace) {
180
- if (!frame?.getPose || !space)
181
- return null;
182
- return sanitizeMatrix4(frame.getPose(space, referenceSpace)?.transform?.matrix);
413
+ function samplePoseMatrix(frame, target, base) {
414
+ return target && frame?.getPose ? matrix(safe(() => frame.getPose(target, base)?.transform?.matrix, undefined)) : null;
183
415
  }
184
- function sampleHand(handedness, hand) {
416
+ function sampleHand(id, handedness, hand, frame, base) {
185
417
  const joints = {};
186
- const entries = hand?.joints ? Object.entries(hand.joints).slice(0, 64) : [];
187
- for (const [name, joint] of entries) {
188
- joints[name] = {
189
- position: sanitizeVec3OrNull(joint?.position),
190
- radius: typeof joint?.radius === "number" && Number.isFinite(joint.radius) ? Math.max(0, joint.radius) : 0
191
- };
418
+ if (hand?.entries && frame?.getJointPose) {
419
+ for (const [name, jointSpace] of hand.entries()) {
420
+ const pose = safe(() => frame.getJointPose(jointSpace, base), undefined);
421
+ if (!pose)
422
+ continue;
423
+ joints[name] = { position: vector3(pose.transform?.position), orientation: quaternion(pose.transform?.orientation), radius: Math.max(0, finiteOrZero(pose.radius)) };
424
+ }
192
425
  }
426
+ else
427
+ for (const [name, joint] of Object.entries(hand?.joints ?? {}).slice(0, 64)) {
428
+ joints[name] = { position: vector3(joint.position), orientation: quaternion(joint.orientation), radius: Math.max(0, finiteOrZero(joint.radius)) };
429
+ }
193
430
  const jointCount = Object.keys(joints).length;
194
- return {
195
- handedness,
196
- tracked: hand?.trackingState === "tracked" && jointCount > 0,
197
- jointCount,
198
- joints
199
- };
431
+ return { id, handedness, tracked: jointCount > 0 && (hand?.entries ? true : hand?.trackingState === "tracked"), jointCount, joints };
200
432
  }
201
- function sampleViewerPose(frame, referenceSpace) {
202
- const untracked = {
203
- tracked: false,
204
- position: null,
205
- orientation: null,
206
- viewMatrix: null,
207
- projectionMatrix: null
208
- };
209
- const pose = frame?.getViewerPose?.(referenceSpace);
210
- if (!pose)
211
- return untracked;
212
- const position = sanitizeVec3OrNull(pose.position);
213
- const orientation = sanitizeQuatOrNull(pose.orientation);
214
- // Strict: a single non-finite entry invalidates the whole matrix. Zeroing
215
- // it (the legacy controller-pose behavior) would silently warp the XR
216
- // camera, so the sample degrades to untracked instead.
217
- const viewMatrix = sanitizeMatrix4Strict(pose.viewMatrix);
218
- const projectionMatrix = sanitizeMatrix4Strict(pose.projectionMatrix);
219
- if (!position && !orientation && !viewMatrix && !projectionMatrix)
220
- return untracked;
221
- return { tracked: true, position, orientation, viewMatrix, projectionMatrix };
433
+ function sampleViewerPose(pose) {
434
+ const position = vector3(pose?.transform?.position ?? pose?.position);
435
+ const orientation = quaternion(pose?.transform?.orientation ?? pose?.orientation);
436
+ const viewMatrix = matrix(pose?.viewMatrix ?? pose?.views?.[0]?.transform?.inverse?.matrix);
437
+ const projectionMatrix = matrix(pose?.projectionMatrix ?? pose?.views?.[0]?.projectionMatrix);
438
+ return { tracked: Boolean(position || orientation || viewMatrix || projectionMatrix), position, orientation, viewMatrix, projectionMatrix };
222
439
  }
223
- function sampleHitTest(result) {
440
+ function sampleHitTest(hit, referenceSpace) {
441
+ const pose = safe(() => hit.getPose?.(referenceSpace), undefined);
442
+ const position = vector3(pose?.transform?.position ?? hit.position);
443
+ if (!position)
444
+ return null;
445
+ const rotation = quaternion(pose?.transform?.orientation);
446
+ const m = matrix(pose?.transform?.matrix);
447
+ const normal = vector3(hit.normal) ?? (m ? [m[4], m[5], m[6]] : rotation ? rotateUp(rotation) : [0, 1, 0]);
448
+ return { position, normal };
449
+ }
450
+ function samplePlane(id, plane, frame, base) {
224
451
  return {
225
- position: sanitizeVec3(result.position, [0, 0, 0]),
226
- normal: sanitizeVec3(result.normal, [0, 1, 0])
452
+ id, matrix: samplePoseMatrix(frame, plane.planeSpace, base),
453
+ polygon: Array.from(plane.polygon ?? []).flatMap((point) => { const v = vector3(point); return v ? [v] : []; }),
454
+ orientation: plane.orientation ?? null, semanticLabel: plane.semanticLabel ?? null,
455
+ lastChangedTime: Number.isFinite(plane.lastChangedTime) ? plane.lastChangedTime : null
227
456
  };
228
457
  }
229
- function sanitizeVec3(value, fallback) {
230
- if (!value || value.length < 3)
231
- return fallback;
232
- const x = finiteOrZero(value[0]);
233
- const y = finiteOrZero(value[1]);
234
- const z = finiteOrZero(value[2]);
235
- return [x, y, z];
458
+ function sampleDepth(view, depth) {
459
+ return { eye: view.eye ?? "none", width: depth.width, height: depth.height, rawValueToMeters: depth.rawValueToMeters,
460
+ transform: matrix(depth.normDepthBufferFromNormView?.matrix), data: depth.data ?? null, texture: depth.texture ?? null,
461
+ textureType: depth.textureType ?? null, imageIndex: depth.imageIndex ?? null };
236
462
  }
237
- function sanitizeVec3OrNull(value) {
238
- if (!value || value.length < 3)
239
- return null;
240
- const x = value[0];
241
- const y = value[1];
242
- const z = value[2];
243
- if (typeof x !== "number" || typeof y !== "number" || typeof z !== "number")
244
- return null;
245
- if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(z))
246
- return null;
247
- return [x, y, z];
463
+ function sampleLight(value) {
464
+ return { direction: vector3(value.primaryLightDirection), intensity: vector3(value.primaryLightIntensity),
465
+ sphericalHarmonics: Array.from(value.sphericalHarmonicsCoefficients ?? [], finiteOrZero) };
248
466
  }
249
- function sanitizeQuatOrNull(value) {
250
- if (!value || value.length < 4)
251
- return null;
252
- const [x, y, z, w] = [value[0], value[1], value[2], value[3]];
253
- if (typeof x !== "number" || typeof y !== "number" || typeof z !== "number" || typeof w !== "number")
254
- return null;
255
- if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(z) || !Number.isFinite(w))
256
- return null;
257
- return [x, y, z, w];
467
+ function rotateUp(q) {
468
+ const [x, y, z, w] = q;
469
+ return [2 * (x * y - z * w), 1 - 2 * (x * x + z * z), 2 * (y * z + x * w)];
258
470
  }
259
- function sanitizeMatrix4Strict(value) {
260
- if (!value || value.length < 16)
261
- return null;
262
- const matrix = value.slice(0, 16);
263
- if (matrix.some((entry) => typeof entry !== "number" || !Number.isFinite(entry)))
471
+ function components(value) {
472
+ if (!value)
264
473
  return null;
265
- return Array.from(matrix);
474
+ if (Array.isArray(value))
475
+ return value;
476
+ const point = value;
477
+ return [point.x, point.y, point.z, point.w ?? NaN];
266
478
  }
267
- function finiteOrZero(value) {
268
- return typeof value === "number" && Number.isFinite(value) ? value : 0;
479
+ function vector3(value) {
480
+ const c = components(value);
481
+ return c && c.length >= 3 && c.slice(0, 3).every(Number.isFinite) ? [c[0], c[1], c[2]] : null;
269
482
  }
270
- function sanitizeMatrix4(value) {
483
+ function quaternion(value) {
484
+ const c = components(value);
485
+ return c && c.length >= 4 && c.slice(0, 4).every(Number.isFinite) ? [c[0], c[1], c[2], c[3]] : null;
486
+ }
487
+ function matrix(value) {
271
488
  if (!value || value.length < 16)
272
489
  return null;
273
- const matrix = Array.from(value.slice(0, 16), finiteOrZero);
274
- return matrix;
490
+ const entries = Array.from({ length: 16 }, (_, i) => value[i]);
491
+ return entries.every(Number.isFinite) ? entries : null;
492
+ }
493
+ function safe(fn, fallback) { try {
494
+ return fn();
275
495
  }
276
- function readNavigatorXR() {
277
- return globalThis.navigator?.xr;
496
+ catch {
497
+ return fallback;
498
+ } }
499
+ function cancelSource(source) { try {
500
+ source?.cancel?.();
278
501
  }
502
+ catch { /* A closed native session may already have invalidated the source. */ } }
503
+ function finiteOrZero(value) { return typeof value === "number" && Number.isFinite(value) ? value : 0; }
504
+ function message(error) { return error instanceof Error ? error.message : String(error); }
505
+ function readNavigatorXR() { return globalThis.navigator?.xr; }
279
506
  //# sourceMappingURL=WebXRSessionController.js.map