gps-plus-slam-app-framework 1.4.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 (243) hide show
  1. package/dist/absolute-orientation-BAxc15J6.d.ts +54 -0
  2. package/dist/{accuracy-circles-Pi2YeCrM.d.ts → accuracy-circles-DkWBkF5g.d.ts} +16 -6
  3. package/dist/{app-selectors-CrOucL00.d.ts → app-selectors-JqydaUMN.d.ts} +1 -1
  4. package/dist/ar/bresenham3d.d.ts +1 -1
  5. package/dist/ar/camera-blit-capture.d.ts +1 -1
  6. package/dist/ar/capability-checker.d.ts +1 -1
  7. package/dist/ar/capture-failure-tracker.d.ts +1 -1
  8. package/dist/ar/chromium-camera-access-workaround.d.ts +1 -1
  9. package/dist/ar/depth-grid-lookup.d.ts +1 -1
  10. package/dist/ar/depth-occluder.d.ts +127 -0
  11. package/dist/ar/depth-occluder.js +299 -0
  12. package/dist/ar/depth-sampler.d.ts +2 -2
  13. package/dist/ar/depth-sampler.js +24 -4
  14. package/dist/ar/depth-unprojection.d.ts +1 -1
  15. package/dist/ar/depth-unprojection.js +52 -21
  16. package/dist/ar/enable-gps-ar.d.ts +1 -1
  17. package/dist/ar/enable-gps-ar.js +1 -1
  18. package/dist/ar/frame-loop.d.ts +1 -1
  19. package/dist/ar/frame-loop.js +12 -2
  20. package/dist/ar/image-capture.d.ts +2 -2
  21. package/dist/ar/image-capture.js +1 -154
  22. package/dist/ar/image-quality.d.ts +2 -0
  23. package/dist/ar/image-quality.js +169 -0
  24. package/dist/ar/index.d.ts +32 -30
  25. package/dist/ar/index.js +7 -5
  26. package/dist/ar/occlusion-mesh-worker.d.ts +58 -0
  27. package/dist/ar/occlusion-mesh-worker.js +129 -0
  28. package/dist/ar/occupancy-grid.d.ts +2 -2
  29. package/dist/ar/occupancy-grid.js +287 -31
  30. package/dist/ar/occupancy-mesher.d.ts +2 -0
  31. package/dist/ar/occupancy-mesher.js +638 -0
  32. package/dist/ar/planar-pnp.d.ts +1 -1
  33. package/dist/ar/qr-debug-view.d.ts +1 -1
  34. package/dist/ar/qr-derived-pose.d.ts +1 -1
  35. package/dist/ar/qr-detection-controller.d.ts +1 -1
  36. package/dist/ar/qr-frontend.d.ts +1 -1
  37. package/dist/ar/qr-pose.d.ts +1 -1
  38. package/dist/ar/qr-size-depth-context.d.ts +2 -2
  39. package/dist/ar/qr-size-measurer.d.ts +1 -1
  40. package/dist/ar/replay-scene.d.ts +1 -1
  41. package/dist/ar/replay-scene.js +1 -1
  42. package/dist/ar/scene-node-names.d.ts +1 -1
  43. package/dist/ar/webxr-nue-basis.d.ts +1 -1
  44. package/dist/ar/webxr-session.d.ts +4 -4
  45. package/dist/ar/webxr-session.js +2 -2
  46. package/dist/ar/xr-camera-texture.d.ts +1 -1
  47. package/dist/ar/xr-error-handler.d.ts +1 -1
  48. package/dist/ar/xr-frame-loop.d.ts +1 -1
  49. package/dist/ar/xr-frame-loop.js +12 -2
  50. package/dist/{ar-Dc72Ksiu.js → ar-BqCudtqE.js} +4 -3
  51. package/dist/{ar-world-group-alignment-DDLkgNf_.d.ts → ar-world-group-alignment-BlvGcIAk.d.ts} +1 -1
  52. package/dist/capture-motion-gate-BGoAMkmf.d.ts +24 -0
  53. package/dist/capture-motion-gate-_NdTSwKi.js +106 -0
  54. package/dist/cell-key-DIB4rOTV.js +51 -0
  55. package/dist/core/index.d.ts +2 -2
  56. package/dist/core/index.js +2 -2
  57. package/dist/{create-slam-app-store-Ce2FMKCp.d.ts → create-slam-app-store-D3nzh9Wb.d.ts} +50 -40
  58. package/dist/create-slam-app-store-DXiAnJwR.js +156 -0
  59. package/dist/{depth-grid-lookup-vMDrGTk_.d.ts → depth-grid-lookup-D5atpNZ3.d.ts} +1 -1
  60. package/dist/{depth-sampler-DwfPYzS7.d.ts → depth-sampler-CwObAwEA.d.ts} +33 -1
  61. package/dist/{depth-unprojection-CK3b9htH.d.ts → depth-unprojection-DRdBU90S.d.ts} +1 -1
  62. package/dist/{enable-gps-ar-CjZ1nEdu.d.ts → enable-gps-ar-DvGzBOrp.d.ts} +4 -4
  63. package/dist/{frame-conversions-D_Af8Dt3.d.ts → frame-conversions-C4bnNOEY.d.ts} +1 -1
  64. package/dist/{fused-path-C0FHe67t.d.ts → fused-path-C5pp0PJU.d.ts} +1 -1
  65. package/dist/geo/h3-proximity.d.ts +1 -1
  66. package/dist/geo/index.d.ts +1 -1
  67. package/dist/geodesic-angle-BTldfFgs.js +39 -0
  68. package/dist/{gps-anchor-C6I9NDHA.d.ts → gps-anchor-BjECcYnD.d.ts} +1 -1
  69. package/dist/{gps-ar-pose-sampler-DaHg2aPZ.d.ts → gps-ar-pose-sampler-DEGNk2CI.d.ts} +1 -1
  70. package/dist/{gps-event-coordinator-C7hwHyLA.d.ts → gps-event-coordinator-CHtIkhb7.d.ts} +21 -11
  71. package/dist/{image-capture-C2Feywet.d.ts → image-capture-Bp6Pd4Md.d.ts} +103 -3
  72. package/dist/image-capture-DVcuqMtX.js +374 -0
  73. package/dist/image-quality-BMx2ko9-.d.ts +158 -0
  74. package/dist/index-D4vrgV4b.d.ts +2 -0
  75. package/dist/index-D74JTX4I.d.ts +22 -0
  76. package/dist/{index-m_LneUSi.d.ts → index-DlsOo2IB.d.ts} +1 -1
  77. package/dist/{index-BvLo2poy.d.ts → index-cCQWuYqy.d.ts} +3 -3
  78. package/dist/index.d.ts +82 -77
  79. package/dist/index.js +15 -10
  80. package/dist/{leaflet-map-overlay-bnb2U1NQ.d.ts → leaflet-map-overlay-BOZFW5kT.d.ts} +64 -10
  81. package/dist/leaflet-map-overlay-CBWPvaX9.js +452 -0
  82. package/dist/lerp-utils-Qs8OhumS.d.ts +42 -0
  83. package/dist/map-data-D7rpMC6f.js +120 -0
  84. package/dist/{map-data-DdjPiSav.d.ts → map-data-PNfGKGWx.d.ts} +22 -2
  85. package/dist/{map-overlay-draw-CPVdAsFq.d.ts → map-overlay-draw-DiroaJed.d.ts} +1 -1
  86. package/dist/{null-storage-backend-CuSdYFsM.d.ts → null-storage-backend-B1-jlpin.d.ts} +2 -2
  87. package/dist/occlusion-mesh-b28lRJ6S.d.ts +194 -0
  88. package/dist/occupancy-grid-BV7RhB7I.d.ts +194 -0
  89. package/dist/occupancy-mesher-C2ecWhNp.d.ts +100 -0
  90. package/dist/{opfs-storage-DMtlt57S.d.ts → opfs-storage-CTkxvxN4.d.ts} +5 -0
  91. package/dist/{opfs-storage-backend-CeQAb0SD.d.ts → opfs-storage-backend-D6Z03fML.d.ts} +2 -2
  92. package/dist/{persistence-middleware-5eearhSV.d.ts → persistence-middleware-CG-t08X4.d.ts} +1 -1
  93. package/dist/{planar-pnp-cjthe7Be.d.ts → planar-pnp-D7bc_rLX.d.ts} +1 -1
  94. package/dist/{qr-debug-view-B8gTzjMm.d.ts → qr-debug-view-BJooqbiP.d.ts} +1 -1
  95. package/dist/{qr-derived-pose-DinKE600.d.ts → qr-derived-pose-BwNWn8oV.d.ts} +2 -2
  96. package/dist/{qr-detected-slice-Cpv-dg82.d.ts → qr-detected-slice-TzVb8bij.d.ts} +4 -4
  97. package/dist/{qr-detection-controller-Cd2Ih4cC.d.ts → qr-detection-controller-Bo4XfQTP.d.ts} +3 -3
  98. package/dist/{qr-frontend-DufZp2xS.d.ts → qr-frontend-BPURlh3S.d.ts} +1 -1
  99. package/dist/{qr-pose-aggregation-CFlmLm6w.d.ts → qr-pose-aggregation-CypnmjA-.d.ts} +1 -1
  100. package/dist/{qr-pose-aggregation-DXR9STHs.js → qr-pose-aggregation-DBiaLW0O.js} +1 -10
  101. package/dist/{qr-size-depth-context-onu36E9s.d.ts → qr-size-depth-context-apAOmbrZ.d.ts} +2 -2
  102. package/dist/{qr-size-measurer-D7739ZpB.d.ts → qr-size-measurer-DxiMqRUw.d.ts} +3 -3
  103. package/dist/recording-options-vASCFzc5.d.ts +667 -0
  104. package/dist/{recording-replayer-CUEK82tM.d.ts → recording-replayer-D0V9WkmY.d.ts} +2 -2
  105. package/dist/{recording-slice-B-uCgGoh.d.ts → recording-slice-DTUtSVgd.d.ts} +2 -2
  106. package/dist/{replay-engine-BSVd7Zzb.d.ts → replay-engine-xdrUgWJi.d.ts} +1 -1
  107. package/dist/{replay-scene-CPt56pps.d.ts → replay-scene-zJ8SFDjE.d.ts} +3 -3
  108. package/dist/sensors/absolute-orientation.d.ts +2 -0
  109. package/dist/sensors/absolute-orientation.js +162 -0
  110. package/dist/sensors/gps-error-handler.d.ts +1 -1
  111. package/dist/sensors/gps.d.ts +1 -1
  112. package/dist/sensors/index.d.ts +5 -4
  113. package/dist/sensors/index.js +2 -1
  114. package/dist/sensors/permission-checker.d.ts +1 -1
  115. package/dist/state/app-selectors.d.ts +1 -1
  116. package/dist/state/combined-root-state.d.ts +1 -1
  117. package/dist/state/create-slam-app-store.d.ts +1 -1
  118. package/dist/state/create-slam-app-store.js +1 -74
  119. package/dist/state/gps-ar-pose-sampler.d.ts +3 -3
  120. package/dist/state/gps-event-coordinator.d.ts +2 -2
  121. package/dist/state/gps-event-coordinator.js +27 -17
  122. package/dist/state/index.d.ts +20 -20
  123. package/dist/state/index.js +4 -4
  124. package/dist/state/persistence-middleware.d.ts +1 -1
  125. package/dist/state/persistence-middleware.js +30 -19
  126. package/dist/state/qr-detected-slice.d.ts +3 -3
  127. package/dist/state/qr-detected-slice.js +1 -1
  128. package/dist/state/recording-options.d.ts +2 -2
  129. package/dist/state/recording-options.js +286 -25
  130. package/dist/state/recording-replayer.d.ts +2 -2
  131. package/dist/state/recording-replayer.js +1 -1
  132. package/dist/state/recording-slice.d.ts +1 -1
  133. package/dist/state/replay-engine.d.ts +1 -1
  134. package/dist/state/store-subscribers.d.ts +2 -2
  135. package/dist/state/store-subscribers.js +2 -1
  136. package/dist/state/subscribe-to-selector.d.ts +1 -1
  137. package/dist/state/tracking-quality.d.ts +2 -2
  138. package/dist/state/tracking-quality.js +49 -128
  139. package/dist/state/tracking-slice.d.ts +1 -1
  140. package/dist/{state-IEw0ZsBu.js → state-BeKvn4B-.js} +1 -1
  141. package/dist/storage/file-system-utils.d.ts +1 -1
  142. package/dist/storage/index.d.ts +8 -8
  143. package/dist/storage/null-storage-backend.d.ts +1 -1
  144. package/dist/storage/opfs-storage-backend.d.ts +1 -1
  145. package/dist/storage/opfs-storage.d.ts +1 -1
  146. package/dist/storage/opfs-storage.js +25 -1
  147. package/dist/storage/storage-backend.d.ts +1 -1
  148. package/dist/storage/zip-coverage-embed.d.ts +1 -1
  149. package/dist/storage/zip-export.d.ts +1 -1
  150. package/dist/storage/zip-reader.d.ts +1 -1
  151. package/dist/{storage-backend-CTJHNdMp.d.ts → storage-backend-sEtmM4yW.d.ts} +1 -1
  152. package/dist/{store-subscribers-BZP81hjh.d.ts → store-subscribers-DScA7tdT.d.ts} +2 -2
  153. package/dist/{subscribe-to-selector-Bp3sLx8L.d.ts → subscribe-to-selector-B9ylM5Oh.d.ts} +1 -1
  154. package/dist/test-utils/browser-mocks.d.ts +1 -1
  155. package/dist/test-utils/zip-round-trip-helpers.d.ts +1 -1
  156. package/dist/{tracking-slice-DJZ9qeGg.d.ts → tracking-slice-DMkHlbny.d.ts} +1 -1
  157. package/dist/types/ar-types.d.ts +1 -1
  158. package/dist/types/geo-types.d.ts +1 -1
  159. package/dist/types/index.d.ts +2 -2
  160. package/dist/utils/concurrency.d.ts +1 -1
  161. package/dist/utils/failure-tracker.d.ts +1 -1
  162. package/dist/utils/format-file-size.d.ts +1 -1
  163. package/dist/utils/fused-path.d.ts +1 -1
  164. package/dist/utils/index.d.ts +8 -7
  165. package/dist/utils/index.js +2 -1
  166. package/dist/utils/list-formatter.d.ts +1 -1
  167. package/dist/utils/logger.d.ts +1 -1
  168. package/dist/visualization/accuracy-circles.d.ts +1 -1
  169. package/dist/visualization/accuracy-circles.js +17 -8
  170. package/dist/visualization/alignment-lerper.d.ts +1 -1
  171. package/dist/visualization/ar-world-group-alignment.d.ts +1 -1
  172. package/dist/visualization/ar-world-group-alignment.js +1 -1
  173. package/dist/visualization/camera-follower.d.ts +1 -1
  174. package/dist/visualization/css3d-renderer-manager.d.ts +1 -1
  175. package/dist/visualization/frame-conversions.d.ts +1 -1
  176. package/dist/visualization/frustum-visibility.d.ts +1 -1
  177. package/dist/visualization/gps-anchor.d.ts +1 -1
  178. package/dist/visualization/gps-compass-cubes.d.ts +1 -1
  179. package/dist/visualization/gps-event-markers.d.ts +1 -1
  180. package/dist/visualization/gps-event-markers.js +1 -1
  181. package/dist/visualization/hit-test-reticle.d.ts +1 -1
  182. package/dist/visualization/index.d.ts +20 -19
  183. package/dist/visualization/index.js +4 -3
  184. package/dist/visualization/leaflet-map-overlay.d.ts +1 -1
  185. package/dist/visualization/leaflet-map-overlay.js +1 -282
  186. package/dist/visualization/lerp-utils.d.ts +2 -2
  187. package/dist/visualization/lerp-utils.js +27 -3
  188. package/dist/visualization/map-data.d.ts +1 -1
  189. package/dist/visualization/map-data.js +1 -32
  190. package/dist/visualization/map-overlay-draw.d.ts +1 -1
  191. package/dist/visualization/map-overlay-draw.js +24 -2
  192. package/dist/visualization/map-overlay.d.ts +1 -1
  193. package/dist/visualization/occluder-mesh-driver.d.ts +130 -0
  194. package/dist/visualization/occluder-mesh-driver.js +219 -0
  195. package/dist/visualization/occlusion-mesh.d.ts +2 -0
  196. package/dist/visualization/occlusion-mesh.js +437 -0
  197. package/dist/visualization/three-dispose.d.ts +1 -1
  198. package/dist/visualization/vis-colors.d.ts +1 -1
  199. package/dist/{webxr-session-CJF9Nijp.js → webxr-session-BCP2XK7k.js} +110 -14
  200. package/dist/{webxr-session-COsOjJwn.d.ts → webxr-session-cRG2SYAQ.d.ts} +83 -10
  201. package/package.json +4 -4
  202. package/dist/index-eaI1Z1KR.d.ts +0 -2
  203. package/dist/lerp-utils-kLxMJ2Xj.d.ts +0 -20
  204. package/dist/occupancy-grid-Bvi2Gw1U.d.ts +0 -82
  205. package/dist/recording-options-D93kksJU.d.ts +0 -369
  206. /package/dist/{alignment-lerper-BUTsQ2yu.d.ts → alignment-lerper-gD70k7Fq.d.ts} +0 -0
  207. /package/dist/{ar-types-BckI1E-O.d.ts → ar-types-Cc5yvBwn.d.ts} +0 -0
  208. /package/dist/{bresenham3d-ByjcmEgM.d.ts → bresenham3d-DGc4nIb-.d.ts} +0 -0
  209. /package/dist/{camera-blit-capture-CqWe1G7s.d.ts → camera-blit-capture-BimSdyih.d.ts} +0 -0
  210. /package/dist/{camera-follower-BTb2Yp12.d.ts → camera-follower-DfBm2J5i.d.ts} +0 -0
  211. /package/dist/{capability-checker-CXqoJYtb.d.ts → capability-checker-BTTpygvZ.d.ts} +0 -0
  212. /package/dist/{capture-failure-tracker-BL5SoG2a.d.ts → capture-failure-tracker-_BVfhpxv.d.ts} +0 -0
  213. /package/dist/{chromium-camera-access-workaround-BNK189P0.d.ts → chromium-camera-access-workaround-C6yJLXz9.d.ts} +0 -0
  214. /package/dist/{concurrency-DqAlKX4j.d.ts → concurrency-DPeGBH0X.d.ts} +0 -0
  215. /package/dist/{css3d-renderer-manager-m-ab19vp.d.ts → css3d-renderer-manager-CPpVtvgs.d.ts} +0 -0
  216. /package/dist/{failure-tracker-Cnc_xWUw.d.ts → failure-tracker-CeKf5HIc.d.ts} +0 -0
  217. /package/dist/{file-system-utils-Bs7lpeVP.d.ts → file-system-utils-DueXHxo9.d.ts} +0 -0
  218. /package/dist/{format-file-size-BFUWvir1.d.ts → format-file-size-DxN-EBLc.d.ts} +0 -0
  219. /package/dist/{frame-loop-BEO9_q3k.d.ts → frame-loop-7GBMBu68.d.ts} +0 -0
  220. /package/dist/{frustum-visibility-D_RGmH98.d.ts → frustum-visibility-CokkgS_i.d.ts} +0 -0
  221. /package/dist/{geo-types-DqiBzPO9.d.ts → geo-types-DrGVu8nS.d.ts} +0 -0
  222. /package/dist/{gps-C5inWeNV.d.ts → gps-CDKDQokE.d.ts} +0 -0
  223. /package/dist/{gps-compass-cubes-BExxrWJ-.d.ts → gps-compass-cubes-1mooS3BS.d.ts} +0 -0
  224. /package/dist/{gps-error-handler-C2bS69oJ.d.ts → gps-error-handler-C34GQuTu.d.ts} +0 -0
  225. /package/dist/{gps-event-markers-Dd0G2bD0.d.ts → gps-event-markers-Di2nBzx-.d.ts} +0 -0
  226. /package/dist/{h3-proximity-B57C6rYL.d.ts → h3-proximity-D0GTHQmr.d.ts} +0 -0
  227. /package/dist/{hit-test-reticle-Dqak9hiR.d.ts → hit-test-reticle-CaQbrhbk.d.ts} +0 -0
  228. /package/dist/{list-formatter-ct8txwvR.d.ts → list-formatter-DLFDnS70.d.ts} +0 -0
  229. /package/dist/{logger-CzQrCe6C.d.ts → logger-CqTd5Kpk.d.ts} +0 -0
  230. /package/dist/{map-overlay-Dci4yvm3.d.ts → map-overlay-BCQgvIgU.d.ts} +0 -0
  231. /package/dist/{permission-checker-CtPtPQRq.d.ts → permission-checker-B-VtjnBA.d.ts} +0 -0
  232. /package/dist/{qr-pose-Bt2reSw7.d.ts → qr-pose-y3vaEd6x.d.ts} +0 -0
  233. /package/dist/{scene-node-names-CdNeQjOE.d.ts → scene-node-names-CGb-ylBd.d.ts} +0 -0
  234. /package/dist/{session-disposers-D8wnO-uW.js → session-disposers-BPrMplIC.js} +0 -0
  235. /package/dist/{three-dispose-BCZkSJp2.d.ts → three-dispose-BMOAmPhb.d.ts} +0 -0
  236. /package/dist/{vis-colors-BZsSLI4i.d.ts → vis-colors-C7HI0u9_.d.ts} +0 -0
  237. /package/dist/{webxr-nue-basis-CW1wsbm6.d.ts → webxr-nue-basis-CaCnYxuq.d.ts} +0 -0
  238. /package/dist/{xr-camera-texture-BmrP7iNZ.d.ts → xr-camera-texture-q0eIDSRg.d.ts} +0 -0
  239. /package/dist/{xr-error-handler-BEjRgZjt.d.ts → xr-error-handler-uVGDu9IX.d.ts} +0 -0
  240. /package/dist/{xr-frame-loop-K8ebdjLw.d.ts → xr-frame-loop-DX3JPpAU.d.ts} +0 -0
  241. /package/dist/{zip-coverage-embed-tFohI2J4.d.ts → zip-coverage-embed-ncVbaZAz.d.ts} +0 -0
  242. /package/dist/{zip-export-DDWXInTR.d.ts → zip-export-CtiUnAlJ.d.ts} +0 -0
  243. /package/dist/{zip-reader-Q2ue6zlC.d.ts → zip-reader-CvruaGvD.d.ts} +0 -0
