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