@flighthq/sensors 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.
@@ -0,0 +1,681 @@
1
+ import { createSignal, emitSignal } from '@flighthq/signals';
2
+ // Begins delivering sensor readings to `sensors`'s signals by subscribing to the active backend's
3
+ // streams. Idempotent: a prior subscription is torn down first. Pair with detachSensors/disposeSensors.
4
+ //
5
+ // Readings passed to signal listeners are scratch-reused objects. Listeners must not retain a
6
+ // reference to a reading across callback boundaries — copy the values if they need to outlive the call.
7
+ export function attachSensors(sensors) {
8
+ detachSensors(sensors);
9
+ const backend = getSensorsBackend();
10
+ const unsubscribeMotion = backend.subscribeMotion((acceleration, rotationRate) => {
11
+ emitSignal(sensors.onAccelerometer, acceleration);
12
+ emitSignal(sensors.onGyroscope, rotationRate);
13
+ });
14
+ const unsubscribeLinearAcceleration = backend.subscribeLinearAcceleration((reading) => {
15
+ emitSignal(sensors.onLinearAcceleration, reading);
16
+ });
17
+ const unsubscribeGravity = backend.subscribeGravity((reading) => {
18
+ emitSignal(sensors.onGravity, reading);
19
+ });
20
+ const unsubscribeOrientation = backend.subscribeOrientation((orientation) => {
21
+ emitSignal(sensors.onOrientation, orientation);
22
+ });
23
+ const unsubscribeAbsoluteOrientation = backend.subscribeAbsoluteOrientation((orientation) => {
24
+ emitSignal(sensors.onAbsoluteOrientation, orientation);
25
+ });
26
+ const unsubscribeMagnetometer = backend.subscribeMagnetometer((reading) => {
27
+ emitSignal(sensors.onMagnetometer, reading);
28
+ });
29
+ const unsubscribeAmbientLight = backend.subscribeAmbientLight((reading) => {
30
+ emitSignal(sensors.onAmbientLight, reading);
31
+ });
32
+ const unsubscribeBarometer = backend.subscribeBarometer((reading) => {
33
+ emitSignal(sensors.onBarometer, reading);
34
+ });
35
+ const unsubscribeProximity = backend.subscribeProximity((reading) => {
36
+ emitSignal(sensors.onProximity, reading);
37
+ });
38
+ const unsubscribeQuaternion = backend.subscribeQuaternion((reading) => {
39
+ emitSignal(sensors.onQuaternion, reading);
40
+ });
41
+ _subscriptions.set(sensors, () => {
42
+ unsubscribeAbsoluteOrientation();
43
+ unsubscribeAmbientLight();
44
+ unsubscribeBarometer();
45
+ unsubscribeGravity();
46
+ unsubscribeLinearAcceleration();
47
+ unsubscribeMagnetometer();
48
+ unsubscribeMotion();
49
+ unsubscribeOrientation();
50
+ unsubscribeProximity();
51
+ unsubscribeQuaternion();
52
+ });
53
+ }
54
+ // Extracts Euler angles (alpha/beta/gamma in degrees) from a quaternion into an OrientationReading,
55
+ // using the ZXY convention that matches the W3C deviceorientation spec. Propagates interval,
56
+ // timestamp, and accuracy from the quaternion. Writes into `out`.
57
+ // Safe when `out` aliases any field of `quaternion` because all inputs are read first.
58
+ export function computeEulerFromQuaternion(out, quaternion) {
59
+ const x = quaternion.x;
60
+ const y = quaternion.y;
61
+ const z = quaternion.z;
62
+ const w = quaternion.w;
63
+ // ZXY (yaw-pitch-roll) decomposition matching deviceorientation alpha/beta/gamma convention.
64
+ const sinBeta = 2 * (w * x - y * z);
65
+ const beta = Math.abs(sinBeta) >= 1 ? (Math.sign(sinBeta) * Math.PI) / 2 : Math.asin(sinBeta);
66
+ const alpha = Math.atan2(2 * (w * z + x * y), 1 - 2 * (x * x + z * z));
67
+ const gamma = Math.atan2(2 * (w * y + x * z), 1 - 2 * (x * x + y * y));
68
+ const toDeg = 180 / Math.PI;
69
+ out.alpha = (((alpha * toDeg) % 360) + 360) % 360; // normalize to [0, 360)
70
+ out.beta = beta * toDeg;
71
+ out.gamma = gamma * toDeg;
72
+ out.interval = quaternion.interval;
73
+ out.timestamp = quaternion.timestamp;
74
+ out.accuracy = quaternion.accuracy;
75
+ }
76
+ // Derives the gravity vector (m/s²) from Euler orientation angles (alpha/beta/gamma in degrees)
77
+ // into a MotionReading. The result is the device-frame projection of the 9.81 m/s² downward
78
+ // gravity vector onto each device axis. Writes into `out`.
79
+ // Safe when `out` aliases any field of `orientation` because all inputs are read first.
80
+ export function computeGravityFromOrientation(out, orientation) {
81
+ const toRad = Math.PI / 180;
82
+ const b = orientation.beta * toRad;
83
+ const g = orientation.gamma * toRad;
84
+ // Gravity components in device frame. g is 9.81 m/s².
85
+ const G = 9.80665;
86
+ const sinG = Math.sin(g);
87
+ const cosG = Math.cos(g);
88
+ const sinB = Math.sin(b);
89
+ const cosB = Math.cos(b);
90
+ out.x = G * cosB * sinG;
91
+ out.y = -G * sinB;
92
+ out.z = G * cosB * cosG;
93
+ out.interval = orientation.interval;
94
+ out.timestamp = orientation.timestamp;
95
+ out.accuracy = orientation.accuracy;
96
+ }
97
+ // Derives an approximate quaternion from Euler orientation angles (alpha/beta/gamma in degrees).
98
+ // Uses the ZXY convention matching the deviceorientation spec. Writes into `out`.
99
+ // Safe when `out` aliases any field of `orientation` because all inputs are read first.
100
+ export function computeQuaternionFromOrientationReading(out, orientation) {
101
+ const toRad = Math.PI / 180;
102
+ const a = orientation.alpha * toRad * 0.5;
103
+ const b = orientation.beta * toRad * 0.5;
104
+ const g = orientation.gamma * toRad * 0.5;
105
+ const ca = Math.cos(a);
106
+ const sa = Math.sin(a);
107
+ const cb = Math.cos(b);
108
+ const sb = Math.sin(b);
109
+ const cg = Math.cos(g);
110
+ const sg = Math.sin(g);
111
+ out.x = sa * sb * cg - ca * cb * sg;
112
+ out.y = sa * cb * sg + ca * sb * cg;
113
+ out.z = ca * cb * sg - sa * sb * cg;
114
+ out.w = ca * cb * cg + sa * sb * sg;
115
+ out.interval = orientation.interval;
116
+ out.timestamp = orientation.timestamp;
117
+ out.accuracy = orientation.accuracy;
118
+ }
119
+ // Converts a quaternion reading into a 3×3 rotation matrix written to `out` (column-major, 9
120
+ // elements). Safe when `out` aliases any field of `quaternion` because all inputs are read first.
121
+ export function computeRotationMatrixFromQuaternion(out, quaternion) {
122
+ const x = quaternion.x;
123
+ const y = quaternion.y;
124
+ const z = quaternion.z;
125
+ const w = quaternion.w;
126
+ const x2 = x + x;
127
+ const y2 = y + y;
128
+ const z2 = z + z;
129
+ const xx = x * x2;
130
+ const xy = x * y2;
131
+ const xz = x * z2;
132
+ const yy = y * y2;
133
+ const yz = y * z2;
134
+ const zz = z * z2;
135
+ const wx = w * x2;
136
+ const wy = w * y2;
137
+ const wz = w * z2;
138
+ out[0] = 1 - (yy + zz);
139
+ out[1] = xy + wz;
140
+ out[2] = xz - wy;
141
+ out[3] = xy - wz;
142
+ out[4] = 1 - (xx + zz);
143
+ out[5] = yz + wx;
144
+ out[6] = xz + wy;
145
+ out[7] = yz - wx;
146
+ out[8] = 1 - (xx + yy);
147
+ }
148
+ // Compensates an OrientationReading for the current screen rotation angle (in degrees, clockwise,
149
+ // as returned by screen.orientation.angle). Raw deviceorientation angles are in device-physical
150
+ // frame; this function rotates alpha/beta/gamma so they are relative to the current screen
151
+ // orientation. Writes into `out`. Safe when `out` aliases `orientation` because all inputs are read first.
152
+ export function computeScreenRelativeOrientation(out, orientation, screenAngle) {
153
+ const alpha = orientation.alpha;
154
+ const beta = orientation.beta;
155
+ const gamma = orientation.gamma;
156
+ // Read all inputs before writing any output (alias safety).
157
+ const toRad = Math.PI / 180;
158
+ const angle = screenAngle * toRad;
159
+ const sinA = Math.sin(angle);
160
+ const cosA = Math.cos(angle);
161
+ // Rotate gamma and beta components by the screen angle in the horizontal plane.
162
+ out.alpha = alpha;
163
+ out.beta = beta * cosA - gamma * sinA;
164
+ out.gamma = beta * sinA + gamma * cosA;
165
+ out.absolute = orientation.absolute;
166
+ out.heading = orientation.heading;
167
+ out.interval = orientation.interval;
168
+ out.timestamp = orientation.timestamp;
169
+ out.accuracy = orientation.accuracy;
170
+ }
171
+ // Rotates a device-frame acceleration vector (m/s²) into the world frame using a quaternion that
172
+ // describes the device's orientation. The quaternion represents the rotation from world to device
173
+ // frame; this function applies the inverse (device-to-world) rotation. Writes into `out`.
174
+ // Safe when `out` aliases `acceleration` because all inputs are read first.
175
+ export function computeWorldAccelerationFromDeviceAcceleration(out, acceleration, quaternion) {
176
+ // Read all inputs before writing any output (alias safety).
177
+ const ax = acceleration.x;
178
+ const ay = acceleration.y;
179
+ const az = acceleration.z;
180
+ const qx = quaternion.x;
181
+ const qy = quaternion.y;
182
+ const qz = quaternion.z;
183
+ const qw = quaternion.w;
184
+ // Apply the inverse (conjugate) quaternion rotation: q^-1 * v * q.
185
+ // Using the efficient vector rotation formula: v' = v + 2*qw*(q × v) + 2*(q × (q × v)).
186
+ const twx = 2 * qw;
187
+ const cx = qy * az - qz * ay;
188
+ const cy = qz * ax - qx * az;
189
+ const cz = qx * ay - qy * ax;
190
+ const ccx = qy * cz - qz * cy;
191
+ const ccy = qz * cx - qx * cz;
192
+ const ccz = qx * cy - qy * cx;
193
+ out.x = ax + twx * cx + 2 * ccx;
194
+ out.y = ay + twx * cy + 2 * ccy;
195
+ out.z = az + twx * cz + 2 * ccz;
196
+ out.interval = acceleration.interval;
197
+ out.timestamp = acceleration.timestamp;
198
+ out.accuracy = acceleration.accuracy;
199
+ }
200
+ // Allocates a zeroed AmbientLightReading with unknown accuracy/interval/timestamp.
201
+ export function createAmbientLightReading() {
202
+ return { accuracy: 'unknown', illuminance: 0, interval: -1, timestamp: -1 };
203
+ }
204
+ // Allocates a zeroed MotionReading with unknown accuracy/interval/timestamp.
205
+ // Used for accelerometer (gravity-included), linear acceleration, gravity vector, and magnetometer readings.
206
+ export function createMotionReading() {
207
+ return { accuracy: 'unknown', interval: -1, timestamp: -1, x: 0, y: 0, z: 0 };
208
+ }
209
+ // Allocates a zeroed OrientationReading. heading is -1 (unknown) and absolute is false until
210
+ // a reading arrives.
211
+ export function createOrientationReading() {
212
+ return {
213
+ absolute: false,
214
+ accuracy: 'unknown',
215
+ alpha: 0,
216
+ beta: 0,
217
+ gamma: 0,
218
+ heading: -1,
219
+ interval: -1,
220
+ timestamp: -1,
221
+ };
222
+ }
223
+ // Allocates a zeroed PressureReading with unknown accuracy/interval/timestamp.
224
+ // altitude is -1 when underivable from pressure alone.
225
+ export function createPressureReading() {
226
+ return { accuracy: 'unknown', altitude: -1, interval: -1, pressure: 0, timestamp: -1 };
227
+ }
228
+ // Allocates a zeroed ProximityReading with unknown accuracy/interval/timestamp.
229
+ // distance and max are -1 when only near/far is known.
230
+ export function createProximityReading() {
231
+ return { accuracy: 'unknown', distance: -1, interval: -1, max: -1, near: false, timestamp: -1 };
232
+ }
233
+ // Allocates a zeroed QuaternionReading (identity quaternion: w=1) with unknown accuracy/interval/timestamp.
234
+ export function createQuaternionReading() {
235
+ return { accuracy: 'unknown', interval: -1, timestamp: -1, w: 1, x: 0, y: 0, z: 0 };
236
+ }
237
+ // Allocates a zeroed RotationRateReading with unknown accuracy/interval/timestamp.
238
+ // alpha/beta/gamma are angular velocity in deg/s around the device z/x/y axes respectively.
239
+ export function createRotationRateReading() {
240
+ return { accuracy: 'unknown', alpha: 0, beta: 0, gamma: 0, interval: -1, timestamp: -1 };
241
+ }
242
+ // Allocates a Sensors event entity with inert signals; call attachSensors to start delivery.
243
+ export function createSensors() {
244
+ return {
245
+ onAbsoluteOrientation: createSignal(),
246
+ onAccelerometer: createSignal(),
247
+ onAmbientLight: createSignal(),
248
+ onBarometer: createSignal(),
249
+ onGravity: createSignal(),
250
+ onGyroscope: createSignal(),
251
+ onLinearAcceleration: createSignal(),
252
+ onMagnetometer: createSignal(),
253
+ onOrientation: createSignal(),
254
+ onProximity: createSignal(),
255
+ onQuaternion: createSignal(),
256
+ };
257
+ }
258
+ // Builds the default web backend over the devicemotion, deviceorientation, and deviceorientationabsolute
259
+ // window events, plus the Generic Sensor API where available. Degrades to no-op subscriptions where
260
+ // window is absent and to a granted permission where the host does not gate sensors.
261
+ //
262
+ // Rate control: the Generic Sensor API honors the `frequency` option; the devicemotion /
263
+ // deviceorientation window event streams do not support rate control and always fire at the
264
+ // browser's default interval.
265
+ export function createWebSensorsBackend() {
266
+ return {
267
+ getPermissionState(sensor) {
268
+ return getWebSensorsPermissionState(sensor);
269
+ },
270
+ isAmbientLightSupported() {
271
+ return getWebGenericSensorConstructor('AmbientLightSensor') !== null;
272
+ },
273
+ isBarometerSupported() {
274
+ // The web platform has no standard Barometer API; always return false.
275
+ return false;
276
+ },
277
+ isGravitySupported() {
278
+ // Gravity is derived from devicemotion (accelerationIncludingGravity - acceleration).
279
+ if (typeof window === 'undefined')
280
+ return false;
281
+ return typeof DeviceMotionEvent !== 'undefined';
282
+ },
283
+ isGyroscopeSupported() {
284
+ if (typeof window === 'undefined')
285
+ return false;
286
+ return typeof DeviceMotionEvent !== 'undefined';
287
+ },
288
+ isLinearAccelerationSupported() {
289
+ // Linear acceleration is the event.acceleration field of devicemotion.
290
+ if (typeof window === 'undefined')
291
+ return false;
292
+ return typeof DeviceMotionEvent !== 'undefined';
293
+ },
294
+ isMagnetometerSupported() {
295
+ return getWebMagnetometerConstructor() !== null;
296
+ },
297
+ isMotionSupported() {
298
+ if (typeof window === 'undefined')
299
+ return false;
300
+ return typeof DeviceMotionEvent !== 'undefined';
301
+ },
302
+ isOrientationSupported() {
303
+ if (typeof window === 'undefined')
304
+ return false;
305
+ return typeof DeviceOrientationEvent !== 'undefined';
306
+ },
307
+ isProximitySupported() {
308
+ return false;
309
+ },
310
+ async requestPermission() {
311
+ const request = getWebMotionPermissionRequest();
312
+ if (request === null)
313
+ return true;
314
+ try {
315
+ const state = await request();
316
+ return state === 'granted';
317
+ }
318
+ catch {
319
+ return false;
320
+ }
321
+ },
322
+ subscribeAbsoluteOrientation(listener, options) {
323
+ if (typeof window === 'undefined')
324
+ return () => { };
325
+ // Try Generic Sensor AbsoluteOrientationSensor first.
326
+ const ctor = getWebGenericSensorConstructor('AbsoluteOrientationSensor');
327
+ if (ctor !== null) {
328
+ try {
329
+ const sensorOptions = options?.frequency !== undefined ? { frequency: options.frequency } : undefined;
330
+ const sensor = new ctor(sensorOptions);
331
+ const handler = () => {
332
+ const q = sensor.quaternion;
333
+ if (q) {
334
+ _quaternionReading.x = q[0] ?? 0;
335
+ _quaternionReading.y = q[1] ?? 0;
336
+ _quaternionReading.z = q[2] ?? 0;
337
+ _quaternionReading.w = q[3] ?? 1;
338
+ // Derive Euler orientation from the quaternion using ZXY convention.
339
+ computeEulerFromQuaternion(_absoluteOrientation, _quaternionReading);
340
+ }
341
+ _absoluteOrientation.absolute = true;
342
+ _absoluteOrientation.heading = -1;
343
+ listener(_absoluteOrientation);
344
+ };
345
+ sensor.addEventListener('reading', handler);
346
+ sensor.start();
347
+ return () => {
348
+ sensor.removeEventListener('reading', handler);
349
+ sensor.stop();
350
+ };
351
+ }
352
+ catch {
353
+ // Fall through to event-based approach.
354
+ }
355
+ }
356
+ // Fall back to deviceorientationabsolute event.
357
+ const handler = (event) => {
358
+ _absoluteOrientation.alpha = event.alpha ?? 0;
359
+ _absoluteOrientation.beta = event.beta ?? 0;
360
+ _absoluteOrientation.gamma = event.gamma ?? 0;
361
+ _absoluteOrientation.absolute = true;
362
+ _absoluteOrientation.heading = -1;
363
+ _absoluteOrientation.interval = -1;
364
+ _absoluteOrientation.timestamp = -1;
365
+ listener(_absoluteOrientation);
366
+ };
367
+ window.addEventListener('deviceorientationabsolute', handler);
368
+ return () => {
369
+ window.removeEventListener('deviceorientationabsolute', handler);
370
+ };
371
+ },
372
+ subscribeAmbientLight(listener, options) {
373
+ const ctor = getWebGenericSensorConstructor('AmbientLightSensor');
374
+ if (ctor === null)
375
+ return () => { };
376
+ try {
377
+ const sensorOptions = options?.frequency !== undefined ? { frequency: options.frequency } : undefined;
378
+ const sensor = new ctor(sensorOptions);
379
+ const handler = () => {
380
+ _ambientLight.illuminance = sensor.illuminance ?? 0;
381
+ _ambientLight.interval = -1;
382
+ _ambientLight.timestamp = -1;
383
+ listener(_ambientLight);
384
+ };
385
+ sensor.addEventListener('reading', handler);
386
+ sensor.start();
387
+ return () => {
388
+ sensor.removeEventListener('reading', handler);
389
+ sensor.stop();
390
+ };
391
+ }
392
+ catch {
393
+ return () => { };
394
+ }
395
+ },
396
+ subscribeBarometer(_listener, _options) {
397
+ // No barometer support on the web platform.
398
+ return () => { };
399
+ },
400
+ subscribeGravity(listener, _options) {
401
+ if (typeof window === 'undefined')
402
+ return () => { };
403
+ // Derive gravity from the devicemotion event: gravity = accelerationIncludingGravity - acceleration.
404
+ // When acceleration (gravity-removed) is unavailable, we cannot derive gravity.
405
+ const handler = (event) => {
406
+ const withGravity = event.accelerationIncludingGravity;
407
+ const linearAccel = event.acceleration;
408
+ if (!withGravity)
409
+ return;
410
+ _gravity.x = (withGravity.x ?? 0) - (linearAccel?.x ?? 0);
411
+ _gravity.y = (withGravity.y ?? 0) - (linearAccel?.y ?? 0);
412
+ _gravity.z = (withGravity.z ?? 0) - (linearAccel?.z ?? 0);
413
+ _gravity.interval = event.interval ?? -1;
414
+ _gravity.timestamp = -1;
415
+ listener(_gravity);
416
+ };
417
+ window.addEventListener('devicemotion', handler);
418
+ return () => {
419
+ window.removeEventListener('devicemotion', handler);
420
+ };
421
+ },
422
+ subscribeLinearAcceleration(listener, _options) {
423
+ if (typeof window === 'undefined')
424
+ return () => { };
425
+ // event.acceleration is the gravity-removed linear acceleration vector.
426
+ const handler = (event) => {
427
+ const accel = event.acceleration;
428
+ if (!accel)
429
+ return;
430
+ _linearAcceleration.x = accel.x ?? 0;
431
+ _linearAcceleration.y = accel.y ?? 0;
432
+ _linearAcceleration.z = accel.z ?? 0;
433
+ _linearAcceleration.interval = event.interval ?? -1;
434
+ _linearAcceleration.timestamp = -1;
435
+ listener(_linearAcceleration);
436
+ };
437
+ window.addEventListener('devicemotion', handler);
438
+ return () => {
439
+ window.removeEventListener('devicemotion', handler);
440
+ };
441
+ },
442
+ subscribeMagnetometer(listener, options) {
443
+ const ctor = getWebMagnetometerConstructor();
444
+ if (ctor === null)
445
+ return () => { };
446
+ try {
447
+ const sensorOptions = options?.frequency !== undefined ? { frequency: options.frequency } : undefined;
448
+ const sensor = new ctor(sensorOptions);
449
+ const handler = () => {
450
+ _magnetometer.x = sensor.x ?? 0;
451
+ _magnetometer.y = sensor.y ?? 0;
452
+ _magnetometer.z = sensor.z ?? 0;
453
+ _magnetometer.interval = -1;
454
+ _magnetometer.timestamp = -1;
455
+ listener(_magnetometer);
456
+ };
457
+ sensor.addEventListener('reading', handler);
458
+ sensor.start();
459
+ return () => {
460
+ sensor.removeEventListener('reading', handler);
461
+ sensor.stop();
462
+ };
463
+ }
464
+ catch {
465
+ return () => { };
466
+ }
467
+ },
468
+ subscribeMotion(listener, _options) {
469
+ if (typeof window === 'undefined')
470
+ return () => { };
471
+ const handler = (event) => {
472
+ const accel = event.accelerationIncludingGravity;
473
+ _motionAcceleration.x = accel?.x ?? 0;
474
+ _motionAcceleration.y = accel?.y ?? 0;
475
+ _motionAcceleration.z = accel?.z ?? 0;
476
+ _motionAcceleration.interval = event.interval ?? -1;
477
+ _motionAcceleration.timestamp = -1;
478
+ const rate = event.rotationRate;
479
+ _motionRotationRate.alpha = rate?.alpha ?? 0;
480
+ _motionRotationRate.beta = rate?.beta ?? 0;
481
+ _motionRotationRate.gamma = rate?.gamma ?? 0;
482
+ _motionRotationRate.interval = event.interval ?? -1;
483
+ _motionRotationRate.timestamp = -1;
484
+ listener(_motionAcceleration, _motionRotationRate);
485
+ };
486
+ window.addEventListener('devicemotion', handler);
487
+ return () => {
488
+ window.removeEventListener('devicemotion', handler);
489
+ };
490
+ },
491
+ subscribeOrientation(listener, _options) {
492
+ if (typeof window === 'undefined')
493
+ return () => { };
494
+ const handler = (event) => {
495
+ _orientation.alpha = event.alpha ?? 0;
496
+ _orientation.beta = event.beta ?? 0;
497
+ _orientation.gamma = event.gamma ?? 0;
498
+ _orientation.absolute = event.absolute ?? false;
499
+ _orientation.interval = -1;
500
+ _orientation.timestamp = -1;
501
+ // webkitCompassHeading is iOS-only; elsewhere the web exposes no compass heading, so report -1.
502
+ const heading = event.webkitCompassHeading;
503
+ _orientation.heading = typeof heading === 'number' ? heading : -1;
504
+ listener(_orientation);
505
+ };
506
+ window.addEventListener('deviceorientation', handler);
507
+ return () => {
508
+ window.removeEventListener('deviceorientation', handler);
509
+ };
510
+ },
511
+ subscribeProximity(_listener, _options) {
512
+ // No proximity sensor support on the standard web platform.
513
+ return () => { };
514
+ },
515
+ subscribeQuaternion(listener, options) {
516
+ const ctor = getWebGenericSensorConstructor('AbsoluteOrientationSensor');
517
+ if (ctor === null)
518
+ return () => { };
519
+ try {
520
+ const sensorOptions = options?.frequency !== undefined ? { frequency: options.frequency } : undefined;
521
+ const sensor = new ctor(sensorOptions);
522
+ const handler = () => {
523
+ const q = sensor.quaternion;
524
+ _quaternionReading.x = q?.[0] ?? 0;
525
+ _quaternionReading.y = q?.[1] ?? 0;
526
+ _quaternionReading.z = q?.[2] ?? 0;
527
+ _quaternionReading.w = q?.[3] ?? 1;
528
+ _quaternionReading.interval = -1;
529
+ _quaternionReading.timestamp = -1;
530
+ listener(_quaternionReading);
531
+ };
532
+ sensor.addEventListener('reading', handler);
533
+ sensor.start();
534
+ return () => {
535
+ sensor.removeEventListener('reading', handler);
536
+ sensor.stop();
537
+ };
538
+ }
539
+ catch {
540
+ return () => { };
541
+ }
542
+ },
543
+ };
544
+ }
545
+ // Stops delivery to `sensors` and forgets its subscription. Safe to call when not attached.
546
+ export function detachSensors(sensors) {
547
+ const unsubscribe = _subscriptions.get(sensors);
548
+ if (unsubscribe !== undefined) {
549
+ unsubscribe();
550
+ _subscriptions.delete(sensors);
551
+ }
552
+ }
553
+ // Releases `sensors` for garbage collection by detaching its backend subscriptions. The signals
554
+ // remain plain GC-managed memory afterward.
555
+ export function disposeSensors(sensors) {
556
+ detachSensors(sensors);
557
+ }
558
+ // The active sensors backend, or a lazily-created web default. There is always a backend.
559
+ export function getSensorsBackend() {
560
+ if (_backend === null)
561
+ _backend = createWebSensorsBackend();
562
+ return _backend;
563
+ }
564
+ // Queries the current permission state for the given sensor without triggering a permission prompt.
565
+ // Returns 'unsupported' when the device has no such sensor.
566
+ export function getSensorsPermissionState(sensor) {
567
+ return getSensorsBackend().getPermissionState(sensor);
568
+ }
569
+ // True if the accelerometer (including gravity) is available on this device.
570
+ export function hasAccelerometer() {
571
+ return getSensorsBackend().isMotionSupported();
572
+ }
573
+ // True if ambient light sensing is available on this device/platform.
574
+ export function hasAmbientLightSensor() {
575
+ return getSensorsBackend().isAmbientLightSupported();
576
+ }
577
+ // True if barometric pressure sensing is available.
578
+ export function hasBarometer() {
579
+ return getSensorsBackend().isBarometerSupported();
580
+ }
581
+ // True if the gravity vector sensor (or derivation) is available on this device.
582
+ export function hasGravitySensor() {
583
+ return getSensorsBackend().isGravitySupported();
584
+ }
585
+ // True if the gyroscope (rotation rate) sensor is available.
586
+ export function hasGyroscope() {
587
+ return getSensorsBackend().isGyroscopeSupported();
588
+ }
589
+ // True if the linear acceleration (gravity-removed) sensor is available.
590
+ export function hasLinearAccelerationSensor() {
591
+ return getSensorsBackend().isLinearAccelerationSupported();
592
+ }
593
+ // True if the magnetometer sensor is available.
594
+ export function hasMagnetometer() {
595
+ return getSensorsBackend().isMagnetometerSupported();
596
+ }
597
+ // True if the device orientation sensor is available.
598
+ export function hasOrientationSensor() {
599
+ return getSensorsBackend().isOrientationSupported();
600
+ }
601
+ // True if a proximity sensor is available.
602
+ export function hasProximitySensor() {
603
+ return getSensorsBackend().isProximitySupported();
604
+ }
605
+ // True if any motion sensors (accelerometer or gyroscope) are available on this device.
606
+ export function isSensorsSupported() {
607
+ return getSensorsBackend().isMotionSupported();
608
+ }
609
+ // Requests sensor permission where the host gates it (iOS); resolves true when granted or ungated.
610
+ export function requestSensorsPermission() {
611
+ return getSensorsBackend().requestPermission();
612
+ }
613
+ // Installs a native host sensors backend; pass null to fall back to the web default.
614
+ export function setSensorsBackend(backend) {
615
+ _backend = backend;
616
+ }
617
+ let _backend = null;
618
+ const _absoluteOrientation = createOrientationReading();
619
+ const _ambientLight = createAmbientLightReading();
620
+ const _gravity = createMotionReading();
621
+ const _linearAcceleration = createMotionReading();
622
+ const _magnetometer = createMotionReading();
623
+ const _motionAcceleration = createMotionReading();
624
+ const _motionRotationRate = createRotationRateReading();
625
+ const _orientation = createOrientationReading();
626
+ const _quaternionReading = createQuaternionReading();
627
+ const _subscriptions = new WeakMap();
628
+ // The Generic Sensor API Magnetometer constructor where the host exposes it, or null.
629
+ function getWebMagnetometerConstructor() {
630
+ if (typeof Magnetometer === 'undefined')
631
+ return null;
632
+ return Magnetometer;
633
+ }
634
+ // A named Generic Sensor API constructor by class name, or null when unavailable.
635
+ function getWebGenericSensorConstructor(name) {
636
+ try {
637
+ const ctor = globalThis[name];
638
+ if (typeof ctor !== 'function')
639
+ return null;
640
+ return ctor;
641
+ }
642
+ catch {
643
+ return null;
644
+ }
645
+ }
646
+ async function getWebSensorsPermissionState(sensor) {
647
+ if (typeof window === 'undefined')
648
+ return 'unsupported';
649
+ // Map our sensor names to W3C Permissions API names.
650
+ const permissionName = sensor === 'magnetometer' ? 'magnetometer' : sensor === 'orientation' ? 'gyroscope' : 'accelerometer';
651
+ if (typeof navigator !== 'undefined' && navigator.permissions) {
652
+ try {
653
+ const status = await navigator.permissions.query({ name: permissionName });
654
+ if (status.state === 'granted')
655
+ return 'granted';
656
+ if (status.state === 'denied')
657
+ return 'denied';
658
+ return 'prompt';
659
+ }
660
+ catch {
661
+ // Permissions API not available or permission name not recognized.
662
+ }
663
+ }
664
+ // iOS / browsers without Permissions API: check if DeviceMotionEvent requires requestPermission.
665
+ if (sensor !== 'magnetometer' && sensor !== 'orientation') {
666
+ const hasMotion = typeof DeviceMotionEvent !== 'undefined';
667
+ if (!hasMotion)
668
+ return 'unsupported';
669
+ }
670
+ // Cannot determine state without prompting; assume 'granted' for ungated platforms.
671
+ return 'granted';
672
+ }
673
+ function getWebMotionPermissionRequest() {
674
+ if (typeof DeviceMotionEvent === 'undefined')
675
+ return null;
676
+ const ctor = DeviceMotionEvent;
677
+ if (typeof ctor.requestPermission !== 'function')
678
+ return null;
679
+ return () => ctor.requestPermission();
680
+ }
681
+ //# sourceMappingURL=sensors.js.map