gps-plus-slam-app-framework 1.0.7 → 1.2.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 (189) hide show
  1. package/README.md +69 -15
  2. package/dist/{app-selectors-BeU0_ab8.d.ts → app-selectors-CcZ5UV-8.d.ts} +1 -1
  3. package/dist/ar/bresenham3d.d.ts +2 -0
  4. package/dist/ar/bresenham3d.js +81 -0
  5. package/dist/ar/camera-blit-capture.d.ts +1 -1
  6. package/dist/ar/camera-blit-capture.js +30 -2
  7. package/dist/ar/capability-checker.d.ts +2 -0
  8. package/dist/ar/capability-checker.js +25 -0
  9. package/dist/ar/capture-failure-tracker.d.ts +1 -1
  10. package/dist/ar/chromium-camera-access-workaround.d.ts +2 -2
  11. package/dist/ar/chromium-camera-access-workaround.js +197 -8
  12. package/dist/ar/depth-sampler.d.ts +3 -3
  13. package/dist/ar/depth-sampler.js +63 -8
  14. package/dist/ar/depth-unprojection.d.ts +2 -0
  15. package/dist/ar/depth-unprojection.js +94 -0
  16. package/dist/ar/enable-gps-ar.d.ts +2 -0
  17. package/dist/ar/enable-gps-ar.js +137 -0
  18. package/dist/ar/frame-loop.d.ts +1 -1
  19. package/dist/ar/frame-loop.js +24 -1
  20. package/dist/ar/image-capture.d.ts +1 -1
  21. package/dist/ar/image-capture.js +2 -1
  22. package/dist/ar/index.d.ts +19 -12
  23. package/dist/ar/index.js +11 -4
  24. package/dist/ar/occupancy-grid.d.ts +2 -0
  25. package/dist/ar/occupancy-grid.js +164 -0
  26. package/dist/ar/replay-scene.d.ts +1 -1
  27. package/dist/ar/replay-scene.js +1 -1
  28. package/dist/ar/scene-node-names.d.ts +1 -1
  29. package/dist/ar/webxr-nue-basis.d.ts +2 -0
  30. package/dist/ar/webxr-nue-basis.js +54 -0
  31. package/dist/ar/webxr-session.d.ts +4 -4
  32. package/dist/ar/webxr-session.js +1 -984
  33. package/dist/ar/xr-camera-texture.d.ts +1 -1
  34. package/dist/ar/xr-error-handler.d.ts +1 -1
  35. package/dist/ar/xr-frame-loop.d.ts +2 -0
  36. package/dist/ar/xr-frame-loop.js +69 -0
  37. package/dist/{ar-types-Ck8LEq8k.d.ts → ar-types-Dan1MVTi.d.ts} +28 -5
  38. package/dist/ar-world-group-alignment-ColHB_5A.d.ts +43 -0
  39. package/dist/bresenham3d-CFfd7n6B.d.ts +59 -0
  40. package/dist/{camera-blit-capture-C1V9I1Z6.d.ts → camera-blit-capture-D5_mNhla.d.ts} +20 -0
  41. package/dist/capability-checker-CQXJHxXg.d.ts +43 -0
  42. package/dist/chromium-camera-access-workaround-Jqbn0apH.d.ts +137 -0
  43. package/dist/core/index.d.ts +1 -1
  44. package/dist/{create-slam-app-store-CNsZ3rcX.d.ts → create-slam-app-store-qNXQ-tq5.d.ts} +5 -5
  45. package/dist/depth-sampler-Bhanfwla.d.ts +169 -0
  46. package/dist/depth-unprojection-Y6i_mJuT.d.ts +52 -0
  47. package/dist/enable-gps-ar-CapmADVN.d.ts +95 -0
  48. package/dist/{file-system-DTApEv_Q.d.ts → file-system-C_eqiEA8.d.ts} +1 -1
  49. package/dist/frame-conversions-BfsafGWR.d.ts +54 -0
  50. package/dist/{frame-loop-QJIxLin0.d.ts → frame-loop-CMewS8cY.d.ts} +5 -0
  51. package/dist/{fused-path-CB4DwWOG.d.ts → fused-path-DJoVu98b.d.ts} +1 -1
  52. package/dist/geo/h3-proximity.d.ts +1 -1
  53. package/dist/geo/index.d.ts +1 -1
  54. package/dist/{gps-anchor-CnWY61XK.d.ts → gps-anchor-dYeCvvp5.d.ts} +12 -1
  55. package/dist/{gps-ar-pose-sampler-DsM1Kb2N.d.ts → gps-ar-pose-sampler-DVb96iOy.d.ts} +1 -1
  56. package/dist/{gps-event-coordinator-DBx9I1YX.d.ts → gps-event-coordinator-BDIQde2x.d.ts} +3 -3
  57. package/dist/hit-test-reticle-BEzEybxZ.d.ts +45 -0
  58. package/dist/{image-capture-DZLxHDN9.d.ts → image-capture-DZ4HFRgS.d.ts} +15 -2
  59. package/dist/index-KJq2amHF.d.ts +37 -0
  60. package/dist/index.d.ts +67 -58
  61. package/dist/index.js +18 -8
  62. package/dist/{leaflet-map-overlay-Cew7XAoO.d.ts → leaflet-map-overlay-6CJC-E1l.d.ts} +1 -1
  63. package/dist/{map-data-sih4g64v.d.ts → map-data-CkloBxIl.d.ts} +1 -1
  64. package/dist/{map-overlay-draw-B3R1c9Xp.d.ts → map-overlay-draw-krOKwDAq.d.ts} +1 -1
  65. package/dist/{null-storage-backend-BKOS7JpL.d.ts → null-storage-backend-CxEjZHRW.d.ts} +2 -2
  66. package/dist/occupancy-grid-BRNRaPyT.d.ts +72 -0
  67. package/dist/{opfs-storage-backend-Cj_7PWoq.d.ts → opfs-storage-backend-CjSAuqg_.d.ts} +2 -2
  68. package/dist/{persistence-middleware-BkHIqvaA.d.ts → persistence-middleware-DwyzgfWr.d.ts} +1 -1
  69. package/dist/{recording-options-Dq6IsKAv.d.ts → recording-options-DJBXuMxV.d.ts} +22 -6
  70. package/dist/{recording-replayer-CmCIIZ02.d.ts → recording-replayer-BL-x-7k0.d.ts} +2 -2
  71. package/dist/{recording-slice-d6OtCrza.d.ts → recording-slice-89O07ebs.d.ts} +10 -2
  72. package/dist/{replay-engine-BGBRQyXs.d.ts → replay-engine-BRqszuQ9.d.ts} +1 -1
  73. package/dist/{replay-scene-DpAGcAKY.d.ts → replay-scene-Bw58VL6p.d.ts} +3 -3
  74. package/dist/sensors/gps-error-handler.d.ts +1 -1
  75. package/dist/sensors/gps.d.ts +1 -1
  76. package/dist/sensors/index.d.ts +3 -3
  77. package/dist/sensors/index.js +1 -1
  78. package/dist/sensors/permission-checker.d.ts +1 -1
  79. package/dist/session-disposers-M-oashRH.js +57 -0
  80. package/dist/state/app-selectors.d.ts +1 -1
  81. package/dist/state/combined-root-state.d.ts +1 -1
  82. package/dist/state/create-slam-app-store.d.ts +1 -1
  83. package/dist/state/gps-ar-pose-sampler.d.ts +3 -3
  84. package/dist/state/gps-event-coordinator.d.ts +1 -1
  85. package/dist/state/index.d.ts +17 -17
  86. package/dist/state/index.js +2 -2
  87. package/dist/state/persistence-middleware.d.ts +1 -1
  88. package/dist/state/persistence-middleware.js +9 -3
  89. package/dist/state/recording-options.d.ts +1 -1
  90. package/dist/state/recording-options.js +5 -3
  91. package/dist/state/recording-replayer.d.ts +2 -2
  92. package/dist/state/recording-slice.d.ts +1 -1
  93. package/dist/state/recording-slice.js +11 -2
  94. package/dist/state/replay-engine.d.ts +1 -1
  95. package/dist/state/store-subscribers.d.ts +2 -2
  96. package/dist/state/subscribe-to-selector.d.ts +1 -1
  97. package/dist/state/tracking-quality.d.ts +1 -1
  98. package/dist/state/tracking-quality.js +1 -1
  99. package/dist/state/tracking-slice.d.ts +1 -1
  100. package/dist/state-BkIfpiyA.js +87 -0
  101. package/dist/storage/file-system-utils.d.ts +1 -1
  102. package/dist/storage/file-system.d.ts +1 -1
  103. package/dist/storage/index.d.ts +8 -8
  104. package/dist/storage/null-storage-backend.d.ts +1 -1
  105. package/dist/storage/opfs-storage-backend.d.ts +1 -1
  106. package/dist/storage/opfs-storage.d.ts +1 -1
  107. package/dist/storage/storage-backend.d.ts +1 -1
  108. package/dist/storage/zip-export.d.ts +1 -1
  109. package/dist/storage/zip-reader.d.ts +1 -1
  110. package/dist/{storage-backend-CT0Il_AB.d.ts → storage-backend-C9HLFtgz.d.ts} +1 -1
  111. package/dist/{store-subscribers-Dce_CoJF.d.ts → store-subscribers-rrkLkFTt.d.ts} +2 -2
  112. package/dist/{subscribe-to-selector-DIHm3BRZ.d.ts → subscribe-to-selector-CP3BKSbF.d.ts} +1 -1
  113. package/dist/test-utils/browser-mocks.d.ts +1 -1
  114. package/dist/test-utils/zip-round-trip-helpers.d.ts +1 -1
  115. package/dist/{tracking-slice-utCNND8t.d.ts → tracking-slice-CdK8c-8P.d.ts} +1 -1
  116. package/dist/types/ar-types.d.ts +2 -2
  117. package/dist/types/geo-types.d.ts +1 -1
  118. package/dist/types/index.d.ts +3 -3
  119. package/dist/utils/concurrency.d.ts +1 -1
  120. package/dist/utils/failure-tracker.d.ts +1 -1
  121. package/dist/utils/format-file-size.d.ts +1 -1
  122. package/dist/utils/fused-path.d.ts +1 -1
  123. package/dist/utils/index.d.ts +6 -6
  124. package/dist/utils/list-formatter.d.ts +1 -1
  125. package/dist/utils/logger.d.ts +1 -1
  126. package/dist/visualization/accuracy-circles.d.ts +1 -1
  127. package/dist/visualization/alignment-lerper.d.ts +1 -1
  128. package/dist/visualization/ar-world-group-alignment.d.ts +2 -0
  129. package/dist/visualization/ar-world-group-alignment.js +51 -0
  130. package/dist/visualization/camera-follower.d.ts +1 -1
  131. package/dist/visualization/css3d-renderer-manager.d.ts +1 -1
  132. package/dist/visualization/frame-conversions.d.ts +2 -0
  133. package/dist/visualization/frame-conversions.js +88 -0
  134. package/dist/visualization/frustum-visibility.d.ts +1 -1
  135. package/dist/visualization/gps-anchor.d.ts +1 -1
  136. package/dist/visualization/gps-anchor.js +216 -1
  137. package/dist/visualization/gps-compass-cubes.d.ts +1 -1
  138. package/dist/visualization/gps-event-markers.d.ts +1 -1
  139. package/dist/visualization/gps-event-markers.js +1 -1
  140. package/dist/visualization/hit-test-reticle.d.ts +2 -0
  141. package/dist/visualization/hit-test-reticle.js +81 -0
  142. package/dist/visualization/index.d.ts +19 -17
  143. package/dist/visualization/index.js +5 -2
  144. package/dist/visualization/leaflet-map-overlay.d.ts +1 -1
  145. package/dist/visualization/lerp-utils.d.ts +1 -1
  146. package/dist/visualization/map-data.d.ts +1 -1
  147. package/dist/visualization/map-overlay-draw.d.ts +1 -1
  148. package/dist/visualization/map-overlay.d.ts +1 -1
  149. package/dist/visualization/three-dispose.d.ts +1 -1
  150. package/dist/visualization/vis-colors.d.ts +1 -1
  151. package/dist/webxr-nue-basis-CqKQmZzT.d.ts +23 -0
  152. package/dist/{webxr-session-DB5cIZnN.d.ts → webxr-session-DtAG9srK.d.ts} +45 -13
  153. package/dist/webxr-session-QhcSSub5.js +1041 -0
  154. package/dist/xr-frame-loop-BtYytZJg.d.ts +70 -0
  155. package/package.json +1 -1
  156. package/dist/chromium-camera-access-workaround-NvChXsi8.d.ts +0 -61
  157. package/dist/depth-sampler-xg8vna6c.d.ts +0 -108
  158. package/dist/gps-anchor-DjC_3MoT.js +0 -257
  159. package/dist/index-pOjUCKEP.d.ts +0 -24
  160. /package/dist/{accuracy-circles-CsxE_7x1.d.ts → accuracy-circles-BpwWAs8G.d.ts} +0 -0
  161. /package/dist/{alignment-lerper-L1RbWHQ4.d.ts → alignment-lerper-Dm9G7JJO.d.ts} +0 -0
  162. /package/dist/{camera-follower-1k8mnw5J.d.ts → camera-follower-C2mwEn7c.d.ts} +0 -0
  163. /package/dist/{capture-failure-tracker-JKb1ST8E.d.ts → capture-failure-tracker-CUeU3L_M.d.ts} +0 -0
  164. /package/dist/{concurrency-CMw8pSaG.d.ts → concurrency-CO1FQvs3.d.ts} +0 -0
  165. /package/dist/{css3d-renderer-manager-CkjCb2HW.d.ts → css3d-renderer-manager-ptu_t3B3.d.ts} +0 -0
  166. /package/dist/{failure-tracker-DnHXOIRa.d.ts → failure-tracker-Cn8Fo9D4.d.ts} +0 -0
  167. /package/dist/{file-system-utils-CtKiiw27.d.ts → file-system-utils-DyAmcuvX.d.ts} +0 -0
  168. /package/dist/{format-file-size-BTS5H62-.d.ts → format-file-size-CLCZz3lC.d.ts} +0 -0
  169. /package/dist/{frustum-visibility-BHnFrSKQ.d.ts → frustum-visibility-CmI96ESS.d.ts} +0 -0
  170. /package/dist/{geo-types-CTuF23gG.d.ts → geo-types-SaPnIEUM.d.ts} +0 -0
  171. /package/dist/{gps-DXoQb9Kf.d.ts → gps-VuezJWK3.d.ts} +0 -0
  172. /package/dist/{gps-compass-cubes-CsLtFv4B.d.ts → gps-compass-cubes-BM1uGiYV.d.ts} +0 -0
  173. /package/dist/{gps-error-handler-Dg3ziRVY.d.ts → gps-error-handler-CJl_2MVm.d.ts} +0 -0
  174. /package/dist/{gps-event-markers-BGr-iLvi.d.ts → gps-event-markers-Dc9FT-q1.d.ts} +0 -0
  175. /package/dist/{h3-proximity-BhfgNbdW.d.ts → h3-proximity-I1msGPgE.d.ts} +0 -0
  176. /package/dist/{index-gzWJMVpo.d.ts → index-CF05QV2m.d.ts} +0 -0
  177. /package/dist/{lerp-utils-B8MM8zCR.d.ts → lerp-utils-BpV-0Dtk.d.ts} +0 -0
  178. /package/dist/{list-formatter-CNqi-E2j.d.ts → list-formatter-RRcBe8Vg.d.ts} +0 -0
  179. /package/dist/{logger-C9vugiQb.d.ts → logger-BEHXsnnq.d.ts} +0 -0
  180. /package/dist/{map-overlay-DAmreyA3.d.ts → map-overlay-bcogY5fK.d.ts} +0 -0
  181. /package/dist/{opfs-storage-DOPkzgeT.d.ts → opfs-storage-DYm8WHq7.d.ts} +0 -0
  182. /package/dist/{permission-checker-DPJl_RU6.d.ts → permission-checker-GpAiSCwd.d.ts} +0 -0
  183. /package/dist/{scene-node-names-6LBjPmwz.d.ts → scene-node-names-Bu0S66H1.d.ts} +0 -0
  184. /package/dist/{three-dispose-C2drkp6f.d.ts → three-dispose-D950t71B.d.ts} +0 -0
  185. /package/dist/{vis-colors-CHQhjal3.d.ts → vis-colors-D6omC64K.d.ts} +0 -0
  186. /package/dist/{xr-camera-texture-BU5YvKAM.d.ts → xr-camera-texture-DBwCaKTd.d.ts} +0 -0
  187. /package/dist/{xr-error-handler-CDY5QxGb.d.ts → xr-error-handler-C0AQRmxs.d.ts} +0 -0
  188. /package/dist/{zip-export-QO4iLIi7.d.ts → zip-export-D_5HAvFz.d.ts} +0 -0
  189. /package/dist/{zip-reader-D1105idz.d.ts → zip-reader-DSEXt7K6.d.ts} +0 -0