@@ -0,0 +1,156 @@
1
+ import { trackingReducer } from "./state/tracking-slice.js";
2
+ import { recordWriteFailure, recordingReducer } from "./state/recording-slice.js";
3
+ import { createTrackingQualityListenerMiddleware, trackingQualityReducer } from "./state/tracking-quality.js";
4
+ import { createPersistenceMiddleware, slicePrefixOf } from "./state/persistence-middleware.js";
5
+ import { arElementsReducer, gpsDataReducer, gpsElementsReducer, sanitizeForDevTools, setColdStartOverrideEnabled, setCompassRotationPriorEnabled, setCompassWebXRConsistencyEnabled, setZeroPos, validateLicenseKey } from "gps-plus-slam-js";
6
+ import { configureStore, createListenerMiddleware } from "@reduxjs/toolkit";
7
+ import { COMMUNITY_LICENSE_KEY } from "gps-plus-slam-js/community-license-key";
8
+ //#region ../src/state/slam-app-store-listener.ts
9
+ /**
10
+ * Build a listener middleware that applies the requested compass opt-ins once
11
+ * `gpsData` exists and a flag is still unset. Register it via `.prepend(...)`
12
+ * (so its effect dispatches *outside* the trigger's `next()` — see module doc).
13
+ *
14
+ * Behaviour:
15
+ * - **Predicate** fires whenever the `gpsData` *object reference* is new since
16
+ * the last apply AND at least one opt-in is still unset. Two terms, each load-
17
+ * bearing:
18
+ * - `s.gpsData !== lastApplied` makes the predicate edge-triggered on gpsData
19
+ * identity. A recreated `gpsData` (store swap / origin reset) is a fresh
20
+ * object, so it still re-triggers the apply — the re-apply semantics the
21
+ * 2026-06-27 field bug demands are preserved. Do NOT weaken this to a
22
+ * `null → non-null` transition: that would drop the re-apply. The point of
23
+ * keying on the *reference* (not on "a flag is unset") is that it also stops
24
+ * the effect from re-firing for the SAME gpsData.
25
+ * - `optIns.some((o) => !o.isSet(s))` keeps the no-op case (all flags already
26
+ * set) from scheduling an effect at all.
27
+ * Why the reference guard matters: without it the predicate is purely level-
28
+ * based, so if an opt-in's `apply` dispatches but `isSet` never flips true
29
+ * (e.g. consumer/library **version skew** where the action type no longer
30
+ * matches the reducer — the packages are published independently), the
31
+ * condition stays true forever and every effect dispatch re-arms it: an
32
+ * unbounded storm that freezes the app. The per-dispatch `isSet` re-check
33
+ * below guards only against dispatching an *already-set* flag twice; it does
34
+ * nothing when the flag never sets. The reference guard is what bounds it.
35
+ * - **Effect** records the gpsData reference it is acting on, then dispatches
36
+ * every still-unset opt-in as a top-level action. `isSet` is re-read against
37
+ * the *current* store state immediately before each dispatch (not one snapshot
38
+ * at effect entry): a dispatch is synchronous, so a flag is already set by the
39
+ * time the next check runs, and an opt-in's own dispatch can re-enter this
40
+ * effect before the loop finishes. Re-checking per dispatch makes that
41
+ * re-entrancy idempotent — a flag is dispatched only while still unset.
42
+ */
43
+ function createSlamAppStoreListenerMiddleware(optIns) {
44
+ const listenerMiddleware = createListenerMiddleware();
45
+ let lastApplied = null;
46
+ listenerMiddleware.startListening({
47
+ predicate: (_action, currentState) => {
48
+ const s = currentState;
49
+ return s.gpsData !== null && s.gpsData !== lastApplied && optIns.some((optIn) => !optIn.isSet(s));
50
+ },
51
+ effect: (_action, api) => {
52
+ const entry = api.getState();
53
+ if (entry.gpsData === null) return;
54
+ lastApplied = entry.gpsData;
55
+ for (const optIn of optIns) {
56
+ const s = api.getState();
57
+ if (s.gpsData === null) return;
58
+ if (!optIn.isSet(s)) optIn.apply((action) => {
59
+ api.dispatch(action);
60
+ });
61
+ }
62
+ }
63
+ });
64
+ return listenerMiddleware.middleware;
65
+ }
66
+ //#endregion
67
+ //#region ../src/state/create-slam-app-store.ts
68
+ /**
69
+ * `createSlamAppStore` — composable Redux store factory for AR+GPS apps.
70
+ *
71
+ * Introduced in Iter 1 of the AppFramework/RecorderApp boundary migration.
72
+ * Wires the three library reducers (`gpsData`, `gpsElements`, `arElements`),
73
+ * the framework's recording lifecycle slice, and the persistence middleware.
74
+ *
75
+ * Recorder-only state (routing screen, ref-points, scenario) is plugged in
76
+ * by the consumer via `extraReducers` / `extraMiddleware`. The factory itself
77
+ * never references those concepts so apps that don't need them (POI viewers,
78
+ * navigation arrows, …) compose freely.
79
+ *
80
+ * The legacy `createRecorderStore` from `store.ts` is built on top of this
81
+ * factory and will move out of the framework in Iter 1D.
82
+ *
83
+ * @see docs/2026-05-03-appframework-vs-recorderapp-boundary-analysis.md — Iter 1
84
+ */
85
+ /**
86
+ * Slice prefixes the framework always persists, derived from the actual
87
+ * library / framework action creators (never hand-typed). A rename of the
88
+ * `gpsData` or `recording` slice therefore propagates here automatically
89
+ * instead of silently dropping that slice's actions from recordings.
90
+ */
91
+ const BUILTIN_PERSISTED_PREFIXES = [slicePrefixOf(setZeroPos.type), slicePrefixOf(recordWriteFailure.type)];
92
+ /**
93
+ * Build a Redux store wired with library + recording slices, persistence
94
+ * middleware, and any caller-supplied extras. See module docstring for the
95
+ * design rationale.
96
+ */
97
+ function createSlamAppStore(options) {
98
+ const { storageBackend, extraReducers, extraMiddleware, persistedExtraPrefixes, onWriteFailure, enableDevChecks = true, licenseKey = COMMUNITY_LICENSE_KEY, trackingQualityOptions, enableCompassColdStartOverride = true, enableCompassRotationPrior = false, enableCompassWebXRConsistency = false } = options;
99
+ validateLicenseKey(licenseKey);
100
+ const builtins = {
101
+ gpsData: gpsDataReducer,
102
+ gpsElements: gpsElementsReducer,
103
+ arElements: arElementsReducer,
104
+ recording: recordingReducer,
105
+ tracking: trackingReducer,
106
+ trackingQuality: trackingQualityReducer
107
+ };
108
+ if (extraReducers) {
109
+ const collisions = Object.keys(extraReducers).filter((key) => Object.prototype.hasOwnProperty.call(builtins, key));
110
+ if (collisions.length > 0) throw new Error(`extraReducers must not overwrite framework-reserved slice(s): ${collisions.join(", ")}. Reserved keys: ${Object.keys(builtins).join(", ")}.`);
111
+ }
112
+ const reducer = {
113
+ ...builtins,
114
+ ...extraReducers ?? {}
115
+ };
116
+ const trackingQualityMiddleware = createTrackingQualityListenerMiddleware(trackingQualityOptions);
117
+ const compassOptIns = [];
118
+ if (enableCompassColdStartOverride) compassOptIns.push({
119
+ isSet: (s) => s.gpsData?.coldStartOverrideEnabled === true,
120
+ apply: (dispatch) => dispatch(setColdStartOverrideEnabled(true))
121
+ });
122
+ if (enableCompassRotationPrior) compassOptIns.push({
123
+ isSet: (s) => s.gpsData?.compassRotationPriorEnabled === true,
124
+ apply: (dispatch) => dispatch(setCompassRotationPriorEnabled(true))
125
+ });
126
+ if (enableCompassWebXRConsistency) compassOptIns.push({
127
+ isSet: (s) => s.gpsData?.compassWebXRConsistencyEnabled === true,
128
+ apply: (dispatch) => dispatch(setCompassWebXRConsistencyEnabled(true))
129
+ });
130
+ const prependedListeners = [trackingQualityMiddleware];
131
+ if (compassOptIns.length > 0) prependedListeners.push(createSlamAppStoreListenerMiddleware(compassOptIns));
132
+ const store = configureStore({
133
+ reducer,
134
+ middleware: (getDefaultMiddleware) => getDefaultMiddleware({
135
+ serializableCheck: enableDevChecks,
136
+ immutableCheck: enableDevChecks
137
+ }).prepend(...prependedListeners).concat(createPersistenceMiddleware({
138
+ storageBackend,
139
+ onWriteFailure,
140
+ persistedPrefixes: [...BUILTIN_PERSISTED_PREFIXES, ...persistedExtraPrefixes ?? []]
141
+ }), ...extraMiddleware ?? []),
142
+ devTools: {
143
+ actionSanitizer: sanitizeForDevTools,
144
+ stateSanitizer: sanitizeForDevTools
145
+ }
146
+ });
147
+ return {
148
+ getState: () => store.getState(),
149
+ dispatch: store.dispatch,
150
+ subscribe: (listener) => store.subscribe(listener),
151
+ writeFrame: (blob, index) => storageBackend.writeFrame(blob, index),
152
+ writeSessionMetadata: (metadata) => storageBackend.writeSessionMetadata(metadata)
153
+ };
154
+ }
155
+ //#endregion
156
+ export { createSlamAppStore as t };
@@ -1,4 +1,4 @@
1
- import { r as DepthPoint } from "./ar-types-BckI1E-O.js";
1
+ import { r as DepthPoint } from "./ar-types-Cc5yvBwn.js";
2
2
 
