gps-plus-slam-app-framework 1.7.0 → 1.8.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 (233) hide show
  1. package/dist/{accuracy-circles-BtqOP_v1.d.ts → accuracy-circles-DkWBkF5g.d.ts} +16 -6
  2. package/dist/{app-selectors-ikTUQZpe.d.ts → app-selectors-JqydaUMN.d.ts} +1 -1
  3. package/dist/ar/bresenham3d.d.ts +1 -1
  4. package/dist/ar/camera-blit-capture.d.ts +1 -1
  5. package/dist/ar/capability-checker.d.ts +1 -1
  6. package/dist/ar/capture-failure-tracker.d.ts +1 -1
  7. package/dist/ar/chromium-camera-access-workaround.d.ts +1 -1
  8. package/dist/ar/depth-grid-lookup.d.ts +1 -1
  9. package/dist/ar/depth-occluder.d.ts +127 -0
  10. package/dist/ar/depth-occluder.js +299 -0
  11. package/dist/ar/depth-sampler.d.ts +2 -2
  12. package/dist/ar/depth-sampler.js +24 -4
  13. package/dist/ar/depth-unprojection.d.ts +1 -1
  14. package/dist/ar/depth-unprojection.js +52 -21
  15. package/dist/ar/enable-gps-ar.d.ts +1 -1
  16. package/dist/ar/enable-gps-ar.js +1 -1
  17. package/dist/ar/frame-loop.d.ts +1 -1
  18. package/dist/ar/frame-loop.js +12 -2
  19. package/dist/ar/image-capture.d.ts +1 -1
  20. package/dist/ar/image-capture.js +1 -1
  21. package/dist/ar/image-quality.d.ts +1 -1
  22. package/dist/ar/image-quality.js +2 -1
  23. package/dist/ar/index.d.ts +32 -31
  24. package/dist/ar/index.js +5 -4
  25. package/dist/ar/occlusion-mesh-worker.d.ts +58 -0
  26. package/dist/ar/occlusion-mesh-worker.js +129 -0
  27. package/dist/ar/occupancy-grid.d.ts +2 -2
  28. package/dist/ar/occupancy-grid.js +287 -31
  29. package/dist/ar/occupancy-mesher.d.ts +2 -0
  30. package/dist/ar/occupancy-mesher.js +638 -0
  31. package/dist/ar/planar-pnp.d.ts +1 -1
  32. package/dist/ar/qr-debug-view.d.ts +1 -1
  33. package/dist/ar/qr-derived-pose.d.ts +1 -1
  34. package/dist/ar/qr-detection-controller.d.ts +1 -1
  35. package/dist/ar/qr-frontend.d.ts +1 -1
  36. package/dist/ar/qr-pose.d.ts +1 -1
  37. package/dist/ar/qr-size-depth-context.d.ts +2 -2
  38. package/dist/ar/qr-size-measurer.d.ts +1 -1
  39. package/dist/ar/replay-scene.d.ts +1 -1
  40. package/dist/ar/replay-scene.js +1 -1
  41. package/dist/ar/scene-node-names.d.ts +1 -1
  42. package/dist/ar/webxr-nue-basis.d.ts +1 -1
  43. package/dist/ar/webxr-session.d.ts +4 -4
  44. package/dist/ar/webxr-session.js +2 -2
  45. package/dist/ar/xr-camera-texture.d.ts +1 -1
  46. package/dist/ar/xr-error-handler.d.ts +1 -1
  47. package/dist/ar/xr-frame-loop.d.ts +1 -1
  48. package/dist/ar/xr-frame-loop.js +12 -2
  49. package/dist/{ar-MFpFlV7j.js → ar-BqCudtqE.js} +3 -2
  50. package/dist/{ar-world-group-alignment-69fgnB39.d.ts → ar-world-group-alignment-BlvGcIAk.d.ts} +1 -1
  51. package/dist/{capture-motion-gate-4EscNZCw.js → capture-motion-gate-_NdTSwKi.js} +17 -5
  52. package/dist/cell-key-DIB4rOTV.js +51 -0
  53. package/dist/core/index.d.ts +1 -1
  54. package/dist/{create-slam-app-store-n_VF63D5.d.ts → create-slam-app-store-D3nzh9Wb.d.ts} +4 -4
  55. package/dist/{create-slam-app-store-DkCHOpvb.js → create-slam-app-store-DXiAnJwR.js} +9 -2
  56. package/dist/{depth-grid-lookup-DQ-IYhO3.d.ts → depth-grid-lookup-D5atpNZ3.d.ts} +1 -1
  57. package/dist/{depth-sampler-asc8exHt.d.ts → depth-sampler-CwObAwEA.d.ts} +33 -1
  58. package/dist/{depth-unprojection-DjsbJW9L.d.ts → depth-unprojection-DRdBU90S.d.ts} +1 -1
  59. package/dist/{enable-gps-ar-DkNhFv4Y.d.ts → enable-gps-ar-DvGzBOrp.d.ts} +4 -4
  60. package/dist/{frame-conversions-DObhj8Wf.d.ts → frame-conversions-C4bnNOEY.d.ts} +1 -1
  61. package/dist/{fused-path-v6eOmLl3.d.ts → fused-path-C5pp0PJU.d.ts} +1 -1
  62. package/dist/geo/h3-proximity.d.ts +1 -1
  63. package/dist/geo/index.d.ts +1 -1
  64. package/dist/{gps-anchor-BeFO2RYa.d.ts → gps-anchor-BjECcYnD.d.ts} +1 -1
  65. package/dist/{gps-ar-pose-sampler-DHMZmYAc.d.ts → gps-ar-pose-sampler-DEGNk2CI.d.ts} +1 -1
  66. package/dist/{gps-event-coordinator-CjWW636x.d.ts → gps-event-coordinator-CHtIkhb7.d.ts} +4 -4
  67. package/dist/{image-capture-CNZDrQXG.d.ts → image-capture-Bp6Pd4Md.d.ts} +3 -3
  68. package/dist/{image-capture-B2-b6tKK.js → image-capture-DVcuqMtX.js} +1 -1
  69. package/dist/{index-BODDUTvD.d.ts → index-DlsOo2IB.d.ts} +1 -1
  70. package/dist/{index-BneSj4_5.d.ts → index-cCQWuYqy.d.ts} +3 -3
  71. package/dist/index.d.ts +82 -80
  72. package/dist/index.js +10 -8
  73. package/dist/{leaflet-map-overlay-CmIyiE8n.d.ts → leaflet-map-overlay-BOZFW5kT.d.ts} +64 -10
  74. package/dist/leaflet-map-overlay-CBWPvaX9.js +452 -0
  75. package/dist/lerp-utils-Qs8OhumS.d.ts +42 -0
  76. package/dist/map-data-D7rpMC6f.js +120 -0
  77. package/dist/{map-data-B4oEehUh.d.ts → map-data-PNfGKGWx.d.ts} +22 -2
  78. package/dist/{map-overlay-draw-ymz1s7fG.d.ts → map-overlay-draw-DiroaJed.d.ts} +1 -1
  79. package/dist/{null-storage-backend-B9rtyDfG.d.ts → null-storage-backend-B1-jlpin.d.ts} +2 -2
  80. package/dist/occlusion-mesh-b28lRJ6S.d.ts +194 -0
  81. package/dist/occupancy-grid-BV7RhB7I.d.ts +194 -0
  82. package/dist/occupancy-mesher-C2ecWhNp.d.ts +100 -0
  83. package/dist/{opfs-storage-3VZh-qH6.d.ts → opfs-storage-CTkxvxN4.d.ts} +5 -0
  84. package/dist/{opfs-storage-backend-JJiUJo-3.d.ts → opfs-storage-backend-D6Z03fML.d.ts} +2 -2
  85. package/dist/{persistence-middleware-DAPTTwB2.d.ts → persistence-middleware-CG-t08X4.d.ts} +1 -1
  86. package/dist/{planar-pnp-my9bqULS.d.ts → planar-pnp-D7bc_rLX.d.ts} +1 -1
  87. package/dist/{qr-debug-view-Brl73HGO.d.ts → qr-debug-view-BJooqbiP.d.ts} +1 -1
  88. package/dist/{qr-derived-pose-CmxTP2m4.d.ts → qr-derived-pose-BwNWn8oV.d.ts} +2 -2
  89. package/dist/{qr-detected-slice-VQssZmQJ.d.ts → qr-detected-slice-TzVb8bij.d.ts} +4 -4
  90. package/dist/{qr-detection-controller-CDurHv8E.d.ts → qr-detection-controller-Bo4XfQTP.d.ts} +3 -3
  91. package/dist/{qr-frontend--fFX25mV.d.ts → qr-frontend-BPURlh3S.d.ts} +1 -1
  92. package/dist/{qr-pose-aggregation-B7tWJq7j.d.ts → qr-pose-aggregation-CypnmjA-.d.ts} +1 -1
  93. package/dist/{qr-size-depth-context-CeAG53fy.d.ts → qr-size-depth-context-apAOmbrZ.d.ts} +2 -2
  94. package/dist/{qr-size-measurer-DQdAsid1.d.ts → qr-size-measurer-DxiMqRUw.d.ts} +3 -3
  95. package/dist/{recording-options-BPCpMHLK.d.ts → recording-options-vASCFzc5.d.ts} +177 -24
  96. package/dist/{recording-replayer-DQKpAGyQ.d.ts → recording-replayer-D0V9WkmY.d.ts} +2 -2
  97. package/dist/{recording-slice-DaxHZvNu.d.ts → recording-slice-DTUtSVgd.d.ts} +2 -2
  98. package/dist/{replay-engine-CmKh_4wf.d.ts → replay-engine-xdrUgWJi.d.ts} +1 -1
  99. package/dist/{replay-scene-DBlDrV_x.d.ts → replay-scene-zJ8SFDjE.d.ts} +3 -3
  100. package/dist/sensors/absolute-orientation.d.ts +1 -1
  101. package/dist/sensors/absolute-orientation.js +23 -6
  102. package/dist/sensors/gps-error-handler.d.ts +1 -1
  103. package/dist/sensors/gps.d.ts +1 -1
  104. package/dist/sensors/index.d.ts +4 -4
  105. package/dist/sensors/permission-checker.d.ts +1 -1
  106. package/dist/state/app-selectors.d.ts +1 -1
  107. package/dist/state/combined-root-state.d.ts +1 -1
  108. package/dist/state/create-slam-app-store.d.ts +1 -1
  109. package/dist/state/create-slam-app-store.js +1 -1
  110. package/dist/state/gps-ar-pose-sampler.d.ts +3 -3
  111. package/dist/state/gps-event-coordinator.d.ts +1 -1
  112. package/dist/state/index.d.ts +19 -19
  113. package/dist/state/index.js +2 -2
  114. package/dist/state/persistence-middleware.d.ts +1 -1
  115. package/dist/state/qr-detected-slice.d.ts +3 -3
  116. package/dist/state/recording-options.d.ts +2 -2
  117. package/dist/state/recording-options.js +134 -15
  118. package/dist/state/recording-replayer.d.ts +2 -2
  119. package/dist/state/recording-replayer.js +1 -1
  120. package/dist/state/recording-slice.d.ts +1 -1
  121. package/dist/state/replay-engine.d.ts +1 -1
  122. package/dist/state/store-subscribers.d.ts +2 -2
  123. package/dist/state/store-subscribers.js +2 -1
  124. package/dist/state/subscribe-to-selector.d.ts +1 -1
  125. package/dist/state/tracking-quality.d.ts +1 -1
  126. package/dist/state/tracking-quality.js +10 -1
  127. package/dist/state/tracking-slice.d.ts +1 -1
  128. package/dist/{state-E51QT_-j.js → state-BeKvn4B-.js} +1 -1
  129. package/dist/storage/file-system-utils.d.ts +1 -1
  130. package/dist/storage/index.d.ts +8 -8
  131. package/dist/storage/null-storage-backend.d.ts +1 -1
  132. package/dist/storage/opfs-storage-backend.d.ts +1 -1
  133. package/dist/storage/opfs-storage.d.ts +1 -1
  134. package/dist/storage/opfs-storage.js +25 -1
  135. package/dist/storage/storage-backend.d.ts +1 -1
  136. package/dist/storage/zip-coverage-embed.d.ts +1 -1
  137. package/dist/storage/zip-export.d.ts +1 -1
  138. package/dist/storage/zip-reader.d.ts +1 -1
  139. package/dist/{storage-backend-BdVLD0dU.d.ts → storage-backend-sEtmM4yW.d.ts} +1 -1
  140. package/dist/{store-subscribers-CA6AJH_8.d.ts → store-subscribers-DScA7tdT.d.ts} +2 -2
  141. package/dist/{subscribe-to-selector-B6EUmQ3I.d.ts → subscribe-to-selector-B9ylM5Oh.d.ts} +1 -1
  142. package/dist/test-utils/browser-mocks.d.ts +1 -1
  143. package/dist/test-utils/zip-round-trip-helpers.d.ts +1 -1
  144. package/dist/{tracking-slice-BuAZxXZ5.d.ts → tracking-slice-DMkHlbny.d.ts} +1 -1
  145. package/dist/types/ar-types.d.ts +1 -1
  146. package/dist/types/geo-types.d.ts +1 -1
  147. package/dist/types/index.d.ts +2 -2
  148. package/dist/utils/concurrency.d.ts +1 -1
  149. package/dist/utils/failure-tracker.d.ts +1 -1
  150. package/dist/utils/format-file-size.d.ts +1 -1
  151. package/dist/utils/fused-path.d.ts +1 -1
  152. package/dist/utils/index.d.ts +7 -7
  153. package/dist/utils/list-formatter.d.ts +1 -1
  154. package/dist/utils/logger.d.ts +1 -1
  155. package/dist/visualization/accuracy-circles.d.ts +1 -1
  156. package/dist/visualization/accuracy-circles.js +17 -8
  157. package/dist/visualization/alignment-lerper.d.ts +1 -1
  158. package/dist/visualization/ar-world-group-alignment.d.ts +1 -1
  159. package/dist/visualization/ar-world-group-alignment.js +1 -1
  160. package/dist/visualization/camera-follower.d.ts +1 -1
  161. package/dist/visualization/css3d-renderer-manager.d.ts +1 -1
  162. package/dist/visualization/frame-conversions.d.ts +1 -1
  163. package/dist/visualization/frustum-visibility.d.ts +1 -1
  164. package/dist/visualization/gps-anchor.d.ts +1 -1
  165. package/dist/visualization/gps-compass-cubes.d.ts +1 -1
  166. package/dist/visualization/gps-event-markers.d.ts +1 -1
  167. package/dist/visualization/gps-event-markers.js +1 -1
  168. package/dist/visualization/hit-test-reticle.d.ts +1 -1
  169. package/dist/visualization/index.d.ts +20 -19
  170. package/dist/visualization/index.js +4 -3
  171. package/dist/visualization/leaflet-map-overlay.d.ts +1 -1
  172. package/dist/visualization/leaflet-map-overlay.js +1 -282
  173. package/dist/visualization/lerp-utils.d.ts +2 -2
  174. package/dist/visualization/lerp-utils.js +27 -3
  175. package/dist/visualization/map-data.d.ts +1 -1
  176. package/dist/visualization/map-data.js +1 -32
  177. package/dist/visualization/map-overlay-draw.d.ts +1 -1
  178. package/dist/visualization/map-overlay-draw.js +24 -2
  179. package/dist/visualization/map-overlay.d.ts +1 -1
  180. package/dist/visualization/occluder-mesh-driver.d.ts +130 -0
  181. package/dist/visualization/occluder-mesh-driver.js +219 -0
  182. package/dist/visualization/occlusion-mesh.d.ts +2 -0
  183. package/dist/visualization/occlusion-mesh.js +437 -0
  184. package/dist/visualization/three-dispose.d.ts +1 -1
  185. package/dist/visualization/vis-colors.d.ts +1 -1
  186. package/dist/{webxr-session-DG-eNfny.js → webxr-session-BCP2XK7k.js} +88 -13
  187. package/dist/{webxr-session-Do5L6G3B.d.ts → webxr-session-cRG2SYAQ.d.ts} +73 -10
  188. package/package.json +4 -4
  189. package/dist/lerp-utils-BLmHhkVe.d.ts +0 -20
  190. package/dist/occupancy-grid-DnOc-6PU.d.ts +0 -82
  191. /package/dist/{absolute-orientation-CAEh840t.d.ts → absolute-orientation-BAxc15J6.d.ts} +0 -0
  192. /package/dist/{alignment-lerper-Bil6vbVw.d.ts → alignment-lerper-gD70k7Fq.d.ts} +0 -0
  193. /package/dist/{ar-types-BFX2r1wk.d.ts → ar-types-Cc5yvBwn.d.ts} +0 -0
  194. /package/dist/{bresenham3d-CU1Gv2W4.d.ts → bresenham3d-DGc4nIb-.d.ts} +0 -0
  195. /package/dist/{camera-blit-capture-NKT9H5-e.d.ts → camera-blit-capture-BimSdyih.d.ts} +0 -0
  196. /package/dist/{camera-follower-CqJJ7zRj.d.ts → camera-follower-DfBm2J5i.d.ts} +0 -0
  197. /package/dist/{capability-checker-DUQLg6gq.d.ts → capability-checker-BTTpygvZ.d.ts} +0 -0
  198. /package/dist/{capture-failure-tracker-gsm8q7yi.d.ts → capture-failure-tracker-_BVfhpxv.d.ts} +0 -0
  199. /package/dist/{capture-motion-gate-0Yl2NpQG.d.ts → capture-motion-gate-BGoAMkmf.d.ts} +0 -0
  200. /package/dist/{chromium-camera-access-workaround-D4K2uwwQ.d.ts → chromium-camera-access-workaround-C6yJLXz9.d.ts} +0 -0
  201. /package/dist/{concurrency-By0oOTZt.d.ts → concurrency-DPeGBH0X.d.ts} +0 -0
  202. /package/dist/{css3d-renderer-manager-B3kGNtcn.d.ts → css3d-renderer-manager-CPpVtvgs.d.ts} +0 -0
  203. /package/dist/{failure-tracker-BO7M7jF_.d.ts → failure-tracker-CeKf5HIc.d.ts} +0 -0
  204. /package/dist/{file-system-utils-DOajnMm-.d.ts → file-system-utils-DueXHxo9.d.ts} +0 -0
  205. /package/dist/{format-file-size-Dc98ulzB.d.ts → format-file-size-DxN-EBLc.d.ts} +0 -0
  206. /package/dist/{frame-loop-CpVjQ1dH.d.ts → frame-loop-7GBMBu68.d.ts} +0 -0
  207. /package/dist/{frustum-visibility-CeoCZchO.d.ts → frustum-visibility-CokkgS_i.d.ts} +0 -0
  208. /package/dist/{geo-types-3Ip1NzVR.d.ts → geo-types-DrGVu8nS.d.ts} +0 -0
  209. /package/dist/{gps-BBO3hKfT.d.ts → gps-CDKDQokE.d.ts} +0 -0
  210. /package/dist/{gps-compass-cubes-DSN87L8p.d.ts → gps-compass-cubes-1mooS3BS.d.ts} +0 -0
  211. /package/dist/{gps-error-handler-BbPsfw-_.d.ts → gps-error-handler-C34GQuTu.d.ts} +0 -0
  212. /package/dist/{gps-event-markers-Cz9RMA2k.d.ts → gps-event-markers-Di2nBzx-.d.ts} +0 -0
  213. /package/dist/{h3-proximity-C-j0pw1j.d.ts → h3-proximity-D0GTHQmr.d.ts} +0 -0
  214. /package/dist/{hit-test-reticle-DV-2TpB9.d.ts → hit-test-reticle-CaQbrhbk.d.ts} +0 -0
  215. /package/dist/{image-quality-smCBoFj1.d.ts → image-quality-BMx2ko9-.d.ts} +0 -0
  216. /package/dist/{index-_Rhgoqkh.d.ts → index-D4vrgV4b.d.ts} +0 -0
  217. /package/dist/{index-BAPokxMb.d.ts → index-D74JTX4I.d.ts} +0 -0
  218. /package/dist/{list-formatter-KNMS0ay2.d.ts → list-formatter-DLFDnS70.d.ts} +0 -0
  219. /package/dist/{logger-CLnPrHiD.d.ts → logger-CqTd5Kpk.d.ts} +0 -0
  220. /package/dist/{map-overlay-Dh2B97MP.d.ts → map-overlay-BCQgvIgU.d.ts} +0 -0
  221. /package/dist/{permission-checker-CDuI8BwC.d.ts → permission-checker-B-VtjnBA.d.ts} +0 -0
  222. /package/dist/{qr-pose-D2rfu5DA.d.ts → qr-pose-y3vaEd6x.d.ts} +0 -0
  223. /package/dist/{scene-node-names-BiShOQDB.d.ts → scene-node-names-CGb-ylBd.d.ts} +0 -0
  224. /package/dist/{session-disposers-zv9OGXPB.js → session-disposers-BPrMplIC.js} +0 -0
  225. /package/dist/{three-dispose-Bt1k7WKh.d.ts → three-dispose-BMOAmPhb.d.ts} +0 -0
  226. /package/dist/{vis-colors-Obw2pLqT.d.ts → vis-colors-C7HI0u9_.d.ts} +0 -0
  227. /package/dist/{webxr-nue-basis-3nfjDJJp.d.ts → webxr-nue-basis-CaCnYxuq.d.ts} +0 -0
  228. /package/dist/{xr-camera-texture-Cva1q3Qj.d.ts → xr-camera-texture-q0eIDSRg.d.ts} +0 -0
  229. /package/dist/{xr-error-handler-CtvHpjfm.d.ts → xr-error-handler-uVGDu9IX.d.ts} +0 -0
  230. /package/dist/{xr-frame-loop-DIYtRcUa.d.ts → xr-frame-loop-DX3JPpAU.d.ts} +0 -0
  231. /package/dist/{zip-coverage-embed-DCAREVmw.d.ts → zip-coverage-embed-ncVbaZAz.d.ts} +0 -0
  232. /package/dist/{zip-export-ol0S_Ofd.d.ts → zip-export-CtiUnAlJ.d.ts} +0 -0
  233. /package/dist/{zip-reader-D9iPQwdQ.d.ts → zip-reader-CvruaGvD.d.ts} +0 -0