@@ -1,985 +1,2 @@
1
- import { createLogger } from "../utils/logger.js";
2
- import { CameraBlitCapture, computeCaptureSize } from "./camera-blit-capture.js";
3
- import { getLastDeviceOrientation } from "../state/gps-event-coordinator.js";
4
- import { DepthSampler } from "./depth-sampler.js";
5
- import { clearFrameUpdates, runFrameUpdates } from "./frame-loop.js";
6
- import { DEFAULT_CAPTURE_CONFIG, ImageCaptureManager } from "./image-capture.js";
7
- import { clearLastRestartedPayload, originReset, poseLost, poseReceived, resetTracking, selectLastRestartedPayload, selectTrackingPhase } from "../state/tracking-slice.js";
8
- import { acquireCameraTexture } from "./xr-camera-texture.js";
9
- import { DEFAULT_RECORDING_OPTIONS, validateArCrashIsolationOptions } from "../state/recording-options.js";
10
- import { SCENE_NODE } from "./scene-node-names.js";
11
- import { createCss3dRendererManager } from "../visualization/css3d-renderer-manager.js";
12
- import * as THREE from "three";
13
- import { nueQuaternionToWebXR as nueQuaternionToWebXR$1, nueToWebXR } from "gps-plus-slam-js";
14
- //#region ../src/ar/webxr-session.ts
15
- /**
16
- * WebXR Session Module
17
- *
18
- * Handles WebXR AR session initialization and frame loop.
19
- *
20
- * ARCHITECTURE NOTE: See docs/architecture-ar-gps-pose-separation.md
21
- *
22
- * Scene Hierarchy:
23
- * scene (GPS world frame — NUE: X=North, Y=Up, Z=East)
24
- * └── arWorldGroup (local space = NUE; receives alignment matrix)
25
- * └── basisChangeNode ('webxr-to-nue', constant WEBXR_TO_NUE matrix)
26
- * └── arpose (local space = WebXR)
27
- * └── camera (raw AR pose from WebXR)
28
- *
29
- * The arWorldGroup is transformed by the GpsPlusSlamJs alignment matrix.
30
- * The camera's LOCAL transform is the raw AR pose from WebXR.
31
- * The camera's WORLD transform is the GPS-aligned world pose.
32
- */
33
- const log = createLogger("WebXR");
34
- function isXRCameraLike(value) {
35
- if (typeof value !== "object" || value === null) return false;
36
- const camera = value;
37
- return typeof camera.width === "number" && Number.isFinite(camera.width) && camera.width > 0 && typeof camera.height === "number" && Number.isFinite(camera.height) && camera.height > 0;
38
- }
39
- /**
40
- * Extract a validated XRCameraLike from an XRViewerPose.
41
- *
42
- * Returns null in every case where the per-frame texture acquisition must be
43
- * skipped — i.e. whenever the caller should treat any previously cached
44
- * texture as stale and clear it. Centralising the preconditions here makes
45
- * it impossible to accidentally fall through without clearing the cache.
46
- *
47
- * Cases that return null:
48
- * - pose is null (tracking lost)
49
- * - pose has no views
50
- * - the first view has no `camera` property (camera-access not granted)
51
- * - the camera property is not a valid XRCameraLike (zero/NaN dimensions)
52
- *
53
- * @see docs/2026-02-06-bug-camera-frames-black.md
54
- */
55
- function getXrCameraFromPose(pose) {
56
- if (!pose) return null;
57
- const view = pose.views[0];
58
- if (!view) return null;
59
- const candidate = view.camera;
60
- return isXRCameraLike(candidate) ? candidate : null;
61
- }
62
- /**
63
- * Decide whether the one-time camera-access grant diagnostic should fire
64
- * on the current XR frame.
65
- *
66
- * The diagnostic is only meaningful when we actually have a valid pose:
67
- * if `pose` is null (e.g. session starts with tracking lost), then the
68
- * derived `xrCamera` is null regardless of permission state, and logging
69
- * "NOT GRANTED" would be a false negative that permanently suppresses the
70
- * correct status because `cameraAccessLoggedOnce` would latch to true.
71
- *
72
- * Additional conditions:
73
- * - `alreadyLogged` prevents spamming the log every frame.
74
- * - `captureActive` ensures we only care when an image capture session
75
- * is running (otherwise the diagnostic has no audience).
76
- *
77
- * This predicate is extracted as a pure function so the guard against the
78
- * "first frame has no pose" false-negative regression is directly testable.
79
- */
80
- function shouldLogCameraAccessDiagnostic(pose, alreadyLogged, captureActive) {
81
- return pose !== null && !alreadyLogged && captureActive;
82
- }
83
- let renderer = null;
84
- let scene = null;
85
- let camera = null;
86
- let xrSession = null;
87
- /**
88
- * Monotonic time of the previous XR frame, in milliseconds (XR `time`
89
- * argument). Reset to 0 by `resetWebXRState()` so the first frame of a
90
- * new session sees `dt = 0` rather than a stale delta from the prior
91
- * session.
92
- */
93
- let lastFrameTime = 0;
94
- /**
95
- * Reset WebXR module state - exported for testing only.
96
- * @internal
97
- */
98
- function resetWebXRState() {
99
- if (renderer) {
100
- renderer.setAnimationLoop(null);
101
- if (renderer.domElement.parentElement) renderer.domElement.parentElement.removeChild(renderer.domElement);
102
- renderer.dispose();
103
- }
104
- renderer = null;
105
- scene = null;
106
- camera = null;
107
- xrSession = null;
108
- arWorldGroup = null;
109
- arPoseNode = null;
110
- latestArPose = null;
111
- lastFrameTime = 0;
112
- clearFrameUpdates();
113
- imageCaptureManager = null;
114
- onImageCaptured = null;
115
- getScreenRotation = null;
116
- onCaptureFailed = null;
117
- onSuspiciousImage = null;
118
- if (trackingPhaseUnsubscribe) {
119
- trackingPhaseUnsubscribe();
120
- trackingPhaseUnsubscribe = null;
121
- }
122
- trackingStore = null;
123
- onTrackingRestarted = null;
124
- onTrackingLost = null;
125
- onTrackingRecovered = null;
126
- depthSampler = null;
127
- onDepthCaptured = null;
128
- onDepthUnavailable = null;
129
- onFrameCallback = null;
130
- if (css3dManager) {
131
- css3dManager.dispose();
132
- css3dManager = null;
133
- }
134
- cameraAccessLoggedOnce = false;
135
- getCameraTextureNullCount = 0;
136
- latestCameraWidth = 0;
137
- latestCameraHeight = 0;
138
- currentArCrashIsolationOptions = { ...DEFAULT_RECORDING_OPTIONS.arCrashIsolation };
139
- cleanupBlitResources();
140
- }
141
- /**
142
- * The AR world group - parent of camera and all AR-tracked content.
143
- * This group's transform = the alignment matrix from GpsPlusSlamJs.
144
- * When the library computes a new alignment, apply it to this group.
145
- */
146
- let arWorldGroup = null;
147
- /**
148
- * The arpose Object3D — intermediate node between arWorldGroup and camera.
149
- * During recording it stays at identity; during replay it receives recorded
150
- * odomPosition/odomRotation from the store subscriber.
151
- */
152
- let arPoseNode = null;
153
- /**
154
- * Stores the latest raw AR pose from WebXR (updated every frame).
155
- * This is read by the GPS callback to create paired GPS+AR events.
156
- */
157
- let latestArPose = null;
158
- /**
159
- * Image capture manager instance (created when AR session starts)
160
- */
161
- let imageCaptureManager = null;
162
- /**
163
- * Callback for when an image is captured (set via setImageCaptureCallback)
164
- */
165
- let onImageCaptured = null;
166
- /**
167
- * Callback for when image capture fails (set via setImageCaptureCallback)
168
- */
169
- let onCaptureFailed = null;
170
- /**
171
- * Callback for when a captured image appears suspicious/black
172
- * (set via setImageCaptureCallback)
173
- */
174
- let onSuspiciousImage = null;
175
- /**
176
- * Screen rotation getter (set via setImageCaptureCallback)
177
- */
178
- let getScreenRotation = null;
179
- /**
180
- * Redux store injected by the host (`setTrackingStore`). When present
181
- * together with `onTrackingRestarted`, `onXRFrame` dispatches
182
- * `poseReceived`/`poseLost`, the XR reference-space reset listener
183
- * dispatches `originReset`, and a `subscribeToSelector` translation
184
- * surface translates phase transitions back into the existing
185
- * `onTrackingLost` / `onTrackingRestarted` / `onTrackingRecovered`
186
- * callbacks. When the store is absent the tracking pipeline simply
187
- * no-ops.
188
- */
189
- let trackingStore = null;
190
- /**
191
- * Unsubscribe handle returned by the phase subscription set up in
192
- * `initAR`. Cleared in `resetWebXRState` and on session-end so we never
193
- * leave dangling listeners on a stale store.
194
- */
195
- let trackingPhaseUnsubscribe = null;
196
- /**
197
- * Callback for when tracking restarts (set via setTrackingCallbacks)
198
- */
199
- let onTrackingRestarted = null;
200
- /**
201
- * Callback for when tracking is lost (set via setTrackingLostCallback)
202
- * Field Test Readiness Issue #3: Provide user feedback when tracking is lost
203
- */
204
- let onTrackingLost = null;
205
- /**
206
- * Callback for when tracking recovers seamlessly without origin reset (Case 1).
207
- * Set via setTrackingRecoveredCallback.
208
- */
209
- let onTrackingRecovered = null;
210
- /**
211
- * Depth sampler instance (created when AR session starts with depth callbacks)
212
- */
213
- let depthSampler = null;
214
- /**
215
- * Callback for when a depth sample is captured (set via setDepthCaptureCallback)
216
- */
217
- let onDepthCaptured = null;
218
- /**
219
- * Callback for when depth sensing is determined to be unavailable.
220
- * Field Test Readiness Issue #8: Notify user if depth was requested but not granted.
221
- */
222
- let onDepthUnavailable = null;
223
- /**
224
- * Per-frame callback for custom updates (e.g., map overlay position).
225
- * Called every XR frame after pose updates but before render.
226
- */
227
- let onFrameCallback = null;
228
- /**
229
- * CSS3D renderer manager for rendering DOM-based 3D objects (e.g., Leaflet map)
230
- * alongside the WebGL render. Created in initAR(), disposed in resetWebXRState().
231
- */
232
- let css3dManager = null;
233
- /**
234
- * Camera blit capture instance for reading WebXR opaque camera textures.
235
- * Created when image capture starts, disposed when it stops.
236
- * @see docs/2026-02-06-bug-camera-frames-black.md
237
- */
238
- let blitCapture = null;
239
- /**
240
- * Latest WebXR camera texture, updated each frame when camera-access is enabled.
241
- * Acquired via Three.js's renderer.xr.getCameraTexture() API (ExternalTexture).
242
- * @see xr-camera-texture.ts
243
- */
244
- let latestCameraTexture = null;
245
- /**
246
- * Latest camera frame dimensions from XRCamera (native resolution).
247
- * Used to dynamically resize the blit render target for full-quality captures.
248
- */
249
- let latestCameraWidth = 0;
250
- let latestCameraHeight = 0;
251
- /**
252
- * Track whether camera-access diagnostic status has been logged.
253
- * Reset on each new session via resetWebXRState().
254
- */
255
- let cameraAccessLoggedOnce = false;
256
- /**
257
- * Counter for throttled getCameraTexture diagnostic logging.
258
- * Only logs the first few null returns to avoid log spam.
259
- */
260
- let getCameraTextureNullCount = 0;
261
- const GET_CAMERA_TEXTURE_LOG_THRESHOLD = 5;
262
- let currentArCrashIsolationOptions = { ...DEFAULT_RECORDING_OPTIONS.arCrashIsolation };
263
- /**
264
- * Dispose blit capture resources and clear the cached camera texture.
265
- * Shared by resetWebXRState() and stopImageCapture() to avoid duplication.
266
- */
267
- function cleanupBlitResources() {
268
- if (blitCapture) {
269
- blitCapture.dispose();
270
- blitCapture = null;
271
- }
272
- latestCameraTexture = null;
273
- }
274
- /**
275
- * Extract the reset transform from an XRReferenceSpaceEvent-like object.
276
- *
277
- * Distinguishes three cases per OdometryTrackingRestartedPayload semantics:
278
- * - Transform property missing (older browsers) → returns `undefined`
279
- * - Transform property present but null (runtime can't determine delta) → returns `null`
280
- * - Transform property present with data → returns `ResetTransformData`
281
- *
282
- * This is a pure function extracted for testability.
283
- *
284
- * @param event - The event object, cast to a record with an optional transform property
285
- * @returns ResetTransformData, null, or undefined
286
- */
287
- function extractResetTransformData(event) {
288
- if (!("transform" in event)) return;
289
- const transform = event.transform;
290
- if (!transform) return null;
291
- const pos = transform.position;
292
- const ori = transform.orientation;
293
- return {
294
- position: [
295
- pos.x,
296
- pos.y,
297
- pos.z
298
- ],
299
- orientation: [
300
- ori.x,
301
- ori.y,
302
- ori.z,
303
- ori.w
304
- ]
305
- };
306
- }
307
- /**
308
- * Extract pose data from an XRViewerPose.
309
- * Returns null if pose or views are unavailable.
310
- *
311
- * This is a pure function extracted for testability.
312
- *
313
- * @param pose - The XRViewerPose from frame.getViewerPose()
314
- * @returns ARPose with position and orientation, or null
315
- */
316
- function extractPoseFromViewer(pose) {
317
- if (!pose) return null;
318
- const view = pose.views[0];
319
- if (!view) return null;
320
- const { position, orientation } = view.transform;
321
- return {
322
- position: {
323
- x: position.x,
324
- y: position.y,
325
- z: position.z
326
- },
327
- orientation: {
328
- x: orientation.x,
329
- y: orientation.y,
330
- z: orientation.z,
331
- w: orientation.w
332
- }
333
- };
334
- }
335
- /**
336
- * Get the current raw AR pose from the latest XR frame.
337
- * This is updated every frame and should be called when GPS arrives
338
- * to get the AR pose at that moment.
339
- *
340
- * IMPORTANT: This returns the RAW pose from WebXR, NOT transformed
341
- * by any alignment matrix. This is what we record to the store.
342
- *
343
- * @returns The latest AR pose, or null if no pose available yet
344
- */
345
- function getCurrentArPose() {
346
- return latestArPose;
347
- }
348
- /**
349
- * Build XR session init options.
350
- * Extracted as a pure function for testability.
351
- *
352
- * @param rootElement - The DOM element for DOM overlay
353
- * @returns XRSessionInit options
354
- * @throws Error if rootElement is null
355
- */
356
- function buildSessionOptions(rootElement, isolationOptions = {}) {
357
- if (!rootElement) throw new Error("App root element not found");
358
- const normalizedOptions = validateArCrashIsolationOptions(isolationOptions);
359
- const optionalFeatures = [];
360
- const sessionOptions = { requiredFeatures: ["local-floor"] };
361
- if (normalizedOptions.enableDomOverlay) {
362
- optionalFeatures.push("dom-overlay");
363
- sessionOptions.domOverlay = { root: rootElement };
364
- }
365
- if (normalizedOptions.enableDepthSensingFeature) {
366
- optionalFeatures.push("depth-sensing");
367
- Object.assign(sessionOptions, { depthSensing: {
368
- usagePreference: ["cpu-optimized"],
369
- dataFormatPreference: ["luminance-alpha", "float32"]
370
- } });
371
- }
372
- if (normalizedOptions.enableCameraAccess) optionalFeatures.push("camera-access");
373
- if (optionalFeatures.length > 0) sessionOptions.optionalFeatures = optionalFeatures;
374
- return sessionOptions;
375
- }
376
- /**
377
- * Check if WebXR immersive-ar is supported
378
- */
379
- async function isWebXRSupported() {
380
- if (!navigator.xr) return false;
381
- try {
382
- return await navigator.xr.isSessionSupported("immersive-ar");
383
- } catch {
384
- return false;
385
- }
386
- }
387
- /**
388
- * Create the scene hierarchy with proper AR/GPS frame separation.
389
- * This is a pure function for testability.
390
- *
391
- * Hierarchy:
392
- * scene (GPS world frame — NUE: X=North, Y=Up, Z=East)
393
- * ├── ambientLight
394
- * ├── directionalLight
395
- * └── arWorldGroup (local space = NUE; receives alignment matrix)
396
- * └── basisChangeNode ('webxr-to-nue', constant WEBXR_TO_NUE matrix)
397
- * └── arpose (Object3D — AR pose; local space = WebXR)
398
- * └── camera (PerspectiveCamera)
399
- *
400
- * basisChangeNode is a static scene-graph node that holds the WEBXR_TO_NUE
401
- * basis-change matrix permanently (matrixAutoUpdate=false). Moving it here
402
- * instead of composing it in applyAlignmentMatrix() keeps arWorldGroup's
403
- * local space in the **NUE axis convention** (X=North, Y=Up, Z=East), so no
404
- * WebXR↔NUE swizzle is needed for children.
405
- *
406
- * CAUTION — two NUE frames: arWorldGroup's local space is the *AR-odometry*
407
- * NUE frame, i.e. the **domain** of the alignment matrix, NOT the GPS-world
408
- * NUE frame of the scene root. Only content authored in AR-odometry
409
- * coordinates (e.g. the camera subtree) may be placed with raw local values.
410
- * GPS-world content (a lat/lon → NUE point) is expressed in the scene-root
411
- * frame and must be pre-multiplied by alignment⁻¹ before being used as a
412
- * local position under arWorldGroup — see createGpsAnchor and the
413
- * alignment-frame bug doc
414
- * (GpsPlusSlamJs_Docs/docs/2026-05-31-gps-anchor-alignment-frame-bug.md).
415
- *
416
- * - Recording: arpose stays at identity; WebXRManager writes to camera.
417
- * - Replay: arpose receives recorded odomPosition/odomRotation;
418
- * camera is owned by user controls (OrbitControls / FPS).
419
- *
420
- * @returns Object containing scene, arWorldGroup, arpose, and camera
421
- */
422
- function createSceneHierarchy() {
423
- const newScene = new THREE.Scene();
424
- const newArWorldGroup = new THREE.Group();
425
- newArWorldGroup.name = "ar-world";
426
- newScene.add(newArWorldGroup);
427
- const newBasisChangeNode = new THREE.Group();
428
- newBasisChangeNode.name = SCENE_NODE.BASIS_CHANGE;
429
- newBasisChangeNode.matrix.copy(WEBXR_TO_NUE);
430
- newBasisChangeNode.matrixAutoUpdate = false;
431
- newArWorldGroup.add(newBasisChangeNode);
432
- const newArPose = new THREE.Object3D();
433
- newArPose.name = "ar-pose";
434
- newBasisChangeNode.add(newArPose);
435
- const newCamera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, .01, 100);
436
- newArPose.add(newCamera);
437
- const ambientLight = new THREE.AmbientLight(16777215, .5);
438
- newScene.add(ambientLight);
439
- const directionalLight = new THREE.DirectionalLight(16777215, .8);
440
- directionalLight.position.set(0, 10, 5);
441
- newScene.add(directionalLight);
442
- return {
443
- scene: newScene,
444
- arWorldGroup: newArWorldGroup,
445
- arpose: newArPose,
446
- camera: newCamera
447
- };
448
- }
449
- /**
450
- * Initialize the AR session and Three.js renderer.
451
- * @param container - DOM element to host the AR canvas and CSS3D overlay.
452
- */
453
- async function initAR(container, isolationOptions = {}) {
454
- if (!navigator.xr) throw new Error("WebXR not available");
455
- if (renderer || xrSession) throw new Error("AR session already initialized — call endARSession() before initAR() again");
456
- currentArCrashIsolationOptions = validateArCrashIsolationOptions(isolationOptions);
457
- renderer = new THREE.WebGLRenderer({
458
- antialias: true,
459
- alpha: true
460
- });
461
- renderer.setPixelRatio(window.devicePixelRatio);
462
- renderer.setSize(window.innerWidth, window.innerHeight);
463
- renderer.xr.enabled = true;
464
- container.insertBefore(renderer.domElement, container.firstChild);
465
- if (currentArCrashIsolationOptions.enableCss3dRenderer) css3dManager = createCss3dRendererManager(container, window.innerWidth, window.innerHeight);
466
- const hierarchy = createSceneHierarchy();
467
- scene = hierarchy.scene;
468
- arWorldGroup = hierarchy.arWorldGroup;
469
- arPoseNode = hierarchy.arpose;
470
- camera = hierarchy.camera;
471
- const sessionOptions = buildSessionOptions(container, currentArCrashIsolationOptions);
472
- xrSession = await navigator.xr.requestSession("immersive-ar", sessionOptions);
473
- xrSession.addEventListener("end", () => {
474
- log.info("Session ended");
475
- if (trackingStore) trackingStore.dispatch(resetTracking());
476
- xrSession = null;
477
- latestArPose = null;
478
- });
479
- await renderer.xr.setSession(xrSession);
480
- if (onTrackingRestarted && trackingStore) {
481
- const store = trackingStore;
482
- store.dispatch(resetTracking());
483
- trackingPhaseUnsubscribe = subscribeToTrackingPhase(store);
484
- const referenceSpace = renderer.xr.getReferenceSpace();
485
- if (referenceSpace) referenceSpace.addEventListener("reset", (event) => {
486
- const transformData = extractResetTransformData(event);
487
- store.dispatch(originReset(transformData));
488
- log.warn("XR reference space reset detected", transformData ? "(transform available)" : "(no transform)");
489
- });
490
- }
491
- if (onDepthCaptured) depthSampler = new DepthSampler({
492
- onSampleCaptured: onDepthCaptured,
493
- getCurrentPose: getCurrentArPose,
494
- onDepthUnavailable: onDepthUnavailable ?? void 0
495
- });
496
- renderer.setAnimationLoop(onXRFrame);
497
- log.info("AR session started");
498
- }
499
- /**
500
- * Snapshot the current `DeviceOrientation` (with documented fallback
501
- * defaults) for inclusion in `poseReceived` payloads.
502
- */
503
- function snapshotDeviceOrientation() {
504
- const orientation = getLastDeviceOrientation();
505
- return {
506
- alpha: orientation?.alpha ?? 0,
507
- beta: orientation?.beta ?? 0,
508
- gamma: orientation?.gamma ?? 0,
509
- absolute: orientation?.absolute ?? false
510
- };
511
- }
512
- /**
513
- * Wire the tracking-slice → host-callbacks translation. The subscriber
514
- * runs synchronously inside each `dispatch`, so the host callbacks fire
515
- * in the same order as a direct invocation would.
516
- *
517
- * Translation rules (locked in by tracking-slice tests):
518
- * - `tracking → lost`: clear `latestArPose` (drops in-flight GPS events)
519
- * and call `onTrackingLost?.()`.
520
- * - `lost → tracking` with `lastRestartedPayload !== null` (Case 2):
521
- * call `onTrackingRestarted?.(payload)` then dispatch
522
- * `clearLastRestartedPayload` so a subsequent loss cycle starts clean.
523
- * - `lost → tracking` with payload null (Case 1: seamless recovery):
524
- * call `onTrackingRecovered?.()`.
525
- * - `initializing → tracking`: no callback (initial acquisition is not
526
- * a restart — same behaviour as the manager).
527
- */
528
- function subscribeToTrackingPhase(store) {
529
- let prev = "initializing";
530
- return store.subscribe(() => {
531
- const next = selectTrackingPhase(store.getState());
532
- if (next === prev) return;
533
- const previous = prev;
534
- prev = next;
535
- if (previous === "tracking" && next === "lost") {
536
- log.warn("Tracking lost");
537
- latestArPose = null;
538
- onTrackingLost?.();
539
- return;
540
- }
541
- if (previous === "lost" && next === "tracking") {
542
- const payload = selectLastRestartedPayload(store.getState());
543
- if (payload !== null) {
544
- log.info("Tracking restarted (origin reset)");
545
- onTrackingRestarted?.(payload);
546
- store.dispatch(clearLastRestartedPayload());
547
- } else {
548
- log.info("Tracking recovered (same coordinate frame)");
549
- onTrackingRecovered?.();
550
- }
551
- }
552
- });
553
- }
554
- /**
555
- * Dispatch the per-frame `poseReceived` / `poseLost` action into the
556
- * tracking slice. No-op when no store is bound or when tracking wiring
557
- * was not requested.
558
- */
559
- function updateTrackingState(arPose) {
560
- if (!trackingStore || !onTrackingRestarted) return;
561
- if (arPose) trackingStore.dispatch(poseReceived({
562
- pose: arPose,
563
- sensorOrientation: snapshotDeviceOrientation()
564
- }));
565
- else trackingStore.dispatch(poseLost());
566
- }
567
- /**
568
- * Called each XR frame
569
- */
570
- function onXRFrame(time, frame) {
571
- if (!renderer || !scene || !camera || !frame) return;
572
- const referenceSpace = renderer.xr.getReferenceSpace();
573
- if (!referenceSpace) return;
574
- const pose = frame.getViewerPose(referenceSpace);
575
- const arPose = extractPoseFromViewer(pose);
576
- updateTrackingState(arPose);
577
- const dt = lastFrameTime === 0 ? 0 : (time - lastFrameTime) / 1e3;
578
- const elapsed = time / 1e3;
579
- lastFrameTime = time;
580
- runFrameUpdates(dt, elapsed);
581
- if (arPose) latestArPose = arPose;
582
- latestCameraTexture = null;
583
- if (currentArCrashIsolationOptions.enableCameraTextureAcquisition) {
584
- const xrCamera = getXrCameraFromPose(pose);
585
- if (shouldLogCameraAccessDiagnostic(pose, cameraAccessLoggedOnce, imageCaptureManager !== null)) {
586
- cameraAccessLoggedOnce = true;
587
- if (xrCamera) log.info("camera-access GRANTED — XRView.camera is available for blit capture");
588
- else log.warn("camera-access NOT GRANTED — XRView.camera is undefined. Image capture will fall back to canvas.toBlob() which may produce black/empty frames in WebXR.");
589
- }
590
- if (xrCamera) {
591
- const result = acquireCameraTexture(renderer, xrCamera);
592
- if (result) {
593
- latestCameraTexture = result.texture;
594
- latestCameraWidth = result.width;
595
- latestCameraHeight = result.height;
596
- } else {
597
- getCameraTextureNullCount++;
598
- if (getCameraTextureNullCount <= GET_CAMERA_TEXTURE_LOG_THRESHOLD) log.warn(`getCameraTexture() returned null (occurrence ${getCameraTextureNullCount}/${GET_CAMERA_TEXTURE_LOG_THRESHOLD}). camera-access is granted but Three.js did not provide a texture.`);
599
- }
600
- }
601
- }
602
- if (imageCaptureManager) imageCaptureManager.onFrame(time);
603
- if (depthSampler) {
604
- const depthInfo = getDepthInfoFromFrame(frame, pose);
605
- depthSampler.onFrame(time, depthInfo);
606
- }
607
- if (onFrameCallback) try {
608
- onFrameCallback();
609
- } catch (error) {
610
- log.error("Error in onFrameCallback:", error);
611
- }
612
- renderer.render(scene, camera);
613
- if (currentArCrashIsolationOptions.enableCss3dRenderer && css3dManager) css3dManager.render(scene, camera);
614
- }
615
- /**
616
- * Extract depth information from an XR frame.
617
- * Returns null if depth sensing is not available.
618
- */
619
- function getDepthInfoFromFrame(frame, pose) {
620
- if (!pose || !pose.views[0]) return null;
621
- const xrFrame = frame;
622
- if (typeof xrFrame.getDepthInformation !== "function") return null;
623
- try {
624
- return xrFrame.getDepthInformation(pose.views[0]) ?? null;
625
- } catch {
626
- return null;
627
- }
628
- }
629
- /**
630
- * Get the current Three.js scene (for adding objects like map)
631
- */
632
- function getScene() {
633
- return scene;
634
- }
635
- /**
636
- * Get the AR world group (for adding AR-tracked content)
637
- * Content added here will be transformed by the alignment matrix.
638
- */
639
- function getArWorldGroup() {
640
- return arWorldGroup;
641
- }
642
- /**
643
- * Get the current camera
644
- */
645
- function getCamera() {
646
- return camera;
647
- }
648
- /**
649
- * Set the scene externally (for replay mode).
650
- * Allows non-WebXR code paths to register a scene so that modules
651
- * calling getScene() receive it.
652
- * @see docs/2026-02-19-replay-mode.md Risk R1
653
- */
654
- function setScene(s) {
655
- scene = s;
656
- }
657
- /**
658
- * Set the AR world group externally (for replay mode).
659
- * Allows non-WebXR code paths to register an arWorldGroup so that
660
- * applyAlignmentMatrix() and visualizers work correctly.
661
- * @see docs/2026-02-19-replay-mode.md Risk R1
662
- */
663
- function setArWorldGroup(g) {
664
- arWorldGroup = g;
665
- }
666
- /**
667
- * Set the camera externally (for replay mode).
668
- * Allows non-WebXR code paths to register a camera so that modules
669
- * calling getCamera() receive it.
670
- * @see docs/2026-02-19-replay-mode.md Risk R1
671
- */
672
- function setCamera(c) {
673
- camera = c;
674
- }
675
- /**
676
- * Get the arpose Object3D (intermediate node between arWorldGroup and camera).
677
- * Returns null before scene initialization.
678
- */
679
- function getArPose() {
680
- return arPoseNode;
681
- }
682
- /**
683
- * Set the arpose Object3D externally (for replay mode).
684
- * Allows non-WebXR code paths to register an arpose so that
685
- * store subscribers can update it with recorded odom data.
686
- */
687
- function setArPose(a) {
688
- arPoseNode = a;
689
- }
690
- /**
691
- * Constant matrix converting WebXR local-floor coordinates to the internal
692
- * NUE (North-Up-East) convention.
693
- *
694
- * WebXR: X=East, Y=Up, Z=South (right-handed, toward viewer)
695
- * NUE: X=North, Y=Up, Z=East (right-handed)
696
- *
697
- * Mapping: NUE_X = -WebXR_Z, NUE_Y = WebXR_Y, NUE_Z = WebXR_X
698
- *
699
- * Row-major:
700
- * [ 0 0 -1 0 ]
701
- * [ 0 1 0 0 ]
702
- * [ 1 0 0 0 ]
703
- * [ 0 0 0 1 ]
704
- *
705
- * Stored column-major (Three.js / gl-matrix convention).
706
- */
707
- const WEBXR_TO_NUE = new THREE.Matrix4().fromArray([
708
- 0,
709
- 0,
710
- 1,
711
- 0,
712
- 0,
713
- 1,
714
- 0,
715
- 0,
716
- -1,
717
- 0,
718
- 0,
719
- 0,
720
- 0,
721
- 0,
722
- 0,
723
- 1
724
- ]);
725
- /**
726
- * Apply an alignment matrix to the AR world group.
727
- *
728
- * The alignment matrix maps odometry positions in NUE space
729
- * to GPS world space (also NUE). It is applied directly to arWorldGroup
730
- * whose local space is NUE.
731
- *
732
- * The WebXR→NUE basis change lives permanently in basisChangeNode (a
733
- * child of arWorldGroup), so the full camera world transform is:
734
- *
735
- * camera_world = arWorldGroup × basisChangeNode × arpose × camera_local
736
- * = alignment × WEBXR_TO_NUE × arpose × camera_local
737
- *
738
- * This is mathematically identical to the previous composition
739
- * (alignment × WEBXR_TO_NUE applied directly to arWorldGroup.matrix),
740
- * but arWorldGroup's local space is now NUE: objects placed as children
741
- * of arWorldGroup use NUE coordinates directly ([1,0,0]=North, [0,0,1]=East).
742
- *
743
- * Replay note: arpose still lives in WebXR space (below basisChangeNode),
744
- * so nuePositionToWebXR() is still required when setting arpose.position.
745
- *
746
- * @param matrix - 16-element column-major matrix (gl-matrix mat4 format)
747
- */
748
- function applyAlignmentMatrix(matrix) {
749
- if (!arWorldGroup) {
750
- log.warn("Cannot apply alignment - arWorldGroup not initialized");
751
- return;
752
- }
753
- if (matrix.length !== 16) {
754
- log.error("Invalid alignment matrix - expected 16 elements");
755
- return;
756
- }
757
- arWorldGroup.matrix.fromArray(matrix);
758
- arWorldGroup.matrixAutoUpdate = false;
759
- arWorldGroup.updateMatrixWorld(true);
760
- }
761
- /**
762
- * Convert a position from internal NUE convention (X=North, Y=Up, Z=East)
763
- * to WebXR local-floor convention (X=East, Y=Up, Z=South).
764
- *
765
- * Delegates to the canonical library implementation (nueToWebXR).
766
- * Accepts `readonly number[]` for call-site convenience.
767
- *
768
- * NUE [n, u, e] → WebXR [e, u, -n]
769
- */
770
- function nuePositionToWebXR(nue) {
771
- return nueToWebXR(nue);
772
- }
773
- /**
774
- * Convert a quaternion from internal NUE convention to WebXR local-floor convention.
775
- *
776
- * Delegates to the canonical library implementation (nueQuaternionToWebXR).
777
- * Accepts `readonly number[]` for call-site convenience.
778
- *
779
- * NUE [x, y, z, w] → WebXR [z, y, -x, w]
780
- */
781
- function nueQuaternionToWebXR(nue) {
782
- return nueQuaternionToWebXR$1(nue);
783
- }
784
- /**
785
- * End the current XR session and clean up all resources.
786
- *
787
- * Stops the animation loop, ends the XR session, then delegates the full
788
- * teardown to {@link resetWebXRState} so every module-level reference is
789
- * cleared (renderer/scene/camera, image-capture, depth, the tracking-phase
790
- * subscription, the frame-update registry, diagnostics, blit resources).
791
- * This is the production cleanup path — call it when the AR experience is
792
- * finished.
793
- */
794
- async function endARSession() {
795
- if (renderer) renderer.setAnimationLoop(null);
796
- try {
797
- if (xrSession) await xrSession.end();
798
- } finally {
799
- resetWebXRState();
800
- }
801
- }
802
- /**
803
- * Set up image capture callbacks.
804
- * Call this before starting image capture to wire up the callback handlers.
805
- *
806
- * @param onCaptured - Called when an image is successfully captured
807
- * @param screenRotationGetter - Returns current device screen rotation (0, 90, 180, 270)
808
- * @param onFailed - Optional callback for when image capture fails (e.g., low memory)
809
- * @param onSuspicious - Optional callback for when a captured image appears black/empty
810
- */
811
- function setImageCaptureCallback(onCaptured, screenRotationGetter, onFailed, onSuspicious) {
812
- onImageCaptured = onCaptured;
813
- getScreenRotation = screenRotationGetter;
814
- onCaptureFailed = onFailed ?? null;
815
- onSuspiciousImage = onSuspicious ?? null;
816
- }
817
- /**
818
- * Start capturing images during recording.
819
- * Must call setImageCaptureCallback first.
820
- * @param config - Optional capture configuration (intervalMs, quality)
821
- * @param resolutionDivisor - Resolution divisor: 1 = full, 2 = half, 4 = quarter (default: 1)
822
- */
823
- function startImageCapture(config, resolutionDivisor = 1) {
824
- if (!renderer) {
825
- log.warn("Cannot start image capture - renderer not initialized");
826
- return;
827
- }
828
- if (!onImageCaptured || !getScreenRotation) {
829
- log.warn("Cannot start image capture - callbacks not set");
830
- return;
831
- }
832
- if (imageCaptureManager || blitCapture) {
833
- log.warn("Image capture already running - stopping previous session");
834
- stopImageCapture();
835
- }
836
- const callbacks = {
837
- getCurrentPose: getCurrentArPose,
838
- getScreenRotation,
839
- onCaptured: onImageCaptured,
840
- onCaptureFailed: onCaptureFailed ?? void 0,
841
- onSuspiciousImage: onSuspiciousImage ?? void 0
842
- };
843
- blitCapture = new CameraBlitCapture();
844
- const currentRenderer = renderer;
845
- const divisor = resolutionDivisor;
846
- callbacks.captureFrame = async (quality) => {
847
- if (!blitCapture || !latestCameraTexture) return new Promise((resolve) => {
848
- currentRenderer.domElement.toBlob((blob) => resolve(blob), "image/jpeg", quality);
849
- });
850
- if (latestCameraWidth > 0 && latestCameraHeight > 0) {
851
- const target = computeCaptureSize(latestCameraWidth, latestCameraHeight, divisor);
852
- blitCapture.resizeIfNeeded(target.width, target.height);
853
- }
854
- return blitCapture.captureToBlob(currentRenderer, latestCameraTexture, quality);
855
- };
856
- log.info(`Blit capture pipeline initialized (resolutionDivisor=${divisor})`);
857
- const mergedConfig = {
858
- ...DEFAULT_CAPTURE_CONFIG,
859
- ...config
860
- };
861
- imageCaptureManager = new ImageCaptureManager(renderer.domElement, callbacks, mergedConfig);
862
- imageCaptureManager.start();
863
- log.info("Image capture started");
864
- }
865
- /**
866
- * Stop capturing images.
867
- */
868
- function stopImageCapture() {
869
- if (imageCaptureManager) {
870
- imageCaptureManager.stop();
871
- log.info(`Image capture stopped (${imageCaptureManager.getFrameCount()} frames captured)`);
872
- imageCaptureManager = null;
873
- }
874
- cleanupBlitResources();
875
- }
876
- /**
877
- * Get the current image capture frame count.
878
- */
879
- function getImageCaptureFrameCount() {
880
- return imageCaptureManager?.getFrameCount() ?? 0;
881
- }
882
- /**
883
- * Inject the Redux store used by the tracking-state slice pipeline.
884
- *
885
- * MUST be called before `initAR()` whenever the host also wires tracking
886
- * callbacks via `setTrackingCallbacks`. Without a store the tracking
887
- * pipeline silently no-ops.
888
- *
889
- * @param store — any store satisfying {@link TrackingSubscribableStore}.
890
- * `null` clears the binding (useful for teardown in tests).
891
- */
892
- function setTrackingStore(store) {
893
- if (trackingPhaseUnsubscribe) {
894
- trackingPhaseUnsubscribe();
895
- trackingPhaseUnsubscribe = null;
896
- }
897
- trackingStore = store;
898
- }
899
- /**
900
- * Set up tracking state callbacks.
901
- * Call this BEFORE initAR() to enable tracking restart detection.
902
- *
903
- * @param onRestarted - Called when tracking restarts after being lost
904
- */
905
- function setTrackingCallbacks(onRestarted) {
906
- onTrackingRestarted = onRestarted;
907
- }
908
- /**
909
- * Set a callback for when AR tracking is lost.
910
- * Call this BEFORE initAR() to enable tracking loss notifications.
911
- * Field Test Readiness Issue #3: Provide user feedback when tracking is lost.
912
- *
913
- * @param callback - Called when tracking is lost (pose becomes null)
914
- */
915
- function setTrackingLostCallback(callback) {
916
- onTrackingLost = callback;
917
- }
918
- /**
919
- * Set a callback for when AR tracking recovers seamlessly (Case 1: same coordinate frame).
920
- * Call this BEFORE initAR() to enable seamless recovery notifications.
921
- *
922
- * @param callback - Called when tracking recovers without origin reset
923
- */
924
- function setTrackingRecoveredCallback(callback) {
925
- onTrackingRecovered = callback;
926
- }
927
- /**
928
- * Set up depth capture callback.
929
- * Call this BEFORE initAR() to enable depth sampling.
930
- *
931
- * @param onCaptured - Called when a depth sample is captured
932
- * @param onUnavailable - Called once if depth is unavailable after threshold
933
- */
934
- function setDepthCaptureCallback(onCaptured, onUnavailable) {
935
- onDepthCaptured = onCaptured;
936
- onDepthUnavailable = onUnavailable ?? null;
937
- }
938
- /**
939
- * Start depth sampling during recording.
940
- * Must call setDepthCaptureCallback before initAR.
941
- */
942
- function startDepthCapture() {
943
- if (!depthSampler) {
944
- log.warn("Cannot start depth capture - sampler not initialized");
945
- return;
946
- }
947
- depthSampler.start();
948
- log.info("Depth capture started");
949
- }
950
- /**
951
- * Stop depth sampling.
952
- */
953
- function stopDepthCapture() {
954
- if (depthSampler) {
955
- const count = depthSampler.getSampleCount();
956
- depthSampler.stop();
957
- log.info(`Depth capture stopped (${count} samples captured)`);
958
- }
959
- }
960
- /**
961
- * Get the current depth sample count.
962
- */
963
- function getDepthSampleCount() {
964
- return depthSampler?.getSampleCount() ?? 0;
965
- }
966
- /**
967
- * Set a per-frame callback for custom updates.
968
- * The callback is invoked every XR frame after pose updates but before render.
969
- * Useful for updating elements that need to follow the camera smoothly
970
- * (e.g., map overlay position).
971
- *
972
- * @param callback - Function to call each frame, or null to clear
973
- */
974
- function setFrameCallback(callback) {
975
- onFrameCallback = callback;
976
- }
977
- /**
978
- * Get the CSS3D renderer manager for live AR mode.
979
- * Returns null if no AR session is active or CSS3D was not created.
980
- */
981
- function getLiveCss3dManager() {
982
- return css3dManager;
983
- }
984
- //#endregion
1
+ import { A as setTrackingLostCallback, C as setArWorldGroup, D as setImageCaptureCallback, E as setFrameCallback, F as startImageCapture, I as stopDepthCapture, L as stopImageCapture, M as setTrackingStore, N as shouldLogCameraAccessDiagnostic, O as setScene, P as startDepthCapture, S as setArPose, T as setDepthCaptureCallback, _ as isWebXRSupported, a as extractPoseFromViewer, b as nueQuaternionToWebXR, c as getArWorldGroup, d as getDepthSampleCount, f as getImageCaptureFrameCount, g as initAR, h as getXrCameraFromPose, i as endARSession, j as setTrackingRecoveredCallback, k as setTrackingCallbacks, l as getCamera, m as getScene, n as buildSessionOptions, o as extractResetTransformData, p as getLiveCss3dManager, r as createSceneHierarchy, s as getArPose, t as applyAlignmentMatrix, u as getCurrentArPose, v as isXRCameraLike, w as setCamera, x as resetWebXRState, y as nuePositionToWebXR } from "../webxr-session-QhcSSub5.js";
985
2
  export { applyAlignmentMatrix, buildSessionOptions, createSceneHierarchy, endARSession, extractPoseFromViewer, extractResetTransformData, getArPose, getArWorldGroup, getCamera, getCurrentArPose, getDepthSampleCount, getImageCaptureFrameCount, getLiveCss3dManager, getScene, getXrCameraFromPose, initAR, isWebXRSupported, isXRCameraLike, nuePositionToWebXR, nueQuaternionToWebXR, resetWebXRState, setArPose, setArWorldGroup, setCamera, setDepthCaptureCallback, setFrameCallback, setImageCaptureCallback, setScene, setTrackingCallbacks, setTrackingLostCallback, setTrackingRecoveredCallback, setTrackingStore, shouldLogCameraAccessDiagnostic, startDepthCapture, startImageCapture, stopDepthCapture, stopImageCapture };