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
package/README.md CHANGED
@@ -108,7 +108,7 @@ startGpsWatch(
108
108
  );
109
109
  ```
110
110
 
111
- See [`GpsPlusSlamJs_MinimalExample`](../GpsPlusSlamJs_MinimalExample/) for a complete, runnable smallest-possible consumer (Three.js scene + status panel, no AR, no recording).
111
+ See [`GpsPlusSlamJs_MinimalExample`](../GpsPlusSlamJs_MinimalExample/) for a complete, runnable smallest-possible consumer (Three.js scene + status panel, no AR, no recording). For the next rung up — a readable AR + GPS + persistence demo (a single GPS anchor that survives a page reload) — see [`GpsPlusSlamJs_AnchorStarter`](../GpsPlusSlamJs_AnchorStarter/). The example ladder is **trivial** (MinimalExample) → **starter** (AnchorStarter) → **full** (RecorderApp).
112
112
 
113
113
  > **Imports.** Prefer subpath imports (`gps-plus-slam-app-framework/ar`, `…/state`, `…/sensors`, `…/storage`, `…/geo`, `…/visualization`, `…/utils`, `…/types`, `…/licensing`). The root barrel re-exports conflict-free names for convenience.
114
114
 
@@ -196,32 +196,85 @@ scene ← GPS-aligned (NUE) space, the scene root
196
196
  (waypoints, POIs, navigation arrows, …)
197
197
  ```
198
198
 
199
- When the alignment solver produces a new matrix, the framework writes it to `arWorldGroup.matrix`. The camera moves with `arWorldGroup`; objects parented directly to `scene` do not.
199
+ When the alignment solver produces a new matrix, the framework writes it to `arWorldGroup.matrix` (smoothly lerped — see `enableArWorldGroupAlignment` below). The camera moves with `arWorldGroup`; objects parented directly to `scene` do not.
200
+
201
+ > **Apply alignment to `arWorldGroup` — the framework default.** Call
202
+ > [`enableArWorldGroupAlignment({ store, arWorldGroup })`](src/visualization/ar-world-group-alignment.ts.md)
203
+ > once after AR starts. It subscribes to the store's alignment matrix, lerps it
204
+ > onto `arWorldGroup.matrix` each frame, and thereby **GPS-registers the view**:
205
+ > the camera and every object parented under `arWorldGroup` (including GPS
206
+ > anchors) shift together as alignment refines, so anchors stay stable in the AR
207
+ > overlay and only ever correct a small residual. The recorder wires its own
208
+ > lerper; the simpler apps use this helper. Forgetting it leaves the camera
209
+ > pure-VIO and forces each anchor to absorb the full alignment delta on every
210
+ > re-registration.
200
211
 
201
212
  **Three options for placing your own `Object3D`:**
202
213
 
203
214
  1. **Add it to `scene`** (with NUE-meter coordinates from `calcRelativeCoordsInMeters(zeroRef, …)`). The object's world pose stays at the correct latitude/longitude/altitude forever, but every time the alignment matrix is corrected the camera shifts inside `arWorldGroup`, so from the user's AR view the object visually "floats". Cheap and correct, but ugly during corrections — fine for small markers (e.g. ref-point spheres), not great for richer GPS-anchored content.
204
215
  2. **Add it to `arWorldGroup`** with a fixed local transform. The object is frozen relative to AR-tracked content and stays visually fixed at the same 3D position for this session. This is a good fit for user-created local content such as a walked path, a temporary marker, a hit-test reticle, or an object the user placed by hand. The tradeoff is that its world / GPS pose drifts every time alignment is corrected, so this mode is not enough when the object must be replayed or shared by GPS coordinate.
205
- 3. **Use `createGpsAnchor` for objects that should stay visually stable at a GPS target.** The anchor owns a single `Object3D` inside `arWorldGroup`, bootstraps from median GPS samples unless `skipBootstrap` is set, and re-derives the object's local pose from the current GPS target and alignment state. In the default `snap-when-offscreen` mode, small corrections are committed while the object is outside the camera frustum, making small alignment corrections hard to notice; larger alignment jumps bypass that gate so the object does not stay in a stale location. Use `snap-every-tick` when correctness is more important than hiding visible position changes.
216
+ 3. **Use `createGpsAnchor` for objects that should stay visually stable at a GPS target.** The anchor owns a single `Object3D` **inside `arWorldGroup`** (the factory throws if `object3D` is not a descendant of the `arWorldGroup` you pass — placing an anchor under the scene root defeats AR stability), bootstraps from median samples unless `skipBootstrap` is set, and re-derives the object's local pose from the current GPS target and alignment state each tick. Because the object rides the (lerped) `arWorldGroup` alignment, its motion relative to the camera between re-registrations is small. In the default `snap-when-offscreen` mode an accepted correction is committed **instantly** while the object is outside the camera frustum, making the (now small, residual) correction hard to notice; larger alignment jumps bypass that gate so the object does not stay in a stale location. Use `snap-every-tick` when correctness is more important than hiding visible position changes. Smoothing is **not** per-anchor: it lives once in the lerped `arWorldGroup.matrix` (`enableArWorldGroupAlignment`), so the whole AR world eases together. This is how the MinimalExample and AnchorStarter get the smooth alignment feel.
206
217
 