3
3
  //#region ../src/ar/depth-grid-lookup.d.ts
4
4
  interface DepthGridLookup {
@@ -1,4 +1,4 @@
1
- import { a as RgbTuple, i as DepthSample, t as ARPose } from "./ar-types-BckI1E-O.js";
1
+ import { a as RgbTuple, i as DepthSample, t as ARPose } from "./ar-types-Cc5yvBwn.js";
2
2
  import { Matrix4 } from "gps-plus-slam-js";
3
3
 
4
4
  //#region ../src/ar/depth-rgb-lookup.d.ts
@@ -66,6 +66,27 @@ interface DepthInfo {
66
66
  getDepthInMeters: (x: number, y: number) => number;
67
67
  /** Column-major projection matrix of the capturing XRView, if known. */
68
68
  projectionMatrix?: Matrix4;
69
+ /**
70
+ * Raw depth buffer (`XRCPUDepthInformation.data`) — a **live reference**
71
+ * valid only within the originating XR frame callback (NOT copied, unlike
72
+ * the matrices: the per-frame buffer is too large to clone and the live
73
+ * depth occluder uploads it synchronously). Absent when the source carries
74
+ * no `data` (e.g. the sparse-only path). Plumbed for the live depth occluder
75
+ * (2026-06-14-webxr-depth-occlusion-plan.md §2); the sparse sampler ignores it.
76
+ */
77
+ data?: ArrayBuffer;
78
+ /**
79
+ * `XRCPUDepthInformation.rawValueToMeters` — multiply a raw depth sample by
80
+ * this to get metres. Preserved only when a finite number. Occluder-only.
81
+ */
82
+ rawValueToMeters?: number;
83
+ /**
84
+ * The `.matrix` (column-major 16-tuple) of
85
+ * `XRDepthInformation.normDepthBufferFromNormView` — the screen-UV → depth-UV
86
+ * transform the occluder shader needs. Copied + validated like
87
+ * `projectionMatrix` (the UA may reuse the backing array). Occluder-only.
88
+ */
89
+ normDepthBufferFromNormView?: Matrix4;
69
90
  }
70
91
  /**
71
92
  * Wrap a raw browser XRDepthInformation object into a {@link DepthInfo}.
@@ -76,11 +97,22 @@ interface DepthInfo {
76
97
  * the UA may reuse across frames) is defensively validated and copied into
77
98
  * a plain serializable 16-tuple; invalid input (wrong length, non-finite
78
99
  * entries) yields a DepthInfo without a matrix rather than an error.
100
+ * - The live-occluder metadata (`data`, `rawValueToMeters`,
101
+ * `normDepthBufferFromNormView`) is preserved when present and valid: `data`
102
+ * by live reference (no clone — see {@link DepthInfo.data}), the scale only
103
+ * when finite, and the UV transform's `.matrix` copied like
104
+ * `projectionMatrix`. The sparse grid sampler ignores all three; a source
105
+ * lacking them (e.g. the existing test doubles) wraps exactly as before.
79
106
  */
80
107
  declare function wrapXRDepthInfo(raw: {
81
108
  width: number;
82
109
  height: number;
83
110
  getDepthInMeters: (x: number, y: number) => number;
111
+ data?: ArrayBuffer;
112
+ rawValueToMeters?: number;
113
+ normDepthBufferFromNormView?: {
114
+ matrix?: ArrayLike<number>;
115
+ } | null;
84
116
  }, projectionMatrix: ArrayLike<number> | undefined): DepthInfo;
85
117
  /**
86
118
  * Samples sparse depth points from WebXR depth sensing.
@@ -1,4 +1,4 @@
1
- import { r as DepthPoint } from "./ar-types-BckI1E-O.js";
1
+ import { r as DepthPoint } from "./ar-types-Cc5yvBwn.js";
2
2
  import { Matrix4, Quaternion, Vector3 } from "gps-plus-slam-js";
3
3
 
4
4
  //#region ../src/ar/depth-unprojection.d.ts
@@ -1,7 +1,7 @@
1
- import { t as ArCrashIsolationOptions } from "./recording-options-D93kksJU.js";
2
- import { n as RawDeviceOrientation, t as GpsPosition } from "./gps-C5inWeNV.js";
3
- import { n as PermissionStatus } from "./permission-checker-CtPtPQRq.js";
4
- import { r as SessionFeatureOptions } from "./webxr-session-COsOjJwn.js";
1
+ import { t as ArCrashIsolationOptions } from "./recording-options-vASCFzc5.js";
2
+ import { n as RawDeviceOrientation, t as GpsPosition } from "./gps-CDKDQokE.js";
3
+ import { n as PermissionStatus } from "./permission-checker-B-VtjnBA.js";
4
+ import { s as SessionFeatureOptions } from "./webxr-session-cRG2SYAQ.js";
5
5
 
6
6
  //#region ../src/ar/enable-gps-ar.d.ts
7
7
  /**
@@ -1,4 +1,4 @@
1
- import { i as LatLongAlt, r as LatLong } from "./index-eaI1Z1KR.js";
1
+ import { i as LatLongAlt, r as LatLong } from "./index-D4vrgV4b.js";
2
2
  import * as THREE from "three";
3
3
 
4
4
  //#region ../src/visualization/frame-conversions.d.ts
@@ -1,4 +1,4 @@
1
- import { t as GpsCoord } from "./geo-types-DqiBzPO9.js";
1
+ import { t as GpsCoord } from "./geo-types-DrGVu8nS.js";
2
2
  import { LatLong, Matrix4, Vector3, fusedGpsFromOdom } from "gps-plus-slam-js";
3
3
 
4
4
  //#region ../src/utils/fused-path.d.ts
@@ -1,2 +1,2 @@
1
- import { a as findNearbyGeoAnchor, c as h3CellsMatch, i as clusterCellsByZoom, l as isH3Index, n as KnownGeoAnchor, o as gpsPathToCoverageCells, r as approxDistanceMetres, s as gpsToH3, t as H3_RESOLUTION } from "../h3-proximity-B57C6rYL.js";
1
+ import { a as findNearbyGeoAnchor, c as h3CellsMatch, i as clusterCellsByZoom, l as isH3Index, n as KnownGeoAnchor, o as gpsPathToCoverageCells, r as approxDistanceMetres, s as gpsToH3, t as H3_RESOLUTION } from "../h3-proximity-D0GTHQmr.js";
2
2
  export { H3_RESOLUTION, KnownGeoAnchor, approxDistanceMetres, clusterCellsByZoom, findNearbyGeoAnchor, gpsPathToCoverageCells, gpsToH3, h3CellsMatch, isH3Index };
@@ -1,2 +1,2 @@
1
- import { a as findNearbyGeoAnchor, c as h3CellsMatch, i as clusterCellsByZoom, l as isH3Index, n as KnownGeoAnchor, o as gpsPathToCoverageCells, r as approxDistanceMetres, s as gpsToH3, t as H3_RESOLUTION } from "../h3-proximity-B57C6rYL.js";
1
+ import { a as findNearbyGeoAnchor, c as h3CellsMatch, i as clusterCellsByZoom, l as isH3Index, n as KnownGeoAnchor, o as gpsPathToCoverageCells, r as approxDistanceMetres, s as gpsToH3, t as H3_RESOLUTION } from "../h3-proximity-D0GTHQmr.js";
2
2
  export { H3_RESOLUTION, type KnownGeoAnchor, approxDistanceMetres, clusterCellsByZoom, findNearbyGeoAnchor, gpsPathToCoverageCells, gpsToH3, h3CellsMatch, isH3Index };
@@ -0,0 +1,39 @@
1
+ import { quat } from "gl-matrix";
2
+ //#region ../src/utils/geodesic-angle.ts
3
+ /**
4
+ * Geodesic-angle kernel — the single shared definition of the shortest-arc
5
+ * rotation angle between two unit quaternions.
6
+ *
7
+ * This used to be hand-rolled in (at least) two places:
8
+ * - `geodesicAngleRad` (private) in `ar/qr-pose-aggregation.ts`, and
9
+ * - `quat.getAngle(...)` + a NaN guard inside `state/tracking-quality.ts`'s
10
+ * `matrixDelta`.
11
+ *
12
+ * Both compute the same number — `acos(2·dot² − 1)` — so they are consolidated
13
+ * here so a third consumer (the capture motion gate's angular-velocity helper,
14
+ * `ar/pose-motion.ts`) reuses one definition instead of adding a fourth copy.
15
+ * See `GpsPlusSlamJs_Docs/docs/2026-06-23-followup-consolidate-geodesic-angle-kernel.md`.
16
+ */
17
+ /**
18
+ * Geodesic (shortest-arc) rotation angle in radians between two **unit**
19
+ * quaternions.
20
+ *
21
+ * Uses `cos θ = 2·dot² − 1`, which is invariant to the sign of either input
22
+ * (`q` ≡ `−q`, the quaternion double cover) because it depends only on `dot²`.
23
+ * The argument is clamped to `[-1, 1]` before `acos` so float error on a
24
+ * near-identical pair (where `2·dot² − 1` can drift slightly above 1) returns
25
+ * `0` rather than `NaN` — this is the guard `matrixDelta` previously applied
26
+ * explicitly around the raw `quat.getAngle`.
27
+ *
28
+ * Precondition: both inputs are (approximately) unit length. Callers that hold
29
+ * un-normalized quaternions must normalize first (gl-matrix `quat.normalize`).
30
+ *
31
+ * @returns angle in radians, in `[0, π]`. Never `NaN` for finite unit inputs.
32
+ */
33
+ function geodesicAngleRad(a, b) {
34
+ const d = quat.dot(a, b);
35
+ const c = Math.min(1, Math.max(-1, 2 * d * d - 1));
36
+ return Math.acos(c);
37
+ }
38
+ //#endregion
39
+ export { geodesicAngleRad as t };
@@ -1,4 +1,4 @@
1
- import { i as LatLongAlt, r as LatLong } from "./index-eaI1Z1KR.js";
1
+ import { i as LatLongAlt, r as LatLong } from "./index-D4vrgV4b.js";
2
2
  import * as THREE from "three";
3
3
 
4
4
  //#region ../src/visualization/gps-anchor.d.ts
@@ -1,4 +1,4 @@
1
- import { t as ARPose } from "./ar-types-BckI1E-O.js";
1
+ import { t as ARPose } from "./ar-types-Cc5yvBwn.js";
2
2
  import { Quaternion, Vector3 } from "gps-plus-slam-js";
3
3
 
4
4
  //#region ../src/state/gps-ar-pose-sampler.d.ts
@@ -1,7 +1,8 @@
1
- import { t as ARPose } from "./ar-types-BckI1E-O.js";
2
- import { n as RawDeviceOrientation, t as GpsPosition } from "./gps-C5inWeNV.js";
3
- import { i as SlamAppStore } from "./create-slam-app-store-Ce2FMKCp.js";
4
- import { Quaternion, RawGpsPoint, RecordGpsEventPayload, Vector3, eulerToQuaternion } from "gps-plus-slam-js";
1
+ import { t as ARPose } from "./ar-types-Cc5yvBwn.js";
2
+ import { n as RawDeviceOrientation, t as GpsPosition } from "./gps-CDKDQokE.js";
3
+ import { t as AbsoluteOrientationReading } from "./absolute-orientation-BAxc15J6.js";
4
+ import { i as SlamAppStore } from "./create-slam-app-store-D3nzh9Wb.js";
5
+ import { Quaternion, RawAbsoluteOrientation, RawGpsPoint, RecordGpsEventPayload, Vector3, eulerToQuaternion } from "gps-plus-slam-js";
5
6
  import { ReducersMapObject } from "@reduxjs/toolkit";
6
7
 
7
8
  //#region ../src/state/gps-event-coordinator.d.ts
@@ -44,28 +45,37 @@ declare function extractOdomRotation(arPose: ARPose): Quaternion;
44
45
  /**
45
46
  * Build a RawGpsPoint from GPS position data.
46
47
  * Returns only raw sensor fields — no derived fields (coordinates, weight,
47
- * zeroRef, deviceRotation). Derived fields are computed by the library
48
- * reducer when the action is dispatched (raw-storage pattern).
48
+ * zeroRef). Derived fields are computed by the library reducer when the
49
+ * action is dispatched (raw-storage pattern).
49
50
  *
50
51
  * @param gpsPosition - GPS position from Geolocation API
51
- * @param deviceOrientation - Optional device orientation from sensors
52
+ * @param deviceOrientation - Accepted for signature stability; no longer
53
+ * read (the legacy `compassAbsolute` field it fed is no longer recorded)
52
54
  * @returns RawGpsPoint ready for the action payload
53
55
  */
54
56
  declare function buildRawGpsPoint(gpsPosition: GpsPosition, deviceOrientation: RawDeviceOrientation | null): RawGpsPoint;
57
+ /**
58
+ * Map a capture-module {@link AbsoluteOrientationReading} to the library's
59
+ * {@link RawAbsoluteOrientation} payload shape (the only difference is the
60
+ * timestamp field name: capture `timestamp` → payload `sampleTimestamp`).
61
+ * Returns `undefined` when the sensor produced no reading (most non-Chrome).
62
+ */
63
+ declare function toRawAbsoluteOrientation(reading: AbsoluteOrientationReading | null): RawAbsoluteOrientation | undefined;
55
64
  /**
56
65
  * Build a RecordGpsEventPayload from GPS position and AR pose.
57
66
  * This is a pure function for testability.
58
67
  *
59
68
  * Stores only raw sensor data in the payload. The library reducer
60
- * computes derived fields (coordinates, weight, deviceRotation)
61
- * when the action is processed (raw-storage pattern).
69
+ * computes derived fields (coordinates, weight) when the action is
70
+ * processed (raw-storage pattern).
62
71
  *
63
72
  * @param gpsPosition - GPS position from Geolocation API
64
73
  * @param arPose - AR pose from WebXR
65
74
  * @param deviceOrientation - Optional device orientation from sensors
75
+ * @param absoluteOrientation - Optional AbsoluteOrientationSensor reading
66
76
  * @returns RecordGpsEventPayload ready for dispatch
67
77
  */
68
- declare function buildRecordGpsEventPayload(gpsPosition: GpsPosition, arPose: ARPose, deviceOrientation: RawDeviceOrientation | null): RecordGpsEventPayload;
78
+ declare function buildRecordGpsEventPayload(gpsPosition: GpsPosition, arPose: ARPose, deviceOrientation: RawDeviceOrientation | null, absoluteOrientation?: AbsoluteOrientationReading | null): RecordGpsEventPayload;
69
79
  /**
70
80
  * Create a GPS position handler that dispatches combined GPS+AR events.
71
81
  *
@@ -79,4 +89,4 @@ declare function buildRecordGpsEventPayload(gpsPosition: GpsPosition, arPose: AR
79
89
  */
80
90
  declare function createGpsPositionHandler(config: RecordingCoordinatorConfig): (position: GpsPosition) => void;
81
91
  //#endregion
82
- export { eulerToQuaternion as a, getLastDeviceOrientation as c, createGpsPositionHandler as i, resetCoordinatorState as l, buildRawGpsPoint as n, extractOdomPosition as o, buildRecordGpsEventPayload as r, extractOdomRotation as s, RecordingCoordinatorConfig as t, updateDeviceOrientation as u };
92
+ export { eulerToQuaternion as a, getLastDeviceOrientation as c, updateDeviceOrientation as d, createGpsPositionHandler as i, resetCoordinatorState as l, buildRawGpsPoint as n, extractOdomPosition as o, buildRecordGpsEventPayload as r, extractOdomRotation as s, RecordingCoordinatorConfig as t, toRawAbsoluteOrientation as u };
@@ -1,4 +1,6 @@
1
- import { o as WebXRQuaternion, s as WebXRVec3, t as ARPose } from "./ar-types-BckI1E-O.js";
1
+ import { o as WebXRQuaternion, s as WebXRVec3, t as ARPose } from "./ar-types-Cc5yvBwn.js";
2
+ import { t as MotionFilterConfig } from "./capture-motion-gate-BGoAMkmf.js";
3
+ import { a as QualityFilterConfig } from "./image-quality-BMx2ko9-.js";
2
4
 
3
5
  //#region ../src/ar/image-capture.d.ts
4
6
  /**
@@ -26,6 +28,17 @@ interface ImageCaptureConfig {
26
28
  * into this config so the whole user options section can flow through the
27
29
  * capture seam as one object (see the field-drop audit, F3). */
28
30
  resolutionDivisor: number;
31
+ /** Motion gate: skip motion-blurred frames by deferring a due capture until
32
+ * device motion settles (or `maxWaitMs` elapses). Mirrored in the persisted
33
+ * `ImageCaptureOptions.motionFilter` and flowed through the same capture
34
+ * seam as `resolutionDivisor`. See `capture-motion-gate.ts`. */
35
+ motionFilter: MotionFilterConfig;
36
+ /** Image-quality gate: drop a blurry/black frame (judged off-thread by the
37
+ * injected `analyzeFrame` callback) and retry the next acceptable frame,
38
+ * bounded by `maxWaitMs`. The increment ON TOP of the motion gate — only ever
39
+ * runs on frames that already passed motion gating. Mirrored in the persisted
40
+ * `ImageCaptureOptions.qualityFilter`. See `image-quality.ts`. */
41
+ qualityFilter: QualityFilterConfig;
29
42
  }
