@plasius/gpu-xr 0.1.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.
package/src/index.js ADDED
@@ -0,0 +1,331 @@
1
+ const DEFAULT_VR_SESSION_INIT = Object.freeze({
2
+ requiredFeatures: ["local-floor"],
3
+ optionalFeatures: ["bounded-floor", "hand-tracking", "layers"],
4
+ });
5
+
6
+ export const xrSessionModes = Object.freeze([
7
+ "inline",
8
+ "immersive-vr",
9
+ "immersive-ar",
10
+ ]);
11
+
12
+ export const xrReferenceSpaceTypes = Object.freeze([
13
+ "viewer",
14
+ "local",
15
+ "local-floor",
16
+ "bounded-floor",
17
+ "unbounded",
18
+ ]);
19
+
20
+ function toStringArray(values) {
21
+ if (!Array.isArray(values)) {
22
+ return [];
23
+ }
24
+ return values
25
+ .filter((value) => typeof value === "string")
26
+ .map((value) => value.trim())
27
+ .filter(Boolean);
28
+ }
29
+
30
+ function dedupeStrings(values) {
31
+ return [...new Set(toStringArray(values))];
32
+ }
33
+
34
+ function readNavigator(navigatorOverride) {
35
+ const currentNavigator = navigatorOverride ?? globalThis.navigator;
36
+ if (!currentNavigator || typeof currentNavigator !== "object") {
37
+ throw new Error(
38
+ "WebXR navigator unavailable. Provide a browser navigator with navigator.xr."
39
+ );
40
+ }
41
+ return currentNavigator;
42
+ }
43
+
44
+ function readXrSystem(navigatorOverride) {
45
+ const currentNavigator = readNavigator(navigatorOverride);
46
+ const xr = currentNavigator.xr;
47
+ if (!xr || typeof xr !== "object") {
48
+ throw new Error(
49
+ "WebXR runtime unavailable. navigator.xr is missing in this environment."
50
+ );
51
+ }
52
+ return xr;
53
+ }
54
+
55
+ function assertSessionMode(mode) {
56
+ if (!xrSessionModes.includes(mode)) {
57
+ const available = xrSessionModes.join(", ");
58
+ throw new Error(
59
+ `Unknown XR session mode \"${mode}\". Available modes: ${available}.`
60
+ );
61
+ }
62
+ }
63
+
64
+ export function mergeXrSessionInit(base = {}, override = {}) {
65
+ const requiredFeatures = dedupeStrings([
66
+ ...toStringArray(base.requiredFeatures),
67
+ ...toStringArray(override.requiredFeatures),
68
+ ]);
69
+
70
+ const optionalFeatures = dedupeStrings([
71
+ ...toStringArray(base.optionalFeatures),
72
+ ...toStringArray(override.optionalFeatures),
73
+ ]);
74
+
75
+ const merged = {
76
+ ...base,
77
+ ...override,
78
+ requiredFeatures,
79
+ optionalFeatures,
80
+ };
81
+
82
+ if (requiredFeatures.length === 0) {
83
+ delete merged.requiredFeatures;
84
+ }
85
+
86
+ if (optionalFeatures.length === 0) {
87
+ delete merged.optionalFeatures;
88
+ }
89
+
90
+ return merged;
91
+ }
92
+
93
+ export async function isXrModeSupported(
94
+ mode = "immersive-vr",
95
+ options = {}
96
+ ) {
97
+ assertSessionMode(mode);
98
+
99
+ const { navigator: navigatorOverride } = options;
100
+ let xr;
101
+ try {
102
+ xr = readXrSystem(navigatorOverride);
103
+ } catch {
104
+ return false;
105
+ }
106
+
107
+ if (typeof xr.isSessionSupported !== "function") {
108
+ return false;
109
+ }
110
+
111
+ try {
112
+ return Boolean(await xr.isSessionSupported(mode));
113
+ } catch {
114
+ return false;
115
+ }
116
+ }
117
+
118
+ export async function requestXrSession(options = {}) {
119
+ const {
120
+ mode = "immersive-vr",
121
+ sessionInit = {},
122
+ baseSessionInit = DEFAULT_VR_SESSION_INIT,
123
+ navigator: navigatorOverride,
124
+ } = options;
125
+
126
+ assertSessionMode(mode);
127
+
128
+ const xr = readXrSystem(navigatorOverride);
129
+
130
+ if (typeof xr.requestSession !== "function") {
131
+ throw new Error("WebXR requestSession API unavailable.");
132
+ }
133
+
134
+ const init = mergeXrSessionInit(baseSessionInit, sessionInit);
135
+ return xr.requestSession(mode, init);
136
+ }
137
+
138
+ export function createXrStore(initialState = {}) {
139
+ const listeners = new Set();
140
+ let state = {
141
+ activeSession: null,
142
+ mode: null,
143
+ isEntering: false,
144
+ lastError: null,
145
+ supportedModes: {},
146
+ ...initialState,
147
+ };
148
+
149
+ const notify = () => {
150
+ for (const listener of listeners) {
151
+ listener(state);
152
+ }
153
+ };
154
+
155
+ return {
156
+ getSnapshot() {
157
+ return state;
158
+ },
159
+ subscribe(listener) {
160
+ listeners.add(listener);
161
+ return () => {
162
+ listeners.delete(listener);
163
+ };
164
+ },
165
+ set(partialState) {
166
+ state = {
167
+ ...state,
168
+ ...partialState,
169
+ };
170
+ notify();
171
+ },
172
+ reset() {
173
+ state = {
174
+ activeSession: null,
175
+ mode: null,
176
+ isEntering: false,
177
+ lastError: null,
178
+ supportedModes: {},
179
+ ...initialState,
180
+ };
181
+ notify();
182
+ },
183
+ };
184
+ }
185
+
186
+ export function createXrManager(options = {}) {
187
+ const {
188
+ navigator: navigatorOverride,
189
+ defaultMode = "immersive-vr",
190
+ baseSessionInit = DEFAULT_VR_SESSION_INIT,
191
+ onSessionStart,
192
+ onSessionEnd,
193
+ } = options;
194
+
195
+ assertSessionMode(defaultMode);
196
+
197
+ const store = createXrStore();
198
+ let activeSessionEndHandler = null;
199
+
200
+ const detachSessionEndHandler = () => {
201
+ const { activeSession } = store.getSnapshot();
202
+ if (
203
+ activeSession &&
204
+ activeSessionEndHandler &&
205
+ typeof activeSession.removeEventListener === "function"
206
+ ) {
207
+ activeSession.removeEventListener("end", activeSessionEndHandler);
208
+ }
209
+ activeSessionEndHandler = null;
210
+ };
211
+
212
+ const handleSessionEnded = () => {
213
+ detachSessionEndHandler();
214
+ store.set({
215
+ activeSession: null,
216
+ mode: null,
217
+ isEntering: false,
218
+ });
219
+ if (typeof onSessionEnd === "function") {
220
+ onSessionEnd();
221
+ }
222
+ };
223
+
224
+ const attachSessionEndHandler = (session) => {
225
+ if (!session || typeof session.addEventListener !== "function") {
226
+ return;
227
+ }
228
+ activeSessionEndHandler = handleSessionEnded;
229
+ session.addEventListener("end", activeSessionEndHandler);
230
+ };
231
+
232
+ const getState = () => store.getSnapshot();
233
+
234
+ const subscribe = (listener) => store.subscribe(listener);
235
+
236
+ const probeSupport = async (modes = [defaultMode]) => {
237
+ const supportedModes = {};
238
+ for (const mode of modes) {
239
+ assertSessionMode(mode);
240
+ supportedModes[mode] = await isXrModeSupported(mode, {
241
+ navigator: navigatorOverride,
242
+ });
243
+ }
244
+ store.set({ supportedModes });
245
+ return supportedModes;
246
+ };
247
+
248
+ const enterSession = async (mode = defaultMode, sessionInit = {}) => {
249
+ assertSessionMode(mode);
250
+
251
+ const existing = store.getSnapshot().activeSession;
252
+ if (existing) {
253
+ return existing;
254
+ }
255
+
256
+ store.set({ isEntering: true, lastError: null });
257
+
258
+ try {
259
+ const session = await requestXrSession({
260
+ mode,
261
+ sessionInit,
262
+ baseSessionInit,
263
+ navigator: navigatorOverride,
264
+ });
265
+
266
+ attachSessionEndHandler(session);
267
+
268
+ store.set({
269
+ activeSession: session,
270
+ mode,
271
+ isEntering: false,
272
+ lastError: null,
273
+ });
274
+
275
+ if (typeof onSessionStart === "function") {
276
+ onSessionStart(session, mode);
277
+ }
278
+
279
+ return session;
280
+ } catch (error) {
281
+ const message =
282
+ error instanceof Error ? error.message : String(error ?? "Unknown XR error");
283
+ store.set({
284
+ isEntering: false,
285
+ lastError: message,
286
+ });
287
+ throw error;
288
+ }
289
+ };
290
+
291
+ const enterVr = async (sessionInit = {}) => {
292
+ return enterSession("immersive-vr", sessionInit);
293
+ };
294
+
295
+ const exitSession = async () => {
296
+ const { activeSession } = store.getSnapshot();
297
+ if (!activeSession) {
298
+ return false;
299
+ }
300
+
301
+ if (typeof activeSession.end === "function") {
302
+ await activeSession.end();
303
+ }
304
+
305
+ // Fallback for test fakes or runtimes that do not emit an end event.
306
+ if (store.getSnapshot().activeSession) {
307
+ handleSessionEnded();
308
+ }
309
+
310
+ return true;
311
+ };
312
+
313
+ const dispose = async () => {
314
+ await exitSession();
315
+ detachSessionEndHandler();
316
+ store.reset();
317
+ };
318
+
319
+ return {
320
+ store,
321
+ getState,
322
+ subscribe,
323
+ probeSupport,
324
+ enterSession,
325
+ enterVr,
326
+ exitSession,
327
+ dispose,
328
+ };
329
+ }
330
+
331
+ export const defaultVrSessionInit = DEFAULT_VR_SESSION_INIT;