207
218
  A pure-function `syncGpsAnchoredMeshes` reconciler (option 1, bulk markers) is shipped by the RecorderApp. Use `createGpsAnchor` when a single visible object, route cue, or POI needs the more careful bootstrap and correction policy.
208
219
 
220
+ > **Worked example.** The [`GpsPlusSlamJs_MinimalExample`](../GpsPlusSlamJs_MinimalExample/) ports the stock three.js `webxr_ar_hittest` example onto this convention and ends in a deliberate side-by-side **contrast demo**: a tap co-spawns an option-1 floater under `scene` and an option-3 `createGpsAnchor` marker under `arWorldGroup` at the same world pose, so the drift difference is visible. It is the canonical reference for options 1–3 and for the `registerXrFrameUpdate` + Enable-GPS-AR seams below.
221
+
222
+ ## DOM-Overlay / HUD stacking convention
223
+
224
+ `initAR(container)` requests the WebXR **`dom-overlay`** feature with
225
+ `domOverlay.root = container` — the **same element you pass in**. During an
226
+ `immersive-ar` session the browser composites **only that element's subtree**
227
+ over the camera feed; everything else on the page is hidden until the session
228
+ ends.
229
+
230
+ > **The rule:** every HUD / overlay / button you want visible **in AR** must be
231
+ > a **DOM descendant of the element you pass to `initAR`**. A sibling overlay
232
+ > works in the 2D pre-AR layout but silently disappears the moment AR starts —
233
+ > a failure that only shows up on real AR hardware, never in a headless test.
234
+
235
+ This is **not** a `z-index` problem; `z-index`/`pointer-events` only govern the
236
+ 2D (pre-AR) layout. Nesting is what determines in-AR visibility.
237
+
238
+ ```html
239
+ <!-- ✅ Correct: HUD is inside the initAR container -->
240
+ <div id="app">
241
+ <!-- three.js canvas is injected here by initAR -->
242
+ <div id="hud">…</div>
243
+ </div>
244
+
245
+ <!-- ❌ Wrong: HUD is a sibling — it vanishes once AR starts -->
246
+ <div id="app"></div>
247
+ <div id="hud">…</div>
248
+ ```
249
+
250
+ ```js
251
+ await initAR(document.getElementById('app')!); // #app's subtree = the overlay root
252
+ ```
253
+
254
+ A repo-meta guard (`tests/repo-config/hud-overlay-nesting.test.js`) asserts this
255
+ structurally for every app's `index.html`, so a new app that authors its overlay
256
+ as a sibling fails CI instead of failing silently in the field. Add new apps to
257
+ that guard's `APP_OVERLAY_CONTRACTS` list.
258
+
209
259
  ## Modules
210
260
 
211
261
  ### `ar/` — WebXR & 3D Scene
212
262
 
213
263
  WebXR session lifecycle, Three.js renderer setup, image/depth capture, replay scene management.
214
264
 
215
- | Export | Description |
216
- | -------------------------------------------- | ------------------------------------------------ |
217
- | `initAR(container)` | Start a WebXR AR session with Three.js rendering |
218
- | `endARSession()` | End the active XR session |
219
- | `startImageCapture()` / `stopImageCapture()` | Toggle camera frame capture |
220
- | `ImageCaptureManager` | Configurable camera frame capture pipeline |
221
- | `DepthSampler` | Depth buffer sampling with configurable grids |
222
- | `CameraBlitCapture` | GPU blit-based camera capture |
223
- | `initReplayScene(container)` | Create a 3D replay scene with orbit/FPS controls |
224
- | `applyChromiumProjectionLayerWorkaround` | Workaround for Chromium camera-access edge cases |
265
+ | Export | Description |
266
+ | ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
267
+ | `initAR(container, isolation?, features?)` | Start a WebXR AR session with Three.js rendering. `features.requestHitTest` opts the session into the WebXR `hit-test` feature |
268
+ | `endARSession()` | End the active XR session |
269
+ | `createEnableGpsArController()` | Headless "Enable GPS AR" orchestration (support check + permission bundling + sensor watches + `initAR`) with observable state; the app renders its own button over it |
270
+ | `registerXrFrameUpdate(cb)` | Per-frame access to the live `XRFrame` + reference space + session (valid only synchronously inside the callback). Enables app-side hit-test / other WebXR features |
271
+ | `isFullySupported(s)` / `capabilityMessage(s)` | WebXR + geolocation capability gating + a user-facing message |
272
+ | `startImageCapture()` / `stopImageCapture()` | Toggle camera frame capture |
273
+ | `ImageCaptureManager` | Configurable camera frame capture pipeline |
274
+ | `DepthSampler` | Depth buffer sampling with configurable grids |
275
+ | `CameraBlitCapture` | GPU blit-based camera capture |
276
+ | `initReplayScene(container)` | Create a 3D replay scene with orbit/FPS controls |
277
+ | `applyChromiumProjectionLayerWorkaround` | Chromium camera-access tab-crash workaround. Always deletes projection-layer hooks (forces `XRWebGLLayer`; required on every affected build incl. Chrome 150) and additionally persists `baseLayer` only on the affected Chrome window (148.0.7778.12 up to 149.0.7821) |
225
278
 