30
43
  /**
31
44
  * Default capture configuration
@@ -79,6 +92,18 @@ interface CapturedFrame {
79
92
  readonly width: number;
80
93
  readonly height: number;
81
94
  }
95
+ /**
96
+ * Verdict returned by the injected off-thread {@link ImageCaptureCallbacks.analyzeFrame}
97
+ * analyzer. The manager only needs `accept`; `reason` is carried for logging/
98
+ * tuning. Structurally compatible with `image-quality.ts`'s `QualityVerdict`, so
99
+ * the recorder worker can return that type directly.
100
+ */
101
+ interface FrameQualityVerdict {
102
+ /** `true` to save the frame, `false` to drop it and retry. */
103
+ readonly accept: boolean;
104
+ /** Optional debug reason (e.g. `'black'` | `'blurry'`); for logging only. */
105
+ readonly reason?: string | null;
106
+ }
82
107
  /**
83
108
  * Callbacks for image capture integration
84
109
  */
@@ -110,6 +135,22 @@ interface ImageCaptureCallbacks {
110
135
  * @see docs/2026-02-06-bug-camera-frames-black.md
111
136
  */
112
137
  captureFrame?: (quality: number) => Promise<CapturedFrame | null>;
138
+ /**
139
+ * Optional off-thread image-quality analyzer (blur/blackness gate). When
140
+ * provided AND `config.qualityFilter.enabled`, a freshly-encoded blob is NOT
141
+ * saved immediately: it is handed to this async callback (a Web Worker
142
+ * round-trip in the recorder) and saved only if the verdict accepts. A reject
143
+ * drops the blob and re-arms capture so the next acceptable (motion-calm)
144
+ * frame fills the slot, bounded by `qualityFilter.maxWaitMs` (after which the
145
+ * next frame is saved regardless, so an interval is never silently lost).
146
+ *
147
+ * The manager itself NEVER touches pixels — the off-main-thread guarantee. The
148
+ * callback MUST settle its promise; if it rejects, or does not settle within
149
+ * `captureTimeoutMs`, the manager fails open (saves the frame) so a hung
150
+ * analyzer cannot deadlock the pipeline. See `image-quality.ts` and
151
+ * `GpsPlusSlamJs_Docs/docs/2026-06-24-image-quality-gate-plan.md`.
152
+ */
153
+ analyzeFrame?: (frame: CapturedFrame) => Promise<FrameQualityVerdict>;
113
154
  }