@@ -0,0 +1,437 @@
1
+ import { WEBXR_TO_NUE } from "../ar/webxr-nue-basis.js";
2
+ import { meshOccupiedCells } from "../ar/occupancy-mesher.js";
3
+ import * as THREE from "three";
4
+ //#region ../src/visualization/occlusion-mesh.ts
5
+ /**
6
+ * Persistent occlusion mesh — a depth-only `THREE.Mesh` of the occupancy grid.
7
+ *
8
+ * Wraps the pure {@link meshOccupiedCells} (face-culled voxel surface) into a
9
+ * THREE object that **writes depth but no color** (`colorWrite = false`,
10
+ * `depthWrite = true`), drawn before virtual content (low `renderOrder`) so real
11
+ * geometry the camera saw earlier hides virtual objects placed behind it —
12
+ * including out-of-view surfaces a single-frame live depth occluder cannot
13
+ * remember (2026-06-13-occupancy-mesh-options-plan.md §4; complements the live
14
+ * occluder in 2026-06-14-webxr-depth-occlusion-plan.md).
15
+ *
16
+ * Reusable across consumer apps (AnchorStarter / MinimalExample want occlusion
17
+ * too); the recorder only owns the off-by-default toggle + scene wiring.
18
+ *
19
+ * Coordinate space: the grid cells (and therefore the mesh positions) are **raw
20
+ * WebXR**, but the parent `arWorldGroup` is AR-odometry NUE. The mesh carries
21
+ * the constant `WEBXR_TO_NUE` basis change as its own local matrix — identical
22
+ * to `OccupancyCubesVisualizer` — so it rides the `alignment × WEBXR_TO_NUE`
23
+ * chain. The parent node is injected (no `getArWorldGroup()`) to stay testable.
24
+ *
25
+ * Scope: this is a full-rebuild occluder (re-mesh the whole snapshot on
26
+ * `update`). The chunked dirty-remesh perf layer (plan §7) is a follow-on.
27
+ *
28
+ * @see occlusion-mesh.ts.md for detailed documentation
29
+ */
30
+ const MESH_NAME = "occupancy-occluder";
31
+ const DEBUG_MESH_NAME = "occupancy-occluder-debug";
32
+ const DEBUG_WIREFRAME_MESH_NAME = "occupancy-occluder-debug-wireframe";
33
+ /** Default render order — well before virtual content (which is ≥ 0). */
34
+ const DEFAULT_RENDER_ORDER = -1;
35
+ /** Opacity of the matcap debug skin — see-through enough to read the real scene
36
+ * behind it while the shape stays legible. */
37
+ const DEBUG_SKIN_OPACITY = .6;
38
+ /** Light-cyan GL lines of the wireframe skin — faint enough not to swamp the
39
+ * shaded skin in the combined style, visible enough to read triangle density. */
40
+ const WIREFRAME_COLOR = 11202303;
41
+ const WIREFRAME_OPACITY = .35;
42
+ /** Which styles shade with the matcap (and therefore need vertex normals —
43
+ * pure 'wireframe' is unlit and keeps the remesh path normal-free). */
44
+ function styleNeedsNormals(style) {
45
+ return style === "matcap" || style === "depth-shaded" || style === "depth-shaded-wireframe";
46
+ }
47
+ /**
48
+ * Depth-shaded debug-skin constants — **module constants, not user settings**
49
+ * (tune in code after the first on-device look; 2026-07-02 debug-viz-styles
50
+ * plan). Exported so the unit/property tests of the GLSL-mirror curves below
51
+ * pin the same numbers the shader bakes in.
52
+ */
53
+ const OCCLUDER_DEPTH_SHADE = {
54
+ /** Distance (m) where the fade starts — nearer stays full-bright cyan.
55
+ * Indoor-scale since the 2026-07-03 field pass (finding F1): the original
56
+ * 1.5 → 10 m range kept a whole room in the "near" band (94% brightness at
57
+ * 3 m), making depth-shaded indistinguishable from matcap on device. */
58
+ FADE_START_M: .75,
59
+ /** Distance (m) where the fade bottoms out at {@link FADE_MIN_BRIGHTNESS}.
60
+ * ~Room depth, so indoor scenes span the full near→far gradient; beyond it
61
+ * everything sits uniformly at the floor (still separated from near). */
62
+ FADE_END_M: 4.5,
63
+ /** Brightness floor far away — never fades to black, so distant mesh stays
64
+ * inspectable (the reason the fog-to-background idea was rejected). */
65
+ FADE_MIN_BRIGHTNESS: .3,
66
+ /** Fresnel exponent — higher hugs the silhouette tighter. */
67
+ RIM_POWER: 2.5,
68
+ /** White-rim contribution at a fully grazing view angle. */
69
+ RIM_STRENGTH: .6
70
+ };
71
+ /** Far-distance fade target (dark desaturated blue) — keeps the occluder's
72
+ * single-color cyan identity while clearly separating far from near. */
73
+ const FADE_TARGET_RGB = [
74
+ .03,
75
+ .07,
76
+ .12
77
+ ];
78
+ /** GLSL `smoothstep` (clamped Hermite), mirrored exactly for the TS curves. */
79
+ function smoothstep(edge0, edge1, x) {
80
+ const t = Math.min(1, Math.max(0, (x - edge0) / (edge1 - edge0)));
81
+ return t * t * (3 - 2 * t);
82
+ }
83
+ /**
84
+ * Pure TS mirror of the shader's **distance fade**: 1 at/inside FADE_START_M,
85
+ * smoothly down to FADE_MIN_BRIGHTNESS at/after FADE_END_M. The fragment color
86
+ * is `mix(darkBlue, matcapColor, fade)` — near = bright cyan, far = dark blue.
87
+ * Mirrored so the curve is unit-testable without a GPU (the
88
+ * `buildFullscreenOcclusionShader` GLSL-mirror precedent).
89
+ */
90
+ function occluderDepthFade(distanceM) {
91
+ const { FADE_START_M, FADE_END_M, FADE_MIN_BRIGHTNESS } = OCCLUDER_DEPTH_SHADE;
92
+ return 1 - (1 - FADE_MIN_BRIGHTNESS) * smoothstep(FADE_START_M, FADE_END_M, distanceM);
93
+ }
94
+ /**
95
+ * Pure TS mirror of the shader's **fresnel rim**: 0 where the surface faces
96
+ * the camera head-on (|cos| = 1), RIM_STRENGTH at a fully grazing angle
97
+ * (cos = 0) — silhouettes brighten, so overlapping shells read as separate.
98
+ *
99
+ * @param cosViewAngle `dot(normal, viewDir)` — cosine of the angle between the
100
+ * surface normal and the view direction, in [-1, 1].
101
+ */
102
+ function occluderFresnelRim(cosViewAngle) {
103
+ const { RIM_POWER, RIM_STRENGTH } = OCCLUDER_DEPTH_SHADE;
104
+ return RIM_STRENGTH * Math.pow(1 - Math.abs(cosViewAngle), RIM_POWER);
105
+ }
106
+ /** A number as a GLSL float literal (fixed decimals so it never reads as int). */
107
+ function glslFloat(value) {
108
+ return value.toFixed(4);
109
+ }
110
+ /**
111
+ * The fragment-shader snippet the depth-shaded material injects before
112
+ * `#include <opaque_fragment>` in three.js's matcap fragment shader — the
113
+ * point where `outgoingLight` (matcap-lit color), `normal` and `viewDir` /
114
+ * `vViewPosition` are all in scope. Exported so tests can pin the exact
115
+ * injected code. Constants are baked as literals (no uniforms — they are
116
+ * module constants by design, see {@link OCCLUDER_DEPTH_SHADE}).
117
+ */
118
+ function buildOccluderDepthShadeSnippet() {
119
+ const { FADE_START_M, FADE_END_M, FADE_MIN_BRIGHTNESS, RIM_POWER } = OCCLUDER_DEPTH_SHADE;
120
+ const { RIM_STRENGTH } = OCCLUDER_DEPTH_SHADE;
121
+ const [r, g, b] = FADE_TARGET_RGB;
122
+ return [
123
+ "// Occluder debug depth-shading: distance fade + fresnel rim (2026-07-02).",
124
+ `float dbgDistance = length( vViewPosition );`,
125
+ `float dbgFade = 1.0 - ( 1.0 - ${glslFloat(FADE_MIN_BRIGHTNESS)} ) * smoothstep( ${glslFloat(FADE_START_M)}, ${glslFloat(FADE_END_M)}, dbgDistance );`,
126
+ `outgoingLight = mix( vec3( ${glslFloat(r)}, ${glslFloat(g)}, ${glslFloat(b)} ), outgoingLight, dbgFade );`,
127
+ `float dbgRim = ${glslFloat(RIM_STRENGTH)} * pow( 1.0 - abs( dot( normal, viewDir ) ), ${glslFloat(RIM_POWER)} );`,
128
+ `outgoingLight += vec3( 1.0 ) * dbgRim;`
129
+ ].join("\n ");
130
+ }
131
+ /**
132
+ * Build a tiny procedural matcap texture (a shaded sphere with a specular
133
+ * highlight) so the debug skin reads as a **shiny** surface with **no scene
134
+ * lights** — `MeshMatcapMaterial` bakes the lighting into this lookup. Generated
135
+ * from a typed array (no canvas/WebGL), so it works headless in tests too.
136
+ */
137
+ function createOccluderDebugMatcap() {
138
+ const size = 64;
139
+ const data = new Uint8Array(size * size * 4);
140
+ const lx = .4;
141
+ const ly = .5;
142
+ const lz = .75;
143
+ const llen = Math.hypot(lx, ly, lz);
144
+ for (let j = 0; j < size; j++) for (let i = 0; i < size; i++) {
145
+ const idx = (j * size + i) * 4;
146
+ const nx = (i + .5) / size * 2 - 1;
147
+ const ny = (j + .5) / size * 2 - 1;
148
+ const r2 = nx * nx + ny * ny;
149
+ let r = 12;
150
+ let g = 12;
151
+ let b = 14;
152
+ if (r2 <= 1) {
153
+ const nz = Math.sqrt(1 - r2);
154
+ const ndl = Math.max(0, (nx * lx + ny * ly + nz * lz) / llen);
155
+ const diff = .25 + .75 * ndl;
156
+ const spec = Math.pow(ndl, 32);
157
+ r = Math.min(255, 255 * (.15 * diff + spec));
158
+ g = Math.min(255, 255 * (.55 * diff + spec));
159
+ b = Math.min(255, 255 * (.78 * diff + spec));
160
+ }
161
+ data[idx] = r;
162
+ data[idx + 1] = g;
163
+ data[idx + 2] = b;
164
+ data[idx + 3] = 255;
165
+ }
166
+ const tex = new THREE.DataTexture(data, size, size, THREE.RGBAFormat);
167
+ tex.needsUpdate = true;
168
+ return tex;
169
+ }
170
+ /**
171
+ * A depth-only occlusion mesh that rebuilds from an occupancy-grid snapshot.
172
+ * Mirrors {@link OccupancyCubesVisualizer}'s lifecycle (inject parent, `update`,
173
+ * `clear`, `dispose`) so the recorder can wire it the same way as the cubes.
174
+ */
175
+ var OcclusionMesh = class {
176
+ arSpaceNode;
177
+ greedy;
178
+ mode;
179
+ material;
180
+ mesh;
181
+ geometry;
182
+ lastAabbs = [];
183
+ disposed = false;
184
+ debugStyle = "off";
185
+ shadedSkin = null;
186
+ wireframeSkin = null;
187
+ matcapTexture = null;
188
+ matcapMaterial = null;
189
+ depthShadedMaterial = null;
190
+ wireframeMaterial = null;
191
+ /**
192
+ * @param arSpaceNode the AR-odometry-NUE node that receives the alignment
193
+ * matrix (`arWorldGroup` live, `replaySceneState.arWorldGroup` in replay).
194
+ */
195
+ constructor(arSpaceNode, options = {}) {
196
+ this.arSpaceNode = arSpaceNode;
197
+ this.greedy = options.greedy ?? true;
198
+ this.mode = options.mode;
199
+ this.geometry = new THREE.BufferGeometry();
200
+ this.material = new THREE.MeshBasicMaterial({
201
+ colorWrite: false,
202
+ depthWrite: true
203
+ });
204
+ this.mesh = new THREE.Mesh(this.geometry, this.material);
205
+ this.mesh.name = MESH_NAME;
206
+ this.mesh.renderOrder = options.renderOrder ?? DEFAULT_RENDER_ORDER;
207
+ this.mesh.frustumCulled = false;
208
+ this.mesh.matrixAutoUpdate = false;
209
+ this.mesh.matrix.copy(WEBXR_TO_NUE);
210
+ this.arSpaceNode.add(this.mesh);
211
+ }
212
+ /** The number of triangles currently drawn. */
213
+ getTriangleCount() {
214
+ const index = this.geometry.getIndex();
215
+ return index ? index.count / 3 : 0;
216
+ }
217
+ /** The AABB list from the most recent {@link update} (physics export hook). */
218
+ getAabbs() {
219
+ return this.lastAabbs;
220
+ }
221
+ /**
222
+ * Re-mesh from a fresh occupied-cell snapshot. Pass
223
+ * `grid.getOccupiedCells(occupancy.minConfidence)` so the occluder shares the
224
+ * same noise floor as the cubes and the COLMAP export.
225
+ *
226
+ * @param getCellPoint optional per-cell measured-centroid provider
227
+ * (`grid.getCellPoint`); only consumed when this occluder was constructed
228
+ * with `mode: 'smooth'` (otherwise ignored). When omitted under `'smooth'`,
229
+ * the surface nets falls back to cell centres.
230
+ */
231
+ update(cells, cellSizeM, getCellPoint) {
232
+ if (this.disposed) return;
233
+ const { positions, indices, aabbs } = meshOccupiedCells(cells, cellSizeM, this.mode ? {
234
+ mode: this.mode,
235
+ getCellPoint
236
+ } : { greedy: this.greedy });
237
+ this.lastAabbs = aabbs;
238
+ this.swapGeometry(positions, indices);
239
+ }
240
+ /**
241
+ * Apply **precomputed** geometry (positions/indices) without meshing — the
242
+ * entry point for the Web Worker offload: the driver posts the occupied-cell
243
+ * snapshot to a worker (`packMeshRequest`/`runMeshRequest`), and this swaps in
244
+ * the returned typed arrays off the render-critical path. Byte-identical result
245
+ * to {@link update} for the same input.
246
+ *
247
+ * The AABB physics hook is NOT populated on this path (the worker returns
248
+ * geometry only; AABBs are an export hook unused by rendering) — {@link getAabbs}
249
+ * returns empty here. Use the synchronous {@link update} if AABBs are needed.
250
+ */
251
+ applyMeshData(positions, indices) {
252
+ if (this.disposed) return;
253
+ this.lastAabbs = [];
254
+ this.swapGeometry(positions, indices);
255
+ }
256
+ /**
257
+ * Replace the geometry wholesale from typed arrays — a full rebuild is the
258
+ * simple first cut; dispose the old buffers to avoid leaking GPU memory across
259
+ * refreshes.
260
+ */
261
+ swapGeometry(positions, indices) {
262
+ const next = new THREE.BufferGeometry();
263
+ next.setAttribute("position", new THREE.BufferAttribute(positions, 3));
264
+ next.setIndex(new THREE.BufferAttribute(indices, 1));
265
+ if (styleNeedsNormals(this.debugStyle)) next.computeVertexNormals();
266
+ this.geometry.dispose();
267
+ this.geometry = next;
268
+ this.mesh.geometry = next;
269
+ this.rebindSkinGeometry(next);
270
+ }
271
+ /** Point every active debug skin at the (new) shared geometry. */
272
+ rebindSkinGeometry(geometry) {
273
+ if (this.shadedSkin) this.shadedSkin.geometry = geometry;
274
+ if (this.wireframeSkin) this.wireframeSkin.geometry = geometry;
275
+ }
276
+ /**
277
+ * Select which **visible debug skin(s)** render the occluder mesh so its
278
+ * shape and structure can be judged on-device (2026-07-02 debug-viz-styles
279
+ * plan): the original `'matcap'` skin, a `'depth-shaded'` variant (distance
280
+ * fade + fresnel rim, separates overlapping layers), a `'wireframe'` overlay
281
+ * (the raw triangulation as GL lines), the combined
282
+ * `'depth-shaded-wireframe'`, or `'off'`.
283
+ *
284
+ * Additive by design: every style adds/removes separate skin meshes sharing
285
+ * the occluder's geometry and **never touches the invisible depth-only
286
+ * mesh**, so occlusion is byte-for-byte unchanged whichever style is active.
287
+ * The skins are `transparent` with `depthWrite:false` (the depth-only mesh
288
+ * already wrote the occluding depth), so they just paint where the occluder
289
+ * is the nearest geometry; the wireframe draws at renderOrder 1 so its lines
290
+ * overlay the shaded surface in the combined style.
291
+ *
292
+ * Vertex normals (the mesher emits none) are computed only for the
293
+ * matcap-based styles — `'wireframe'` is unlit, so like `'off'` it keeps the
294
+ * remesh path normal-free.
295
+ *
296
+ * Only meaningful when this occluder is actually meshing the grid (it is the
297
+ * persistent occluder's mesh); setting a style on an empty/disabled occluder
298
+ * is a harmless no-op until {@link update} feeds geometry.
299
+ */
300
+ setDebugStyle(style) {
301
+ if (this.disposed || style === this.debugStyle) return;
302
+ this.debugStyle = style;
303
+ this.applyShadedSkin(this.shadedMaterialFor(style));
304
+ this.applyWireframeSkin(style === "wireframe" || style === "depth-shaded-wireframe");
305
+ }
306
+ /** Which matcap-based material the style shades with (null = no shaded skin). */
307
+ shadedMaterialFor(style) {
308
+ if (style === "matcap") return this.getMatcapMaterial();
309
+ if (style === "depth-shaded" || style === "depth-shaded-wireframe") return this.getDepthShadedMaterial();
310
+ return null;
311
+ }
312
+ /** Add/retarget/remove the single shaded skin node for the desired material. */
313
+ applyShadedSkin(material) {
314
+ if (material) {
315
+ this.geometry.computeVertexNormals();
316
+ if (this.shadedSkin) this.shadedSkin.material = material;
317
+ else {
318
+ this.shadedSkin = this.createSkinMesh(DEBUG_MESH_NAME, 0, material);
319
+ this.arSpaceNode.add(this.shadedSkin);
320
+ }
321
+ } else if (this.shadedSkin) {
322
+ this.arSpaceNode.remove(this.shadedSkin);
323
+ this.shadedSkin = null;
324
+ }
325
+ }
326
+ /** Add/remove the wireframe skin — drawn after the shaded skin (renderOrder
327
+ * 1) so its lines sit on top of the surface in the combined style. */
328
+ applyWireframeSkin(want) {
329
+ if (want) {
330
+ if (!this.wireframeSkin) {
331
+ this.wireframeSkin = this.createSkinMesh(DEBUG_WIREFRAME_MESH_NAME, 1, this.getWireframeMaterial());
332
+ this.arSpaceNode.add(this.wireframeSkin);
333
+ }
334
+ } else if (this.wireframeSkin) {
335
+ this.arSpaceNode.remove(this.wireframeSkin);
336
+ this.wireframeSkin = null;
337
+ }
338
+ }
339
+ /**
340
+ * Toggle the matcap debug rendering of the occluder mesh.
341
+ *
342
+ * @deprecated Superseded by {@link setDebugStyle} (2026-07-02) — this is a
343
+ * thin wrapper mapping `enabled ? 'matcap' : 'off'`, kept so existing
344
+ * consumers of the boolean API keep working unchanged.
345
+ */
346
+ setDebugVisualization(enabled) {
347
+ this.setDebugStyle(enabled ? "matcap" : "off");
348
+ }
349
+ /** A debug skin mesh sharing the occluder's geometry, transform and culling
350
+ * behaviour — only name, renderOrder and material differ per skin. */
351
+ createSkinMesh(name, renderOrder, material) {
352
+ const skin = new THREE.Mesh(this.geometry, material);
353
+ skin.name = name;
354
+ skin.renderOrder = renderOrder;
355
+ skin.frustumCulled = false;
356
+ skin.matrixAutoUpdate = false;
357
+ skin.matrix.copy(WEBXR_TO_NUE);
358
+ return skin;
359
+ }
360
+ /** The procedural matcap texture, shared by both matcap-based materials. */
361
+ getMatcapTexture() {
362
+ if (!this.matcapTexture) this.matcapTexture = createOccluderDebugMatcap();
363
+ return this.matcapTexture;
364
+ }
365
+ getMatcapMaterial() {
366
+ if (!this.matcapMaterial) this.matcapMaterial = new THREE.MeshMatcapMaterial({
367
+ matcap: this.getMatcapTexture(),
368
+ transparent: true,
369
+ opacity: DEBUG_SKIN_OPACITY,
370
+ depthWrite: false
371
+ });
372
+ return this.matcapMaterial;
373
+ }
374
+ getDepthShadedMaterial() {
375
+ if (!this.depthShadedMaterial) {
376
+ const material = new THREE.MeshMatcapMaterial({
377
+ matcap: this.getMatcapTexture(),
378
+ transparent: true,
379
+ opacity: DEBUG_SKIN_OPACITY,
380
+ depthWrite: false
381
+ });
382
+ material.onBeforeCompile = (shader) => {
383
+ shader.fragmentShader = shader.fragmentShader.replace("#include <opaque_fragment>", `${buildOccluderDepthShadeSnippet()}\n\t#include <opaque_fragment>`);
384
+ };
385
+ material.customProgramCacheKey = () => "occluder-depth-shaded";
386
+ this.depthShadedMaterial = material;
387
+ }
388
+ return this.depthShadedMaterial;
389
+ }
390
+ getWireframeMaterial() {
391
+ if (!this.wireframeMaterial) this.wireframeMaterial = new THREE.MeshBasicMaterial({
392
+ wireframe: true,
393
+ color: WIREFRAME_COLOR,
394
+ transparent: true,
395
+ opacity: WIREFRAME_OPACITY,
396
+ depthWrite: false
397
+ });
398
+ return this.wireframeMaterial;
399
+ }
400
+ /** Empty the mesh (e.g. on store swap); the node stays in the scene. */
401
+ clear() {
402
+ if (this.disposed) return;
403
+ const next = new THREE.BufferGeometry();
404
+ this.geometry.dispose();
405
+ this.geometry = next;
406
+ this.mesh.geometry = next;
407
+ this.rebindSkinGeometry(next);
408
+ this.lastAabbs = [];
409
+ }
410
+ /** Remove the mesh from its parent and release GPU resources. */
411
+ dispose() {
412
+ if (this.disposed) return;
413
+ this.disposed = true;
414
+ this.arSpaceNode.remove(this.mesh);
415
+ if (this.shadedSkin) {
416
+ this.arSpaceNode.remove(this.shadedSkin);
417
+ this.shadedSkin = null;
418
+ }
419
+ if (this.wireframeSkin) {
420
+ this.arSpaceNode.remove(this.wireframeSkin);
421
+ this.wireframeSkin = null;
422
+ }
423
+ this.matcapTexture?.dispose();
424
+ this.matcapTexture = null;
425
+ this.matcapMaterial?.dispose();
426
+ this.matcapMaterial = null;
427
+ this.depthShadedMaterial?.dispose();
428
+ this.depthShadedMaterial = null;
429
+ this.wireframeMaterial?.dispose();
430
+ this.wireframeMaterial = null;
431
+ this.geometry.dispose();
432
+ this.material.dispose();
433
+ this.lastAabbs = [];
434
+ }
435
+ };
436
+ //#endregion
437
+ export { OCCLUDER_DEPTH_SHADE, OcclusionMesh, buildOccluderDepthShadeSnippet, occluderDepthFade, occluderFresnelRim };
@@ -1,2 +1,2 @@
1
- import { n as disposeMeshArray, r as disposeObject3D, t as DisposeOptions } from "../three-dispose-Bt1k7WKh.js";
1
+ import { n as disposeMeshArray, r as disposeObject3D, t as DisposeOptions } from "../three-dispose-BMOAmPhb.js";
2
2
  export { DisposeOptions, disposeMeshArray, disposeObject3D };
