gps-plus-slam-app-framework 1.3.0 → 1.7.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 (247) hide show
  1. package/dist/absolute-orientation-CAEh840t.d.ts +54 -0
  2. package/dist/{app-selectors-B8VQoMDY.d.ts → app-selectors-ikTUQZpe.d.ts} +1 -1
  3. package/dist/ar/bresenham3d.d.ts +1 -1
  4. package/dist/ar/camera-blit-capture.d.ts +2 -2
  5. package/dist/ar/camera-blit-capture.js +74 -9
  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 +2 -0
  10. package/dist/ar/depth-grid-lookup.js +43 -0
  11. package/dist/ar/depth-sampler.d.ts +2 -2
  12. package/dist/ar/depth-sampler.js +2 -4
  13. package/dist/ar/depth-unprojection.d.ts +1 -1
  14. package/dist/ar/enable-gps-ar.d.ts +1 -1
  15. package/dist/ar/enable-gps-ar.js +26 -1
  16. package/dist/ar/frame-loop.d.ts +1 -1
  17. package/dist/ar/image-capture.d.ts +2 -2
  18. package/dist/ar/image-capture.js +1 -154
  19. package/dist/ar/image-quality.d.ts +2 -0
  20. package/dist/ar/image-quality.js +168 -0
  21. package/dist/ar/index.d.ts +31 -19
  22. package/dist/ar/index.js +17 -5
  23. package/dist/ar/occupancy-grid.d.ts +1 -1
  24. package/dist/ar/occupancy-grid.js +29 -2
  25. package/dist/ar/planar-pnp.d.ts +2 -0
  26. package/dist/ar/planar-pnp.js +425 -0
  27. package/dist/ar/qr-debug-view.d.ts +2 -0
  28. package/dist/ar/qr-debug-view.js +89 -0
  29. package/dist/ar/qr-derived-pose.d.ts +2 -0
  30. package/dist/ar/qr-derived-pose.js +114 -0
  31. package/dist/ar/qr-detection-controller.d.ts +2 -0
  32. package/dist/ar/qr-detection-controller.js +2 -0
  33. package/dist/ar/qr-frontend.d.ts +2 -0
  34. package/dist/ar/qr-frontend.js +60 -0
  35. package/dist/ar/qr-pose.d.ts +2 -0
  36. package/dist/ar/qr-pose.js +267 -0
  37. package/dist/ar/qr-size-depth-context.d.ts +3 -0
  38. package/dist/ar/qr-size-depth-context.js +42 -0
  39. package/dist/ar/qr-size-measurer.d.ts +2 -0
  40. package/dist/ar/qr-size-measurer.js +2 -0
  41. package/dist/ar/replay-scene.d.ts +1 -1
  42. package/dist/ar/replay-scene.js +1 -1
  43. package/dist/ar/scene-node-names.d.ts +1 -1
  44. package/dist/ar/webxr-nue-basis.d.ts +1 -1
  45. package/dist/ar/webxr-session.d.ts +4 -4
  46. package/dist/ar/webxr-session.js +2 -2
  47. package/dist/ar/xr-camera-texture.d.ts +1 -1
  48. package/dist/ar/xr-error-handler.d.ts +1 -1
  49. package/dist/ar/xr-frame-loop.d.ts +1 -1
  50. package/dist/ar-MFpFlV7j.js +382 -0
  51. package/dist/{ar-world-group-alignment-C9lwwIyI.d.ts → ar-world-group-alignment-69fgnB39.d.ts} +1 -1
  52. package/dist/{camera-blit-capture-CnxUDbKx.d.ts → camera-blit-capture-NKT9H5-e.d.ts} +57 -4
  53. package/dist/capture-motion-gate-0Yl2NpQG.d.ts +24 -0
  54. package/dist/capture-motion-gate-4EscNZCw.js +94 -0
  55. package/dist/concurrency-By0oOTZt.d.ts +50 -0
  56. package/dist/core/index.d.ts +2 -2
  57. package/dist/core/index.js +2 -2
  58. package/dist/create-slam-app-store-DkCHOpvb.js +149 -0
  59. package/dist/{create-slam-app-store-BljusOua.d.ts → create-slam-app-store-n_VF63D5.d.ts} +50 -40
  60. package/dist/depth-grid-lookup-DQ-IYhO3.d.ts +21 -0
  61. package/dist/{depth-sampler-Bhanfwla.d.ts → depth-sampler-asc8exHt.d.ts} +1 -1
  62. package/dist/{depth-unprojection-Y6i_mJuT.d.ts → depth-unprojection-DjsbJW9L.d.ts} +1 -1
  63. package/dist/{enable-gps-ar-BfUJOgpW.d.ts → enable-gps-ar-DkNhFv4Y.d.ts} +4 -4
  64. package/dist/{frame-conversions-BfsafGWR.d.ts → frame-conversions-DObhj8Wf.d.ts} +1 -1
  65. package/dist/{fused-path-DJoVu98b.d.ts → fused-path-v6eOmLl3.d.ts} +1 -1
  66. package/dist/geo/h3-proximity.d.ts +2 -2
  67. package/dist/geo/h3-proximity.js +61 -2
  68. package/dist/geo/index.d.ts +2 -2
  69. package/dist/geo/index.js +2 -2
  70. package/dist/geodesic-angle-BTldfFgs.js +39 -0
  71. package/dist/{gps-anchor-dYeCvvp5.d.ts → gps-anchor-BeFO2RYa.d.ts} +1 -1
  72. package/dist/{gps-ar-pose-sampler-DVb96iOy.d.ts → gps-ar-pose-sampler-DHMZmYAc.d.ts} +1 -1
  73. package/dist/{gps-event-coordinator-CWIDOk4U.d.ts → gps-event-coordinator-CjWW636x.d.ts} +21 -11
  74. package/dist/{gps-event-markers-Dc9FT-q1.d.ts → gps-event-markers-Cz9RMA2k.d.ts} +18 -0
  75. package/dist/{h3-proximity-I1msGPgE.d.ts → h3-proximity-C-j0pw1j.d.ts} +39 -1
  76. package/dist/image-capture-B2-b6tKK.js +374 -0
  77. package/dist/{image-capture-qAZ5Fh5X.d.ts → image-capture-CNZDrQXG.d.ts} +103 -3
  78. package/dist/image-quality-smCBoFj1.d.ts +158 -0
  79. package/dist/index-BAPokxMb.d.ts +22 -0
  80. package/dist/{index-BYYUdf9v.d.ts → index-BODDUTvD.d.ts} +1 -1
  81. package/dist/index-BneSj4_5.d.ts +399 -0
  82. package/dist/index-_Rhgoqkh.d.ts +2 -0
  83. package/dist/index.d.ts +80 -67
  84. package/dist/index.js +24 -12
  85. package/dist/{leaflet-map-overlay-6CJC-E1l.d.ts → leaflet-map-overlay-CmIyiE8n.d.ts} +1 -1
  86. package/dist/{map-data-CkloBxIl.d.ts → map-data-B4oEehUh.d.ts} +1 -1
  87. package/dist/{map-overlay-draw-krOKwDAq.d.ts → map-overlay-draw-ymz1s7fG.d.ts} +1 -1
  88. package/dist/{null-storage-backend-uKQE8tl7.d.ts → null-storage-backend-B9rtyDfG.d.ts} +2 -2
  89. package/dist/{occupancy-grid-BRNRaPyT.d.ts → occupancy-grid-DnOc-6PU.d.ts} +12 -2
  90. package/dist/{opfs-storage-D5zAn_lx.d.ts → opfs-storage-3VZh-qH6.d.ts} +23 -4
  91. package/dist/{opfs-storage-backend-6aYMUZlJ.d.ts → opfs-storage-backend-JJiUJo-3.d.ts} +3 -3
  92. package/dist/{persistence-middleware-B9ei4Nhx.d.ts → persistence-middleware-DAPTTwB2.d.ts} +1 -1
  93. package/dist/planar-pnp-my9bqULS.d.ts +67 -0
  94. package/dist/qr-debug-view-Brl73HGO.d.ts +29 -0
  95. package/dist/qr-derived-pose-CmxTP2m4.d.ts +122 -0
  96. package/dist/qr-detected-slice-VQssZmQJ.d.ts +166 -0
  97. package/dist/qr-detection-controller-B-tLVd-S.js +119 -0
  98. package/dist/qr-detection-controller-CDurHv8E.d.ts +48 -0
  99. package/dist/qr-frontend--fFX25mV.d.ts +58 -0
  100. package/dist/qr-pose-D2rfu5DA.d.ts +138 -0
  101. package/dist/qr-pose-aggregation-B7tWJq7j.d.ts +90 -0
  102. package/dist/qr-pose-aggregation-DBiaLW0O.js +191 -0
  103. package/dist/qr-size-depth-context-CeAG53fy.d.ts +13 -0
  104. package/dist/qr-size-measurer-DLb33geH.js +596 -0
  105. package/dist/qr-size-measurer-DQdAsid1.d.ts +165 -0
  106. package/dist/recording-options-BPCpMHLK.d.ts +514 -0
  107. package/dist/{recording-replayer-r0DZ9ALp.d.ts → recording-replayer-DQKpAGyQ.d.ts} +2 -2
  108. package/dist/recording-slice-DaxHZvNu.d.ts +55 -0
  109. package/dist/{replay-engine-XpDa0dy1.d.ts → replay-engine-CmKh_4wf.d.ts} +7 -4
  110. package/dist/{replay-scene-Bw58VL6p.d.ts → replay-scene-DBlDrV_x.d.ts} +3 -3
  111. package/dist/sensors/absolute-orientation.d.ts +2 -0
  112. package/dist/sensors/absolute-orientation.js +145 -0
  113. package/dist/sensors/gps-error-handler.d.ts +1 -1
  114. package/dist/sensors/gps.d.ts +1 -1
  115. package/dist/sensors/index.d.ts +5 -4
  116. package/dist/sensors/index.js +2 -1
  117. package/dist/sensors/permission-checker.d.ts +1 -1
  118. package/dist/sensors/permission-checker.js +1 -1
  119. package/dist/state/app-selectors.d.ts +1 -1
  120. package/dist/state/combined-root-state.d.ts +1 -1
  121. package/dist/state/create-slam-app-store.d.ts +1 -1
  122. package/dist/state/create-slam-app-store.js +1 -74
  123. package/dist/state/gps-ar-pose-sampler.d.ts +3 -3
  124. package/dist/state/gps-event-coordinator.d.ts +2 -2
  125. package/dist/state/gps-event-coordinator.js +27 -17
  126. package/dist/state/index.d.ts +20 -17
  127. package/dist/state/index.js +5 -4
  128. package/dist/state/persistence-middleware.d.ts +1 -1
  129. package/dist/state/persistence-middleware.js +30 -19
  130. package/dist/state/qr-detected-slice.d.ts +4 -0
  131. package/dist/state/qr-detected-slice.js +311 -0
  132. package/dist/state/recording-options.d.ts +2 -2
  133. package/dist/state/recording-options.js +273 -7
  134. package/dist/state/recording-replayer.d.ts +2 -2
  135. package/dist/state/recording-replayer.js +1 -1
  136. package/dist/state/recording-slice.d.ts +1 -1
  137. package/dist/state/recording-slice.js +5 -1
  138. package/dist/state/replay-engine.d.ts +1 -1
  139. package/dist/state/replay-engine.js +7 -3
  140. package/dist/state/store-subscribers.d.ts +2 -2
  141. package/dist/state/subscribe-to-selector.d.ts +1 -1
  142. package/dist/state/tracking-quality.d.ts +2 -2
  143. package/dist/state/tracking-quality.js +40 -128
  144. package/dist/state/tracking-slice.d.ts +1 -1
  145. package/dist/{state-BkIfpiyA.js → state-E51QT_-j.js} +2 -1
  146. package/dist/storage/file-system-utils.d.ts +1 -1
  147. package/dist/storage/index.d.ts +9 -9
  148. package/dist/storage/index.js +4 -4
  149. package/dist/storage/null-storage-backend.d.ts +1 -1
  150. package/dist/storage/opfs-storage-backend.d.ts +1 -1
  151. package/dist/storage/opfs-storage-backend.js +3 -3
  152. package/dist/storage/opfs-storage.d.ts +1 -1
  153. package/dist/storage/opfs-storage.js +5 -4
  154. package/dist/storage/storage-backend.d.ts +1 -1
  155. package/dist/storage/zip-coverage-embed.d.ts +2 -0
  156. package/dist/storage/zip-coverage-embed.js +88 -0
  157. package/dist/storage/zip-export.d.ts +2 -2
  158. package/dist/storage/zip-export.js +37 -59
  159. package/dist/storage/zip-reader.d.ts +1 -1
  160. package/dist/{storage-backend-Do6YDpNo.d.ts → storage-backend-BdVLD0dU.d.ts} +1 -1
  161. package/dist/{store-subscribers-0YWyOLvV.d.ts → store-subscribers-CA6AJH_8.d.ts} +2 -2
  162. package/dist/{subscribe-to-selector-CZ6k1NFP.d.ts → subscribe-to-selector-B6EUmQ3I.d.ts} +1 -1
  163. package/dist/test-utils/browser-mocks.d.ts +7 -1
  164. package/dist/test-utils/browser-mocks.js +6 -0
  165. package/dist/test-utils/zip-round-trip-helpers.d.ts +9 -1
  166. package/dist/test-utils/zip-round-trip-helpers.js +5 -1
  167. package/dist/{tracking-slice-CdK8c-8P.d.ts → tracking-slice-BuAZxXZ5.d.ts} +1 -1
  168. package/dist/types/ar-types.d.ts +1 -1
  169. package/dist/types/geo-types.d.ts +1 -1
  170. package/dist/types/index.d.ts +2 -2
  171. package/dist/utils/concurrency.d.ts +2 -2
  172. package/dist/utils/concurrency.js +66 -2
  173. package/dist/utils/failure-tracker.d.ts +1 -1
  174. package/dist/utils/format-file-size.d.ts +1 -1
  175. package/dist/utils/fused-path.d.ts +1 -1
  176. package/dist/utils/index.d.ts +8 -7
  177. package/dist/utils/index.js +2 -1
  178. package/dist/utils/list-formatter.d.ts +1 -1
  179. package/dist/utils/logger.d.ts +1 -1
  180. package/dist/visualization/accuracy-circles.d.ts +1 -1
  181. package/dist/visualization/alignment-lerper.d.ts +1 -1
  182. package/dist/visualization/ar-world-group-alignment.d.ts +1 -1
  183. package/dist/visualization/ar-world-group-alignment.js +1 -1
  184. package/dist/visualization/camera-follower.d.ts +1 -1
  185. package/dist/visualization/css3d-renderer-manager.d.ts +1 -1
  186. package/dist/visualization/frame-conversions.d.ts +1 -1
  187. package/dist/visualization/frustum-visibility.d.ts +1 -1
  188. package/dist/visualization/gps-anchor.d.ts +1 -1
  189. package/dist/visualization/gps-compass-cubes.d.ts +1 -1
  190. package/dist/visualization/gps-event-markers.d.ts +1 -1
  191. package/dist/visualization/gps-event-markers.js +40 -5
  192. package/dist/visualization/hit-test-reticle.d.ts +1 -1
  193. package/dist/visualization/hit-test-reticle.js +1 -0
  194. package/dist/visualization/index.d.ts +18 -18
  195. package/dist/visualization/leaflet-map-overlay.d.ts +1 -1
  196. package/dist/visualization/leaflet-map-overlay.js +15 -3
  197. package/dist/visualization/lerp-utils.d.ts +1 -1
  198. package/dist/visualization/map-data.d.ts +1 -1
  199. package/dist/visualization/map-overlay-draw.d.ts +1 -1
  200. package/dist/visualization/map-overlay.d.ts +1 -1
  201. package/dist/visualization/three-dispose.d.ts +1 -1
  202. package/dist/visualization/vis-colors.d.ts +1 -1
  203. package/dist/{webxr-session-TIaD883f.js → webxr-session-DG-eNfny.js} +244 -11
  204. package/dist/{webxr-session-GjZ2BIpS.d.ts → webxr-session-Do5L6G3B.d.ts} +86 -8
  205. package/dist/zip-coverage-embed-DCAREVmw.d.ts +41 -0
  206. package/dist/{zip-export-G14hkSPG.d.ts → zip-export-ol0S_Ofd.d.ts} +27 -23
  207. package/package.json +2 -2
  208. package/dist/concurrency-CO1FQvs3.d.ts +0 -24
  209. package/dist/file-system-CwXTNHmB.d.ts +0 -114
  210. package/dist/index-CF05QV2m.d.ts +0 -2
  211. package/dist/recording-options-DJBXuMxV.d.ts +0 -182
  212. package/dist/recording-slice-89O07ebs.d.ts +0 -36
  213. package/dist/storage/file-system.d.ts +0 -2
  214. package/dist/storage/file-system.js +0 -289
  215. /package/dist/{accuracy-circles-BpwWAs8G.d.ts → accuracy-circles-BtqOP_v1.d.ts} +0 -0
  216. /package/dist/{alignment-lerper-Dm9G7JJO.d.ts → alignment-lerper-Bil6vbVw.d.ts} +0 -0
  217. /package/dist/{ar-types-Dan1MVTi.d.ts → ar-types-BFX2r1wk.d.ts} +0 -0
  218. /package/dist/{bresenham3d-CFfd7n6B.d.ts → bresenham3d-CU1Gv2W4.d.ts} +0 -0
  219. /package/dist/{camera-follower-C2mwEn7c.d.ts → camera-follower-CqJJ7zRj.d.ts} +0 -0
  220. /package/dist/{capability-checker-CQXJHxXg.d.ts → capability-checker-DUQLg6gq.d.ts} +0 -0
  221. /package/dist/{capture-failure-tracker-CUeU3L_M.d.ts → capture-failure-tracker-gsm8q7yi.d.ts} +0 -0
  222. /package/dist/{chromium-camera-access-workaround-Jqbn0apH.d.ts → chromium-camera-access-workaround-D4K2uwwQ.d.ts} +0 -0
  223. /package/dist/{css3d-renderer-manager-ptu_t3B3.d.ts → css3d-renderer-manager-B3kGNtcn.d.ts} +0 -0
  224. /package/dist/{failure-tracker-Cn8Fo9D4.d.ts → failure-tracker-BO7M7jF_.d.ts} +0 -0
  225. /package/dist/{file-system-utils-CXIoL-DM.d.ts → file-system-utils-DOajnMm-.d.ts} +0 -0
  226. /package/dist/{format-file-size-CLCZz3lC.d.ts → format-file-size-Dc98ulzB.d.ts} +0 -0
  227. /package/dist/{frame-loop-CMewS8cY.d.ts → frame-loop-CpVjQ1dH.d.ts} +0 -0
  228. /package/dist/{frustum-visibility-CmI96ESS.d.ts → frustum-visibility-CeoCZchO.d.ts} +0 -0
  229. /package/dist/{geo-types-SaPnIEUM.d.ts → geo-types-3Ip1NzVR.d.ts} +0 -0
  230. /package/dist/{gps-VuezJWK3.d.ts → gps-BBO3hKfT.d.ts} +0 -0
  231. /package/dist/{gps-compass-cubes-BM1uGiYV.d.ts → gps-compass-cubes-DSN87L8p.d.ts} +0 -0
  232. /package/dist/{gps-error-handler-CJl_2MVm.d.ts → gps-error-handler-BbPsfw-_.d.ts} +0 -0
  233. /package/dist/{hit-test-reticle-BEzEybxZ.d.ts → hit-test-reticle-DV-2TpB9.d.ts} +0 -0
  234. /package/dist/{lerp-utils-BpV-0Dtk.d.ts → lerp-utils-BLmHhkVe.d.ts} +0 -0
  235. /package/dist/{list-formatter-RRcBe8Vg.d.ts → list-formatter-KNMS0ay2.d.ts} +0 -0
  236. /package/dist/{logger-BEHXsnnq.d.ts → logger-CLnPrHiD.d.ts} +0 -0
  237. /package/dist/{map-overlay-bcogY5fK.d.ts → map-overlay-Dh2B97MP.d.ts} +0 -0
  238. /package/dist/{permission-checker-GpAiSCwd.d.ts → permission-checker-CDuI8BwC.d.ts} +0 -0
  239. /package/dist/{scene-node-names-Bu0S66H1.d.ts → scene-node-names-BiShOQDB.d.ts} +0 -0
  240. /package/dist/{session-disposers-M-oashRH.js → session-disposers-zv9OGXPB.js} +0 -0
  241. /package/dist/{three-dispose-D950t71B.d.ts → three-dispose-Bt1k7WKh.d.ts} +0 -0
  242. /package/dist/{vis-colors-D6omC64K.d.ts → vis-colors-Obw2pLqT.d.ts} +0 -0
  243. /package/dist/{webxr-nue-basis-CqKQmZzT.d.ts → webxr-nue-basis-3nfjDJJp.d.ts} +0 -0
  244. /package/dist/{xr-camera-texture-DBwCaKTd.d.ts → xr-camera-texture-Cva1q3Qj.d.ts} +0 -0
  245. /package/dist/{xr-error-handler-C0AQRmxs.d.ts → xr-error-handler-CtvHpjfm.d.ts} +0 -0
  246. /package/dist/{xr-frame-loop-BtYytZJg.d.ts → xr-frame-loop-DIYtRcUa.d.ts} +0 -0
  247. /package/dist/{zip-reader-DSEXt7K6.d.ts → zip-reader-D9iPQwdQ.d.ts} +0 -0