114
155
  /**
115
156
  * Manages periodic image capture from a canvas.
@@ -129,6 +170,30 @@ declare class ImageCaptureManager {
129
170
  private frameCount;
130
171
  private captureInProgress;
131
172
  private captureTimeoutId;
173
+ /** True while an `analyzeFrame` verdict is in flight; blocks a second capture
174
+ * for the same interval without throttling normal cadence (captureInProgress
175
+ * is released after encode, NOT held across the round-trip). */
176
+ private awaitingVerdict;
177
+ /** The current interval's capture was rejected by the quality gate; bypass the
178
+ * interval guard so the next acceptable frame is grabbed immediately. */
179
+ private retryPending;
180
+ /** Frame time (ms) of the FIRST quality attempt for the current interval; the
181
+ * quality `maxWaitMs` fallback measures from here, independent of the motion
182
+ * gate's own due clock. `null` between intervals. */
183
+ private qualityDeadlineBase;
184
+ /** Safety timeout for a hung `analyzeFrame` (fail-open). */
185
+ private verdictTimeoutId;
186
+ /** Sliding window of recent per-frame velocities for the motion gate. */
187
+ private readonly motionWindow;
188
+ /** Previous frame's pose, for the per-frame velocity delta. */
189
+ private prevPose;
190
+ /** Previous frame's timestamp (ms), for the per-frame velocity delta. */
191
+ private prevTime;
192
+ /** Whether THIS frame's velocity sample was rejected as a tracking glitch. */
193
+ private lastSampleWasGlitch;
194
+ /** When the current capture first became due (ms); null while not due. The
195
+ * `maxWaitMs` fallback measures from here, not from `lastCaptureTime`. */
196
+ private dueTime;
132
197
  constructor(canvas: HTMLCanvasElement, callbacks: ImageCaptureCallbacks, config?: ImageCaptureConfig);
