gps-plus-slam-app-framework 1.4.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 (212) hide show
  1. package/dist/absolute-orientation-CAEh840t.d.ts +54 -0
  2. package/dist/{app-selectors-CrOucL00.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 +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-sampler.d.ts +2 -2
  10. package/dist/ar/depth-unprojection.d.ts +1 -1
  11. package/dist/ar/enable-gps-ar.d.ts +1 -1
  12. package/dist/ar/enable-gps-ar.js +1 -1
  13. package/dist/ar/frame-loop.d.ts +1 -1
  14. package/dist/ar/image-capture.d.ts +2 -2
  15. package/dist/ar/image-capture.js +1 -154
  16. package/dist/ar/image-quality.d.ts +2 -0
  17. package/dist/ar/image-quality.js +168 -0
  18. package/dist/ar/index.d.ts +31 -30
  19. package/dist/ar/index.js +6 -5
  20. package/dist/ar/occupancy-grid.d.ts +1 -1
  21. package/dist/ar/planar-pnp.d.ts +1 -1
  22. package/dist/ar/qr-debug-view.d.ts +1 -1
  23. package/dist/ar/qr-derived-pose.d.ts +1 -1
  24. package/dist/ar/qr-detection-controller.d.ts +1 -1
  25. package/dist/ar/qr-frontend.d.ts +1 -1
  26. package/dist/ar/qr-pose.d.ts +1 -1
  27. package/dist/ar/qr-size-depth-context.d.ts +2 -2
  28. package/dist/ar/qr-size-measurer.d.ts +1 -1
  29. package/dist/ar/replay-scene.d.ts +1 -1
  30. package/dist/ar/replay-scene.js +1 -1
  31. package/dist/ar/scene-node-names.d.ts +1 -1
  32. package/dist/ar/webxr-nue-basis.d.ts +1 -1
  33. package/dist/ar/webxr-session.d.ts +4 -4
  34. package/dist/ar/webxr-session.js +2 -2
  35. package/dist/ar/xr-camera-texture.d.ts +1 -1
  36. package/dist/ar/xr-error-handler.d.ts +1 -1
  37. package/dist/ar/xr-frame-loop.d.ts +1 -1
  38. package/dist/{ar-Dc72Ksiu.js → ar-MFpFlV7j.js} +3 -3
  39. package/dist/{ar-world-group-alignment-DDLkgNf_.d.ts → ar-world-group-alignment-69fgnB39.d.ts} +1 -1
  40. package/dist/capture-motion-gate-0Yl2NpQG.d.ts +24 -0
  41. package/dist/capture-motion-gate-4EscNZCw.js +94 -0
  42. package/dist/core/index.d.ts +2 -2
  43. package/dist/core/index.js +2 -2
  44. package/dist/create-slam-app-store-DkCHOpvb.js +149 -0
  45. package/dist/{create-slam-app-store-Ce2FMKCp.d.ts → create-slam-app-store-n_VF63D5.d.ts} +50 -40
  46. package/dist/{depth-grid-lookup-vMDrGTk_.d.ts → depth-grid-lookup-DQ-IYhO3.d.ts} +1 -1
  47. package/dist/{depth-sampler-DwfPYzS7.d.ts → depth-sampler-asc8exHt.d.ts} +1 -1
  48. package/dist/{depth-unprojection-CK3b9htH.d.ts → depth-unprojection-DjsbJW9L.d.ts} +1 -1
  49. package/dist/{enable-gps-ar-CjZ1nEdu.d.ts → enable-gps-ar-DkNhFv4Y.d.ts} +4 -4
  50. package/dist/{frame-conversions-D_Af8Dt3.d.ts → frame-conversions-DObhj8Wf.d.ts} +1 -1
  51. package/dist/{fused-path-C0FHe67t.d.ts → fused-path-v6eOmLl3.d.ts} +1 -1
  52. package/dist/geo/h3-proximity.d.ts +1 -1
  53. package/dist/geo/index.d.ts +1 -1
  54. package/dist/geodesic-angle-BTldfFgs.js +39 -0
  55. package/dist/{gps-anchor-C6I9NDHA.d.ts → gps-anchor-BeFO2RYa.d.ts} +1 -1
  56. package/dist/{gps-ar-pose-sampler-DaHg2aPZ.d.ts → gps-ar-pose-sampler-DHMZmYAc.d.ts} +1 -1
  57. package/dist/{gps-event-coordinator-C7hwHyLA.d.ts → gps-event-coordinator-CjWW636x.d.ts} +21 -11
  58. package/dist/image-capture-B2-b6tKK.js +374 -0
  59. package/dist/{image-capture-C2Feywet.d.ts → image-capture-CNZDrQXG.d.ts} +103 -3
  60. package/dist/image-quality-smCBoFj1.d.ts +158 -0
  61. package/dist/index-BAPokxMb.d.ts +22 -0
  62. package/dist/{index-m_LneUSi.d.ts → index-BODDUTvD.d.ts} +1 -1
  63. package/dist/{index-BvLo2poy.d.ts → index-BneSj4_5.d.ts} +3 -3
  64. package/dist/index-_Rhgoqkh.d.ts +2 -0
  65. package/dist/index.d.ts +80 -77
  66. package/dist/index.js +11 -8
  67. package/dist/{leaflet-map-overlay-bnb2U1NQ.d.ts → leaflet-map-overlay-CmIyiE8n.d.ts} +1 -1
  68. package/dist/{map-data-DdjPiSav.d.ts → map-data-B4oEehUh.d.ts} +1 -1
  69. package/dist/{map-overlay-draw-CPVdAsFq.d.ts → map-overlay-draw-ymz1s7fG.d.ts} +1 -1
  70. package/dist/{null-storage-backend-CuSdYFsM.d.ts → null-storage-backend-B9rtyDfG.d.ts} +2 -2
  71. package/dist/{occupancy-grid-Bvi2Gw1U.d.ts → occupancy-grid-DnOc-6PU.d.ts} +2 -2
  72. package/dist/{opfs-storage-backend-CeQAb0SD.d.ts → opfs-storage-backend-JJiUJo-3.d.ts} +2 -2
  73. package/dist/{persistence-middleware-5eearhSV.d.ts → persistence-middleware-DAPTTwB2.d.ts} +1 -1
  74. package/dist/{planar-pnp-cjthe7Be.d.ts → planar-pnp-my9bqULS.d.ts} +1 -1
  75. package/dist/{qr-debug-view-B8gTzjMm.d.ts → qr-debug-view-Brl73HGO.d.ts} +1 -1
  76. package/dist/{qr-derived-pose-DinKE600.d.ts → qr-derived-pose-CmxTP2m4.d.ts} +2 -2
  77. package/dist/{qr-detected-slice-Cpv-dg82.d.ts → qr-detected-slice-VQssZmQJ.d.ts} +4 -4
  78. package/dist/{qr-detection-controller-Cd2Ih4cC.d.ts → qr-detection-controller-CDurHv8E.d.ts} +3 -3
  79. package/dist/{qr-frontend-DufZp2xS.d.ts → qr-frontend--fFX25mV.d.ts} +1 -1
  80. package/dist/{qr-pose-aggregation-CFlmLm6w.d.ts → qr-pose-aggregation-B7tWJq7j.d.ts} +1 -1
  81. package/dist/{qr-pose-aggregation-DXR9STHs.js → qr-pose-aggregation-DBiaLW0O.js} +1 -10
  82. package/dist/{qr-size-depth-context-onu36E9s.d.ts → qr-size-depth-context-CeAG53fy.d.ts} +2 -2
  83. package/dist/{qr-size-measurer-D7739ZpB.d.ts → qr-size-measurer-DQdAsid1.d.ts} +3 -3
  84. package/dist/{recording-options-D93kksJU.d.ts → recording-options-BPCpMHLK.d.ts} +154 -9
  85. package/dist/{recording-replayer-CUEK82tM.d.ts → recording-replayer-DQKpAGyQ.d.ts} +2 -2
  86. package/dist/{recording-slice-B-uCgGoh.d.ts → recording-slice-DaxHZvNu.d.ts} +2 -2
  87. package/dist/{replay-engine-BSVd7Zzb.d.ts → replay-engine-CmKh_4wf.d.ts} +1 -1
  88. package/dist/{replay-scene-CPt56pps.d.ts → replay-scene-DBlDrV_x.d.ts} +3 -3
  89. package/dist/sensors/absolute-orientation.d.ts +2 -0
  90. package/dist/sensors/absolute-orientation.js +145 -0
  91. package/dist/sensors/gps-error-handler.d.ts +1 -1
  92. package/dist/sensors/gps.d.ts +1 -1
  93. package/dist/sensors/index.d.ts +5 -4
  94. package/dist/sensors/index.js +2 -1
  95. package/dist/sensors/permission-checker.d.ts +1 -1
  96. package/dist/state/app-selectors.d.ts +1 -1
  97. package/dist/state/combined-root-state.d.ts +1 -1
  98. package/dist/state/create-slam-app-store.d.ts +1 -1
  99. package/dist/state/create-slam-app-store.js +1 -74
  100. package/dist/state/gps-ar-pose-sampler.d.ts +3 -3
  101. package/dist/state/gps-event-coordinator.d.ts +2 -2
  102. package/dist/state/gps-event-coordinator.js +27 -17
  103. package/dist/state/index.d.ts +20 -20
  104. package/dist/state/index.js +4 -4
  105. package/dist/state/persistence-middleware.d.ts +1 -1
  106. package/dist/state/persistence-middleware.js +30 -19
  107. package/dist/state/qr-detected-slice.d.ts +3 -3
  108. package/dist/state/qr-detected-slice.js +1 -1
  109. package/dist/state/recording-options.d.ts +2 -2
  110. package/dist/state/recording-options.js +155 -13
  111. package/dist/state/recording-replayer.d.ts +2 -2
  112. package/dist/state/recording-replayer.js +1 -1
  113. package/dist/state/recording-slice.d.ts +1 -1
  114. package/dist/state/replay-engine.d.ts +1 -1
  115. package/dist/state/store-subscribers.d.ts +2 -2
  116. package/dist/state/subscribe-to-selector.d.ts +1 -1
  117. package/dist/state/tracking-quality.d.ts +2 -2
  118. package/dist/state/tracking-quality.js +40 -128
  119. package/dist/state/tracking-slice.d.ts +1 -1
  120. package/dist/{state-IEw0ZsBu.js → state-E51QT_-j.js} +1 -1
  121. package/dist/storage/file-system-utils.d.ts +1 -1
  122. package/dist/storage/index.d.ts +8 -8
  123. package/dist/storage/null-storage-backend.d.ts +1 -1
  124. package/dist/storage/opfs-storage-backend.d.ts +1 -1
  125. package/dist/storage/opfs-storage.d.ts +1 -1
  126. package/dist/storage/storage-backend.d.ts +1 -1
  127. package/dist/storage/zip-coverage-embed.d.ts +1 -1
  128. package/dist/storage/zip-export.d.ts +1 -1
  129. package/dist/storage/zip-reader.d.ts +1 -1
  130. package/dist/{storage-backend-CTJHNdMp.d.ts → storage-backend-BdVLD0dU.d.ts} +1 -1
  131. package/dist/{store-subscribers-BZP81hjh.d.ts → store-subscribers-CA6AJH_8.d.ts} +2 -2
  132. package/dist/{subscribe-to-selector-Bp3sLx8L.d.ts → subscribe-to-selector-B6EUmQ3I.d.ts} +1 -1
  133. package/dist/test-utils/browser-mocks.d.ts +1 -1
  134. package/dist/test-utils/zip-round-trip-helpers.d.ts +1 -1
  135. package/dist/{tracking-slice-DJZ9qeGg.d.ts → tracking-slice-BuAZxXZ5.d.ts} +1 -1
  136. package/dist/types/ar-types.d.ts +1 -1
  137. package/dist/types/geo-types.d.ts +1 -1
  138. package/dist/types/index.d.ts +2 -2
  139. package/dist/utils/concurrency.d.ts +1 -1
  140. package/dist/utils/failure-tracker.d.ts +1 -1
  141. package/dist/utils/format-file-size.d.ts +1 -1
  142. package/dist/utils/fused-path.d.ts +1 -1
  143. package/dist/utils/index.d.ts +8 -7
  144. package/dist/utils/index.js +2 -1
  145. package/dist/utils/list-formatter.d.ts +1 -1
  146. package/dist/utils/logger.d.ts +1 -1
  147. package/dist/visualization/accuracy-circles.d.ts +1 -1
  148. package/dist/visualization/alignment-lerper.d.ts +1 -1
  149. package/dist/visualization/ar-world-group-alignment.d.ts +1 -1
  150. package/dist/visualization/ar-world-group-alignment.js +1 -1
  151. package/dist/visualization/camera-follower.d.ts +1 -1
  152. package/dist/visualization/css3d-renderer-manager.d.ts +1 -1
  153. package/dist/visualization/frame-conversions.d.ts +1 -1
  154. package/dist/visualization/frustum-visibility.d.ts +1 -1
  155. package/dist/visualization/gps-anchor.d.ts +1 -1
  156. package/dist/visualization/gps-compass-cubes.d.ts +1 -1
  157. package/dist/visualization/gps-event-markers.d.ts +1 -1
  158. package/dist/visualization/gps-event-markers.js +1 -1
  159. package/dist/visualization/hit-test-reticle.d.ts +1 -1
  160. package/dist/visualization/index.d.ts +18 -18
  161. package/dist/visualization/leaflet-map-overlay.d.ts +1 -1
  162. package/dist/visualization/lerp-utils.d.ts +1 -1
  163. package/dist/visualization/map-data.d.ts +1 -1
  164. package/dist/visualization/map-overlay-draw.d.ts +1 -1
  165. package/dist/visualization/map-overlay.d.ts +1 -1
  166. package/dist/visualization/three-dispose.d.ts +1 -1
  167. package/dist/visualization/vis-colors.d.ts +1 -1
  168. package/dist/{webxr-session-CJF9Nijp.js → webxr-session-DG-eNfny.js} +25 -4
  169. package/dist/{webxr-session-COsOjJwn.d.ts → webxr-session-Do5L6G3B.d.ts} +19 -9
  170. package/package.json +2 -2
  171. package/dist/index-eaI1Z1KR.d.ts +0 -2
  172. /package/dist/{accuracy-circles-Pi2YeCrM.d.ts → accuracy-circles-BtqOP_v1.d.ts} +0 -0
  173. /package/dist/{alignment-lerper-BUTsQ2yu.d.ts → alignment-lerper-Bil6vbVw.d.ts} +0 -0
  174. /package/dist/{ar-types-BckI1E-O.d.ts → ar-types-BFX2r1wk.d.ts} +0 -0
  175. /package/dist/{bresenham3d-ByjcmEgM.d.ts → bresenham3d-CU1Gv2W4.d.ts} +0 -0
  176. /package/dist/{camera-blit-capture-CqWe1G7s.d.ts → camera-blit-capture-NKT9H5-e.d.ts} +0 -0
  177. /package/dist/{camera-follower-BTb2Yp12.d.ts → camera-follower-CqJJ7zRj.d.ts} +0 -0
  178. /package/dist/{capability-checker-CXqoJYtb.d.ts → capability-checker-DUQLg6gq.d.ts} +0 -0
  179. /package/dist/{capture-failure-tracker-BL5SoG2a.d.ts → capture-failure-tracker-gsm8q7yi.d.ts} +0 -0
  180. /package/dist/{chromium-camera-access-workaround-BNK189P0.d.ts → chromium-camera-access-workaround-D4K2uwwQ.d.ts} +0 -0
  181. /package/dist/{concurrency-DqAlKX4j.d.ts → concurrency-By0oOTZt.d.ts} +0 -0
  182. /package/dist/{css3d-renderer-manager-m-ab19vp.d.ts → css3d-renderer-manager-B3kGNtcn.d.ts} +0 -0
  183. /package/dist/{failure-tracker-Cnc_xWUw.d.ts → failure-tracker-BO7M7jF_.d.ts} +0 -0
  184. /package/dist/{file-system-utils-Bs7lpeVP.d.ts → file-system-utils-DOajnMm-.d.ts} +0 -0
  185. /package/dist/{format-file-size-BFUWvir1.d.ts → format-file-size-Dc98ulzB.d.ts} +0 -0
  186. /package/dist/{frame-loop-BEO9_q3k.d.ts → frame-loop-CpVjQ1dH.d.ts} +0 -0
  187. /package/dist/{frustum-visibility-D_RGmH98.d.ts → frustum-visibility-CeoCZchO.d.ts} +0 -0
  188. /package/dist/{geo-types-DqiBzPO9.d.ts → geo-types-3Ip1NzVR.d.ts} +0 -0
  189. /package/dist/{gps-C5inWeNV.d.ts → gps-BBO3hKfT.d.ts} +0 -0
  190. /package/dist/{gps-compass-cubes-BExxrWJ-.d.ts → gps-compass-cubes-DSN87L8p.d.ts} +0 -0
  191. /package/dist/{gps-error-handler-C2bS69oJ.d.ts → gps-error-handler-BbPsfw-_.d.ts} +0 -0
  192. /package/dist/{gps-event-markers-Dd0G2bD0.d.ts → gps-event-markers-Cz9RMA2k.d.ts} +0 -0
  193. /package/dist/{h3-proximity-B57C6rYL.d.ts → h3-proximity-C-j0pw1j.d.ts} +0 -0
  194. /package/dist/{hit-test-reticle-Dqak9hiR.d.ts → hit-test-reticle-DV-2TpB9.d.ts} +0 -0
  195. /package/dist/{lerp-utils-kLxMJ2Xj.d.ts → lerp-utils-BLmHhkVe.d.ts} +0 -0
  196. /package/dist/{list-formatter-ct8txwvR.d.ts → list-formatter-KNMS0ay2.d.ts} +0 -0
  197. /package/dist/{logger-CzQrCe6C.d.ts → logger-CLnPrHiD.d.ts} +0 -0
  198. /package/dist/{map-overlay-Dci4yvm3.d.ts → map-overlay-Dh2B97MP.d.ts} +0 -0
  199. /package/dist/{opfs-storage-DMtlt57S.d.ts → opfs-storage-3VZh-qH6.d.ts} +0 -0
  200. /package/dist/{permission-checker-CtPtPQRq.d.ts → permission-checker-CDuI8BwC.d.ts} +0 -0
  201. /package/dist/{qr-pose-Bt2reSw7.d.ts → qr-pose-D2rfu5DA.d.ts} +0 -0
  202. /package/dist/{scene-node-names-CdNeQjOE.d.ts → scene-node-names-BiShOQDB.d.ts} +0 -0
  203. /package/dist/{session-disposers-D8wnO-uW.js → session-disposers-zv9OGXPB.js} +0 -0
  204. /package/dist/{three-dispose-BCZkSJp2.d.ts → three-dispose-Bt1k7WKh.d.ts} +0 -0
  205. /package/dist/{vis-colors-BZsSLI4i.d.ts → vis-colors-Obw2pLqT.d.ts} +0 -0
  206. /package/dist/{webxr-nue-basis-CW1wsbm6.d.ts → webxr-nue-basis-3nfjDJJp.d.ts} +0 -0
  207. /package/dist/{xr-camera-texture-BmrP7iNZ.d.ts → xr-camera-texture-Cva1q3Qj.d.ts} +0 -0
  208. /package/dist/{xr-error-handler-BEjRgZjt.d.ts → xr-error-handler-CtvHpjfm.d.ts} +0 -0
  209. /package/dist/{xr-frame-loop-K8ebdjLw.d.ts → xr-frame-loop-DIYtRcUa.d.ts} +0 -0
  210. /package/dist/{zip-coverage-embed-tFohI2J4.d.ts → zip-coverage-embed-DCAREVmw.d.ts} +0 -0
  211. /package/dist/{zip-export-DDWXInTR.d.ts → zip-export-ol0S_Ofd.d.ts} +0 -0
  212. /package/dist/{zip-reader-Q2ue6zlC.d.ts → zip-reader-D9iPQwdQ.d.ts} +0 -0
@@ -1,4 +1,4 @@
1
- import { t as GpsCoord } from "./geo-types-DqiBzPO9.js";
1
+ import { t as GpsCoord } from "./geo-types-3Ip1NzVR.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-C-j0pw1j.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-C-j0pw1j.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-_Rhgoqkh.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-BFX2r1wk.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-BFX2r1wk.js";
2
+ import { n as RawDeviceOrientation, t as GpsPosition } from "./gps-BBO3hKfT.js";
3
+ import { t as AbsoluteOrientationReading } from "./absolute-orientation-CAEh840t.js";
4
+ import { i as SlamAppStore } from "./create-slam-app-store-n_VF63D5.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 };
@@ -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-BckI1E-O.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 };