@@ -29,6 +29,44 @@ interface KnownGeoAnchor {
29
29
  * Compute the H3 resolution-11 index for a GPS position.
30
30
  */
31
31
  declare function gpsToH3(lat: number, lng: number): string;
32
+ /** Minimal GPS coordinate shape (Leaflet convention: `lng`, not `lon`). */
33
+ interface LatLngLike {
34
+ readonly lat: number;
35
+ readonly lng: number;
36
+ }
37
+ /**
38
+ * Reduce a recorded GPS path to the deduplicated set of res-11 H3 cells it
39
+ * crossed — the per-tour "coverage index" stored in `SessionMetadata.h3Cells`.
40
+ *
41
+ * Cells are returned in first-seen (chronological) order and deduplicated, so a
42
+ * tour that dwells in one ~25 m cell contributes a single cell. This keeps the
43
+ * stored index small (see the metadata-size risk in the map-browser feedback
44
+ * doc, §7). Non-finite coordinates are skipped defensively — bad sensor data
45
+ * must not poison the index or throw inside `latLngToCell`.
46
+ */
47
+ declare function gpsPathToCoverageCells(path: readonly LatLngLike[]): string[];
48
+ /**
49
+ * Coarsen res-11 coverage cells to a target resolution for map zoom-clustering.
50
+ *
51
+ * Uses h3-js `cellToParent` — the ONLY correct way to coarsen an H3 cell. NEVER
52
+ * truncate the hex-string id: resolution is encoded in the high bits with
53
+ * trailing `f` padding, so slicing yields INVALID cells, not parents (verified;
54
+ * see the D1 gotcha in
55
+ * docs/2026-06-14-map-centric-recording-browser-and-h3-index-user-feedback.md).
56
+ *
57
+ * `targetRes` is clamped to `[0, H3_RESOLUTION]` because `cellToParent` throws
58
+ * when asked for a FINER resolution than the cell, and res 11 is the finest
59
+ * data stored — so the highest map zooms render the stored cells unclustered
60
+ * (`cellToParent(cell, 11) === cell`). A non-finite `targetRes` (e.g. a bad
61
+ * zoom→res mapping) degrades to unclustered output rather than throwing.
62
+ * Invalid input cells (corrupt/legacy metadata) are skipped defensively, as are
63
+ * valid cells coarser than `res` (which have no parent at the finer target res
64
+ * and would otherwise throw "incompatible resolutions").
65
+ *
66
+ * Output is deduplicated in first-seen order: sibling cells under one parent
67
+ * collapse to a single tile.
68
+ */
69
+ declare function clusterCellsByZoom(cells: readonly string[], targetRes: number): string[];
32
70
  /**
33
71
  * Approximate distance in metres using equirectangular projection.
34
72
  * Accurate to < 0.1% for distances under 1 km at mid-latitudes,
@@ -60,4 +98,4 @@ declare function h3CellsMatch(h3a: string, h3b: string): boolean;
60
98
  */
61
99
  declare function isH3Index(id: string): boolean;
62
100
  //#endregion
63
- export { gpsToH3 as a, findNearbyGeoAnchor as i, KnownGeoAnchor as n, h3CellsMatch as o, approxDistanceMetres as r, isH3Index as s, H3_RESOLUTION as t };
101
+ export { findNearbyGeoAnchor as a, h3CellsMatch as c, clusterCellsByZoom as i, isH3Index as l, KnownGeoAnchor as n, gpsPathToCoverageCells as o, approxDistanceMetres as r, gpsToH3 as s, H3_RESOLUTION as t };
@@ -0,0 +1,374 @@
1
+ import { createLogger } from "./utils/logger.js";
2
+ import { t as geodesicAngleRad } from "./geodesic-angle-BTldfFgs.js";
3
+ import { n as MotionWindow, r as decideCapture, t as DEFAULT_MOTION_FILTER } from "./capture-motion-gate-4EscNZCw.js";
4
+ import { DEFAULT_QUALITY_FILTER } from "./ar/image-quality.js";
5
+ import { quat } from "gl-matrix";
6
+ //#region ../src/ar/pose-motion.ts
7
+ /**
8
+ * Pose-motion helpers — stateless angular/linear velocity from two AR poses.
9
+ *
10
+ * The capture motion gate (blurry-frame skipping, see
11
+ * `GpsPlusSlamJs_Docs/docs/2026-06-23-blurry-frame-motion-gating-plan.md` §4.1)
12
+ * rejects frames whose device motion is too fast to yield a sharp image. Motion
13
+ * is derived from consecutive WebXR poses — cheaper than any image-content
14
+ * metric because it needs no GPU readback.
15
+ *
16
+ * This module holds ONLY the pure, stateless math (angle ÷ dt, distance ÷ dt).
17
+ * The stateful policy (sliding window, glitch rejection, the capture decision)
18
+ * lives in `ImageCaptureManager` and `capture-motion-gate.ts` — keeping the
19
+ * reusable atom separate from the capture-specific policy (plan §4.1 "Scope").
20
+ *
21
+ * Angular velocity reuses the shared `geodesicAngleRad` kernel rather than a
22
+ * fresh `acos` (CLAUDE.md: search first, prefer refactor over re-implementation).
23
+ */
24
+ /**
25
+ * Convert a WebXR object-form quaternion to a normalized gl-matrix quaternion.
26
+ * Normalizing defends against slightly non-unit inputs (the gate must not throw
27
+ * on imperfect tracking data).
28
+ */
29
+ function toGlQuat(q) {
30
+ const g = quat.fromValues(q.x, q.y, q.z, q.w);
31
+ return quat.normalize(g, g);
32
+ }
33
+ /**
34
+ * Angular velocity in radians/second between two orientations.
35
+ *
36
+ * @param qPrev - orientation at the previous frame.
37
+ * @param qCur - orientation at the current frame.
38
+ * @param dtSeconds - elapsed time in seconds (must be > 0).
39
+ * @returns geodesic angle between the orientations divided by `dtSeconds`.
40
+ * Returns `0` when `dtSeconds <= 0` (degenerate/duplicate frame timestamp) so
41
+ * a bad delta can never produce `Infinity`/`NaN` and spuriously flip the
42
+ * motion gate. Double-cover safe (`q` ≡ `−q`) via the shared kernel.
43
+ */
44
+ function angularVelocity(qPrev, qCur, dtSeconds) {
45
+ if (!(dtSeconds > 0)) return 0;
46
+ return geodesicAngleRad(toGlQuat(qPrev), toGlQuat(qCur)) / dtSeconds;
47
+ }
48
+ /**
49
+ * Linear velocity in metres/second between two positions.
50
+ *
51
+ * @param pPrev - position at the previous frame.
52
+ * @param pCur - position at the current frame.
53
+ * @param dtSeconds - elapsed time in seconds (must be > 0).
54
+ * @returns straight-line distance divided by `dtSeconds`. Returns `0` when
55
+ * `dtSeconds <= 0` (same guard rationale as {@link angularVelocity}).
56
+ */
57
+ function linearVelocity(pPrev, pCur, dtSeconds) {
58
+ if (!(dtSeconds > 0)) return 0;
59
+ const dx = pCur.x - pPrev.x;
60
+ const dy = pCur.y - pPrev.y;
61
+ const dz = pCur.z - pPrev.z;
62
+ return Math.sqrt(dx * dx + dy * dy + dz * dz) / dtSeconds;
63
+ }
64
+ //#endregion
65
+ //#region ../src/ar/image-capture.ts
66
+ /**
67
+ * Image Capture Module
68
+ *
69
+ * Captures periodic JPEG screenshots from the WebGL canvas during AR recording.
70
+ * Uses async toBlob() for better performance (non-blocking).
71
+ *
72
+ * Includes validation to detect suspiciously small images (likely black/empty)
73
+ * which can occur on mobile devices when the WebGL context hasn't composited yet.
74
+ */
75
+ const log = createLogger("ImageCapture");
76
+ /**
77
+ * Minimum expected blob size in bytes for a valid JPEG image.
78
+ * A black/empty 1920x1080 JPEG still compresses to ~2-3KB due to headers.
79
+ * A real camera frame at 0.7 quality should be 50KB+.
80
+ * We use a conservative threshold to catch obviously broken captures.
81
+ */
82
+ const MIN_VALID_IMAGE_BYTES = 5e3;
83
+ /**
84
+ * Default capture configuration
85
+ */
86
+ const DEFAULT_CAPTURE_CONFIG = {
87
+ intervalMs: 2e3,
88
+ quality: .7,
89
+ captureTimeoutMs: 5e3,
90
+ resolutionDivisor: 1,
91
+ motionFilter: DEFAULT_MOTION_FILTER,
92
+ qualityFilter: DEFAULT_QUALITY_FILTER
93
+ };
94
+ /**
95
+ * Manages periodic image capture from a canvas.
96
+ *
97
+ * Usage:
98
+ * 1. Create with canvas and callbacks
99
+ * 2. Call start() when recording begins
100
+ * 3. Call onFrame(time) each XR frame
101
+ * 4. Call stop() when recording ends
102
+ */
103
+ var ImageCaptureManager = class {
104
+ canvas;
105
+ callbacks;
106
+ config;
107
+ capturing = false;
108
+ lastCaptureTime = 0;
109
+ frameCount = 0;
110
+ captureInProgress = false;
111
+ captureTimeoutId = null;
112
+ /** True while an `analyzeFrame` verdict is in flight; blocks a second capture
113
+ * for the same interval without throttling normal cadence (captureInProgress
114
+ * is released after encode, NOT held across the round-trip). */
115
+ awaitingVerdict = false;
116
+ /** The current interval's capture was rejected by the quality gate; bypass the
117
+ * interval guard so the next acceptable frame is grabbed immediately. */
118
+ retryPending = false;
119
+ /** Frame time (ms) of the FIRST quality attempt for the current interval; the
120
+ * quality `maxWaitMs` fallback measures from here, independent of the motion
121
+ * gate's own due clock. `null` between intervals. */
122
+ qualityDeadlineBase = null;
123
+ /** Safety timeout for a hung `analyzeFrame` (fail-open). */
124
+ verdictTimeoutId = null;
125
+ /** Sliding window of recent per-frame velocities for the motion gate. */
126
+ motionWindow = new MotionWindow();
127
+ /** Previous frame's pose, for the per-frame velocity delta. */
128
+ prevPose = null;
129
+ /** Previous frame's timestamp (ms), for the per-frame velocity delta. */
130
+ prevTime = 0;
131
+ /** Whether THIS frame's velocity sample was rejected as a tracking glitch. */
132
+ lastSampleWasGlitch = false;
133
+ /** When the current capture first became due (ms); null while not due. The
134
+ * `maxWaitMs` fallback measures from here, not from `lastCaptureTime`. */
135
+ dueTime = null;
136
+ constructor(canvas, callbacks, config = DEFAULT_CAPTURE_CONFIG) {
137
+ this.canvas = canvas;
138
+ this.callbacks = callbacks;
139
+ this.config = config;
140
+ }
141
+ /**
142
+ * Start capturing images.
143
+ * Resets frame counter and last capture time.
144
+ */
145
+ start() {
146
+ this.capturing = true;
147
+ this.lastCaptureTime = 0;
148
+ this.frameCount = 0;
149
+ this.motionWindow.reset();
150
+ this.prevPose = null;
151
+ this.prevTime = 0;
152
+ this.lastSampleWasGlitch = false;
153
+ this.dueTime = null;
154
+ this.clearVerdictTimeout();
155
+ this.awaitingVerdict = false;
156
+ this.retryPending = false;
157
+ this.qualityDeadlineBase = null;
158
+ }
159
+ /**
160
+ * Stop capturing images.
161
+ * Clears any pending safety timeout and resets in-flight capture state.
162
+ */
163
+ stop() {
164
+ this.capturing = false;
165
+ this.clearCaptureTimeout();
166
+ this.captureInProgress = false;
167
+ this.clearVerdictTimeout();
168
+ this.awaitingVerdict = false;
169
+ this.retryPending = false;
170
+ this.qualityDeadlineBase = null;
171
+ }
172
+ /**
173
+ * Check if capture manager is active.
174
+ */
175
+ isCapturing() {
176
+ return this.capturing;
177
+ }
178
+ /**
179
+ * Get the current frame count.
180
+ */
181
+ getFrameCount() {
182
+ return this.frameCount;
183
+ }
184
+ /**
185
+ * Called each XR frame to check if a capture is needed.
186
+ *
187
+ * @param time - Frame timestamp in milliseconds (from requestAnimationFrame)
188
+ */
189
+ onFrame(time) {
190
+ if (!this.capturing) return;
191
+ const pose = this.callbacks.getCurrentPose();
192
+ this.updateMotionWindow(time, pose);
193
+ if (this.captureInProgress || this.awaitingVerdict) return;
194
+ const elapsed = time - this.lastCaptureTime;
195
+ if (!this.retryPending && this.lastCaptureTime > 0 && elapsed < this.config.intervalMs) {
196
+ this.dueTime = null;
197
+ return;
198
+ }
199
+ if (!pose) return;
200
+ if (this.dueTime === null) this.dueTime = time;
201
+ if (!this.shouldCaptureNow(time)) return;
202
+ this.captureInProgress = true;
203
+ this.lastCaptureTime = time;
204
+ this.dueTime = null;
205
+ const timestamp = performance.timeOrigin + time;
206
+ const screenRotation = this.callbacks.getScreenRotation();
207
+ this.captureTimeoutId = setTimeout(() => {
208
+ if (this.captureInProgress) {
209
+ log.error(`Capture timeout after ${this.config.captureTimeoutMs}ms — force-resetting captureInProgress`);
210
+ this.captureInProgress = false;
211
+ this.captureTimeoutId = null;
212
+ }
213
+ }, this.config.captureTimeoutMs);
214
+ if (this.callbacks.captureFrame) this.callbacks.captureFrame(this.config.quality).then((frame) => {
215
+ this.handleCapturedBlob(frame?.blob ?? null, frame?.width, frame?.height, timestamp, time, pose, screenRotation);
216
+ }).catch(() => {
217
+ this.clearCaptureTimeout();
218
+ this.captureInProgress = false;
219
+ this.callbacks.onCaptureFailed?.();
220
+ });
221
+ else this.canvas.toBlob((blob) => {
222
+ this.handleCapturedBlob(blob, this.canvas.width, this.canvas.height, timestamp, time, pose, screenRotation);
223
+ }, "image/jpeg", this.config.quality);
224
+ }
225
+ /**
226
+ * Sample per-frame device motion into the sliding window. Called every frame
227
+ * (even during an in-flight capture or before the interval elapses) so the
228
+ * window reflects truly instantaneous motion the moment a capture becomes
229
+ * due. A frame with no pose, or a non-positive dt (duplicate timestamp),
230
+ * records no sample and is treated as "not a glitch".
231
+ */
232
+ updateMotionWindow(time, pose) {
233
+ if (!pose) {
234
+ this.lastSampleWasGlitch = false;
235
+ this.motionWindow.reset();
236
+ this.prevPose = null;
237
+ this.prevTime = 0;
238
+ this.retryPending = false;
239
+ this.qualityDeadlineBase = null;
240
+ return;
241
+ }
242
+ if (this.prevPose && this.prevTime > 0) {
243
+ const dt = (time - this.prevTime) / 1e3;
244
+ if (dt > 0) {
245
+ const angVel = angularVelocity(this.prevPose.orientation, pose.orientation, dt);
246
+ const linVel = linearVelocity(this.prevPose.position, pose.position, dt);
247
+ this.lastSampleWasGlitch = !this.motionWindow.push(angVel, linVel);
248
+ } else this.lastSampleWasGlitch = false;
249
+ } else this.lastSampleWasGlitch = false;
250
+ this.prevPose = pose;
251
+ this.prevTime = time;
252
+ }
253
+ /**
254
+ * Apply the motion gate to a due capture. Returns `true` to capture now,
255
+ * `false` to defer to a later frame.
256
+ *
257
+ * - Filter disabled → always `true` (legacy behavior).
258
+ * - `maxWaitMs` elapsed → always `true` (never-calm safety fallback; an
259
+ * interval is never silently lost).
260
+ * - Current frame's sample was a tracking glitch → `false` (don't grab a
261
+ * relocalization-teleport frame; the fallback still guarantees progress).
262
+ * - No measurable motion yet (first capture / all-glitch) → `true` (the very
263
+ * first capture is never blocked; only measurable motion defers).
264
+ * - Otherwise → defer to {@link decideCapture} over the windowed maxima.
265
+ */
266
+ shouldCaptureNow(time) {
267
+ const mf = this.config.motionFilter;
268
+ if (!mf || !mf.enabled) return true;
269
+ const msSinceDue = this.dueTime === null ? 0 : time - this.dueTime;
270
+ if (msSinceDue >= mf.maxWaitMs) return true;
271
+ if (this.lastSampleWasGlitch) return false;
272
+ if (!this.motionWindow.hasSamples()) return true;
273
+ return decideCapture({
274
+ windowMaxAngular: this.motionWindow.maxAngular(),
275
+ windowMaxLinear: this.motionWindow.maxLinear(),
276
+ maxAngularVelocity: mf.maxAngularVelocity,
277
+ maxLinearVelocity: mf.maxLinearVelocity,
278
+ msSinceDue,
279
+ maxWaitMs: mf.maxWaitMs
280
+ }) === "capture";
281
+ }
282
+ /**
283
+ * Clear the safety timeout for captureInProgress.
284
+ * Called when capture completes (success or failure) before the timeout fires.
285
+ */
286
+ clearCaptureTimeout() {
287
+ if (this.captureTimeoutId !== null) {
288
+ clearTimeout(this.captureTimeoutId);
289
+ this.captureTimeoutId = null;
290
+ }
291
+ }
292
+ /** Clear the safety timeout for a hung `analyzeFrame` verdict. */
293
+ clearVerdictTimeout() {
294
+ if (this.verdictTimeoutId !== null) {
295
+ clearTimeout(this.verdictTimeoutId);
296
+ this.verdictTimeoutId = null;
297
+ }
298
+ }
299
+ /**
300
+ * Common handler for captured blobs (from either canvas.toBlob or captureFrame).
301
+ * Releases `captureInProgress` after the synchronous encode, then either saves
302
+ * immediately (legacy / gate-off) or runs the off-thread image-quality gate.
303
+ *
304
+ * @param time - the XR frame time the capture was dispatched at; the quality
305
+ * retry-deadline clock and `lastCaptureTime` are measured from it.
306
+ */
307
+ handleCapturedBlob(blob, width, height, timestamp, time, pose, screenRotation) {
308
+ this.clearCaptureTimeout();
309
+ this.captureInProgress = false;
310
+ if (!blob) {
311
+ this.callbacks.onCaptureFailed?.();
312
+ return;
313
+ }
314
+ const qf = this.config.qualityFilter;
315
+ const analyze = this.callbacks.analyzeFrame;
316
+ if (!analyze || !qf || !qf.enabled) {
317
+ this.saveCapture(blob, width, height, timestamp, pose, screenRotation);
318
+ return;
319
+ }
320
+ if (this.qualityDeadlineBase === null) this.qualityDeadlineBase = time;
321
+ if (time - this.qualityDeadlineBase >= qf.maxWaitMs) {
322
+ this.saveCapture(blob, width, height, timestamp, pose, screenRotation);
323
+ return;
324
+ }
325
+ this.awaitingVerdict = true;
326
+ let settled = false;
327
+ const finish = (accept, viaTimeout) => {
328
+ if (settled || !this.capturing) return;
329
+ settled = true;
330
+ this.clearVerdictTimeout();
331
+ this.awaitingVerdict = false;
332
+ if (viaTimeout) log.error("analyzeFrame verdict timed out — saving frame fail-open to keep the pipeline live");
333
+ if (accept) this.saveCapture(blob, width, height, timestamp, pose, screenRotation);
334
+ else this.retryPending = true;
335
+ };
336
+ this.verdictTimeoutId = setTimeout(() => finish(true, true), this.config.captureTimeoutMs);
337
+ analyze({
338
+ blob,
339
+ width: width ?? 0,
340
+ height: height ?? 0
341
+ }).then((verdict) => finish(verdict.accept, false)).catch((err) => {
342
+ log.error("analyzeFrame failed — saving frame without quality gate", err);
343
+ finish(true, false);
344
+ });
345
+ }
346
+ /**
347
+ * Persist an accepted frame: allocate its 1-based index, run the suspicious-
348
+ * size belt-and-suspenders check, reset the per-interval gate clocks, and fire
349
+ * `onCaptured`. Called from the legacy path, the quality fallbacks, and an
350
+ * accepting verdict — the single place a frame becomes durable.
351
+ */
352
+ saveCapture(blob, width, height, timestamp, pose, screenRotation) {
353
+ if (!this.capturing) return;
354
+ this.retryPending = false;
355
+ this.qualityDeadlineBase = null;
356
+ const frameIndex = ++this.frameCount;
357
+ if (blob.size < 5e3) {
358
+ log.error(`Suspicious image at frame ${frameIndex}: blob size ${blob.size} bytes is below minimum ${MIN_VALID_IMAGE_BYTES} bytes. Image may be black/empty.`);
359
+ this.callbacks.onSuspiciousImage?.(blob.size, frameIndex);
360
+ }
361
+ this.callbacks.onCaptured({
362
+ blob,
363
+ timestamp,
364
+ frameIndex,
365
+ position: pose.position,
366
+ rotation: pose.orientation,
367
+ screenRotation,
368
+ ...width && width > 0 ? { width } : {},
369
+ ...height && height > 0 ? { height } : {}
370
+ });
371
+ }
372
+ };
373
+ //#endregion
374
+ export { ImageCaptureManager as n, MIN_VALID_IMAGE_BYTES as r, DEFAULT_CAPTURE_CONFIG as t };
@@ -1,4 +1,6 @@
1
- import { o as WebXRQuaternion, s as WebXRVec3, t as ARPose } from "./ar-types-Dan1MVTi.js";
1
+ import { o as WebXRQuaternion, s as WebXRVec3, t as ARPose } from "./ar-types-BFX2r1wk.js";
2
+ import { t as MotionFilterConfig } from "./capture-motion-gate-0Yl2NpQG.js";
3
+ import { a as QualityFilterConfig } from "./image-quality-smCBoFj1.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 };