gps-plus-slam-app-framework 1.3.0 → 1.4.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 (225) hide show
  1. package/dist/{app-selectors-B8VQoMDY.d.ts → app-selectors-CrOucL00.d.ts} +1 -1
  2. package/dist/ar/bresenham3d.d.ts +1 -1
  3. package/dist/ar/camera-blit-capture.d.ts +2 -2
  4. package/dist/ar/camera-blit-capture.js +74 -9
  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 +2 -0
  9. package/dist/ar/depth-grid-lookup.js +43 -0
  10. package/dist/ar/depth-sampler.d.ts +2 -2
  11. package/dist/ar/depth-sampler.js +2 -4
  12. package/dist/ar/depth-unprojection.d.ts +1 -1
  13. package/dist/ar/enable-gps-ar.d.ts +1 -1
  14. package/dist/ar/enable-gps-ar.js +26 -1
  15. package/dist/ar/frame-loop.d.ts +1 -1
  16. package/dist/ar/image-capture.d.ts +1 -1
  17. package/dist/ar/index.d.ts +30 -19
  18. package/dist/ar/index.js +15 -4
  19. package/dist/ar/occupancy-grid.d.ts +1 -1
  20. package/dist/ar/occupancy-grid.js +29 -2
  21. package/dist/ar/planar-pnp.d.ts +2 -0
  22. package/dist/ar/planar-pnp.js +425 -0
  23. package/dist/ar/qr-debug-view.d.ts +2 -0
  24. package/dist/ar/qr-debug-view.js +89 -0
  25. package/dist/ar/qr-derived-pose.d.ts +2 -0
  26. package/dist/ar/qr-derived-pose.js +114 -0
  27. package/dist/ar/qr-detection-controller.d.ts +2 -0
  28. package/dist/ar/qr-detection-controller.js +2 -0
  29. package/dist/ar/qr-frontend.d.ts +2 -0
  30. package/dist/ar/qr-frontend.js +60 -0
  31. package/dist/ar/qr-pose.d.ts +2 -0
  32. package/dist/ar/qr-pose.js +267 -0
  33. package/dist/ar/qr-size-depth-context.d.ts +3 -0
  34. package/dist/ar/qr-size-depth-context.js +42 -0
  35. package/dist/ar/qr-size-measurer.d.ts +2 -0
  36. package/dist/ar/qr-size-measurer.js +2 -0
  37. package/dist/ar/replay-scene.d.ts +1 -1
  38. package/dist/ar/replay-scene.js +1 -1
  39. package/dist/ar/scene-node-names.d.ts +1 -1
  40. package/dist/ar/webxr-nue-basis.d.ts +1 -1
  41. package/dist/ar/webxr-session.d.ts +4 -4
  42. package/dist/ar/webxr-session.js +2 -2
  43. package/dist/ar/xr-camera-texture.d.ts +1 -1
  44. package/dist/ar/xr-error-handler.d.ts +1 -1
  45. package/dist/ar/xr-frame-loop.d.ts +1 -1
  46. package/dist/ar-Dc72Ksiu.js +382 -0
  47. package/dist/{ar-world-group-alignment-C9lwwIyI.d.ts → ar-world-group-alignment-DDLkgNf_.d.ts} +1 -1
  48. package/dist/{camera-blit-capture-CnxUDbKx.d.ts → camera-blit-capture-CqWe1G7s.d.ts} +57 -4
  49. package/dist/concurrency-DqAlKX4j.d.ts +50 -0
  50. package/dist/core/index.d.ts +1 -1
  51. package/dist/{create-slam-app-store-BljusOua.d.ts → create-slam-app-store-Ce2FMKCp.d.ts} +5 -5
  52. package/dist/depth-grid-lookup-vMDrGTk_.d.ts +21 -0
  53. package/dist/{depth-sampler-Bhanfwla.d.ts → depth-sampler-DwfPYzS7.d.ts} +1 -1
  54. package/dist/{depth-unprojection-Y6i_mJuT.d.ts → depth-unprojection-CK3b9htH.d.ts} +1 -1
  55. package/dist/{enable-gps-ar-BfUJOgpW.d.ts → enable-gps-ar-CjZ1nEdu.d.ts} +4 -4
  56. package/dist/{frame-conversions-BfsafGWR.d.ts → frame-conversions-D_Af8Dt3.d.ts} +1 -1
  57. package/dist/{fused-path-DJoVu98b.d.ts → fused-path-C0FHe67t.d.ts} +1 -1
  58. package/dist/geo/h3-proximity.d.ts +2 -2
  59. package/dist/geo/h3-proximity.js +61 -2
  60. package/dist/geo/index.d.ts +2 -2
  61. package/dist/geo/index.js +2 -2
  62. package/dist/{gps-anchor-dYeCvvp5.d.ts → gps-anchor-C6I9NDHA.d.ts} +1 -1
  63. package/dist/{gps-ar-pose-sampler-DVb96iOy.d.ts → gps-ar-pose-sampler-DaHg2aPZ.d.ts} +1 -1
  64. package/dist/{gps-event-coordinator-CWIDOk4U.d.ts → gps-event-coordinator-C7hwHyLA.d.ts} +3 -3
  65. package/dist/{gps-event-markers-Dc9FT-q1.d.ts → gps-event-markers-Dd0G2bD0.d.ts} +18 -0
  66. package/dist/{h3-proximity-I1msGPgE.d.ts → h3-proximity-B57C6rYL.d.ts} +39 -1
  67. package/dist/{image-capture-qAZ5Fh5X.d.ts → image-capture-C2Feywet.d.ts} +1 -1
  68. package/dist/index-BvLo2poy.d.ts +399 -0
  69. package/dist/{index-BYYUdf9v.d.ts → index-m_LneUSi.d.ts} +1 -1
  70. package/dist/index.d.ts +77 -67
  71. package/dist/index.js +18 -9
  72. package/dist/{leaflet-map-overlay-6CJC-E1l.d.ts → leaflet-map-overlay-bnb2U1NQ.d.ts} +1 -1
  73. package/dist/{map-data-CkloBxIl.d.ts → map-data-DdjPiSav.d.ts} +1 -1
  74. package/dist/{map-overlay-draw-krOKwDAq.d.ts → map-overlay-draw-CPVdAsFq.d.ts} +1 -1
  75. package/dist/{null-storage-backend-uKQE8tl7.d.ts → null-storage-backend-CuSdYFsM.d.ts} +2 -2
  76. package/dist/{occupancy-grid-BRNRaPyT.d.ts → occupancy-grid-Bvi2Gw1U.d.ts} +12 -2
  77. package/dist/{opfs-storage-D5zAn_lx.d.ts → opfs-storage-DMtlt57S.d.ts} +23 -4
  78. package/dist/{opfs-storage-backend-6aYMUZlJ.d.ts → opfs-storage-backend-CeQAb0SD.d.ts} +3 -3
  79. package/dist/{persistence-middleware-B9ei4Nhx.d.ts → persistence-middleware-5eearhSV.d.ts} +1 -1
  80. package/dist/planar-pnp-cjthe7Be.d.ts +67 -0
  81. package/dist/qr-debug-view-B8gTzjMm.d.ts +29 -0
  82. package/dist/qr-derived-pose-DinKE600.d.ts +122 -0
  83. package/dist/qr-detected-slice-Cpv-dg82.d.ts +166 -0
  84. package/dist/qr-detection-controller-B-tLVd-S.js +119 -0
  85. package/dist/qr-detection-controller-Cd2Ih4cC.d.ts +48 -0
  86. package/dist/qr-frontend-DufZp2xS.d.ts +58 -0
  87. package/dist/qr-pose-Bt2reSw7.d.ts +138 -0
  88. package/dist/qr-pose-aggregation-CFlmLm6w.d.ts +90 -0
  89. package/dist/qr-pose-aggregation-DXR9STHs.js +200 -0
  90. package/dist/qr-size-depth-context-onu36E9s.d.ts +13 -0
  91. package/dist/qr-size-measurer-D7739ZpB.d.ts +165 -0
  92. package/dist/qr-size-measurer-DLb33geH.js +596 -0
  93. package/dist/recording-options-D93kksJU.d.ts +369 -0
  94. package/dist/{recording-replayer-r0DZ9ALp.d.ts → recording-replayer-CUEK82tM.d.ts} +2 -2
  95. package/dist/recording-slice-B-uCgGoh.d.ts +55 -0
  96. package/dist/{replay-engine-XpDa0dy1.d.ts → replay-engine-BSVd7Zzb.d.ts} +7 -4
  97. package/dist/{replay-scene-Bw58VL6p.d.ts → replay-scene-CPt56pps.d.ts} +3 -3
  98. package/dist/sensors/gps-error-handler.d.ts +1 -1
  99. package/dist/sensors/gps.d.ts +1 -1
  100. package/dist/sensors/index.d.ts +3 -3
  101. package/dist/sensors/permission-checker.d.ts +1 -1
  102. package/dist/sensors/permission-checker.js +1 -1
  103. package/dist/state/app-selectors.d.ts +1 -1
  104. package/dist/state/combined-root-state.d.ts +1 -1
  105. package/dist/state/create-slam-app-store.d.ts +1 -1
  106. package/dist/state/gps-ar-pose-sampler.d.ts +3 -3
  107. package/dist/state/gps-event-coordinator.d.ts +1 -1
  108. package/dist/state/index.d.ts +20 -17
  109. package/dist/state/index.js +3 -2
  110. package/dist/state/persistence-middleware.d.ts +1 -1
  111. package/dist/state/qr-detected-slice.d.ts +4 -0
  112. package/dist/state/qr-detected-slice.js +311 -0
  113. package/dist/state/recording-options.d.ts +2 -2
  114. package/dist/state/recording-options.js +128 -4
  115. package/dist/state/recording-replayer.d.ts +2 -2
  116. package/dist/state/recording-slice.d.ts +1 -1
  117. package/dist/state/recording-slice.js +5 -1
  118. package/dist/state/replay-engine.d.ts +1 -1
  119. package/dist/state/replay-engine.js +7 -3
  120. package/dist/state/store-subscribers.d.ts +2 -2
  121. package/dist/state/subscribe-to-selector.d.ts +1 -1
  122. package/dist/state/tracking-quality.d.ts +1 -1
  123. package/dist/state/tracking-slice.d.ts +1 -1
  124. package/dist/{state-BkIfpiyA.js → state-IEw0ZsBu.js} +1 -0
  125. package/dist/storage/file-system-utils.d.ts +1 -1
  126. package/dist/storage/index.d.ts +9 -9
  127. package/dist/storage/index.js +4 -4
  128. package/dist/storage/null-storage-backend.d.ts +1 -1
  129. package/dist/storage/opfs-storage-backend.d.ts +1 -1
  130. package/dist/storage/opfs-storage-backend.js +3 -3
  131. package/dist/storage/opfs-storage.d.ts +1 -1
  132. package/dist/storage/opfs-storage.js +5 -4
  133. package/dist/storage/storage-backend.d.ts +1 -1
  134. package/dist/storage/zip-coverage-embed.d.ts +2 -0
  135. package/dist/storage/zip-coverage-embed.js +88 -0
  136. package/dist/storage/zip-export.d.ts +2 -2
  137. package/dist/storage/zip-export.js +37 -59
  138. package/dist/storage/zip-reader.d.ts +1 -1
  139. package/dist/{storage-backend-Do6YDpNo.d.ts → storage-backend-CTJHNdMp.d.ts} +1 -1
  140. package/dist/{store-subscribers-0YWyOLvV.d.ts → store-subscribers-BZP81hjh.d.ts} +2 -2
  141. package/dist/{subscribe-to-selector-CZ6k1NFP.d.ts → subscribe-to-selector-Bp3sLx8L.d.ts} +1 -1
  142. package/dist/test-utils/browser-mocks.d.ts +7 -1
  143. package/dist/test-utils/browser-mocks.js +6 -0
  144. package/dist/test-utils/zip-round-trip-helpers.d.ts +9 -1
  145. package/dist/test-utils/zip-round-trip-helpers.js +5 -1
  146. package/dist/{tracking-slice-CdK8c-8P.d.ts → tracking-slice-DJZ9qeGg.d.ts} +1 -1
  147. package/dist/types/ar-types.d.ts +1 -1
  148. package/dist/types/geo-types.d.ts +1 -1
  149. package/dist/types/index.d.ts +2 -2
  150. package/dist/utils/concurrency.d.ts +2 -2
  151. package/dist/utils/concurrency.js +66 -2
  152. package/dist/utils/failure-tracker.d.ts +1 -1
  153. package/dist/utils/format-file-size.d.ts +1 -1
  154. package/dist/utils/fused-path.d.ts +1 -1
  155. package/dist/utils/index.d.ts +6 -6
  156. package/dist/utils/list-formatter.d.ts +1 -1
  157. package/dist/utils/logger.d.ts +1 -1
  158. package/dist/visualization/accuracy-circles.d.ts +1 -1
  159. package/dist/visualization/alignment-lerper.d.ts +1 -1
  160. package/dist/visualization/ar-world-group-alignment.d.ts +1 -1
  161. package/dist/visualization/ar-world-group-alignment.js +1 -1
  162. package/dist/visualization/camera-follower.d.ts +1 -1
  163. package/dist/visualization/css3d-renderer-manager.d.ts +1 -1
  164. package/dist/visualization/frame-conversions.d.ts +1 -1
  165. package/dist/visualization/frustum-visibility.d.ts +1 -1
  166. package/dist/visualization/gps-anchor.d.ts +1 -1
  167. package/dist/visualization/gps-compass-cubes.d.ts +1 -1
  168. package/dist/visualization/gps-event-markers.d.ts +1 -1
  169. package/dist/visualization/gps-event-markers.js +40 -5
  170. package/dist/visualization/hit-test-reticle.d.ts +1 -1
  171. package/dist/visualization/hit-test-reticle.js +1 -0
  172. package/dist/visualization/index.d.ts +18 -18
  173. package/dist/visualization/leaflet-map-overlay.d.ts +1 -1
  174. package/dist/visualization/leaflet-map-overlay.js +15 -3
  175. package/dist/visualization/lerp-utils.d.ts +1 -1
  176. package/dist/visualization/map-data.d.ts +1 -1
  177. package/dist/visualization/map-overlay-draw.d.ts +1 -1
  178. package/dist/visualization/map-overlay.d.ts +1 -1
  179. package/dist/visualization/three-dispose.d.ts +1 -1
  180. package/dist/visualization/vis-colors.d.ts +1 -1
  181. package/dist/{webxr-session-TIaD883f.js → webxr-session-CJF9Nijp.js} +221 -9
  182. package/dist/{webxr-session-GjZ2BIpS.d.ts → webxr-session-COsOjJwn.d.ts} +76 -8
  183. package/dist/zip-coverage-embed-tFohI2J4.d.ts +41 -0
  184. package/dist/{zip-export-G14hkSPG.d.ts → zip-export-DDWXInTR.d.ts} +27 -23
  185. package/package.json +2 -2
  186. package/dist/concurrency-CO1FQvs3.d.ts +0 -24
  187. package/dist/file-system-CwXTNHmB.d.ts +0 -114
  188. package/dist/recording-options-DJBXuMxV.d.ts +0 -182
  189. package/dist/recording-slice-89O07ebs.d.ts +0 -36
  190. package/dist/storage/file-system.d.ts +0 -2
  191. package/dist/storage/file-system.js +0 -289
  192. /package/dist/{accuracy-circles-BpwWAs8G.d.ts → accuracy-circles-Pi2YeCrM.d.ts} +0 -0
  193. /package/dist/{alignment-lerper-Dm9G7JJO.d.ts → alignment-lerper-BUTsQ2yu.d.ts} +0 -0
  194. /package/dist/{ar-types-Dan1MVTi.d.ts → ar-types-BckI1E-O.d.ts} +0 -0
  195. /package/dist/{bresenham3d-CFfd7n6B.d.ts → bresenham3d-ByjcmEgM.d.ts} +0 -0
  196. /package/dist/{camera-follower-C2mwEn7c.d.ts → camera-follower-BTb2Yp12.d.ts} +0 -0
  197. /package/dist/{capability-checker-CQXJHxXg.d.ts → capability-checker-CXqoJYtb.d.ts} +0 -0
  198. /package/dist/{capture-failure-tracker-CUeU3L_M.d.ts → capture-failure-tracker-BL5SoG2a.d.ts} +0 -0
  199. /package/dist/{chromium-camera-access-workaround-Jqbn0apH.d.ts → chromium-camera-access-workaround-BNK189P0.d.ts} +0 -0
  200. /package/dist/{css3d-renderer-manager-ptu_t3B3.d.ts → css3d-renderer-manager-m-ab19vp.d.ts} +0 -0
  201. /package/dist/{failure-tracker-Cn8Fo9D4.d.ts → failure-tracker-Cnc_xWUw.d.ts} +0 -0
  202. /package/dist/{file-system-utils-CXIoL-DM.d.ts → file-system-utils-Bs7lpeVP.d.ts} +0 -0
  203. /package/dist/{format-file-size-CLCZz3lC.d.ts → format-file-size-BFUWvir1.d.ts} +0 -0
  204. /package/dist/{frame-loop-CMewS8cY.d.ts → frame-loop-BEO9_q3k.d.ts} +0 -0
  205. /package/dist/{frustum-visibility-CmI96ESS.d.ts → frustum-visibility-D_RGmH98.d.ts} +0 -0
  206. /package/dist/{geo-types-SaPnIEUM.d.ts → geo-types-DqiBzPO9.d.ts} +0 -0
  207. /package/dist/{gps-VuezJWK3.d.ts → gps-C5inWeNV.d.ts} +0 -0
  208. /package/dist/{gps-compass-cubes-BM1uGiYV.d.ts → gps-compass-cubes-BExxrWJ-.d.ts} +0 -0
  209. /package/dist/{gps-error-handler-CJl_2MVm.d.ts → gps-error-handler-C2bS69oJ.d.ts} +0 -0
  210. /package/dist/{hit-test-reticle-BEzEybxZ.d.ts → hit-test-reticle-Dqak9hiR.d.ts} +0 -0
  211. /package/dist/{index-CF05QV2m.d.ts → index-eaI1Z1KR.d.ts} +0 -0
  212. /package/dist/{lerp-utils-BpV-0Dtk.d.ts → lerp-utils-kLxMJ2Xj.d.ts} +0 -0
  213. /package/dist/{list-formatter-RRcBe8Vg.d.ts → list-formatter-ct8txwvR.d.ts} +0 -0
  214. /package/dist/{logger-BEHXsnnq.d.ts → logger-CzQrCe6C.d.ts} +0 -0
  215. /package/dist/{map-overlay-bcogY5fK.d.ts → map-overlay-Dci4yvm3.d.ts} +0 -0
  216. /package/dist/{permission-checker-GpAiSCwd.d.ts → permission-checker-CtPtPQRq.d.ts} +0 -0
  217. /package/dist/{scene-node-names-Bu0S66H1.d.ts → scene-node-names-CdNeQjOE.d.ts} +0 -0
  218. /package/dist/{session-disposers-M-oashRH.js → session-disposers-D8wnO-uW.js} +0 -0
  219. /package/dist/{three-dispose-D950t71B.d.ts → three-dispose-BCZkSJp2.d.ts} +0 -0
  220. /package/dist/{vis-colors-D6omC64K.d.ts → vis-colors-BZsSLI4i.d.ts} +0 -0
  221. /package/dist/{webxr-nue-basis-CqKQmZzT.d.ts → webxr-nue-basis-CW1wsbm6.d.ts} +0 -0
  222. /package/dist/{xr-camera-texture-DBwCaKTd.d.ts → xr-camera-texture-BmrP7iNZ.d.ts} +0 -0
  223. /package/dist/{xr-error-handler-C0AQRmxs.d.ts → xr-error-handler-BEjRgZjt.d.ts} +0 -0
  224. /package/dist/{xr-frame-loop-BtYytZJg.d.ts → xr-frame-loop-K8ebdjLw.d.ts} +0 -0
  225. /package/dist/{zip-reader-DSEXt7K6.d.ts → zip-reader-Q2ue6zlC.d.ts} +0 -0