@@ -1,2 +1,2 @@
1
- import { t as VIS_COLORS } from "../vis-colors-Obw2pLqT.js";
1
+ import { t as VIS_COLORS } from "../vis-colors-C7HI0u9_.js";
2
2
  export { VIS_COLORS };
@@ -5,10 +5,10 @@ import { DepthSampler, wrapXRDepthInfo } from "./ar/depth-sampler.js";
5
5
  import { WEBXR_TO_NUE } from "./ar/webxr-nue-basis.js";
6
6
  import { clearFrameUpdates, runFrameUpdates } from "./ar/frame-loop.js";
7
7
  import { clearXrFrameUpdates, runXrFrameUpdates } from "./ar/xr-frame-loop.js";
8
- import { n as ImageCaptureManager, t as DEFAULT_CAPTURE_CONFIG } from "./image-capture-B2-b6tKK.js";
8
+ import { n as ImageCaptureManager, t as DEFAULT_CAPTURE_CONFIG } from "./image-capture-DVcuqMtX.js";
9
9
  import { clearLastRestartedPayload, originReset, poseLost, poseReceived, resetTracking, selectLastRestartedPayload, selectTrackingPhase } from "./state/tracking-slice.js";
10
10
  import { acquireCameraTexture } from "./ar/xr-camera-texture.js";