133
198
  /**
134
199
  * Start capturing images.
@@ -154,16 +219,51 @@ declare class ImageCaptureManager {
154
219
  * @param time - Frame timestamp in milliseconds (from requestAnimationFrame)
155
220
  */
156
221
  onFrame(time: number): void;
222
+ /**
223
+ * Sample per-frame device motion into the sliding window. Called every frame
224
+ * (even during an in-flight capture or before the interval elapses) so the
225
+ * window reflects truly instantaneous motion the moment a capture becomes
226
+ * due. A frame with no pose, or a non-positive dt (duplicate timestamp),
227
+ * records no sample and is treated as "not a glitch".
228
+ */
229
+ private updateMotionWindow;
230
+ /**
231
+ * Apply the motion gate to a due capture. Returns `true` to capture now,
232
+ * `false` to defer to a later frame.
233
+ *
234
+ * - Filter disabled → always `true` (legacy behavior).
235
+ * - `maxWaitMs` elapsed → always `true` (never-calm safety fallback; an
236
+ * interval is never silently lost).
237
+ * - Current frame's sample was a tracking glitch → `false` (don't grab a
238
+ * relocalization-teleport frame; the fallback still guarantees progress).
239
+ * - No measurable motion yet (first capture / all-glitch) → `true` (the very
240
+ * first capture is never blocked; only measurable motion defers).
241
+ * - Otherwise → defer to {@link decideCapture} over the windowed maxima.
242
+ */
243
+ private shouldCaptureNow;
157
244
  /**
158
245
  * Clear the safety timeout for captureInProgress.
159
246
  * Called when capture completes (success or failure) before the timeout fires.
160
247
  */