@@ -0,0 +1,90 @@
1
+ import { i as Pose } from "./qr-pose-Bt2reSw7.js";
2
+ import { Quaternion } from "gps-plus-slam-js";
3
+
4
+ //#region ../src/ar/qr-pose-aggregation.d.ts
5
+ /** Default inlier half-angle (deg) — start ~12°, confirm on device. */
6
+ declare const DEFAULT_ROTATION_INLIER_ANGLE_DEG = 12;
7
+ interface AverageRotationOptions {
8
+ /**
9
+ * Reject samples whose geodesic angle to the reference exceeds this many
10
+ * degrees. Default {@link DEFAULT_ROTATION_INLIER_ANGLE_DEG}.
11
+ */
12
+ inlierAngleDeg?: number;
13
+ /**
14
+ * Provisional reference rotation for the first inlier pass. When omitted, a
15
+ * robust reference is chosen by MODE-FINDING (the sample with the most other
16
+ * samples within the inlier threshold — the densest cluster). This matters:
17
+ * the naive "use the latest sample" breaks exactly when the latest frame is
18
+ * the bad one, which is the whole reason for this filter.
19
+ */
20
+ reference?: Quaternion;
21
+ }
22
+ interface AverageRotationResult {
23
+ /** Robust mean rotation, a unit quaternion `[x,y,z,w]` (canonicalized w ≥ 0). */
24
+ quat: Quaternion;
25
+ /** How many samples lie within the inlier threshold of the final mean. */
26
+ inlierCount: number;
27
+ /** Max geodesic angle (deg) among the inliers to the final mean — the spread. */
28
+ maxAngleDeg: number;
29
+ }
30
+ /**
31
+ * Robust mean of a set of rotations (Option R1). Picks a reference (mode-finding,
32
+ * or `options.reference`), rejects samples beyond the angular threshold, averages
33
+ * the inliers, then does ONE refit pass against the provisional mean. Returns
34
+ * `null` for an empty input.
35
+ */
36
+ declare function averageRotation(quats: readonly Quaternion[], options?: AverageRotationOptions): AverageRotationResult | null;
37
+ interface AggregateQrPoseResult {
38
+ /** Filtered pose: per-axis median position + robust mean rotation. */
39
+ pose: Pose;
40
+ /** Max absolute per-axis deviation from the median position (meters). */
41
+ translationSpreadM: number;
42
+ /** Max geodesic angle among rotation inliers to the mean (degrees). */
43
+ rotationSpreadDeg: number;
44
+ /** Rotation inlier count (≤ pose count). */
45
+ inlierCount: number;
46
+ }
47
+ /**
48
+ * Aggregate a window of raw poses into one filtered pose + the spreads the
49
+ * stability gate consumes. Returns `null` for an empty window.
50
+ */
51
+ declare function aggregateQrPose(poses: readonly Pose[], options?: AverageRotationOptions): AggregateQrPoseResult | null;
52
+ /**
53
+ * Where the pose-stability gate sits for one marker, mirroring the size
54
+ * lifecycle (`QrSizeStatus`):
55
+ * - `unknown` — no observations yet.
56
+ * - `measuring` — observations accumulating but not enough / not converged.
57
+ * - `stable` — ≥ `minObservations` AND both spreads below threshold; the pose
58
+ * may now drive the high-weight vote / the smooth overlay.
59
+ */
60
+ type QrPoseStabilityStatus = 'unknown' | 'measuring' | 'stable';
61
+ interface QrPoseStabilityOptions extends AverageRotationOptions {
62
+ /** Sliding-window size: aggregate only the last `window` poses. Default 8. */
63
+ window?: number;
64
+ /** Minimum observations before the gate can be `stable`. Default 5. */
65
+ minObservations?: number;
66
+ /** Max translation spread (m) allowed for `stable`. Default 0.03. */
67
+ maxTranslationSpreadM?: number;
68
+ /** Max rotation spread (deg) allowed for `stable`. Default 5. */
69
+ maxRotationSpreadDeg?: number;
70
+ }
71
+ interface QrPoseStability {
72
+ status: QrPoseStabilityStatus;
73
+ /** The filtered pose (present once there is ≥1 observation), else `null`. */
74
+ pose: Pose | null;
75
+ translationSpreadM: number;
76
+ rotationSpreadDeg: number;
77
+ inlierCount: number;
78
+ /** Observations actually aggregated (≤ `window`). */
79
+ sampleCount: number;
80
+ }
81
+ /**
82
+ * Evaluate the pose-stability lifecycle over the last `window` of `poses`
83
+ * (oldest→newest). The returned `pose` is the filtered aggregate whenever there
84
+ * is data, but it is only the gate's responsibility (`status === 'stable'`) to
85
+ * decide when a consumer may TRUST it for the vote — `selectStableQrPose` reads
86
+ * the pose only in the `stable` state.
87
+ */
88
+ declare function evaluateQrPoseStability(poses: readonly Pose[], options?: QrPoseStabilityOptions): QrPoseStability;
89
+ //#endregion
90
+ export { QrPoseStability as a, aggregateQrPose as c, DEFAULT_ROTATION_INLIER_ANGLE_DEG as i, averageRotation as l, AverageRotationOptions as n, QrPoseStabilityOptions as o, AverageRotationResult as r, QrPoseStabilityStatus as s, AggregateQrPoseResult as t, evaluateQrPoseStability as u };
@@ -0,0 +1,200 @@
1
+ import { quat } from "gl-matrix";
2
+ //#region ../src/ar/qr-pose-aggregation.ts
3
+ /**
4
+ * QR pose aggregation — sliding-window pose stabilization
5
+ * (docs `2026-06-16-followup-qr-pose-stabilization-sliding-window.md`).
6
+ *
7
+ * Pure, device-free, store-free robust aggregation of a STATIC fiducial's pose
8
+ * across a short sliding window of detections. A QR is static in the world, so
9
+ * the last N world poses should be (nearly) identical — disagreement is noise,
10
+ * which robust aggregation removes. The aggregated pose is what drives the
11
+ * high-weight GPS vote and the debug overlay, instead of a raw single-frame pose
12
+ * whose ROTATION can swing (the on-device symptom this targets).
13
+ *
14
+ * Three layers, smallest first:
15
+ * - {@link averageRotation} — Option R1: angle-thresholded inlier set + hemisphere-
16
+ * aligned quaternion average. The real new work; translation already had
17
+ * {@link medianQrPosition} but rotation had no counterpart. Quaternion double
18
+ * cover (`q` ≡ `−q`) is handled explicitly — the single most common bug here.
19
+ * - {@link aggregateQrPose} — per-axis median position + R1 rotation, reporting
20
+ * the spreads + inlier count the stability gate needs.
21
+ * - {@link evaluateQrPoseStability} — the `unknown → measuring → stable` lifecycle
22
+ * over the last `window` poses, mirroring the size lifecycle. The pose is only
23
+ * exposed (for the vote / overlay) once `stable`.
24
+ *
25
+ * Operates on plain `Pose` lists (no slice import) so BOTH the framework
26
+ * controller and the demo controller can reuse it, and it is unit-testable
27
+ * without a store. The slice selectors `selectStableQrPose` /
28
+ * `selectQrPoseStability` wrap these over a marker's ring buffer.
29
+ *
30
+ * INVARIANT: the caller must feed RAW per-detection world poses here. Never feed
31
+ * a previously-aggregated pose back in — the window would average its own output
32
+ * and collapse toward whatever it first locked onto (a feedback loop that defeats
33
+ * outlier rejection). The `qrDetected` ring buffer stores raw poses for exactly
34
+ * this reason.
35
+ *
36
+ * @see ./qr-pose.ts — the `Pose` type and the (raw) pose solve this aggregates.
37
+ * @see ../state/qr-detected-slice.ts — `selectStableQrPose` / `selectQrPoseStability`.
38
+ */
39
+ const RAD2DEG = 180 / Math.PI;
40
+ const DEG2RAD = Math.PI / 180;
41
+ /** Default inlier half-angle (deg) — start ~12°, confirm on device. */
42
+ const DEFAULT_ROTATION_INLIER_ANGLE_DEG = 12;
43
+ function toGl(q) {
44
+ const g = quat.fromValues(q[0], q[1], q[2], q[3]);
45
+ return quat.normalize(g, g);
46
+ }
47
+ /**
48
+ * Geodesic (shortest-arc) angle in radians between two unit quaternions.
49
+ * Double-cover safe: uses `2·dot² − 1 = cos θ`, which is invariant to the sign
50
+ * of either quaternion (`q` ≡ `−q`), and clamps for float error before `acos`.
51
+ */
52
+ function geodesicAngleRad(a, b) {
53
+ const d = quat.dot(a, b);
54
+ const c = Math.min(1, Math.max(-1, 2 * d * d - 1));
55
+ return Math.acos(c);
56
+ }
57
+ /**
58
+ * Average the samples within `thresholdRad` of `ref`. Each inlier is flipped
59
+ * into `ref`'s hemisphere (sign of the dot) BEFORE the linear average so the
60
+ * double cover cannot cancel the sum; the normalized linear average equals the
61
+ * Markley eigenvector mean for a tight cluster and needs no eigensolver.
62
+ * Returns `null` when no sample is within threshold.
63
+ */
64
+ function meanOfInliers(samples, ref, thresholdRad) {
65
+ const inliers = [];
66
+ for (const s of samples) if (geodesicAngleRad(s, ref) <= thresholdRad) inliers.push(s);
67
+ if (inliers.length === 0) return null;
68
+ const acc = quat.fromValues(0, 0, 0, 0);
69
+ for (const s of inliers) {
70
+ const sign = quat.dot(s, ref) < 0 ? -1 : 1;
71
+ acc[0] += sign * s[0];
72
+ acc[1] += sign * s[1];
73
+ acc[2] += sign * s[2];
74
+ acc[3] += sign * s[3];
75
+ }
76
+ const mean = quat.normalize(quat.create(), acc);
77
+ let maxAngleRad = 0;
78
+ for (const s of inliers) maxAngleRad = Math.max(maxAngleRad, geodesicAngleRad(s, mean));
79
+ return {
80
+ mean,
81
+ inlierCount: inliers.length,
82
+ maxAngleRad
83
+ };
84
+ }
85
+ /**
86
+ * Pick the densest sample as the reference: the one with the most OTHER samples
87
+ * within the inlier threshold. For a single rigid pose, consensus-based outlier
88
+ * rejection degenerates to this mode-finding, and it is robust to the latest
89
+ * sample being an outlier.
90
+ */
91
+ function bestReference(samples, thresholdRad) {
92
+ let best = samples[0];
93
+ let bestCount = -1;
94
+ for (const cand of samples) {
95
+ let count = 0;
96
+ for (const s of samples) if (geodesicAngleRad(s, cand) <= thresholdRad) count++;
97
+ if (count > bestCount) {
98
+ bestCount = count;
99
+ best = cand;
100
+ }
101
+ }
102
+ return best;
103
+ }
104
+ /**
105
+ * Robust mean of a set of rotations (Option R1). Picks a reference (mode-finding,
106
+ * or `options.reference`), rejects samples beyond the angular threshold, averages
107
+ * the inliers, then does ONE refit pass against the provisional mean. Returns
108
+ * `null` for an empty input.
109
+ */
110
+ function averageRotation(quats, options = {}) {
111
+ if (quats.length === 0) return null;
112
+ const thresholdRad = (options.inlierAngleDeg ?? 12) * DEG2RAD;
113
+ const samples = quats.map(toGl);
114
+ const provisional = meanOfInliers(samples, options.reference ? toGl(options.reference) : bestReference(samples, thresholdRad), thresholdRad);
115
+ if (!provisional) return null;
116
+ const refined = meanOfInliers(samples, provisional.mean, thresholdRad) ?? provisional;
117
+ const m = refined.mean;
118
+ const s = m[3] < 0 ? -1 : 1;
119
+ return {
120
+ quat: [
121
+ s * m[0],
122
+ s * m[1],
123
+ s * m[2],
124
+ s * m[3]
125
+ ],
126
+ inlierCount: refined.inlierCount,
127
+ maxAngleDeg: refined.maxAngleRad * RAD2DEG
128
+ };
129
+ }
130
+ /** Per-axis median of a numeric list (lower-middle for even n, matching
131
+ * `medianQrPosition` in the slice so the two agree on the same data). */
132
+ function medianOf(values) {
133
+ const sorted = [...values].sort((a, b) => a - b);
134
+ return sorted[Math.floor((sorted.length - 1) / 2)];
135
+ }
136
+ function medianPosition(positions) {
137
+ return [
138
+ medianOf(positions.map((p) => p[0])),
139
+ medianOf(positions.map((p) => p[1])),
140
+ medianOf(positions.map((p) => p[2]))
141
+ ];
142
+ }
143
+ /** Max absolute per-axis deviation from `center` across all positions. */
144
+ function translationSpread(positions, center) {
145
+ let max = 0;
146
+ for (const p of positions) max = Math.max(max, Math.abs(p[0] - center[0]), Math.abs(p[1] - center[1]), Math.abs(p[2] - center[2]));
147
+ return max;
148
+ }
149
+ /**
150
+ * Aggregate a window of raw poses into one filtered pose + the spreads the
151
+ * stability gate consumes. Returns `null` for an empty window.
152
+ */
153
+ function aggregateQrPose(poses, options = {}) {
154
+ if (poses.length === 0) return null;
155
+ const positions = poses.map((p) => p.position);
156
+ const position = medianPosition(positions);
157
+ const rotation = averageRotation(poses.map((p) => p.rotation), options);
158
+ if (!rotation) return null;
159
+ return {
160
+ pose: {
161
+ position,
162
+ rotation: rotation.quat
163
+ },
164
+ translationSpreadM: translationSpread(positions, position),
165
+ rotationSpreadDeg: rotation.maxAngleDeg,
166
+ inlierCount: rotation.inlierCount
167
+ };
168
+ }
169
+ const UNKNOWN_STABILITY = {
170
+ status: "unknown",
171
+ pose: null,
172
+ translationSpreadM: 0,
173
+ rotationSpreadDeg: 0,
174
+ inlierCount: 0,
175
+ sampleCount: 0
176
+ };
177
+ /**
178
+ * Evaluate the pose-stability lifecycle over the last `window` of `poses`
179
+ * (oldest→newest). The returned `pose` is the filtered aggregate whenever there
180
+ * is data, but it is only the gate's responsibility (`status === 'stable'`) to
181
+ * decide when a consumer may TRUST it for the vote — `selectStableQrPose` reads
182
+ * the pose only in the `stable` state.
183
+ */
184
+ function evaluateQrPoseStability(poses, options = {}) {
185
+ const { window = 8, minObservations = 5, maxTranslationSpreadM = .03, maxRotationSpreadDeg = 5 } = options;
186
+ if (poses.length === 0) return { ...UNKNOWN_STABILITY };
187
+ const windowPoses = poses.length > window ? poses.slice(poses.length - window) : poses;
188
+ const agg = aggregateQrPose(windowPoses, options);
189
+ if (!agg) return { ...UNKNOWN_STABILITY };
190
+ return {
191
+ status: windowPoses.length >= minObservations && agg.translationSpreadM <= maxTranslationSpreadM && agg.rotationSpreadDeg <= maxRotationSpreadDeg ? "stable" : "measuring",
192
+ pose: agg.pose,
193
+ translationSpreadM: agg.translationSpreadM,
194
+ rotationSpreadDeg: agg.rotationSpreadDeg,
195
+ inlierCount: agg.inlierCount,
196
+ sampleCount: windowPoses.length
197
+ };
198
+ }
199
+ //#endregion
200
+ export { evaluateQrPoseStability as i, aggregateQrPose as n, averageRotation as r, DEFAULT_ROTATION_INLIER_ANGLE_DEG as t };
@@ -0,0 +1,13 @@
1
+ import { i as DepthSample } from "./ar-types-BckI1E-O.js";
2
+ import { n as QrSizeDepthContext } from "./qr-size-measurer-D7739ZpB.js";
3
+
4
+ //#region ../src/ar/qr-size-depth-context.d.ts
5
+ /**
6
+ * Build a {@link QrSizeDepthContext} from one depth sample, or `null` when it has
7
+ * no projection matrix / a singular one (the unprojector cannot be built). The
8
+ * grid lookup is **bilinear** (depth varies smoothly across a small QR face,
9
+ * rather than snapping to one nearest node). Best-effort — never throws.
10
+ */
11
+ declare function createQrSizeDepthContext(sample: DepthSample): QrSizeDepthContext | null;
12
+ //#endregion
13
+ export { createQrSizeDepthContext as t };
@@ -0,0 +1,165 @@
1
+ import { r as DepthPoint } from "./ar-types-BckI1E-O.js";
2
+ import { t as DepthUnprojector } from "./depth-unprojection-CK3b9htH.js";
3
+ import { r as Point2 } from "./qr-pose-Bt2reSw7.js";
4
+ import { Vector3 } from "gps-plus-slam-js";
5
+
6
+ //#region ../src/ar/qr-size-from-depth.d.ts
7
+ /** Where the size lifecycle currently sits for one marker (Note 3 / Note 4). */
8
+ type QrSizeStatus = /** No size authored and none measured yet — size-dependent features blocked. */'unknown' /** Measurements are accumulating but the estimate has not converged. */ | 'measuring' /** A reliably-estimated (or authored) size — size-dependent features unlock. */ | 'estimated';
9
+ /** Per-marker physical-size estimate (drives the Note 3 size lifecycle). */
10
+ interface QrSizeEstimate {
11
+ status: QrSizeStatus;
12
+ /** Running median side length, meters, or `null` while unknown. */
13
+ estimateM: number | null;
14
+ /** How many accepted samples back the estimate. */
15
+ sampleCount: number;
16
+ /**
17
+ * Robust confidence half-width of the estimate, meters (`1.4826·MAD/√N`), so
18
+ * it TIGHTENS as samples accumulate (WS-B) — not the raw max−min. 0 when <2
19
+ * samples. The HUD renders this as the shrinking `±mm`.
20
+ */
21
+ spreadM: number;
22
+ }
23
+ /** One per-observation size read from a single detection's depth samples. */
24
+ interface QrSizeObservation {
25
+ /** Median of the 4 unprojected edge lengths, meters. */
26
+ sizeM: number;
27
+ /** Consistency score in [0,1]; 1 = a perfect planar square facing the camera. */
28
+ quality: number;
29
+ }
30
+ /**
31
+ * Estimate a QR's physical side length from one detection's depth samples.
32
+ *
33
+ * @param corners - the 4 corner depth samples, ordered TL, TR, BR, BL (matching
34
+ * `buildObjectPoints` / the detector corner-order normalization).
35
+ * @param interiorSamples - a few interior depth samples (may be empty); used
36
+ * only to strengthen the planarity check against corner edge depth-bleed.
37
+ * @param unprojector - built once per depth sample via `createDepthUnprojector`.
38
+ * @returns `{ sizeM, quality }`, or `null` when a corner cannot be unprojected
39
+ * or the quad is degenerate (collinear / zero-area).
40
+ */
41
+ declare function estimateQrSizeFromDepth(corners: readonly [DepthPoint, DepthPoint, DepthPoint, DepthPoint], interiorSamples: readonly DepthPoint[], unprojector: DepthUnprojector): QrSizeObservation | null;
42
+ interface QrSizeAccumulatorOptions {
43
+ /** Minimum observation quality to ACCEPT a sample. Default 0.8. */
44
+ qualityThreshold?: number;
45
+ /** Accepted samples required before the estimate can be `estimated`. Default 8. */
46
+ minSamples?: number;
47
+ /**
48
+ * Max confidence half-width (`spreadM`, m) allowed for the `estimated` status.
49
+ * Default 0.01. `spreadM` is a robust standard-error (`1.4826·MAD/√N`) that
50
+ * TIGHTENS as samples accumulate — not the raw max−min — so it converges
51
+ * instead of being pinned by one early stray.
52
+ */
53
+ maxSpreadM?: number;
54
+ /**
55
+ * Optional cap on retained accepted sizes. **Default: unbounded** (lifelong
56
+ * refinement — WS-B). The QR's physical size never changes, so keeping the
57
+ * full session history lets the robust median tighten the longer the QR is
58
+ * seen. Set a finite cap only if memory/perf demands a bounded window; the
59
+ * estimate then becomes a sliding-window median again.
60
+ */
61
+ maxSamples?: number;
62
+ }
63
+ interface QrSizeAccumulator {
64
+ /**
65
+ * Offer one observation (or `null` for a failed read). Low-quality / null
66
+ * observations are ignored. Returns the updated {@link QrSizeEstimate}.
67
+ */
68
+ add(observation: QrSizeObservation | null): QrSizeEstimate;
69
+ /** The current estimate without adding a sample. */
70
+ current(): QrSizeEstimate;
71
+ /** Drop all samples back to `unknown`. */
72
+ reset(): void;
73
+ }
74
+ declare function createQrSizeAccumulator(options?: QrSizeAccumulatorOptions): QrSizeAccumulator;
75
+ //#endregion
76
+ //#region ../src/ar/qr-size-measurer.d.ts
77
+ /**
78
+ * Robustness knobs for the depth-at-corners stage (on top of the accumulator
79
+ * options). QR corners sit on a high-contrast print boundary where the coarse
80
+ * WebXR depth grid often has no near reading; these let a measurement still
81
+ * succeed instead of returning `null` every frame.
82
+ */
83
+ interface QrSizeMeasurerOptions extends QrSizeAccumulatorOptions {
84
+ /**
85
+ * Points-per-side of the interior depth lattice sampled across the QR quad for
86
+ * the PRIMARY dense plane-fit estimate (WS-A). `latticeSize × latticeSize`
87
+ * points are sampled strictly inside the quad; reads with no depth are
88
+ * skipped. Default `7` (≤49 reads). Set to a small value (or rely on the
89
+ * corner fallback) to disable dense sampling. See {@link estimateQrSizeFromDepthDense}.
90
+ */
91
+ latticeSize?: number;
92
+ /**
93
+ * When a corner pixel has no depth, retry at points inset toward the centroid
94
+ * by these fractions (in order) and borrow the first valid depth — keeping the
95
+ * TRUE corner screen position so the measured size is not shrunk. Default
96
+ * `[0.12, 0.25]`. Set to `[]` to disable inset fallback. Used by the corner-
97
+ * based FALLBACK estimate (when the interior lattice is too sparse for a fit).
98
+ */
99
+ cornerInsetFractions?: number[];
100
+ /**
101
+ * Max corners whose depth may be reconstructed by a planar fit through the
102
+ * other three when still missing after the inset fallback. Default `1`
103
+ * (tolerate one un-sampleable corner); `0` disables reconstruction. Used by
104
+ * the corner-based FALLBACK estimate.
105
+ */
106
+ maxReconstructedCorners?: number;
107
+ }
108
+ /** The per-frame depth access the measurer needs (a subset of the demo's `DepthContext`). */
109
+ interface QrSizeDepthContext {
110
+ /** Depth (m) at a normalized screen point, or `null` if unavailable there. */
111
+ depthAt: (screenX: number, screenY: number) => number | null;
112
+ /** Unprojector for the current depth sample (`createDepthUnprojector`). */
113
+ unprojector: DepthUnprojector;
114
+ }
115
+ /** Minimal frame dimensions needed to normalize pixel corners to screen coords. */
116
+ interface ImageSize {
117
+ width: number;
118
+ height: number;
119
+ }
120
+ /** One measurement: the accumulated estimate plus the raw depth samples used. */
121
+ interface QrSizeMeasurement {
122
+ /** The per-marker running estimate AFTER folding in this observation. */
123
+ estimate: QrSizeEstimate;
124
+ /**
125
+ * The 4 corner depth samples (detector order, normalized screen coords),
126
+ * best-effort. `null` when corner depth could not be sampled (e.g. a small QR
127
+ * whose corners fall between depth nodes) but the dense interior fit still
128
+ * produced an estimate — the dense path does not need corner depths.
129
+ */
130
+ cornerSamples: [DepthPoint, DepthPoint, DepthPoint, DepthPoint] | null;
131
+ /**
132
+ * The interior depth reads used for the estimate: the dense lattice on the
133
+ * primary path, or the single centroid sample on the corner-based fallback
134
+ * (may be empty).
135
+ */
136
+ interiorSamples: DepthPoint[];
137
+ }
138
+ interface QrSizeMeasurer {
139
+ /**
140
+ * Measure one detection's size from depth and fold it into the per-`text`
141
+ * accumulator. The PRIMARY path is the dense plane fit (WS-A): sample an
142
+ * interior lattice, fit the QR plane, recover the corners by ray-plane
143
+ * intersection. When the lattice is too sparse for a fit it FALLS BACK to the
144
+ * corner-based estimate (corner depths + inset/reconstruct robustness).
145
+ *
146
+ * Returns `null` only when neither path can run — `corners.length !== 4`, or
147
+ * the interior lattice yields too few reads AND the corner depths cannot be
148
+ * sampled (≥2 corners missing). A degenerate quad does not throw: the (null)
149
+ * observation is simply not accumulated and the prior estimate stands.
150
+ */
151
+ measure(text: string, corners: readonly Point2[], image: ImageSize, ctx: QrSizeDepthContext): QrSizeMeasurement | null;
152
+ /** Current estimate for a marker without adding a sample. */
153
+ current(text: string): QrSizeEstimate;
154
+ /** Drop one marker's samples (`text` given) or all of them. */
155
+ reset(text?: string): void;
156
+ }
157
+ /**
158
+ * Create a size measurer. Accumulator `options` (quality threshold, min samples,
159
+ * spread, cap) are forwarded to every per-marker {@link createQrSizeAccumulator};
160
+ * the depth-at-corners knobs ({@link QrSizeMeasurerOptions.cornerInsetFractions},
161
+ * {@link QrSizeMeasurerOptions.maxReconstructedCorners}) tune corner sampling.
162
+ */
163
+ declare function createQrSizeMeasurer(options?: QrSizeMeasurerOptions): QrSizeMeasurer;
164
+ //#endregion
165
+ export { QrSizeMeasurerOptions as a, QrSizeAccumulatorOptions as c, QrSizeStatus as d, createQrSizeAccumulator as f, QrSizeMeasurer as i, QrSizeEstimate as l, QrSizeDepthContext as n, createQrSizeMeasurer as o, estimateQrSizeFromDepth as p, QrSizeMeasurement as r, QrSizeAccumulator as s, ImageSize as t, QrSizeObservation as u };