@aura3d/input 4.7.18 → 5.0.1
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.
- package/dist/ActionMap.d.ts.map +1 -1
- package/dist/ActionMap.js.map +1 -1
- package/dist/ComboDetector.d.ts.map +1 -1
- package/dist/ComboDetector.js.map +1 -1
- package/dist/GamepadDevice.d.ts.map +1 -1
- package/dist/GamepadDevice.js.map +1 -1
- package/dist/GamepadInput.d.ts.map +1 -1
- package/dist/GamepadInput.js.map +1 -1
- package/dist/GestureControls.d.ts.map +1 -1
- package/dist/GestureControls.js.map +1 -1
- package/dist/GestureRecognizer.d.ts +5 -1
- package/dist/GestureRecognizer.d.ts.map +1 -1
- package/dist/GestureRecognizer.js +74 -18
- package/dist/GestureRecognizer.js.map +1 -1
- package/dist/Haptics.d.ts.map +1 -1
- package/dist/Haptics.js.map +1 -1
- package/dist/InputReplay.d.ts.map +1 -1
- package/dist/InputReplay.js +42 -13
- package/dist/InputReplay.js.map +1 -1
- package/dist/InputSnapshot.d.ts +62 -7
- package/dist/InputSnapshot.d.ts.map +1 -1
- package/dist/InputSnapshot.js +36 -5
- package/dist/InputSnapshot.js.map +1 -1
- package/dist/InputSystem.d.ts +10 -1
- package/dist/InputSystem.d.ts.map +1 -1
- package/dist/InputSystem.js +175 -12
- package/dist/InputSystem.js.map +1 -1
- package/dist/InputValueProcessors.d.ts.map +1 -1
- package/dist/InputValueProcessors.js.map +1 -1
- package/dist/InteractionSystem.d.ts +9 -4
- package/dist/InteractionSystem.d.ts.map +1 -1
- package/dist/InteractionSystem.js +93 -18
- package/dist/InteractionSystem.js.map +1 -1
- package/dist/KeyboardDevice.d.ts.map +1 -1
- package/dist/KeyboardDevice.js.map +1 -1
- package/dist/PickingRay.d.ts.map +1 -1
- package/dist/PickingRay.js.map +1 -1
- package/dist/PointerDevice.d.ts +61 -11
- package/dist/PointerDevice.d.ts.map +1 -1
- package/dist/PointerDevice.js +306 -34
- package/dist/PointerDevice.js.map +1 -1
- package/dist/PointerLeases.d.ts +22 -0
- package/dist/PointerLeases.d.ts.map +1 -0
- package/dist/PointerLeases.js +432 -0
- package/dist/PointerLeases.js.map +1 -0
- package/dist/TouchLayouts.d.ts.map +1 -1
- package/dist/TouchLayouts.js.map +1 -1
- package/dist/VirtualTouchControls.d.ts.map +1 -1
- package/dist/VirtualTouchControls.js.map +1 -1
- package/dist/WebXRSessionController.d.ts +161 -51
- package/dist/WebXRSessionController.d.ts.map +1 -1
- package/dist/WebXRSessionController.js +430 -203
- package/dist/WebXRSessionController.js.map +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- 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
|
-
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
get
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
80
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
95
|
-
|
|
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
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
283
|
+
end() {
|
|
284
|
+
++this.generation;
|
|
285
|
+
this.starting = false;
|
|
140
286
|
const session = this.session;
|
|
141
|
-
|
|
142
|
-
this.
|
|
143
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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,
|
|
162
|
-
const
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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,
|
|
180
|
-
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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(
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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(
|
|
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
|
-
|
|
226
|
-
|
|
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
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
|
238
|
-
|
|
239
|
-
|
|
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
|
|
250
|
-
|
|
251
|
-
|
|
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
|
|
260
|
-
if (!value
|
|
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
|
-
|
|
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
|
|
268
|
-
|
|
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
|
|
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
|
|
274
|
-
return
|
|
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
|
-
|
|
277
|
-
return
|
|
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
|