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
@@ -0,0 +1,596 @@
1
+ import { vec3 } from "gl-matrix";
2
+ //#region ../src/ar/qr-size-from-depth.ts
3
+ /**
4
+ * QR size from depth — Note 4 of the QR-tracking follow-up plan
5
+ * (docs `2026-06-15-followup-qr-tracking-generalization-overlay-and-north.md`).
6
+ *
7
+ * MEASURE a QR's printed physical size directly from the depth map, so the
8
+ * QR content + printed size are irrelevant and `qr.physicalSizeM` need not be
9
+ * hand-authored. The 4 corner pixels (and a few interior samples to catch edge
10
+ * depth-bleed) are unprojected to 3D via the existing `createDepthUnprojector`
11
+ * (depth + projection + camera pose → raw-WebXR points); the pairwise side
12
+ * lengths of the unprojected square give a per-observation size estimate WITH
13
+ * metric scale from depth — no `solvePnP` scale assumption needed.
14
+ *
15
+ * Two pieces:
16
+ * - {@link estimateQrSizeFromDepth} — one observation → `{ sizeM, quality }`.
17
+ * `quality ∈ [0,1]` falls as the 4 sides disagree, the diagonals deviate from
18
+ * `√2·side`, or the corners/interior points are non-planar (depth noise / not
19
+ * a planar square facing us). A caller rejects low-quality reads.
20
+ * - {@link createQrSizeAccumulator} — a robust running MEDIAN over accepted
21
+ * observations, reporting the Note 3 size lifecycle (`unknown → measuring →
22
+ * estimated`). The median is robust to depth noise; the lifecycle gate
23
+ * (min sample count + low spread) is what later promotes a measured size to
24
+ * drive size-dependent features.
25
+ *
26
+ * The size-estimate VALUE types ({@link QrSizeStatus}, {@link QrSizeEstimate})
27
+ * live here (not in the `qrDetected` state slice) so the slice can import them
28
+ * WITHOUT the `ar` layer ever importing `state` — that would close a cycle.
29
+ *
30
+ * @see depth-unprojection.ts — `createDepthUnprojector` (the unprojection it composes).
31
+ * @see ../state/qr-detected-slice.ts — consumes `QrSizeEstimate` (the size lifecycle).
32
+ */
33
+ const EPS$1 = 1e-9;
34
+ /**
35
+ * Absolute floor (meters) for the robust plane-fit inlier band, used when the
36
+ * MAD-derived σ is ~0 (a near-perfect plane). Comfortably above float round-
37
+ * trip noise yet far below a real depth outlier at QR scale. See WS-A.
38
+ */
39
+ const PLANE_INLIER_FLOOR_M = .005;
40
+ function dist(a, b) {
41
+ return Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]);
42
+ }
43
+ /** Median of a numeric list (mean of the middle two for even n). */
44
+ function median(values) {
45
+ const sorted = [...values].sort((a, b) => a - b);
46
+ const n = sorted.length;
47
+ const mid = n >> 1;
48
+ return n % 2 === 1 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
49
+ }
50
+ /**
51
+ * Estimate a QR's physical side length from one detection's depth samples.
52
+ *
53
+ * @param corners - the 4 corner depth samples, ordered TL, TR, BR, BL (matching
54
+ * `buildObjectPoints` / the detector corner-order normalization).
55
+ * @param interiorSamples - a few interior depth samples (may be empty); used
56
+ * only to strengthen the planarity check against corner edge depth-bleed.
57
+ * @param unprojector - built once per depth sample via `createDepthUnprojector`.
58
+ * @returns `{ sizeM, quality }`, or `null` when a corner cannot be unprojected
59
+ * or the quad is degenerate (collinear / zero-area).
60
+ */
61
+ function estimateQrSizeFromDepth(corners, interiorSamples, unprojector) {
62
+ const world = [];
63
+ for (const c of corners) {
64
+ const p = unprojector.unproject(c);
65
+ if (!p) return null;
66
+ world.push(p);
67
+ }
68
+ const [c0, c1, c2, c3] = world;
69
+ const edges = [
70
+ dist(c0, c1),
71
+ dist(c1, c2),
72
+ dist(c2, c3),
73
+ dist(c3, c0)
74
+ ];
75
+ const meanEdge = (edges[0] + edges[1] + edges[2] + edges[3]) / 4;
76
+ if (!(meanEdge > EPS$1)) return null;
77
+ const u = vec3.subtract(vec3.create(), [
78
+ c1[0],
79
+ c1[1],
80
+ c1[2]
81
+ ], [
82
+ c0[0],
83
+ c0[1],
84
+ c0[2]
85
+ ]);
86
+ const v = vec3.subtract(vec3.create(), [
87
+ c3[0],
88
+ c3[1],
89
+ c3[2]
90
+ ], [
91
+ c0[0],
92
+ c0[1],
93
+ c0[2]
94
+ ]);
95
+ const normal = vec3.cross(vec3.create(), u, v);
96
+ const normalLen = vec3.length(normal);
97
+ if (!(normalLen > EPS$1)) return null;
98
+ vec3.scale(normal, normal, 1 / normalLen);
99
+ const planeOffset = (p) => Math.abs(normal[0] * (p[0] - c0[0]) + normal[1] * (p[1] - c0[1]) + normal[2] * (p[2] - c0[2]));
100
+ let maxPlaneOffset = planeOffset(c2);
101
+ for (const s of interiorSamples) {
102
+ const p = unprojector.unproject(s);
103
+ if (!p) continue;
104
+ maxPlaneOffset = Math.max(maxPlaneOffset, planeOffset(p));
105
+ }
106
+ const edgeErr = Math.max(...edges.map((e) => Math.abs(e - meanEdge))) / meanEdge;
107
+ const expectedDiag = meanEdge * Math.SQRT2;
108
+ const diagErr = Math.max(Math.abs(dist(c0, c2) - expectedDiag), Math.abs(dist(c1, c3) - expectedDiag)) / expectedDiag;
109
+ const planeErr = maxPlaneOffset / meanEdge;
110
+ const quality = Math.max(0, Math.min(1, 1 - Math.max(edgeErr, diagErr, planeErr)));
111
+ return {
112
+ sizeM: median(edges),
113
+ quality
114
+ };
115
+ }
116
+ /**
117
+ * Least-squares plane through a point set, refined from a seed normal. Avoids a
118
+ * full eigensolver: the seed (from LMS) picks which axis is most aligned with
119
+ * the normal, that axis becomes the dependent variable of a 2-parameter linear
120
+ * fit through the centroid (well-conditioned precisely because the plane is not
121
+ * near-vertical in that axis), and the 2×2 normal equations are solved in closed
122
+ * form. Returns `null` for fewer than 3 points or a singular (collinear) fit.
123
+ */
124
+ function fitPlaneLeastSquares(points, seedNormal) {
125
+ const n = points.length;
126
+ if (n < 3) return null;
127
+ let cx = 0;
128
+ let cy = 0;
129
+ let cz = 0;
130
+ for (const p of points) {
131
+ cx += p[0];
132
+ cy += p[1];
133
+ cz += p[2];
134
+ }
135
+ cx /= n;
136
+ cy /= n;
137
+ cz /= n;
138
+ const ax = Math.abs(seedNormal[0]);
139
+ const ay = Math.abs(seedNormal[1]);
140
+ const az = Math.abs(seedNormal[2]);
141
+ const dep = ax >= ay && ax >= az ? 0 : ay >= az ? 1 : 2;
142
+ const split = (p) => dep === 0 ? [
143
+ p[1] - cy,
144
+ p[2] - cz,
145
+ p[0] - cx
146
+ ] : dep === 1 ? [
147
+ p[0] - cx,
148
+ p[2] - cz,
149
+ p[1] - cy
150
+ ] : [
151
+ p[0] - cx,
152
+ p[1] - cy,
153
+ p[2] - cz
154
+ ];
155
+ let Suu = 0;
156
+ let Suw = 0;
157
+ let Sww = 0;
158
+ let Sud = 0;
159
+ let Swd = 0;
160
+ for (const p of points) {
161
+ const [u, w, d] = split(p);
162
+ Suu += u * u;
163
+ Suw += u * w;
164
+ Sww += w * w;
165
+ Sud += u * d;
166
+ Swd += w * d;
167
+ }
168
+ const det = Suu * Sww - Suw * Suw;
169
+ if (Math.abs(det) < EPS$1) return null;
170
+ const a = (Sud * Sww - Swd * Suw) / det;
171
+ const b = (Swd * Suu - Sud * Suw) / det;
172
+ const nUWD = [
173
+ -a,
174
+ -b,
175
+ 1
176
+ ];
177
+ const nxyz = dep === 0 ? [
178
+ nUWD[2],
179
+ nUWD[0],
180
+ nUWD[1]
181
+ ] : dep === 1 ? [
182
+ nUWD[0],
183
+ nUWD[2],
184
+ nUWD[1]
185
+ ] : [
186
+ nUWD[0],
187
+ nUWD[1],
188
+ nUWD[2]
189
+ ];
190
+ const len = Math.hypot(nxyz[0], nxyz[1], nxyz[2]);
191
+ if (!(len > EPS$1)) return null;
192
+ const sign = nxyz[0] * seedNormal[0] + nxyz[1] * seedNormal[1] + nxyz[2] * seedNormal[2] < 0 ? -1 : 1;
193
+ const normal = [
194
+ sign * nxyz[0] / len,
195
+ sign * nxyz[1] / len,
196
+ sign * nxyz[2] / len
197
+ ];
198
+ return {
199
+ centroid: [
200
+ cx,
201
+ cy,
202
+ cz
203
+ ],
204
+ normal
205
+ };
206
+ }
207
+ /**
208
+ * Robustly fit a plane to 3D points. A plain least-squares (PCA) plane is
209
+ * fragile here: one gross depth outlier (edge depth-bleed, a stray background
210
+ * read) can dominate the off-plane variance and flip which axis PCA calls the
211
+ * normal, so MAD rejection then runs against a wrong normal. Instead we use
212
+ * Least-Median-of-Squares — pick the candidate plane (from a point triple) that
213
+ * minimizes the MEDIAN squared orthogonal residual (≈50% breakdown, no
214
+ * threshold) — derive a robust scale from that median, select inliers, then
215
+ * least-squares refit on the inliers for an accurate normal/centroid.
216
+ *
217
+ * Deterministic (fixed-seed triple sampling) so property tests are
218
+ * reproducible. Returns `null` for fewer than 3 points or a degenerate
219
+ * (collinear / coincident) set where no non-degenerate triple exists.
220
+ */
221
+ function fitPlaneRobust(points) {
222
+ const n = points.length;
223
+ if (n < 3) return null;
224
+ let best = null;
225
+ let rng = 625341585;
226
+ const nextIdx = () => {
227
+ rng = rng * 1103515245 + 12345 & 2147483647;
228
+ return rng % n;
229
+ };
230
+ const TRIALS = 200;
231
+ for (let t = 0; t < TRIALS; t++) {
232
+ const i = nextIdx();
233
+ const j = nextIdx();
234
+ const k = nextIdx();
235
+ if (i === j || j === k || i === k) continue;
236
+ const a = points[i];
237
+ const b = points[j];
238
+ const c = points[k];
239
+ const ux = b[0] - a[0];
240
+ const uy = b[1] - a[1];
241
+ const uz = b[2] - a[2];
242
+ const vx = c[0] - a[0];
243
+ const vy = c[1] - a[1];
244
+ const vz = c[2] - a[2];
245
+ let nx = uy * vz - uz * vy;
246
+ let ny = uz * vx - ux * vz;
247
+ let nz = ux * vy - uy * vx;
248
+ const len = Math.hypot(nx, ny, nz);
249
+ if (!(len > EPS$1)) continue;
250
+ nx /= len;
251
+ ny /= len;
252
+ nz /= len;
253
+ const medSq = median(points.map((p) => {
254
+ const d = nx * (p[0] - a[0]) + ny * (p[1] - a[1]) + nz * (p[2] - a[2]);
255
+ return d * d;
256
+ }));
257
+ if (!best || medSq < best.medSq) best = {
258
+ normal: [
259
+ nx,
260
+ ny,
261
+ nz
262
+ ],
263
+ p0: a,
264
+ medSq
265
+ };
266
+ }
267
+ if (!best) return null;
268
+ const sigma = 1.4826 * (1 + 5 / Math.max(1, n - 3)) * Math.sqrt(best.medSq);
269
+ const band = Math.max(2.5 * sigma, PLANE_INLIER_FLOOR_M);
270
+ const { normal: bn, p0 } = best;
271
+ let inliers = points.filter((p) => Math.abs(bn[0] * (p[0] - p0[0]) + bn[1] * (p[1] - p0[1]) + bn[2] * (p[2] - p0[2])) <= band);
272
+ if (inliers.length < 3) inliers = [...points];
273
+ const refit = fitPlaneLeastSquares(inliers, bn);
274
+ if (!refit) return null;
275
+ const refitDist = (p) => refit.normal[0] * (p[0] - refit.centroid[0]) + refit.normal[1] * (p[1] - refit.centroid[1]) + refit.normal[2] * (p[2] - refit.centroid[2]);
276
+ const rms = Math.sqrt(inliers.reduce((acc, p) => acc + refitDist(p) ** 2, 0) / inliers.length);
277
+ return {
278
+ point: refit.centroid,
279
+ normal: refit.normal,
280
+ inlierCount: inliers.length,
281
+ rms
282
+ };
283
+ }
284
+ /**
285
+ * Intersect the camera ray through a normalized screen point with a plane.
286
+ * The ray is recovered by unprojecting the screen point at two distinct depths
287
+ * (both lie on the corner's pixel ray); no explicit camera center is needed.
288
+ * Returns the metric 3D intersection, or `null` for a parallel/degenerate ray.
289
+ */
290
+ function intersectScreenRayWithPlane(screen, plane, unprojector) {
291
+ const a = unprojector.unproject({
292
+ ...screen,
293
+ depthM: 1
294
+ });
295
+ const b = unprojector.unproject({
296
+ ...screen,
297
+ depthM: 2
298
+ });
299
+ if (!a || !b) return null;
300
+ const dir = [
301
+ b[0] - a[0],
302
+ b[1] - a[1],
303
+ b[2] - a[2]
304
+ ];
305
+ const { normal: n, point: pp } = plane;
306
+ const denom = n[0] * dir[0] + n[1] * dir[1] + n[2] * dir[2];
307
+ if (Math.abs(denom) < EPS$1) return null;
308
+ const t = (n[0] * (pp[0] - a[0]) + n[1] * (pp[1] - a[1]) + n[2] * (pp[2] - a[2])) / denom;
309
+ const hit = [
310
+ a[0] + t * dir[0],
311
+ a[1] + t * dir[1],
312
+ a[2] + t * dir[2]
313
+ ];
314
+ return hit.every((v) => Number.isFinite(v)) ? hit : null;
315
+ }
316
+ /**
317
+ * Estimate a QR's physical side length by fitting the QR plane to many interior
318
+ * depth reads and intersecting the 4 corner pixel rays with that plane.
319
+ *
320
+ * @param cornerScreens - the 4 corner screen positions, ordered TL, TR, BR, BL.
321
+ * @param samples - interior (and optionally corner) depth reads across the QR
322
+ * face used for the robust plane fit. Need ≥3 non-collinear usable reads.
323
+ * @param unprojector - built once per depth sample via `createDepthUnprojector`.
324
+ * @returns `{ sizeM, quality }`, or `null` when the plane is under-determined
325
+ * (too few/collinear reads) or a corner ray cannot meet the plane.
326
+ */
327
+ function estimateQrSizeFromDepthDense(cornerScreens, samples, unprojector) {
328
+ const points = [];
329
+ for (const s of samples) {
330
+ const p = unprojector.unproject(s);
331
+ if (p) points.push(p);
332
+ }
333
+ const plane = fitPlaneRobust(points);
334
+ if (!plane) return null;
335
+ const corners = [];
336
+ for (const screen of cornerScreens) {
337
+ const hit = intersectScreenRayWithPlane(screen, plane, unprojector);
338
+ if (!hit) return null;
339
+ corners.push(hit);
340
+ }
341
+ const [c0, c1, c2, c3] = corners;
342
+ const edges = [
343
+ dist(c0, c1),
344
+ dist(c1, c2),
345
+ dist(c2, c3),
346
+ dist(c3, c0)
347
+ ];
348
+ const meanEdge = (edges[0] + edges[1] + edges[2] + edges[3]) / 4;
349
+ if (!(meanEdge > EPS$1)) return null;
350
+ const edgeErr = Math.max(...edges.map((e) => Math.abs(e - meanEdge))) / meanEdge;
351
+ const expectedDiag = meanEdge * Math.SQRT2;
352
+ const diagErr = Math.max(Math.abs(dist(c0, c2) - expectedDiag), Math.abs(dist(c1, c3) - expectedDiag)) / expectedDiag;
353
+ const planeErr = plane.rms / meanEdge;
354
+ const quality = Math.max(0, Math.min(1, 1 - Math.max(edgeErr, diagErr, planeErr)));
355
+ return {
356
+ sizeM: median(edges),
357
+ quality
358
+ };
359
+ }
360
+ const UNKNOWN = {
361
+ status: "unknown",
362
+ estimateM: null,
363
+ sampleCount: 0,
364
+ spreadM: 0
365
+ };
366
+ function createQrSizeAccumulator(options = {}) {
367
+ const { qualityThreshold = .8, minSamples = 8, maxSpreadM = .01, maxSamples = Infinity } = options;
368
+ let sizes = [];
369
+ let everConverged = false;
370
+ function estimate() {
371
+ if (sizes.length === 0) return { ...UNKNOWN };
372
+ const med = median(sizes);
373
+ const spreadM = 1.4826 * median(sizes.map((s) => Math.abs(s - med))) / Math.sqrt(sizes.length);
374
+ if (sizes.length >= minSamples && spreadM <= maxSpreadM) everConverged = true;
375
+ return {
376
+ status: everConverged ? "estimated" : "measuring",
377
+ estimateM: med,
378
+ sampleCount: sizes.length,
379
+ spreadM
380
+ };
381
+ }
382
+ return {
383
+ add(observation) {
384
+ if (observation && Number.isFinite(observation.sizeM) && observation.sizeM > 0 && observation.quality >= qualityThreshold) {
385
+ sizes.push(observation.sizeM);
386
+ if (sizes.length > maxSamples) sizes = sizes.slice(-maxSamples);
387
+ }
388
+ return estimate();
389
+ },
390
+ current: estimate,
391
+ reset() {
392
+ sizes = [];
393
+ everConverged = false;
394
+ }
395
+ };
396
+ }
397
+ //#endregion
398
+ //#region ../src/ar/qr-size-measurer.ts
399
+ const EPS = 1e-9;
400
+ /** Pixel corner → normalized screen point for the given frame. */
401
+ function toScreen(corner, image) {
402
+ return {
403
+ x: corner.x / image.width,
404
+ y: corner.y / image.height
405
+ };
406
+ }
407
+ /**
408
+ * Depth at a corner with inset fallback: try the corner pixel first, then points
409
+ * progressively inset toward the centroid. The returned {@link DepthPoint} always
410
+ * carries the TRUE corner's screen position — only the depth VALUE may be borrowed
411
+ * from an inset sample (valid because the marker face is locally planar over the
412
+ * few-percent inset) — so the unprojected square keeps its real size. `null` when
413
+ * no inset offers a depth read either.
414
+ */
415
+ function sampleCornerDepth(corner, centroid, image, depthAt, insetFractions) {
416
+ const s = toScreen(corner, image);
417
+ const exact = depthAt(s.x, s.y);
418
+ if (exact !== null) return {
419
+ screenX: s.x,
420
+ screenY: s.y,
421
+ depthM: exact
422
+ };
423
+ for (const f of insetFractions) {
424
+ const si = toScreen({
425
+ x: corner.x + f * (centroid.x - corner.x),
426
+ y: corner.y + f * (centroid.y - corner.y)
427
+ }, image);
428
+ const depthM = depthAt(si.x, si.y);
429
+ if (depthM !== null) return {
430
+ screenX: s.x,
431
+ screenY: s.y,
432
+ depthM
433
+ };
434
+ }
435
+ return null;
436
+ }
437
+ /**
438
+ * Estimate a missing corner's depth by fitting a plane `depth = f(screenX,
439
+ * screenY)` through three known corner samples and evaluating it at the missing
440
+ * corner's screen position. `null` when the three are collinear in screen space
441
+ * (degenerate plane).
442
+ */
443
+ function reconstructDepth(known, at) {
444
+ if (known.length < 3) return null;
445
+ const [a, b, c] = known;
446
+ const abx = b.screenX - a.screenX;
447
+ const aby = b.screenY - a.screenY;
448
+ const abz = b.depthM - a.depthM;
449
+ const acx = c.screenX - a.screenX;
450
+ const acy = c.screenY - a.screenY;
451
+ const acz = c.depthM - a.depthM;
452
+ const nx = aby * acz - abz * acy;
453
+ const ny = abz * acx - abx * acz;
454
+ const nz = abx * acy - aby * acx;
455
+ if (Math.abs(nz) < EPS) return null;
456
+ const depthM = a.depthM - (nx * (at.x - a.screenX) + ny * (at.y - a.screenY)) / nz;
457
+ return Number.isFinite(depthM) ? depthM : null;
458
+ }
459
+ /**
460
+ * Build the 4 corner depth samples with inset fallback and at-most-`maxRecon`
461
+ * planar reconstruction. `null` when more corners than allowed lack a depth read.
462
+ */
463
+ function cornerDepthPoints(corners, image, depthAt, insetFractions, maxRecon) {
464
+ if (corners.length !== 4) return null;
465
+ const centroid = {
466
+ x: corners.reduce((s, c) => s + c.x, 0) / corners.length,
467
+ y: corners.reduce((s, c) => s + c.y, 0) / corners.length
468
+ };
469
+ const sampled = corners.map((corner) => sampleCornerDepth(corner, centroid, image, depthAt, insetFractions));
470
+ const missing = sampled.filter((p) => p === null).length;
471
+ if (missing === 0) return sampled;
472
+ if (missing > maxRecon) return null;
473
+ const known = sampled.filter((p) => p !== null);
474
+ const out = [];
475
+ for (let i = 0; i < 4; i++) {
476
+ const s = sampled[i];
477
+ if (s) {
478
+ out.push(s);
479
+ continue;
480
+ }
481
+ const at = toScreen(corners[i], image);
482
+ const depthM = reconstructDepth(known, at);
483
+ if (depthM === null) return null;
484
+ out.push({
485
+ screenX: at.x,
486
+ screenY: at.y,
487
+ depthM
488
+ });
489
+ }
490
+ return out;
491
+ }
492
+ /**
493
+ * Sample an `n×n` interior lattice across the detected quad (bilinear in pixel
494
+ * space, strictly inside via `(i+0.5)/n` fractions so the high-contrast print
495
+ * boundary is avoided) and return the reads that have depth. These feed the
496
+ * dense plane fit; reads with no depth are simply skipped.
497
+ */
498
+ function latticeDepthPoints(corners, image, depthAt, n) {
499
+ if (corners.length !== 4 || n < 1) return [];
500
+ const [tl, tr, br, bl] = corners;
501
+ const out = [];
502
+ for (let i = 0; i < n; i++) {
503
+ const u = (i + .5) / n;
504
+ const topX = tl.x + (tr.x - tl.x) * u;
505
+ const topY = tl.y + (tr.y - tl.y) * u;
506
+ const botX = bl.x + (br.x - bl.x) * u;
507
+ const botY = bl.y + (br.y - bl.y) * u;
508
+ for (let j = 0; j < n; j++) {
509
+ const v = (j + .5) / n;
510
+ const s = toScreen({
511
+ x: topX + (botX - topX) * v,
512
+ y: topY + (botY - topY) * v
513
+ }, image);
514
+ const depthM = depthAt(s.x, s.y);
515
+ if (depthM !== null) out.push({
516
+ screenX: s.x,
517
+ screenY: s.y,
518
+ depthM
519
+ });
520
+ }
521
+ }
522
+ return out;
523
+ }
524
+ /** The 4 corner screen positions (detector order) for the dense ray-plane fit. */
525
+ function cornerScreens(corners, image) {
526
+ if (corners.length !== 4) return null;
527
+ return corners.map((c) => {
528
+ const s = toScreen(c, image);
529
+ return {
530
+ screenX: s.x,
531
+ screenY: s.y
532
+ };
533
+ });
534
+ }
535
+ /** The QR centroid as a single interior depth sample (may be empty). */
536
+ function interiorDepthPoints(corners, image, depthAt) {
537
+ const s = toScreen({
538
+ x: corners.reduce((s, c) => s + c.x, 0) / corners.length,
539
+ y: corners.reduce((s, c) => s + c.y, 0) / corners.length
540
+ }, image);
541
+ const depthM = depthAt(s.x, s.y);
542
+ return depthM === null ? [] : [{
543
+ screenX: s.x,
544
+ screenY: s.y,
545
+ depthM
546
+ }];
547
+ }
548
+ /**
549
+ * Create a size measurer. Accumulator `options` (quality threshold, min samples,
550
+ * spread, cap) are forwarded to every per-marker {@link createQrSizeAccumulator};
551
+ * the depth-at-corners knobs ({@link QrSizeMeasurerOptions.cornerInsetFractions},
552
+ * {@link QrSizeMeasurerOptions.maxReconstructedCorners}) tune corner sampling.
553
+ */
554
+ function createQrSizeMeasurer(options = {}) {
555
+ const insetFractions = options.cornerInsetFractions ?? [.12, .25];
556
+ const maxRecon = options.maxReconstructedCorners ?? 1;
557
+ const latticeSize = options.latticeSize ?? 7;
558
+ const accumulators = /* @__PURE__ */ new Map();
559
+ function accumulatorFor(text) {
560
+ let acc = accumulators.get(text);
561
+ if (!acc) {
562
+ acc = createQrSizeAccumulator(options);
563
+ accumulators.set(text, acc);
564
+ }
565
+ return acc;
566
+ }
567
+ return {
568
+ measure(text, corners, image, ctx) {
569
+ const screens = cornerScreens(corners, image);
570
+ if (!screens) return null;
571
+ const cornerSamples = cornerDepthPoints(corners, image, ctx.depthAt, insetFractions, maxRecon);
572
+ const lattice = latticeDepthPoints(corners, image, ctx.depthAt, latticeSize);
573
+ let observation = estimateQrSizeFromDepthDense(screens, lattice, ctx.unprojector);
574
+ let interiorSamples = lattice;
575
+ if (observation === null) {
576
+ if (!cornerSamples) return null;
577
+ interiorSamples = interiorDepthPoints(corners, image, ctx.depthAt);
578
+ observation = estimateQrSizeFromDepth(cornerSamples, interiorSamples, ctx.unprojector);
579
+ }
580
+ return {
581
+ estimate: accumulatorFor(text).add(observation),
582
+ cornerSamples,
583
+ interiorSamples
584
+ };
585
+ },
586
+ current(text) {
587
+ return accumulatorFor(text).current();
588
+ },
589
+ reset(text) {
590
+ if (text === void 0) accumulators.clear();
591
+ else accumulators.delete(text);
592
+ }
593
+ };
594
+ }
595
+ //#endregion
596
+ export { createQrSizeAccumulator as n, estimateQrSizeFromDepth as r, createQrSizeMeasurer as t };