226
279
  ### `sensors/` — GPS & Permissions
227
280
 
@@ -354,10 +407,11 @@ src/
354
407
 
355
408
  This framework is licensed under **Apache 2.0** — see [LICENSE](LICENSE).
356
409
 
357
- > **Note:** This package depends on [gps-plus-slam-js](https://www.npmjs.com/package/gps-plus-slam-js), which is a **closed-source, proprietary** library distributed via npm under a separate license. A community license key is bundled with the framework for frictionless evaluation. See the core library's EULA for usage terms.
410
+ > **Note:** This package depends on [gps-plus-slam-js](https://www.npmjs.com/package/gps-plus-slam-js), which is a **closed-source, proprietary** library distributed via npm under a separate license. A free community license key is bundled with the framework so you can start building right away — no signup or API key required. See the core library's EULA for the full terms.
358
411
 
359
412
  ## See Also
360
413
 
361
414
  - [gps-plus-slam-js](https://www.npmjs.com/package/gps-plus-slam-js) — Core alignment algorithms (closed-source)
362
- - [`GpsPlusSlamJs_MinimalExample`](../GpsPlusSlamJs_MinimalExample/) — Smallest possible consumer of this framework
415
+ - [`GpsPlusSlamJs_MinimalExample`](../GpsPlusSlamJs_MinimalExample/) — Smallest possible consumer of this framework (trivial rung)
416
+ - [`GpsPlusSlamJs_AnchorStarter`](../GpsPlusSlamJs_AnchorStarter/) — Meaningful-minimal starter: a persistent GPS anchor across reload (starter rung)
363
417
  - [`GpsPlusSlamJs_RecorderApp`](../GpsPlusSlamJs_RecorderApp/) — Full-featured recording app built on this framework
@@ -1,4 +1,4 @@
1
- import { L as CombinedRootState, r as SlamAppRootState } from "./create-slam-app-store-CNsZ3rcX.js";
1
+ import { L as CombinedRootState, r as SlamAppRootState } from "./create-slam-app-store-qNXQ-tq5.js";
2
2
  import * as _$gps_plus_slam_js0 from "gps-plus-slam-js";
3
3
  import { ArImageCapture, GpsPoint, LatLong, Matrix4, Quaternion, Vector3 } from "gps-plus-slam-js";
4
4
  import * as _$_reduxjs_toolkit0 from "@reduxjs/toolkit";
@@ -0,0 +1,2 @@
1
+ import { n as MAX_TRACE_STEPS, r as bresenham3d, t as GridCell } from "../bresenham3d-CFfd7n6B.js";
2
+ export { GridCell, MAX_TRACE_STEPS, bresenham3d };
@@ -0,0 +1,81 @@
1
+ //#region ../src/ar/bresenham3d.ts
2
+ /**
3
+ * Main-thread safety cap: the maximum dominant-axis (Chebyshev) span a single
4
+ * trace may cover. The trace runs synchronously, one iteration per
5
+ * dominant-axis step, so an unbounded span freezes the UI. Finite-but-absurd
6
+ * coordinates (a tracking glitch, a corrupt projectionMatrix unprojecting to a
7
+ * huge world point) quantize to safe integers and pass the integer check, so
8
+ * this is the only thing standing between such input and a multi-second
9
+ * (potentially multi-billion-iteration) freeze.
10
+ *
11
+ * Chosen generously — at the grid's 0.15 m cells this is ~150 km, far beyond
12
+ * any real AR scene, so it never trips on legitimate carving or raycasting.
13
+ * It is a circuit breaker against programmer/data error, not a ray-length
14
+ * policy: exceeding it throws (loud — surfaces the upstream bug) rather than
15
+ * silently truncating the trace.
16
+ */
17
+ const MAX_TRACE_STEPS = 1e6;
18
+ /**
19
+ * Trace the line from `start` to `end`, calling `visitCell` per cell.
20
+ *
21
+ * @param visitCell - return `false` to stop the trace early.
22
+ * @param stopDistance - dominant-axis steps before `end` at which to stop
23
+ * (default 0 = trace all the way to `end`).
24
+ * @throws TypeError when a coordinate is not a safe integer (cells must be
25
+ * quantized before tracing — programmer error, not a data error).
26
+ * @throws RangeError when `stopDistance` is not a non-negative safe integer.
27
+ * The loop terminates on a counter (`i--`) that decrements unconditionally,
28
+ * so a negative or fractional value still terminates (it merely traces past
29
+ * `end`); but `NaN`/`-Infinity` never satisfy `i <= stopDistance` and would
30
+ * spin forever. Rejecting up front both prevents that freeze and enforces
31
+ * the "dominant-axis steps before end" contract.
32
+ * @throws RangeError when the dominant-axis span exceeds {@link MAX_TRACE_STEPS}
33
+ * (circuit breaker against a synchronous main-thread freeze from
34
+ * finite-but-absurd coordinates).
35
+ */
36
+ function bresenham3d(start, end, visitCell, stopDistance = 0) {
37
+ assertIntegerCell(start);
38
+ assertIntegerCell(end);
39
+ if (!Number.isSafeInteger(stopDistance) || stopDistance < 0) throw new RangeError(`bresenham3d stopDistance must be a non-negative safe integer, got ${stopDistance}`);
40
+ let [x, y, z] = start;
41
+ const dx = Math.abs(end[0] - x);
42
+ const sx = x < end[0] ? 1 : -1;
43
+ const dy = Math.abs(end[1] - y);
44
+ const sy = y < end[1] ? 1 : -1;
45
+ const dz = Math.abs(end[2] - z);
46
+ const sz = z < end[2] ? 1 : -1;
47
+ const dm = Math.max(dx, dy, dz);
48
+ if (dm > 1e6) throw new RangeError(`bresenham3d dominant-axis span ${dm} exceeds MAX_TRACE_STEPS ${MAX_TRACE_STEPS}; cells [${start.join(", ")}]→[${end.join(", ")}] are too far apart to trace synchronously`);
49
+ let i = dm;
50
+ let errX = Math.floor(dm / 2);
51
+ let errY = errX;
52
+ let errZ = errX;
53
+ for (;;) {
54
+ if (!visitCell([
55
+ x,
56
+ y,
57
+ z
58
+ ])) return;
59
+ if (i-- <= stopDistance) return;
60
+ errX -= dx;
61
+ if (errX < 0) {
62
+ errX += dm;
63
+ x += sx;
64
+ }
65
+ errY -= dy;
66
+ if (errY < 0) {
67
+ errY += dm;
68
+ y += sy;
69
+ }
70
+ errZ -= dz;
71
+ if (errZ < 0) {
72
+ errZ += dm;
73
+ z += sz;
74
+ }
75
+ }
76
+ }
77
+ function assertIntegerCell(cell) {
78
+ if (!cell.every((v) => Number.isSafeInteger(v))) throw new TypeError(`bresenham3d requires integer cell coordinates, got [${cell.join(", ")}]`);
79
+ }
80
+ //#endregion
81
+ export { MAX_TRACE_STEPS, bresenham3d };
@@ -1,2 +1,2 @@
1
- import { i as computeCaptureSize, n as CameraBlitCaptureConfig, r as DEFAULT_BLIT_CONFIG, t as CameraBlitCapture } from "../camera-blit-capture-C1V9I1Z6.js";
1
+ import { i as computeCaptureSize, n as CameraBlitCaptureConfig, r as DEFAULT_BLIT_CONFIG, t as CameraBlitCapture } from "../camera-blit-capture-D5_mNhla.js";
2
2
  export { CameraBlitCapture, CameraBlitCaptureConfig, DEFAULT_BLIT_CONFIG, computeCaptureSize };
@@ -118,8 +118,32 @@ var CameraBlitCapture = class {
118
118
  * @returns JPEG Blob, or null if capture fails
119
119
  */
120
120
  async captureToBlob(renderer, cameraTexture, quality) {
121
+ if (!this.captureToPixels(renderer, cameraTexture)) return null;
122
+ try {
123
+ return await this.pixelsToJpegBlob(quality);
124
+ } catch (error) {
125
+ log.error("Blit capture failed:", error);
126
+ return null;
127
+ }
128
+ }
129
+ /**
130
+ * Run the blit + readback (steps A+B of {@link captureToBlob}) and return
131
+ * the raw RGBA pixel buffer without JPEG encoding — the cheap path for
132
+ * per-point color sampling (occupancy-grid port plan Iter 8).
133
+ *
134
+ * IMPORTANT: Must be called within the XR animation frame callback, while
135
+ * the camera texture is valid. The returned `pixels` is the INTERNAL
136
+ * buffer — valid only until the next capture or `resizeIfNeeded`; consume
137
+ * it synchronously (e.g. via `createRgbLookup`) or copy it.
138
+ *
139
+ * Note: the buffer is in WebGL readback order (bottom-row-first / RGBA);
140
+ * `createRgbLookup` handles the y-flip.
141
+ *
142
+ * @returns the buffer with its dimensions, or null on failure/dispose.
143
+ */
144
+ captureToPixels(renderer, cameraTexture) {
121
145
  if (this.disposed) {
122
- log.warn("captureToBlob called after dispose");
146
+ log.warn("captureToPixels called after dispose");
123
147
  return null;
124
148
  }
125
149
  try {
@@ -133,7 +157,11 @@ var CameraBlitCapture = class {
133
157
  renderer.setRenderTarget(currentRenderTarget);
134
158
  renderer.xr.enabled = currentXrEnabled;
135
159
  this.blitMaterial.uniforms.tDiffuse.value = null;
136
- return await this.pixelsToJpegBlob(quality);
160
+ return {
161
+ pixels: this.pixelBuffer,
162
+ width: this.width,
163
+ height: this.height
164
+ };
137
165
  } catch (error) {
138
166
  log.error("Blit capture failed:", error);
139
167
  return null;
@@ -0,0 +1,2 @@
1
+ import { i as isFullySupported, n as CapabilitySupport, r as capabilityMessage, t as CapabilityMessageOptions } from "../capability-checker-CQXJHxXg.js";
2
+ export { CapabilityMessageOptions, CapabilitySupport, capabilityMessage, isFullySupported };
@@ -0,0 +1,25 @@
1
+ //#region ../src/ar/capability-checker.ts
2
+ /** True only when both AR and GPS are available — the demo can run. */
3
+ function isFullySupported(support) {
4
+ return support.webxr && support.geolocation;
5
+ }
6
+ /**
7
+ * Build the capability-gated message shown when the demo cannot run. Names
8
+ * exactly which capabilities are missing so the user understands *why*, and
9
+ * always points them at the supported environment (an AR phone, outdoors).
10
+ * Returns `null` when everything is supported (no message needed).
11
+ *
12
+ * The trailing "to try …" clause is app-specific and supplied via
13
+ * `options.contextLabel`; without it the message ends neutrally at
14
+ * "…outdoors.".
15
+ */
16
+ function capabilityMessage(support, options = {}) {
17
+ if (isFullySupported(support)) return null;
18
+ const missing = [];
19
+ if (!support.webxr) missing.push("WebXR augmented reality");
20
+ if (!support.geolocation) missing.push("GPS / geolocation");
21
+ const suffix = options.contextLabel ? `, to try ${options.contextLabel}` : "";
22
+ return `This demo needs ${missing.join(" and ")}, which this device or browser does not provide. Open it on an AR-capable phone (e.g. Chrome on Android with ARCore), outdoors${suffix}.`;
23
+ }
24
+ //#endregion
25
+ export { capabilityMessage, isFullySupported };
@@ -1,2 +1,2 @@
1
- import { a as createCaptureFailureTracker, i as DEFAULT_CAPTURE_TRACKER_CONFIG, n as CaptureFailureTracker, r as CaptureFailureTrackerConfig, t as CAPTURE_FAILURE_WARNING } from "../capture-failure-tracker-JKb1ST8E.js";
1
+ import { a as createCaptureFailureTracker, i as DEFAULT_CAPTURE_TRACKER_CONFIG, n as CaptureFailureTracker, r as CaptureFailureTrackerConfig, t as CAPTURE_FAILURE_WARNING } from "../capture-failure-tracker-CUeU3L_M.js";
2
2
  export { CAPTURE_FAILURE_WARNING, CaptureFailureTracker, CaptureFailureTrackerConfig, DEFAULT_CAPTURE_TRACKER_CONFIG, createCaptureFailureTracker };
@@ -1,2 +1,2 @@
1
- import { n as applyChromiumProjectionLayerWorkaround, t as ChromiumProjectionLayerWorkaroundResult } from "../chromium-camera-access-workaround-NvChXsi8.js";
2
- export { ChromiumProjectionLayerWorkaroundResult, applyChromiumProjectionLayerWorkaround };
1
+ import { a as applyChromiumProjectionLayerWorkaround, i as ChromiumProjectionLayerWorkaroundResult, n as BASELAYER_WINDOW_MIN, o as needsBaseLayerPersistence, r as ChromeVersion, s as parseChromeVersion, t as BASELAYER_WINDOW_MAX } from "../chromium-camera-access-workaround-Jqbn0apH.js";
2
+ export { BASELAYER_WINDOW_MAX, BASELAYER_WINDOW_MIN, ChromeVersion, ChromiumProjectionLayerWorkaroundResult, applyChromiumProjectionLayerWorkaround, needsBaseLayerPersistence, parseChromeVersion };
@@ -1,20 +1,207 @@
1
+ import { createLogger } from "../utils/logger.js";
1
2
  //#region ../src/ar/chromium-camera-access-workaround.ts
2
3
  /**
4
+ * Chromium WebXR camera-access tab-crash workaround.
5
+ *
6
+ * Background:
7
+ * Requesting `camera-access` as an optional WebXR feature on Android Chrome
8
+ * 147+ causes a fatal renderer-process crash (`CrRendererMain`) 1–2 seconds
9
+ * after entering AR. The crash reproduces in the upstream three.js
10
+ * `webxr_ar_hittest.html` example with `optionalFeatures: ['camera-access']`
11
+ * added — i.e. it is not specific to this app's session setup. See:
12
+ *
13
+ * - GpsPlusSlamJs_Docs/docs/2026-04-22-camera-access-reproducer-plan.md
14
+ * - GpsPlusSlamJs_Docs/docs/2026-06-04-camera-access-crash-regression-chrome-148.md
15
+ * - https://github.com/mrdoob/three.js/issues/33404
16
+ * - https://issues.chromium.org/issues/507508099 (root-cause, marked Fixed)
17
+ *
18
+ * There is NOT a single crash here — the Chromium tracker notes the crash on
19
+ * Chrome 147 is different from the crash on 148+, and the simple workaround
20
+ * below only helped on a subset of versions. The empirically-verified
21
+ * (on-device) timeline is:
22
+ *
23
+ * - 147 : deleting `createProjectionLayer` / `layers` forces three.js onto
24
+ * `XRWebGLLayer` and sidesteps the crash (deletes-only is sufficient).
25
+ * - 148.0.7778.12 .. 149.0.7821 : the delete-only trick is NOT sufficient on
26
+ * its own. An ADDITIONAL patch is needed: persist the `baseLayer`
27
+ * reference across `XRSession.prototype.updateRenderState` so three.js's
28
+ * later `depthNear`/`depthFar` update does not drop the active
29
+ * `glBaseLayer`. (Confirmed on-device on `148.0.7778.215`: BOTH the
30
+ * deletes and the baseLayer patch are required for the page not to
31
+ * crash. Earlier 148 builds (`< .7778.12`) are below the window per the
32
+ * tracker, which reported delete-only worked there.)
33
+ * - > 149.0.7821 (incl. Chrome 150) : the delete-only path is STILL required
34
+ * (confirmed on-device: Chrome 150 only stops crashing when the deletes
35
+ * are applied), but the extra baseLayer-persistence patch is NOT needed.
36
+ * The earlier assumption that Chromium fully fixed this on patched
37
+ * builds did not hold on real devices, so we no longer skip the
38
+ * workaround on "patched" Chrome.
39
+ *
40
+ * Resulting policy (see {@link applyChromiumProjectionLayerWorkaround}):
41
+ * - ALWAYS apply the deletes (every Chrome build, and unknown/non-Chromium
42
+ * environments — restoring the original always-on behavior).
43
+ * - Apply the baseLayer-persistence patch ONLY when a detected Chrome build
44
+ * falls inside the affected window
45
+ * [{@link BASELAYER_WINDOW_MIN} .. {@link BASELAYER_WINDOW_MAX}].
46
+ *
47
+ * Caveats:
48
+ * - This is a Chromium-specific hack and the upstream comment explicitly
49
+ * warns it "might break webxr on other devices" — keep it opt-in.
50
+ * - Must run BEFORE any WebXR session setup. Three.js reads these prototype
51
+ * members lazily when the first session starts, so calling this at app
52
+ * bootstrap (before `initAR()`) is sufficient.
53
+ * - Idempotent: safe to call repeatedly. Safe on environments where the
54
+ * prototypes do not exist (e.g. desktop browsers, jsdom).
55
+ */
56
+ const log = createLogger("chromium-camera-access");
57
+ /** Marker set on our patched `updateRenderState` so we never double-wrap it. */
58
+ const BASE_LAYER_PATCH_MARKER = "__gpsBaseLayerPersistencePatch";
59
+ /**
60
+ * Inclusive lower bound of the Chrome window that additionally needs the
61
+ * `baseLayer`-persistence patch (on top of the deletes).
62
+ *
63
+ * Set to `148.0.7778.12`, the issue tracker's figure for when the delete-only
64
+ * trick stopped being sufficient. On-device confirmation: `148.0.7778.215`
65
+ * (well inside this window) crashes with deletes-only and needs BOTH patches.
66
+ * Earlier 148 builds (`< .7778.12`) and all of Chrome 147 stay below this
67
+ * bound (deletes-only) per the documented timeline — we have no on-device
68
+ * evidence that they need the baseLayer patch, so we do not apply it there.
69
+ */
70
+ const BASELAYER_WINDOW_MIN = [
71
+ 148,
72
+ 0,
73
+ 7778,
74
+ 12
75
+ ];
76
+ /**
77
+ * Inclusive upper bound of the Chrome window that additionally needs the
78
+ * `baseLayer`-persistence patch. The crash is fixed after `149.0.7819.0` and
79
+ * the camera image is correctly populated after `149.0.7821.0`; above this
80
+ * build the extra patch is no longer required (the deletes still are — see
81
+ * the module header and the on-device matrix for Chrome 150).
82
+ *
83
+ * @see https://github.com/mrdoob/three.js/issues/33404
84
+ */
85
+ const BASELAYER_WINDOW_MAX = [
86
+ 149,
87
+ 0,
88
+ 7821,
89
+ 0
90
+ ];
91
+ /**
92
+ * Parse a Chrome/Chromium version from a user-agent string.
93
+ *
94
+ * Matches the `Chrome/<major>.<minor>.<build>.<patch>` token (and the iOS
95
+ * `CriOS/...` variant). Returns null for non-Chromium user agents.
96
+ */
97
+ function parseChromeVersion(userAgent) {
98
+ const match = /(?:Chrome|CriOS)\/(\d+)\.(\d+)\.(\d+)\.(\d+)/.exec(userAgent);
99
+ if (!match) return null;
100
+ return [
101
+ Number(match[1]),
102
+ Number(match[2]),
103
+ Number(match[3]),
104
+ Number(match[4])
105
+ ];
106
+ }
107
+ /** Lexicographically compare two version tuples: returns true if `a` > `b`. */
108
+ function isVersionAfter(a, b) {
109
+ for (let i = 0; i < a.length; i++) {
110
+ const av = a[i] ?? 0;
111
+ const bv = b[i] ?? 0;
112
+ if (av !== bv) return av > bv;
113
+ }
114
+ return false;
115
+ }
116
+ /** True if `a` >= `b` (lexicographic). */
117
+ function isVersionAtLeast(a, b) {
118
+ return !isVersionAfter(b, a);
119
+ }
120
+ /**
121
+ * True when the detected Chrome build falls inside the affected window
122
+ * [{@link BASELAYER_WINDOW_MIN} .. {@link BASELAYER_WINDOW_MAX}] (inclusive)
123
+ * and therefore additionally needs the `baseLayer`-persistence patch. Outside
124
+ * the window (including non-Chromium user agents) returns false — only the
125
+ * deletes are needed there.
126
+ */
127
+ function needsBaseLayerPersistence(userAgent) {
128
+ const version = parseChromeVersion(userAgent);
129
+ if (version === null) return false;
130
+ return isVersionAtLeast(version, BASELAYER_WINDOW_MIN) && isVersionAtLeast(BASELAYER_WINDOW_MAX, version);
131
+ }
132
+ function getUserAgent() {
133
+ return globalThis.navigator?.userAgent ?? "";
134
+ }
135
+ /**
136
+ * Wrap `XRSession.prototype.updateRenderState` so the `baseLayer` reference is
137
+ * persisted across calls. Three.js calls `updateRenderState({ depthNear,
138
+ * depthFar })` after the base layer was set; on affected Chrome builds that
139
+ * second call drops the active `glBaseLayer`, which leads to the crash on
140
+ * camera-texture access. Re-supplying the last-seen `baseLayer` keeps it
141
+ * alive.
142
+ *
143
+ * The last-seen `baseLayer` is tracked **per `XRSession` instance** via a
144
+ * `WeakMap` keyed on the call's `this`. A single shared variable would leak a
145
+ * previous session's `baseLayer` into a later session: an `XRWebGLLayer` is
146
+ * bound to the session it was created for, so re-supplying a stale layer to a
147
+ * different session throws `InvalidStateError`. Keying per session also lets
148
+ * the entries be garbage-collected with their sessions.
149
+ *
150
+ * Idempotent: a marker on the wrapper prevents double-wrapping.
151
+ *
152
+ * @returns true if the prototype was wrapped on this call.
153
+ */
154
+ function patchUpdateRenderStateForBaseLayerPersistence() {
155
+ const session = globalThis.XRSession;
156
+ const original = session?.prototype.updateRenderState;
157
+ if (!session || typeof original !== "function") return false;
158
+ if (original[BASE_LAYER_PATCH_MARKER]) return false;
159
+ const lastBaseLayerBySession = /* @__PURE__ */ new WeakMap();
160
+ const patched = function(rawInit) {
161
+ const init = typeof rawInit === "object" && rawInit !== null ? rawInit : {};
162
+ if (typeof this !== "object" || this === null) return original.call(this, init);
163
+ if (init.baseLayer !== void 0) lastBaseLayerBySession.set(this, init.baseLayer);
164
+ const remembered = lastBaseLayerBySession.get(this);
165
+ return original.call(this, remembered !== void 0 ? {
166
+ ...init,
167
+ baseLayer: remembered
168
+ } : init);
169
+ };
170
+ patched[BASE_LAYER_PATCH_MARKER] = true;
171
+ session.prototype.updateRenderState = patched;
172
+ return true;
173
+ }
174
+ /**
3
175
  * Apply the Chromium camera-access tab-crash workaround.
4
176
  *
5
- * Removes `XRWebGLBinding.prototype.createProjectionLayer` (three.js r184)
6
- * and `XRRenderState.prototype.layers` (three.js r158) so three.js falls
7
- * back to `XRWebGLLayer`, which avoids the renderer-process crash that
8
- * happens when both projection layers and `camera-access` are enabled.
177
+ * Policy (derived from on-device testing — see the module header):
178
+ *
179
+ * - ALWAYS removes `XRWebGLBinding.prototype.createProjectionLayer`
180
+ * (three.js r184) and `XRRenderState.prototype.layers` (three.js r158) so
181
+ * three.js falls back to `XRWebGLLayer`. This is required on every affected
182
+ * Chrome build observed so far, including Chrome 150, and is also applied
183
+ * on unknown/non-Chromium environments (restoring the original always-on
184
+ * behavior).
185
+ * - ADDITIONALLY persists the `baseLayer` across
186
+ * `XRSession.prototype.updateRenderState`, but ONLY when a detected Chrome
187
+ * build falls inside the affected window
188
+ * [{@link BASELAYER_WINDOW_MIN} .. {@link BASELAYER_WINDOW_MAX}]. Outside
189
+ * that window the extra patch is unnecessary (and is skipped on unknown
190
+ * environments to avoid touching projection-layer devices like Quest).
9
191
  *
10
192
  * Call once during bootstrap, before any `requestSession()` call.
11
193
  *
12
- * @returns which prototype members were actually deleted on this call.
194
+ * @param options.userAgent override the detected user agent (for testing).
195
+ * @returns which prototype members were actually changed on this call.
13
196
  */
14
- function applyChromiumProjectionLayerWorkaround() {
197
+ function applyChromiumProjectionLayerWorkaround(options) {
198
+ const userAgent = options?.userAgent ?? getUserAgent();
199
+ const version = parseChromeVersion(userAgent);
15
200
  const result = {
16
201
  deletedCreateProjectionLayer: false,
17
- deletedRenderStateLayers: false
202
+ deletedRenderStateLayers: false,
203
+ patchedUpdateRenderState: false,
204
+ detectedChromeVersion: version ? version.join(".") : null
18
205
  };
19
206
  const binding = globalThis.XRWebGLBinding;
20
207
  if (binding && "createProjectionLayer" in binding.prototype) {
@@ -26,7 +213,9 @@ function applyChromiumProjectionLayerWorkaround() {
26
213
  delete renderState.prototype.layers;
27
214
  result.deletedRenderStateLayers = true;
28
215
  }
216
+ if (needsBaseLayerPersistence(userAgent)) result.patchedUpdateRenderState = patchUpdateRenderStateForBaseLayerPersistence();
217
+ log.info("applied workaround", result);
29
218
  return result;
30
219
  }
31
220
  //#endregion
32
- export { applyChromiumProjectionLayerWorkaround };
221
+ export { BASELAYER_WINDOW_MAX, BASELAYER_WINDOW_MIN, applyChromiumProjectionLayerWorkaround, needsBaseLayerPersistence, parseChromeVersion };
@@ -1,3 +1,3 @@
1
- import { i as DepthSample } from "../ar-types-Ck8LEq8k.js";
2
- import { i as DepthSamplerConfig, n as DepthSampler, r as DepthSamplerCallbacks, t as DepthInfo } from "../depth-sampler-xg8vna6c.js";
3
- export { DepthInfo, DepthSample, DepthSampler, DepthSamplerCallbacks, DepthSamplerConfig };
1
+ import { i as DepthSample } from "../ar-types-Dan1MVTi.js";
2
+ import { a as wrapXRDepthInfo, i as DepthSamplerConfig, n as DepthSampler, r as DepthSamplerCallbacks, t as DepthInfo } from "../depth-sampler-Bhanfwla.js";
3
+ export { DepthInfo, DepthSample, DepthSampler, DepthSamplerCallbacks, DepthSamplerConfig, wrapXRDepthInfo };