11
- import { n as runSessionDisposers } from "./session-disposers-zv9OGXPB.js";
11
+ import { n as runSessionDisposers } from "./session-disposers-BPrMplIC.js";
12
12
  import { DEFAULT_RECORDING_OPTIONS, validateArCrashIsolationOptions } from "./state/recording-options.js";
13
13
  import { SCENE_NODE } from "./ar/scene-node-names.js";
14
14
  import { createCss3dRendererManager } from "./visualization/css3d-renderer-manager.js";
@@ -225,6 +225,8 @@ function resetWebXRState() {
225
225
  onTrackingRestarted = null;
226
226
  onTrackingLost = null;
227
227
  onTrackingRecovered = null;
228
+ onSessionEnd = null;
229
+ endRequestedByApp = false;
228
230
  depthSampler = null;
229
231
  onDepthCaptured = null;
230
232
  onDepthUnavailable = null;
@@ -329,6 +331,20 @@ let onTrackingLost = null;
329
331
  */
330
332
  let onTrackingRecovered = null;
331
333
  /**
334
+ * Host callback fired exactly once whenever the XRSession ends — on BOTH the
335
+ * app-initiated and the system-initiated path (set via setSessionEndCallback).
336
+ * Cleared by resetWebXRState() like every other module-level callback.
337
+ */
338
+ let onSessionEnd = null;
339
+ /**
340
+ * Set by endARSession() immediately before its `xrSession.end()` so the
341
+ * shared 'end' listener can tell the two trigger paths apart (the app's own
342
+ * end() fires the same 'end' event the system path does). Consumed (reset to
343
+ * false) by handleSessionEnded(); also cleared defensively in
344
+ * resetWebXRState() in case end() rejects without the event ever firing.
345
+ */
346
+ let endRequestedByApp = false;
347
+ /**
332
348
  * Depth sampler instance (created when AR session starts with depth callbacks)
333
349
  */
334
350
  let depthSampler = null;
@@ -563,7 +579,7 @@ function getCurrentArPose() {
563
579
  * @param isolationOptions - Crash-isolation diagnostic flags (DOM overlay,
564
580
  * depth-sensing, camera-access)
565
581
  * @param sessionFeatures - Opt-in standard WebXR features that are independent
566
- * of crash isolation (currently `requestHitTest`)
582
+ * of crash isolation (`requestHitTest`, `requestDepthOcclusion`)
567
583
  * @returns XRSessionInit options
568
584
  * @throws Error if rootElement is null
569
585
  */
@@ -576,7 +592,7 @@ function buildSessionOptions(rootElement, isolationOptions = {}, sessionFeatures
576
592
  optionalFeatures.push("dom-overlay");
577
593
  sessionOptions.domOverlay = { root: rootElement };
578
594
  }
579
- if (normalizedOptions.enableDepthSensingFeature) {
595
+ if (normalizedOptions.enableDepthSensingFeature || sessionFeatures.requestDepthOcclusion) {
580
596
  optionalFeatures.push("depth-sensing");
581
597
  Object.assign(sessionOptions, { depthSensing: {
582
598
  usagePreference: ["cpu-optimized"],
@@ -600,6 +616,23 @@ async function isWebXRSupported() {
600
616
  }
601
617
  }
602
618
  /**
619
+ * AR camera frustum constants — the single source of truth for live AR and
620
+ * replay (both build their camera via {@link createSceneHierarchy}).
621
+ *
622
+ * F2 (2026-07-04 user feedback): far raised 100 → 200 m so objects in the
623
+ * reported 100–200 m range are no longer frustum-culled. The far-plane
624
+ * distance itself is essentially free at this app's object counts; the real
625
+ * constraint is depth precision — far/near = 2×10⁴ is comfortable for a
626
+ * 24-bit depth buffer. Revisit the ratio if AR_CAMERA_NEAR ever shrinks.
627
+ *
628
+ * Note: these apply to WebGL content only. The CSS3D minimap is composited by
629
+ * the browser from the camera fov alone — near/far do not clip it (F1 in the
630
+ * same feedback doc).
631
+ */
632
+ const AR_CAMERA_FOV = 70;
633
+ const AR_CAMERA_NEAR = .01;
634
+ const AR_CAMERA_FAR = 200;
635
+ /**
603
636
  * Create the scene hierarchy with proper AR/GPS frame separation.
604
637
  * This is a pure function for testability.
605
638
  *
@@ -647,7 +680,7 @@ function createSceneHierarchy() {
647
680
  const newArPose = new THREE.Object3D();
648
681
  newArPose.name = "ar-pose";
649
682
  newBasisChangeNode.add(newArPose);
650
- const newCamera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, .01, 100);
683
+ const newCamera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, AR_CAMERA_NEAR, 200);
651
684
  newArPose.add(newCamera);
652
685
  const ambientLight = new THREE.AmbientLight(16777215, .5);
653
686
  newScene.add(ambientLight);
@@ -689,10 +722,7 @@ async function initAR(container, isolationOptions = {}, sessionFeatures = {}) {
689
722
  const sessionOptions = buildSessionOptions(container, currentArCrashIsolationOptions, sessionFeatures);
690
723
  xrSession = await navigator.xr.requestSession("immersive-ar", sessionOptions);
691
724
  xrSession.addEventListener("end", () => {
692
- log.info("Session ended");
693
- if (trackingStore) trackingStore.dispatch(resetTracking());
694
- xrSession = null;
695
- latestArPose = null;
725
+ handleSessionEnded();
696
726
  });
697
727
  await renderer.xr.setSession(xrSession);
698
728
  if (onTrackingRestarted && trackingStore) {
@@ -847,8 +877,15 @@ function onXRFrame(time, frame) {
847
877
  if (currentArCrashIsolationOptions.enableCss3dRenderer && css3dManager) css3dManager.render(scene, camera);
848
878
  }
849
879
  /**
850
- * Extract depth information from an XR frame.
851
- * Returns null if depth sensing is not available.
880
+ * Extract depth information from an XR frame. Returns `null` if depth sensing is
881
+ * not available (no pose/view, no `getDepthInformation`, or the call throws).
882
+ *
883
+ * Exported so a consumer can feed the **live depth occluder** from a
884
+ * `registerXrFrameUpdate` callback — the callback has the live `frame` +
885
+ * `referenceSpace`, computes `pose = frame.getViewerPose(referenceSpace)`, and
886
+ * passes both here to obtain the per-frame {@link DepthInfo} (with the widened
887
+ * `data` / `rawValueToMeters` / `normDepthBufferFromNormView` / `projectionMatrix`
888
+ * the occluder needs). The same wrapped depth the sparse grid sampler consumes.
852
889
  */
853
890
  function getDepthInfoFromFrame(frame, pose) {
854
891
  const view = pose?.views[0];
@@ -984,6 +1021,41 @@ function nueQuaternionToWebXR$1(nue) {
984
1021
  return nueQuaternionToWebXR(nue);
985
1022
  }
986
1023
  /**
1024
+ * Register a host callback fired exactly once whenever the XRSession ends —
1025
+ * app-initiated ({@link endARSession}) AND system-initiated (e.g. the Android
1026
+ * back gesture, which ends an immersive session directly and uncancelably).
1027
+ * `info.requestedByApp` discriminates the two. Fired AFTER the full teardown,
1028
+ * so the host can immediately start a fresh session from inside the callback.
1029
+ *
1030
+ * Cleared by resetWebXRState() (i.e. after every session end) like the other
1031
+ * module-level callbacks — re-register before/with each new session.
1032
+ * Pass `null` to unregister. F3, 2026-07-04 user feedback.
1033
+ */
1034
+ function setSessionEndCallback(cb) {
1035
+ onSessionEnd = cb;
1036
+ }
1037
+ /**
1038
+ * The ONE teardown for a session that has ended, shared by both trigger
1039
+ * paths via the XRSession 'end' listener. Before F3 the system-initiated
1040
+ * path (back gesture) only nulled `xrSession`/`latestArPose`, leaving the
1041
+ * renderer compositing the 3D scene over a black camera background and the
1042
+ * host app never notified — the "haunted scene" from
1043
+ * docs/2026-02-15-lifecycle-orphans.md §1.
1044
+ */
1045
+ function handleSessionEnded() {
1046
+ log.info("Session ended");
1047
+ const callback = onSessionEnd;
1048
+ const requestedByApp = endRequestedByApp;
1049
+ endRequestedByApp = false;
1050
+ if (trackingStore) trackingStore.dispatch(resetTracking());
1051
+ resetWebXRState();
1052
+ if (callback) try {
1053
+ callback({ requestedByApp });
1054
+ } catch (err) {
1055
+ log.error("Session-end callback threw:", err);
1056
+ }
1057
+ }
1058
+ /**
987
1059
  * End the current XR session and clean up all resources.
988
1060
  *
989
1061
  * Stops the animation loop, ends the XR session, then delegates the full
@@ -996,7 +1068,10 @@ function nueQuaternionToWebXR$1(nue) {
996
1068
  async function endARSession() {
997
1069
  if (renderer) renderer.setAnimationLoop(null);
998
1070
  try {
999
- if (xrSession) await xrSession.end();
1071
+ if (xrSession) {
1072
+ endRequestedByApp = true;
1073
+ await xrSession.end();
1074
+ }
1000
1075
  } finally {
1001
1076
  resetWebXRState();
1002
1077
  }
@@ -1284,4 +1359,4 @@ function getLiveCss3dManager() {
1284
1359
  return css3dManager;
1285
1360
  }
1286
1361
  //#endregion
1287
- export { setFrameCallback as A, startDepthCapture as B, nueQuaternionToWebXR$1 as C, setCamera as D, setArWorldGroup as E, setTrackingLostCallback as F, CameraFrameSource as G, stopCameraFrameCapture as H, setTrackingRecoveredCallback as I, setTrackingStore as L, setImageQualityAnalyzer as M, setScene as N, setCameraFrameCallback as O, setTrackingCallbacks as P, shouldLogCameraAccessDiagnostic as R, nuePositionToWebXR as S, setArPose as T, stopDepthCapture as U, startImageCapture as V, stopImageCapture as W, getScene as _, endARSession as a, isWebXRSupported as b, getArPose as c, getCameraFrameCaptureSize as d, getCameraFrameCount as f, getLiveCss3dManager as g, getImageCaptureFrameCount as h, createSceneHierarchy as i, setImageCaptureCallback as j, setDepthCaptureCallback as k, getArWorldGroup as l, getDepthSampleCount as m, applyAlignmentMatrix as n, extractPoseFromViewer as o, getCurrentArPose as p, buildSessionOptions as r, extractResetTransformData as s, DEFAULT_CAMERA_FRAME_CAPTURE_SIZE as t, getCamera as u, getXrCameraFromPose as v, resetWebXRState as w, isXRCameraLike as x, initAR as y, startCameraFrameCapture as z };
1362
+ export { setArWorldGroup as A, setTrackingLostCallback as B, initAR as C, nueQuaternionToWebXR$1 as D, nuePositionToWebXR as E, setImageCaptureCallback as F, startDepthCapture as G, setTrackingStore as H, setImageQualityAnalyzer as I, stopDepthCapture as J, startImageCapture as K, setScene as L, setCameraFrameCallback as M, setDepthCaptureCallback as N, resetWebXRState as O, setFrameCallback as P, setSessionEndCallback as R, getXrCameraFromPose as S, isXRCameraLike as T, shouldLogCameraAccessDiagnostic as U, setTrackingRecoveredCallback as V, startCameraFrameCapture as W, CameraFrameSource as X, stopImageCapture as Y, getDepthInfoFromFrame as _, applyAlignmentMatrix as a, getLiveCss3dManager as b, endARSession as c, getArPose as d, getArWorldGroup as f, getCurrentArPose as g, getCameraFrameCount as h, DEFAULT_CAMERA_FRAME_CAPTURE_SIZE as i, setCamera as j, setArPose as k, extractPoseFromViewer as l, getCameraFrameCaptureSize as m, AR_CAMERA_FOV as n, buildSessionOptions as o, getCamera as p, stopCameraFrameCapture as q, AR_CAMERA_NEAR as r, createSceneHierarchy as s, AR_CAMERA_FAR as t, extractResetTransformData as u, getDepthSampleCount as v, isWebXRSupported as w, getScene as x, getImageCaptureFrameCount as y, setTrackingCallbacks as z };