161
248
  private clearCaptureTimeout;
249
+ /** Clear the safety timeout for a hung `analyzeFrame` verdict. */
250
+ private clearVerdictTimeout;
162
251
  /**
163
252
  * Common handler for captured blobs (from either canvas.toBlob or captureFrame).
164
- * Handles null blobs, suspicious image detection, and dispatching onCaptured.
253
+ * Releases `captureInProgress` after the synchronous encode, then either saves
254
+ * immediately (legacy / gate-off) or runs the off-thread image-quality gate.
255
+ *
256
+ * @param time - the XR frame time the capture was dispatched at; the quality
257
+ * retry-deadline clock and `lastCaptureTime` are measured from it.
165
258
  */
166
259
  private handleCapturedBlob;
260
+ /**
261
+ * Persist an accepted frame: allocate its 1-based index, run the suspicious-
262
+ * size belt-and-suspenders check, reset the per-interval gate clocks, and fire
263
+ * `onCaptured`. Called from the legacy path, the quality fallbacks, and an
264
+ * accepting verdict — the single place a frame becomes durable.
265
+ */
266
+ private saveCapture;
167
267
  }
168
268
  //#endregion
169
- export { ImageCaptureConfig as a, ImageCaptureCallbacks as i, CapturedImage as n, ImageCaptureManager as o, DEFAULT_CAPTURE_CONFIG as r, MIN_VALID_IMAGE_BYTES as s, CapturedFrame as t };
269
+ export { ImageCaptureCallbacks as a, MIN_VALID_IMAGE_BYTES as c, FrameQualityVerdict as i, CapturedImage as n, ImageCaptureConfig as o, DEFAULT_CAPTURE_CONFIG as r, ImageCaptureManager as s, CapturedFrame as t };