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.
- package/dist/absolute-orientation-CAEh840t.d.ts +54 -0
- package/dist/{app-selectors-B8VQoMDY.d.ts → app-selectors-ikTUQZpe.d.ts} +1 -1
- package/dist/ar/bresenham3d.d.ts +1 -1
- package/dist/ar/camera-blit-capture.d.ts +2 -2
- package/dist/ar/camera-blit-capture.js +74 -9
- package/dist/ar/capability-checker.d.ts +1 -1
- package/dist/ar/capture-failure-tracker.d.ts +1 -1
- package/dist/ar/chromium-camera-access-workaround.d.ts +1 -1
- package/dist/ar/depth-grid-lookup.d.ts +2 -0
- package/dist/ar/depth-grid-lookup.js +43 -0
- package/dist/ar/depth-sampler.d.ts +2 -2
- package/dist/ar/depth-sampler.js +2 -4
- package/dist/ar/depth-unprojection.d.ts +1 -1
- package/dist/ar/enable-gps-ar.d.ts +1 -1
- package/dist/ar/enable-gps-ar.js +26 -1
- package/dist/ar/frame-loop.d.ts +1 -1
- package/dist/ar/image-capture.d.ts +2 -2
- package/dist/ar/image-capture.js +1 -154
- package/dist/ar/image-quality.d.ts +2 -0
- package/dist/ar/image-quality.js +168 -0
- package/dist/ar/index.d.ts +31 -19
- package/dist/ar/index.js +17 -5
- package/dist/ar/occupancy-grid.d.ts +1 -1
- package/dist/ar/occupancy-grid.js +29 -2
- package/dist/ar/planar-pnp.d.ts +2 -0
- package/dist/ar/planar-pnp.js +425 -0
- package/dist/ar/qr-debug-view.d.ts +2 -0
- package/dist/ar/qr-debug-view.js +89 -0
- package/dist/ar/qr-derived-pose.d.ts +2 -0
- package/dist/ar/qr-derived-pose.js +114 -0
- package/dist/ar/qr-detection-controller.d.ts +2 -0
- package/dist/ar/qr-detection-controller.js +2 -0
- package/dist/ar/qr-frontend.d.ts +2 -0
- package/dist/ar/qr-frontend.js +60 -0
- package/dist/ar/qr-pose.d.ts +2 -0
- package/dist/ar/qr-pose.js +267 -0
- package/dist/ar/qr-size-depth-context.d.ts +3 -0
- package/dist/ar/qr-size-depth-context.js +42 -0
- package/dist/ar/qr-size-measurer.d.ts +2 -0
- package/dist/ar/qr-size-measurer.js +2 -0
- package/dist/ar/replay-scene.d.ts +1 -1
- package/dist/ar/replay-scene.js +1 -1
- package/dist/ar/scene-node-names.d.ts +1 -1
- package/dist/ar/webxr-nue-basis.d.ts +1 -1
- package/dist/ar/webxr-session.d.ts +4 -4
- package/dist/ar/webxr-session.js +2 -2
- package/dist/ar/xr-camera-texture.d.ts +1 -1
- package/dist/ar/xr-error-handler.d.ts +1 -1
- package/dist/ar/xr-frame-loop.d.ts +1 -1
- package/dist/ar-MFpFlV7j.js +382 -0
- package/dist/{ar-world-group-alignment-C9lwwIyI.d.ts → ar-world-group-alignment-69fgnB39.d.ts} +1 -1
- package/dist/{camera-blit-capture-CnxUDbKx.d.ts → camera-blit-capture-NKT9H5-e.d.ts} +57 -4
- package/dist/capture-motion-gate-0Yl2NpQG.d.ts +24 -0
- package/dist/capture-motion-gate-4EscNZCw.js +94 -0
- package/dist/concurrency-By0oOTZt.d.ts +50 -0
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.js +2 -2
- package/dist/create-slam-app-store-DkCHOpvb.js +149 -0
- package/dist/{create-slam-app-store-BljusOua.d.ts → create-slam-app-store-n_VF63D5.d.ts} +50 -40
- package/dist/depth-grid-lookup-DQ-IYhO3.d.ts +21 -0
- package/dist/{depth-sampler-Bhanfwla.d.ts → depth-sampler-asc8exHt.d.ts} +1 -1
- package/dist/{depth-unprojection-Y6i_mJuT.d.ts → depth-unprojection-DjsbJW9L.d.ts} +1 -1
- package/dist/{enable-gps-ar-BfUJOgpW.d.ts → enable-gps-ar-DkNhFv4Y.d.ts} +4 -4
- package/dist/{frame-conversions-BfsafGWR.d.ts → frame-conversions-DObhj8Wf.d.ts} +1 -1
- package/dist/{fused-path-DJoVu98b.d.ts → fused-path-v6eOmLl3.d.ts} +1 -1
- package/dist/geo/h3-proximity.d.ts +2 -2
- package/dist/geo/h3-proximity.js +61 -2
- package/dist/geo/index.d.ts +2 -2
- package/dist/geo/index.js +2 -2
- package/dist/geodesic-angle-BTldfFgs.js +39 -0
- package/dist/{gps-anchor-dYeCvvp5.d.ts → gps-anchor-BeFO2RYa.d.ts} +1 -1
- package/dist/{gps-ar-pose-sampler-DVb96iOy.d.ts → gps-ar-pose-sampler-DHMZmYAc.d.ts} +1 -1
- package/dist/{gps-event-coordinator-CWIDOk4U.d.ts → gps-event-coordinator-CjWW636x.d.ts} +21 -11
- package/dist/{gps-event-markers-Dc9FT-q1.d.ts → gps-event-markers-Cz9RMA2k.d.ts} +18 -0
- package/dist/{h3-proximity-I1msGPgE.d.ts → h3-proximity-C-j0pw1j.d.ts} +39 -1
- package/dist/image-capture-B2-b6tKK.js +374 -0
- package/dist/{image-capture-qAZ5Fh5X.d.ts → image-capture-CNZDrQXG.d.ts} +103 -3
- package/dist/image-quality-smCBoFj1.d.ts +158 -0
- package/dist/index-BAPokxMb.d.ts +22 -0
- package/dist/{index-BYYUdf9v.d.ts → index-BODDUTvD.d.ts} +1 -1
- package/dist/index-BneSj4_5.d.ts +399 -0
- package/dist/index-_Rhgoqkh.d.ts +2 -0
- package/dist/index.d.ts +80 -67
- package/dist/index.js +24 -12
- package/dist/{leaflet-map-overlay-6CJC-E1l.d.ts → leaflet-map-overlay-CmIyiE8n.d.ts} +1 -1
- package/dist/{map-data-CkloBxIl.d.ts → map-data-B4oEehUh.d.ts} +1 -1
- package/dist/{map-overlay-draw-krOKwDAq.d.ts → map-overlay-draw-ymz1s7fG.d.ts} +1 -1
- package/dist/{null-storage-backend-uKQE8tl7.d.ts → null-storage-backend-B9rtyDfG.d.ts} +2 -2
- package/dist/{occupancy-grid-BRNRaPyT.d.ts → occupancy-grid-DnOc-6PU.d.ts} +12 -2
- package/dist/{opfs-storage-D5zAn_lx.d.ts → opfs-storage-3VZh-qH6.d.ts} +23 -4
- package/dist/{opfs-storage-backend-6aYMUZlJ.d.ts → opfs-storage-backend-JJiUJo-3.d.ts} +3 -3
- package/dist/{persistence-middleware-B9ei4Nhx.d.ts → persistence-middleware-DAPTTwB2.d.ts} +1 -1
- package/dist/planar-pnp-my9bqULS.d.ts +67 -0
- package/dist/qr-debug-view-Brl73HGO.d.ts +29 -0
- package/dist/qr-derived-pose-CmxTP2m4.d.ts +122 -0
- package/dist/qr-detected-slice-VQssZmQJ.d.ts +166 -0
- package/dist/qr-detection-controller-B-tLVd-S.js +119 -0
- package/dist/qr-detection-controller-CDurHv8E.d.ts +48 -0
- package/dist/qr-frontend--fFX25mV.d.ts +58 -0
- package/dist/qr-pose-D2rfu5DA.d.ts +138 -0
- package/dist/qr-pose-aggregation-B7tWJq7j.d.ts +90 -0
- package/dist/qr-pose-aggregation-DBiaLW0O.js +191 -0
- package/dist/qr-size-depth-context-CeAG53fy.d.ts +13 -0
- package/dist/qr-size-measurer-DLb33geH.js +596 -0
- package/dist/qr-size-measurer-DQdAsid1.d.ts +165 -0
- package/dist/recording-options-BPCpMHLK.d.ts +514 -0
- package/dist/{recording-replayer-r0DZ9ALp.d.ts → recording-replayer-DQKpAGyQ.d.ts} +2 -2
- package/dist/recording-slice-DaxHZvNu.d.ts +55 -0
- package/dist/{replay-engine-XpDa0dy1.d.ts → replay-engine-CmKh_4wf.d.ts} +7 -4
- package/dist/{replay-scene-Bw58VL6p.d.ts → replay-scene-DBlDrV_x.d.ts} +3 -3
- package/dist/sensors/absolute-orientation.d.ts +2 -0
- package/dist/sensors/absolute-orientation.js +145 -0
- package/dist/sensors/gps-error-handler.d.ts +1 -1
- package/dist/sensors/gps.d.ts +1 -1
- package/dist/sensors/index.d.ts +5 -4
- package/dist/sensors/index.js +2 -1
- package/dist/sensors/permission-checker.d.ts +1 -1
- package/dist/sensors/permission-checker.js +1 -1
- package/dist/state/app-selectors.d.ts +1 -1
- package/dist/state/combined-root-state.d.ts +1 -1
- package/dist/state/create-slam-app-store.d.ts +1 -1
- package/dist/state/create-slam-app-store.js +1 -74
- package/dist/state/gps-ar-pose-sampler.d.ts +3 -3
- package/dist/state/gps-event-coordinator.d.ts +2 -2
- package/dist/state/gps-event-coordinator.js +27 -17
- package/dist/state/index.d.ts +20 -17
- package/dist/state/index.js +5 -4
- package/dist/state/persistence-middleware.d.ts +1 -1
- package/dist/state/persistence-middleware.js +30 -19
- package/dist/state/qr-detected-slice.d.ts +4 -0
- package/dist/state/qr-detected-slice.js +311 -0
- package/dist/state/recording-options.d.ts +2 -2
- package/dist/state/recording-options.js +273 -7
- package/dist/state/recording-replayer.d.ts +2 -2
- package/dist/state/recording-replayer.js +1 -1
- package/dist/state/recording-slice.d.ts +1 -1
- package/dist/state/recording-slice.js +5 -1
- package/dist/state/replay-engine.d.ts +1 -1
- package/dist/state/replay-engine.js +7 -3
- package/dist/state/store-subscribers.d.ts +2 -2
- package/dist/state/subscribe-to-selector.d.ts +1 -1
- package/dist/state/tracking-quality.d.ts +2 -2
- package/dist/state/tracking-quality.js +40 -128
- package/dist/state/tracking-slice.d.ts +1 -1
- package/dist/{state-BkIfpiyA.js → state-E51QT_-j.js} +2 -1
- package/dist/storage/file-system-utils.d.ts +1 -1
- package/dist/storage/index.d.ts +9 -9
- package/dist/storage/index.js +4 -4
- package/dist/storage/null-storage-backend.d.ts +1 -1
- package/dist/storage/opfs-storage-backend.d.ts +1 -1
- package/dist/storage/opfs-storage-backend.js +3 -3
- package/dist/storage/opfs-storage.d.ts +1 -1
- package/dist/storage/opfs-storage.js +5 -4
- package/dist/storage/storage-backend.d.ts +1 -1
- package/dist/storage/zip-coverage-embed.d.ts +2 -0
- package/dist/storage/zip-coverage-embed.js +88 -0
- package/dist/storage/zip-export.d.ts +2 -2
- package/dist/storage/zip-export.js +37 -59
- package/dist/storage/zip-reader.d.ts +1 -1
- package/dist/{storage-backend-Do6YDpNo.d.ts → storage-backend-BdVLD0dU.d.ts} +1 -1
- package/dist/{store-subscribers-0YWyOLvV.d.ts → store-subscribers-CA6AJH_8.d.ts} +2 -2
- package/dist/{subscribe-to-selector-CZ6k1NFP.d.ts → subscribe-to-selector-B6EUmQ3I.d.ts} +1 -1
- package/dist/test-utils/browser-mocks.d.ts +7 -1
- package/dist/test-utils/browser-mocks.js +6 -0
- package/dist/test-utils/zip-round-trip-helpers.d.ts +9 -1
- package/dist/test-utils/zip-round-trip-helpers.js +5 -1
- package/dist/{tracking-slice-CdK8c-8P.d.ts → tracking-slice-BuAZxXZ5.d.ts} +1 -1
- package/dist/types/ar-types.d.ts +1 -1
- package/dist/types/geo-types.d.ts +1 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/utils/concurrency.d.ts +2 -2
- package/dist/utils/concurrency.js +66 -2
- package/dist/utils/failure-tracker.d.ts +1 -1
- package/dist/utils/format-file-size.d.ts +1 -1
- package/dist/utils/fused-path.d.ts +1 -1
- package/dist/utils/index.d.ts +8 -7
- package/dist/utils/index.js +2 -1
- package/dist/utils/list-formatter.d.ts +1 -1
- package/dist/utils/logger.d.ts +1 -1
- package/dist/visualization/accuracy-circles.d.ts +1 -1
- package/dist/visualization/alignment-lerper.d.ts +1 -1
- package/dist/visualization/ar-world-group-alignment.d.ts +1 -1
- package/dist/visualization/ar-world-group-alignment.js +1 -1
- package/dist/visualization/camera-follower.d.ts +1 -1
- package/dist/visualization/css3d-renderer-manager.d.ts +1 -1
- package/dist/visualization/frame-conversions.d.ts +1 -1
- package/dist/visualization/frustum-visibility.d.ts +1 -1
- package/dist/visualization/gps-anchor.d.ts +1 -1
- package/dist/visualization/gps-compass-cubes.d.ts +1 -1
- package/dist/visualization/gps-event-markers.d.ts +1 -1
- package/dist/visualization/gps-event-markers.js +40 -5
- package/dist/visualization/hit-test-reticle.d.ts +1 -1
- package/dist/visualization/hit-test-reticle.js +1 -0
- package/dist/visualization/index.d.ts +18 -18
- package/dist/visualization/leaflet-map-overlay.d.ts +1 -1
- package/dist/visualization/leaflet-map-overlay.js +15 -3
- package/dist/visualization/lerp-utils.d.ts +1 -1
- package/dist/visualization/map-data.d.ts +1 -1
- package/dist/visualization/map-overlay-draw.d.ts +1 -1
- package/dist/visualization/map-overlay.d.ts +1 -1
- package/dist/visualization/three-dispose.d.ts +1 -1
- package/dist/visualization/vis-colors.d.ts +1 -1
- package/dist/{webxr-session-TIaD883f.js → webxr-session-DG-eNfny.js} +244 -11
- package/dist/{webxr-session-GjZ2BIpS.d.ts → webxr-session-Do5L6G3B.d.ts} +86 -8
- package/dist/zip-coverage-embed-DCAREVmw.d.ts +41 -0
- package/dist/{zip-export-G14hkSPG.d.ts → zip-export-ol0S_Ofd.d.ts} +27 -23
- package/package.json +2 -2
- package/dist/concurrency-CO1FQvs3.d.ts +0 -24
- package/dist/file-system-CwXTNHmB.d.ts +0 -114
- package/dist/index-CF05QV2m.d.ts +0 -2
- package/dist/recording-options-DJBXuMxV.d.ts +0 -182
- package/dist/recording-slice-89O07ebs.d.ts +0 -36
- package/dist/storage/file-system.d.ts +0 -2
- package/dist/storage/file-system.js +0 -289
- /package/dist/{accuracy-circles-BpwWAs8G.d.ts → accuracy-circles-BtqOP_v1.d.ts} +0 -0
- /package/dist/{alignment-lerper-Dm9G7JJO.d.ts → alignment-lerper-Bil6vbVw.d.ts} +0 -0
- /package/dist/{ar-types-Dan1MVTi.d.ts → ar-types-BFX2r1wk.d.ts} +0 -0
- /package/dist/{bresenham3d-CFfd7n6B.d.ts → bresenham3d-CU1Gv2W4.d.ts} +0 -0
- /package/dist/{camera-follower-C2mwEn7c.d.ts → camera-follower-CqJJ7zRj.d.ts} +0 -0
- /package/dist/{capability-checker-CQXJHxXg.d.ts → capability-checker-DUQLg6gq.d.ts} +0 -0
- /package/dist/{capture-failure-tracker-CUeU3L_M.d.ts → capture-failure-tracker-gsm8q7yi.d.ts} +0 -0
- /package/dist/{chromium-camera-access-workaround-Jqbn0apH.d.ts → chromium-camera-access-workaround-D4K2uwwQ.d.ts} +0 -0
- /package/dist/{css3d-renderer-manager-ptu_t3B3.d.ts → css3d-renderer-manager-B3kGNtcn.d.ts} +0 -0
- /package/dist/{failure-tracker-Cn8Fo9D4.d.ts → failure-tracker-BO7M7jF_.d.ts} +0 -0
- /package/dist/{file-system-utils-CXIoL-DM.d.ts → file-system-utils-DOajnMm-.d.ts} +0 -0
- /package/dist/{format-file-size-CLCZz3lC.d.ts → format-file-size-Dc98ulzB.d.ts} +0 -0
- /package/dist/{frame-loop-CMewS8cY.d.ts → frame-loop-CpVjQ1dH.d.ts} +0 -0
- /package/dist/{frustum-visibility-CmI96ESS.d.ts → frustum-visibility-CeoCZchO.d.ts} +0 -0
- /package/dist/{geo-types-SaPnIEUM.d.ts → geo-types-3Ip1NzVR.d.ts} +0 -0
- /package/dist/{gps-VuezJWK3.d.ts → gps-BBO3hKfT.d.ts} +0 -0
- /package/dist/{gps-compass-cubes-BM1uGiYV.d.ts → gps-compass-cubes-DSN87L8p.d.ts} +0 -0
- /package/dist/{gps-error-handler-CJl_2MVm.d.ts → gps-error-handler-BbPsfw-_.d.ts} +0 -0
- /package/dist/{hit-test-reticle-BEzEybxZ.d.ts → hit-test-reticle-DV-2TpB9.d.ts} +0 -0
- /package/dist/{lerp-utils-BpV-0Dtk.d.ts → lerp-utils-BLmHhkVe.d.ts} +0 -0
- /package/dist/{list-formatter-RRcBe8Vg.d.ts → list-formatter-KNMS0ay2.d.ts} +0 -0
- /package/dist/{logger-BEHXsnnq.d.ts → logger-CLnPrHiD.d.ts} +0 -0
- /package/dist/{map-overlay-bcogY5fK.d.ts → map-overlay-Dh2B97MP.d.ts} +0 -0
- /package/dist/{permission-checker-GpAiSCwd.d.ts → permission-checker-CDuI8BwC.d.ts} +0 -0
- /package/dist/{scene-node-names-Bu0S66H1.d.ts → scene-node-names-BiShOQDB.d.ts} +0 -0
- /package/dist/{session-disposers-M-oashRH.js → session-disposers-zv9OGXPB.js} +0 -0
- /package/dist/{three-dispose-D950t71B.d.ts → three-dispose-Bt1k7WKh.d.ts} +0 -0
- /package/dist/{vis-colors-D6omC64K.d.ts → vis-colors-Obw2pLqT.d.ts} +0 -0
- /package/dist/{webxr-nue-basis-CqKQmZzT.d.ts → webxr-nue-basis-3nfjDJJp.d.ts} +0 -0
- /package/dist/{xr-camera-texture-DBwCaKTd.d.ts → xr-camera-texture-Cva1q3Qj.d.ts} +0 -0
- /package/dist/{xr-error-handler-C0AQRmxs.d.ts → xr-error-handler-CtvHpjfm.d.ts} +0 -0
- /package/dist/{xr-frame-loop-BtYytZJg.d.ts → xr-frame-loop-DIYtRcUa.d.ts} +0 -0
- /package/dist/{zip-reader-DSEXt7K6.d.ts → zip-reader-D9iPQwdQ.d.ts} +0 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { i as Pose, r as Point2, s as SolvePnpSquare } from "./qr-pose-D2rfu5DA.js";
|
|
2
|
+
import { a as QrSizeMeasurerOptions, n as QrSizeDepthContext } from "./qr-size-measurer-DQdAsid1.js";
|
|
3
|
+
import { Matrix4 } from "gps-plus-slam-js";
|
|
4
|
+
|
|
5
|
+
//#region ../src/ar/qr-derived-pose.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* One RAW QR observation — the authoritative recorded shape (D-A). Everything
|
|
8
|
+
* the derive layer needs to recompute size + pose is here EXCEPT depth (injected
|
|
9
|
+
* via `resolveDepthAt`). Structural on purpose: the slice's `QrDetectionEntry`
|
|
10
|
+
* supplies these fields; this module never imports the slice.
|
|
11
|
+
*/
|
|
12
|
+
interface RawQrObservation {
|
|
13
|
+
/** Decoded payload (text/URL) — also the marker key. */
|
|
14
|
+
text: string;
|
|
15
|
+
/** The 4 detected corners in detector-buffer PIXELS, order TL,TR,BR,BL. */
|
|
16
|
+
corners: readonly Point2[];
|
|
17
|
+
/** Capturing camera pose in raw-WebXR/odom space (rides `arWorldGroup`). */
|
|
18
|
+
cameraPose: Pose;
|
|
19
|
+
/** Column-major GL projection of the detector frame (→ PnP intrinsics). */
|
|
20
|
+
projectionMatrix: Matrix4;
|
|
21
|
+
/** Detector-buffer width in pixels (to normalize corners + build intrinsics). */
|
|
22
|
+
imageWidth: number;
|
|
23
|
+
/** Detector-buffer height in pixels. */
|
|
24
|
+
imageHeight: number;
|
|
25
|
+
/**
|
|
26
|
+
* Detection time in the producer's injected clock — the depth as-of join key.
|
|
27
|
+
* MUST share the depth stream's clock, which is EPOCH ms
|
|
28
|
+
* (`DepthSample.timestamp = performance.timeOrigin + frameTs`); the RAW recorder
|
|
29
|
+
* path stamps `Date.now()` (epoch), NOT relative `performance.now()`, or the
|
|
30
|
+
* `≤` join in {@link deriveQrSizeM} never matches. See open topic A.
|
|
31
|
+
*/
|
|
32
|
+
timestamp: number;
|
|
33
|
+
}
|
|
34
|
+
/** Injected dependencies for the derive-on-read pose. */
|
|
35
|
+
interface DeriveQrPoseDeps {
|
|
36
|
+
/**
|
|
37
|
+
* The as-of depth join: the depth-sampling context that was active at
|
|
38
|
+
* `timestamp`, or `null` when no usable depth covers it. Live wires the
|
|
39
|
+
* current depth sample; replay wires a lookup over the recorded depth stream.
|
|
40
|
+
*/
|
|
41
|
+
resolveDepthAt: (timestamp: number) => QrSizeDepthContext | null;
|
|
42
|
+
/** PnP backend — the pure-JS {@link PlanarPnpSquare} in production. */
|
|
43
|
+
solver: SolvePnpSquare;
|
|
44
|
+
/** Size-measurer tuning, forwarded to {@link createQrSizeMeasurer}. */
|
|
45
|
+
sizeOptions?: QrSizeMeasurerOptions;
|
|
46
|
+
/** Reprojection-error gate forwarded to {@link solveQrPose} (default 4 px). */
|
|
47
|
+
maxReprojectionErrorPx?: number;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Replay a marker's observation history through a fresh size measurer (each
|
|
51
|
+
* observation sized against its own as-of depth context) and return the running
|
|
52
|
+
* median size in meters, or `null` if no observation could be sized yet.
|
|
53
|
+
*
|
|
54
|
+
* Re-runs the whole history per call — O(history), bounded by the slice ring
|
|
55
|
+
* buffer. Cheap for v1; memoize if the debug-viz cadence makes it hot.
|
|
56
|
+
*/
|
|
57
|
+
declare function deriveQrSizeM(text: string, observations: readonly RawQrObservation[], resolveDepthAt: DeriveQrPoseDeps['resolveDepthAt'], sizeOptions?: QrSizeMeasurerOptions): number | null;
|
|
58
|
+
/**
|
|
59
|
+
* Solve the world pose of ONE raw observation given a known metric size. Pure
|
|
60
|
+
* (no depth): intrinsics come from the observation's projection + frame size,
|
|
61
|
+
* the corners + camera pose are recorded. Returns `null` when PnP rejects the
|
|
62
|
+
* quad or exceeds the reprojection gate.
|
|
63
|
+
*/
|
|
64
|
+
declare function solveQrPoseFromObservation(observation: RawQrObservation, sizeM: number, solver: SolvePnpSquare, maxReprojectionErrorPx?: number): Pose | null;
|
|
65
|
+
/** A best-effort derived placement: the solved world pose AND the metric size
|
|
66
|
+
* that solved it (the size the debug cube needs). */
|
|
67
|
+
interface DerivedQrPlacement {
|
|
68
|
+
/** Solved QR world pose (rides `arWorldGroup`). */
|
|
69
|
+
pose: Pose;
|
|
70
|
+
/** Running-median metric side length used to solve `pose`. */
|
|
71
|
+
sizeM: number;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Derive the best-effort {@link DerivedQrPlacement} for a marker: accumulate the
|
|
75
|
+
* size across the whole observation history (each against its as-of depth), then
|
|
76
|
+
* solve the LATEST observation with that size — returning BOTH the pose and the
|
|
77
|
+
* size (the debug cube needs the size; deriving once avoids two history passes).
|
|
78
|
+
* Returns `null` when there is no history, no observation can be sized, or PnP
|
|
79
|
+
* rejects the latest quad — callers render nothing (best-effort viz) rather than
|
|
80
|
+
* throwing.
|
|
81
|
+
*/
|
|
82
|
+
declare function deriveQrPlacement(text: string, observations: readonly RawQrObservation[], deps: DeriveQrPoseDeps): DerivedQrPlacement | null;
|
|
83
|
+
/**
|
|
84
|
+
* Derive only the best-effort solved WORLD pose for a marker (the size is an
|
|
85
|
+
* internal of {@link deriveQrPlacement}). Returns `null` under the same
|
|
86
|
+
* conditions. Kept for callers that need the pose alone.
|
|
87
|
+
*/
|
|
88
|
+
declare function deriveSolvedQrPose(text: string, observations: readonly RawQrObservation[], deps: DeriveQrPoseDeps): Pose | null;
|
|
89
|
+
/**
|
|
90
|
+
* A stateful, INCREMENTAL placement deriver — the live/replay-hot variant of
|
|
91
|
+
* {@link deriveQrPlacement}. The stateless `deriveQrPlacement` rebuilds a fresh
|
|
92
|
+
* measurer and replays the WHOLE observation history on every call; driven by a
|
|
93
|
+
* store subscriber that fires on every action, that is O(history) per action and
|
|
94
|
+
* caused an on-device framerate ramp-down once the per-marker ring filled
|
|
95
|
+
* (perf-degradation follow-up; open topic D). This deriver instead keeps one
|
|
96
|
+
* persistent size measurer and folds in **only observations newer than the last
|
|
97
|
+
* fold** — O(1) per new detection, matching the demo's incremental accumulator.
|
|
98
|
+
*
|
|
99
|
+
* Equivalence: folding each observation exactly once (in arrival order, against
|
|
100
|
+
* its as-of depth via `resolveDepthAt`) yields the same running-median size as a
|
|
101
|
+
* full replay over the same observations, so `update()`'s result equals
|
|
102
|
+
* {@link deriveQrPlacement} over the same sequence. It runs identically live and
|
|
103
|
+
* on replay (only reads what it is handed), preserving the re-test guarantee:
|
|
104
|
+
* the accumulated size is the whole **session** history (the slice ring cap only
|
|
105
|
+
* bounds what the live store can hand back, not what was already folded), which
|
|
106
|
+
* is exactly what an offline re-derivation over the full recorded stream sees.
|
|
107
|
+
*/
|
|
108
|
+
interface IncrementalQrPlacement {
|
|
109
|
+
/**
|
|
110
|
+
* Fold any observations newer than the last fold for `text`, then return the
|
|
111
|
+
* current best-effort placement (or `null` when not yet sizeable / PnP-rejected).
|
|
112
|
+
* **F1 (memo):** if the newest observation's timestamp is unchanged since the
|
|
113
|
+
* last call, returns the cached placement WITHOUT folding or re-solving — so
|
|
114
|
+
* unrelated store changes (depth, GPS) cost a `Map` lookup, not a re-derive.
|
|
115
|
+
*/
|
|
116
|
+
update(text: string, observations: readonly RawQrObservation[]): DerivedQrPlacement | null;
|
|
117
|
+
/** Drop a marker's accumulated size + memo (e.g. on `clearQrMarker`). */
|
|
118
|
+
reset(text: string): void;
|
|
119
|
+
}
|
|
120
|
+
declare function createIncrementalQrPlacement(deps: DeriveQrPoseDeps): IncrementalQrPlacement;
|
|
121
|
+
//#endregion
|
|
122
|
+
export { createIncrementalQrPlacement as a, deriveSolvedQrPose as c, RawQrObservation as i, solveQrPoseFromObservation as l, DerivedQrPlacement as n, deriveQrPlacement as o, IncrementalQrPlacement as r, deriveQrSizeM as s, DeriveQrPoseDeps as t };
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { i as Pose, r as Point2 } from "./qr-pose-D2rfu5DA.js";
|
|
2
|
+
import { l as QrSizeEstimate } from "./qr-size-measurer-DQdAsid1.js";
|
|
3
|
+
import { i as RawQrObservation, n as DerivedQrPlacement, t as DeriveQrPoseDeps } from "./qr-derived-pose-CmxTP2m4.js";
|
|
4
|
+
import { a as QrPoseStability, o as QrPoseStabilityOptions } from "./qr-pose-aggregation-B7tWJq7j.js";
|
|
5
|
+
import { Matrix4, Vector3 } from "gps-plus-slam-js";
|
|
6
|
+
import * as _$_reduxjs_toolkit0 from "@reduxjs/toolkit";
|
|
7
|
+
|
|
8
|
+
//#region ../src/state/qr-detected-slice.d.ts
|
|
9
|
+
/**
|
|
10
|
+
* Default ring-buffer cap per marker. Bounded so an overlay that never prunes
|
|
11
|
+
* still cannot leak; large enough to support a robust median over a sliding
|
|
12
|
+
* window and a short motion path. Override via {@link setQrMaxHistory}.
|
|
13
|
+
*/
|
|
14
|
+
declare const DEFAULT_QR_MAX_HISTORY = 32;
|
|
15
|
+
/**
|
|
16
|
+
* One detection observation. Detection-agnostic by design (Note 1): `text` is
|
|
17
|
+
* the label/payload, `reprojectionErrorPx` the confidence proxy, the two poses
|
|
18
|
+
* the 3D placement. `timestamp` is the producer's injected clock — see the field
|
|
19
|
+
* note: the RAW recorder path stamps EPOCH ms so the derive-on-read depth as-of
|
|
20
|
+
* join lines up with the depth stream (clock-domain coupling).
|
|
21
|
+
*/
|
|
22
|
+
interface QrDetectionEntry {
|
|
23
|
+
/** Decoded payload (text/URL) — also the marker key. */
|
|
24
|
+
text: string;
|
|
25
|
+
/**
|
|
26
|
+
* Detection time in the producer's injected clock. For the RAW recorder path
|
|
27
|
+
* this MUST be **EPOCH ms** (`Date.now()`), because the depth stream it joins
|
|
28
|
+
* against is epoch (`DepthSample.timestamp = performance.timeOrigin + frameTs`,
|
|
29
|
+
* `ar/depth-sampler.ts`) and the size as-of join pairs this with the depth
|
|
30
|
+
* sample whose timestamp is `≤` this one (`ar/qr-derived-pose`). A relative
|
|
31
|
+
* `performance.now()` stamp (~1e5) never satisfies that join, so the size — and
|
|
32
|
+
* the debug cube — never resolve. See the recorder live-QR plan open topic A.
|
|
33
|
+
*/
|
|
34
|
+
timestamp: number;
|
|
35
|
+
/** The 4 detected corners in detector-buffer PIXELS, order TL,TR,BR,BL. */
|
|
36
|
+
corners?: readonly Point2[];
|
|
37
|
+
/** Capturing camera pose in raw-WebXR/odom space (PnP camera-relative base). */
|
|
38
|
+
cameraPose?: Pose;
|
|
39
|
+
/** Column-major GL projection of the detector frame (→ PnP intrinsics). */
|
|
40
|
+
projectionMatrix?: Matrix4;
|
|
41
|
+
/** Detector-buffer width in pixels (to normalize corners + build intrinsics). */
|
|
42
|
+
imageWidth?: number;
|
|
43
|
+
/** Detector-buffer height in pixels. */
|
|
44
|
+
imageHeight?: number;
|
|
45
|
+
/** QR pose in raw-WebXR/odom space (rides `arWorldGroup`). */
|
|
46
|
+
qrPoseWorld?: Pose;
|
|
47
|
+
/** QR pose relative to the WebXR camera (pre-composition). */
|
|
48
|
+
qrPoseInCamera?: Pose;
|
|
49
|
+
/** RMS reprojection error in pixels (lower = better fit). */
|
|
50
|
+
reprojectionErrorPx?: number;
|
|
51
|
+
}
|
|
52
|
+
/** Per-marker state: a bounded detection history + the size lifecycle. */
|
|
53
|
+
interface QrMarkerState {
|
|
54
|
+
text: string;
|
|
55
|
+
/** Bounded ring buffer, OLDEST first / NEWEST last. */
|
|
56
|
+
detections: QrDetectionEntry[];
|
|
57
|
+
size: QrSizeEstimate;
|
|
58
|
+
}
|
|
59
|
+
interface QrDetectedState {
|
|
60
|
+
/** Ring-buffer cap applied per marker on `recordQrDetection`. */
|
|
61
|
+
maxHistory: number;
|
|
62
|
+
/** Markers keyed by decoded payload. */
|
|
63
|
+
markers: Record<string, QrMarkerState>;
|
|
64
|
+
}
|
|
65
|
+
declare const recordQrDetection: _$_reduxjs_toolkit0.ActionCreatorWithPayload<QrDetectionEntry, "qrDetected/recordQrDetection">, recordQrSizeEstimate: _$_reduxjs_toolkit0.ActionCreatorWithPayload<{
|
|
66
|
+
text: string;
|
|
67
|
+
estimate: QrSizeEstimate;
|
|
68
|
+
}, "qrDetected/recordQrSizeEstimate">, pruneQrDetections: _$_reduxjs_toolkit0.ActionCreatorWithPayload<{
|
|
69
|
+
text: string;
|
|
70
|
+
count: number;
|
|
71
|
+
}, "qrDetected/pruneQrDetections">, clearQrMarker: _$_reduxjs_toolkit0.ActionCreatorWithPayload<{
|
|
72
|
+
text: string;
|
|
73
|
+
}, "qrDetected/clearQrMarker">, clearAllQrMarkers: _$_reduxjs_toolkit0.ActionCreatorWithoutPayload<"qrDetected/clearAllQrMarkers">, setQrMaxHistory: _$_reduxjs_toolkit0.ActionCreatorWithPayload<number, "qrDetected/setQrMaxHistory">;
|
|
74
|
+
declare const qrDetectedReducer: _$_reduxjs_toolkit0.Reducer<QrDetectedState>;
|
|
75
|
+
/** Minimal root shape the selectors need (avoids a store import cycle). */
|
|
76
|
+
interface RootWithQrDetected {
|
|
77
|
+
qrDetected: QrDetectedState;
|
|
78
|
+
}
|
|
79
|
+
declare function selectQrMarkers(state: RootWithQrDetected): Record<string, QrMarkerState>;
|
|
80
|
+
declare function selectQrMarker(state: RootWithQrDetected, text: string): QrMarkerState | undefined;
|
|
81
|
+
/**
|
|
82
|
+
* The newest detection for a marker (the natural overlay-persistence source —
|
|
83
|
+
* an overlay reads this and keeps the last pose across detection misses rather
|
|
84
|
+
* than unmounting/flickering).
|
|
85
|
+
*/
|
|
86
|
+
declare function selectLatestQrDetection(state: RootWithQrDetected, text: string): QrDetectionEntry | undefined;
|
|
87
|
+
declare function selectQrSize(state: RootWithQrDetected, text: string): QrSizeEstimate | undefined;
|
|
88
|
+
/**
|
|
89
|
+
* Resolve a marker's measured size for the high-weight GPS vote
|
|
90
|
+
* (framework-wiring-options Part B, Option a). Returns the running-median
|
|
91
|
+
* `estimateM` ONCE the size lifecycle reports `'estimated'`, else `null` ("keep
|
|
92
|
+
* scanning"). This is the bridge an app injects as the QR controller's
|
|
93
|
+
* `resolveSizeM`, so a size-less-but-geo level votes the moment the slice
|
|
94
|
+
* converges — **without** the `ar` controller ever importing the slice (which
|
|
95
|
+
* would close the `ar → state` cycle):
|
|
96
|
+
*
|
|
97
|
+
* ```ts
|
|
98
|
+
* resolveSizeM: (text) => selectResolvedQrSizeM(store.getState(), text),
|
|
99
|
+
* ```
|
|
100
|
+
*
|
|
101
|
+
* Per the Note 3 decision (confirmed) a reliably-estimated *measured* size
|
|
102
|
+
* drives the vote under the *same* gate as an authored size — no extra
|
|
103
|
+
* size-specific bar — because the §7 occupancy + reprojection + outlier-rejection
|
|
104
|
+
* backstops already bound a bad read.
|
|
105
|
+
*/
|
|
106
|
+
declare function selectResolvedQrSizeM(state: RootWithQrDetected, text: string): number | null;
|
|
107
|
+
/**
|
|
108
|
+
* Robust per-axis median of the world positions across a marker's detection
|
|
109
|
+
* window — the "running estimate" the bounded buffer enables (Note 3). Outliers
|
|
110
|
+
* in fewer than half the samples cannot move the result outside the inlier
|
|
111
|
+
* range. Skips RAW-only entries that carry no solved pose (D-A); returns `null`
|
|
112
|
+
* for an empty window (or one with no solved poses at all).
|
|
113
|
+
*/
|
|
114
|
+
declare function medianQrPosition(entries: readonly QrDetectionEntry[]): Vector3 | null;
|
|
115
|
+
/**
|
|
116
|
+
* The pose-stability lifecycle for a marker, derived from its raw detection ring
|
|
117
|
+
* buffer (NOT stored separately — the detections are the source of truth, unlike
|
|
118
|
+
* the depth-measured size which is accumulated outside the buffer). Mirrors the
|
|
119
|
+
* size lifecycle's `unknown → measuring → stable` shape so the HUD can show
|
|
120
|
+
* "converging…" vs "stable". See `ar/qr-pose-aggregation.ts`.
|
|
121
|
+
*/
|
|
122
|
+
declare function selectQrPoseStability(state: RootWithQrDetected, text: string, options?: QrPoseStabilityOptions): QrPoseStability;
|
|
123
|
+
/**
|
|
124
|
+
* The robust filtered QR world pose, exposed ONLY once the stability gate is
|
|
125
|
+
* `stable` (else `null` — "keep accumulating"). This is the pose analogue of
|
|
126
|
+
* {@link selectResolvedQrSizeM}: the `resolveStablePose`-style bridge an app
|
|
127
|
+
* injects into the QR controller / demo so the `ar` layer never imports the
|
|
128
|
+
* slice. The high-weight vote and the smooth overlay consume THIS, never the raw
|
|
129
|
+
* latest pose (which stays available via {@link selectLatestQrDetection} for
|
|
130
|
+
* scanning feedback / overlay persistence across misses).
|
|
131
|
+
*
|
|
132
|
+
* ```ts
|
|
133
|
+
* resolveStablePose: (text) => selectStableQrPose(store.getState(), text),
|
|
134
|
+
* ```
|
|
135
|
+
*/
|
|
136
|
+
declare function selectStableQrPose(state: RootWithQrDetected, text: string, options?: QrPoseStabilityOptions): Pose | null;
|
|
137
|
+
/**
|
|
138
|
+
* Derive the best-effort solved WORLD pose for a marker from its RAW observation
|
|
139
|
+
* history (decision D-A): accumulate size over the history against the injected
|
|
140
|
+
* `resolveDepthAt` as-of join, then PnP-solve the latest observation. Returns
|
|
141
|
+
* `null` when the marker is unknown, has no raw observations, cannot be sized
|
|
142
|
+
* yet, or PnP rejects the latest quad — the re-test-friendly replacement for the
|
|
143
|
+
* once-stored solved pose. Runs identically live and on replay (only
|
|
144
|
+
* `deps.resolveDepthAt` differs). See `ar/qr-derived-pose.ts`.
|
|
145
|
+
*/
|
|
146
|
+
declare function selectSolvedQrPose(state: RootWithQrDetected, text: string, deps: DeriveQrPoseDeps): Pose | null;
|
|
147
|
+
/**
|
|
148
|
+
* The marker's stored entries narrowed to {@link RawQrObservation}s (entries
|
|
149
|
+
* carrying the full raw detector output), oldest→newest. Empty for an unknown
|
|
150
|
+
* marker or one with only solved-pose-only (transitional) entries. This is the
|
|
151
|
+
* shared mapping the derive-on-read selectors and the incremental deriver
|
|
152
|
+
* consume — exposed so a stateful consumer (the recorder's debug controller)
|
|
153
|
+
* can hand raw observations to `createIncrementalQrPlacement` without
|
|
154
|
+
* re-implementing the `toRawObservation` guard.
|
|
155
|
+
*/
|
|
156
|
+
declare function selectQrRawObservations(state: RootWithQrDetected, text: string): RawQrObservation[];
|
|
157
|
+
/**
|
|
158
|
+
* Derive the best-effort {@link DerivedQrPlacement} (solved pose AND the metric
|
|
159
|
+
* size that solved it) for a marker from its RAW observation history. The debug
|
|
160
|
+
* viz needs the size to draw the cube, so this returns both in one history pass
|
|
161
|
+
* (decision D-A; see {@link deriveQrPlacement}). `null` under the same
|
|
162
|
+
* conditions as {@link selectSolvedQrPose}. Runs identically live and on replay.
|
|
163
|
+
*/
|
|
164
|
+
declare function selectDerivedQrPlacement(state: RootWithQrDetected, text: string, deps: DeriveQrPoseDeps): DerivedQrPlacement | null;
|
|
165
|
+
//#endregion
|
|
166
|
+
export { setQrMaxHistory as C, selectStableQrPose as S, selectQrPoseStability as _, RootWithQrDetected as a, selectResolvedQrSizeM as b, medianQrPosition as c, recordQrDetection as d, recordQrSizeEstimate as f, selectQrMarkers as g, selectQrMarker as h, QrMarkerState as i, pruneQrDetections as l, selectLatestQrDetection as m, QrDetectedState as n, clearAllQrMarkers as o, selectDerivedQrPlacement as p, QrDetectionEntry as r, clearQrMarker as s, DEFAULT_QR_MAX_HISTORY as t, qrDetectedReducer as u, selectQrRawObservations as v, selectSolvedQrPose as x, selectQrSize as y };
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { createLogger } from "./utils/logger.js";
|
|
2
|
+
import { validateQuad } from "./ar/qr-pose.js";
|
|
3
|
+
//#region ../src/ar/detection-scheduler.ts
|
|
4
|
+
const log = createLogger("DetectionScheduler");
|
|
5
|
+
const defaultNow = () => typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
|
|
6
|
+
function createDetectionScheduler(config) {
|
|
7
|
+
const { detect, minIntervalMs, requiredLockCount = 3, now = defaultNow, onLocked, onMiss, onError } = config;
|
|
8
|
+
let inFlight = false;
|
|
9
|
+
let consecutiveLocks = 0;
|
|
10
|
+
let lastStart = -Infinity;
|
|
11
|
+
return {
|
|
12
|
+
get inFlight() {
|
|
13
|
+
return inFlight;
|
|
14
|
+
},
|
|
15
|
+
get consecutiveLocks() {
|
|
16
|
+
return consecutiveLocks;
|
|
17
|
+
},
|
|
18
|
+
get locked() {
|
|
19
|
+
return consecutiveLocks >= requiredLockCount;
|
|
20
|
+
},
|
|
21
|
+
offerFrame(image) {
|
|
22
|
+
if (inFlight) return;
|
|
23
|
+
const t = now();
|
|
24
|
+
if (t - lastStart < minIntervalMs) return;
|
|
25
|
+
lastStart = t;
|
|
26
|
+
inFlight = true;
|
|
27
|
+
(async () => detect(image))().then((result) => {
|
|
28
|
+
if (result) {
|
|
29
|
+
consecutiveLocks = Math.min(consecutiveLocks + 1, requiredLockCount);
|
|
30
|
+
if (consecutiveLocks >= requiredLockCount) try {
|
|
31
|
+
onLocked?.(result);
|
|
32
|
+
} catch (err) {
|
|
33
|
+
log.error("onLocked callback threw:", err);
|
|
34
|
+
}
|
|
35
|
+
} else {
|
|
36
|
+
consecutiveLocks = 0;
|
|
37
|
+
try {
|
|
38
|
+
onMiss?.();
|
|
39
|
+
} catch (err) {
|
|
40
|
+
log.error("onMiss callback threw:", err);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}).catch((err) => {
|
|
44
|
+
consecutiveLocks = 0;
|
|
45
|
+
try {
|
|
46
|
+
onError?.(err);
|
|
47
|
+
} catch (callbackErr) {
|
|
48
|
+
log.error("onError callback threw:", callbackErr);
|
|
49
|
+
}
|
|
50
|
+
}).finally(() => {
|
|
51
|
+
inFlight = false;
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/** The QR path: a detection scheduler whose result is a {@link QrPoseSolution}. */
|
|
57
|
+
function createQrDetectionScheduler(config) {
|
|
58
|
+
return createDetectionScheduler(config);
|
|
59
|
+
}
|
|
60
|
+
//#endregion
|
|
61
|
+
//#region ../src/ar/qr-detection-controller.ts
|
|
62
|
+
/**
|
|
63
|
+
* Create the thin RAW producer. On each accepted decode it emits a
|
|
64
|
+
* {@link RawQrObservation} via `recordDetection`; it does not size or solve.
|
|
65
|
+
*/
|
|
66
|
+
function createQrDetectionController(deps) {
|
|
67
|
+
const { detect, getCameraPose, getProjectionMatrix, recordDetection, now, minIntervalMs = 0, requiredLockCount = 2, onStatus } = deps;
|
|
68
|
+
const timestampNow = now ?? (() => Date.now());
|
|
69
|
+
let status = "idle";
|
|
70
|
+
function setStatus(next) {
|
|
71
|
+
if (status === next) return;
|
|
72
|
+
status = next;
|
|
73
|
+
onStatus?.(next);
|
|
74
|
+
}
|
|
75
|
+
async function runDetect(image) {
|
|
76
|
+
if (status === "idle") setStatus("scanning");
|
|
77
|
+
const detection = await detect(image);
|
|
78
|
+
if (!detection) return null;
|
|
79
|
+
if (!validateQuad(detection.corners).ok) return null;
|
|
80
|
+
const cameraPose = getCameraPose();
|
|
81
|
+
const projectionMatrix = getProjectionMatrix();
|
|
82
|
+
if (!cameraPose || !projectionMatrix) return null;
|
|
83
|
+
return {
|
|
84
|
+
text: detection.text,
|
|
85
|
+
corners: detection.corners,
|
|
86
|
+
cameraPose,
|
|
87
|
+
projectionMatrix,
|
|
88
|
+
imageWidth: image.width,
|
|
89
|
+
imageHeight: image.height,
|
|
90
|
+
timestamp: timestampNow()
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
const scheduler = createDetectionScheduler({
|
|
94
|
+
detect: runDetect,
|
|
95
|
+
minIntervalMs,
|
|
96
|
+
requiredLockCount,
|
|
97
|
+
...now ? { now } : {},
|
|
98
|
+
onLocked: (observation) => {
|
|
99
|
+
recordDetection(observation);
|
|
100
|
+
setStatus("tracking");
|
|
101
|
+
},
|
|
102
|
+
onMiss: () => {
|
|
103
|
+
if (status === "tracking") setStatus("scanning");
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
return {
|
|
107
|
+
offerFrame(image) {
|
|
108
|
+
scheduler.offerFrame(image);
|
|
109
|
+
},
|
|
110
|
+
get status() {
|
|
111
|
+
return status;
|
|
112
|
+
},
|
|
113
|
+
reset() {
|
|
114
|
+
setStatus("idle");
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
//#endregion
|
|
119
|
+
export { createDetectionScheduler as n, createQrDetectionScheduler as r, createQrDetectionController as t };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { i as Pose } from "./qr-pose-D2rfu5DA.js";
|
|
2
|
+
import { i as QrDetection, o as RgbaImage } from "./qr-frontend--fFX25mV.js";
|
|
3
|
+
import { i as RawQrObservation } from "./qr-derived-pose-CmxTP2m4.js";
|
|
4
|
+
import { Matrix4 } from "gps-plus-slam-js";
|
|
5
|
+
|
|
6
|
+
//#region ../src/ar/qr-detection-controller.d.ts
|
|
7
|
+
/** Minimal scan-status machine (NO QR detail — per the recorder scope). */
|
|
8
|
+
type QrScanStatus = 'idle' | 'scanning' | 'tracking';
|
|
9
|
+
/** Sink for one recorded RAW observation — e.g. dispatch `recordQrDetection`. */
|
|
10
|
+
type RawObservationSink = (observation: RawQrObservation) => void;
|
|
11
|
+
interface QrDetectionControllerDeps {
|
|
12
|
+
/** Detect + decode one frame (BarcodeDetector front-end fed by the capture). */
|
|
13
|
+
detect: (image: RgbaImage) => Promise<QrDetection | null>;
|
|
14
|
+
/**
|
|
15
|
+
* Capturing camera pose in raw-WebXR/odom space, read when a detection
|
|
16
|
+
* resolves (mirrors the demo's post-detect depth read). `null` → skip record.
|
|
17
|
+
*/
|
|
18
|
+
getCameraPose: () => Pose | null;
|
|
19
|
+
/**
|
|
20
|
+
* Column-major GL projection matrix of the detector-frame buffer the corners
|
|
21
|
+
* are in (→ PnP intrinsics on read). `null` → skip record.
|
|
22
|
+
*/
|
|
23
|
+
getProjectionMatrix: () => Matrix4 | null;
|
|
24
|
+
/** Record one RAW observation — e.g. `(o) => store.dispatch(recordQrDetection(o))`. */
|
|
25
|
+
recordDetection: RawObservationSink;
|
|
26
|
+
/** Injectable clock (ms) for the detection timestamp + scheduler. */
|
|
27
|
+
now?: () => number;
|
|
28
|
+
/** Throttle: start a detection at most once per this many ms. Default 0. */
|
|
29
|
+
minIntervalMs?: number;
|
|
30
|
+
/** Consecutive successes before a lock is recorded. Default 2. */
|
|
31
|
+
requiredLockCount?: number;
|
|
32
|
+
/** Status-change notifications (idle → scanning → tracking). */
|
|
33
|
+
onStatus?: (status: QrScanStatus) => void;
|
|
34
|
+
}
|
|
35
|
+
interface QrDetectionController {
|
|
36
|
+
/** Offer the latest camera frame; throttled/coalesced internally. */
|
|
37
|
+
offerFrame(image: RgbaImage): void;
|
|
38
|
+
readonly status: QrScanStatus;
|
|
39
|
+
/** Return to idle (e.g. on session end). */
|
|
40
|
+
reset(): void;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Create the thin RAW producer. On each accepted decode it emits a
|
|
44
|
+
* {@link RawQrObservation} via `recordDetection`; it does not size or solve.
|
|
45
|
+
*/
|
|
46
|
+
declare function createQrDetectionController(deps: QrDetectionControllerDeps): QrDetectionController;
|
|
47
|
+
//#endregion
|
|
48
|
+
export { createQrDetectionController as a, RawObservationSink as i, QrDetectionControllerDeps as n, QrScanStatus as r, QrDetectionController as t };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { r as Point2 } from "./qr-pose-D2rfu5DA.js";
|
|
2
|
+
|
|
3
|
+
//#region ../src/ar/qr-frontend.d.ts
|
|
4
|
+
/** Raw RGBA pixels of the frame fed to detection (top-left origin). */
|
|
5
|
+
interface RgbaImage {
|
|
6
|
+
data: Uint8ClampedArray;
|
|
7
|
+
width: number;
|
|
8
|
+
height: number;
|
|
9
|
+
}
|
|
10
|
+
/** One decoded QR: its 4 corner pixels and the decoded text (the level URL). */
|
|
11
|
+
interface QrDetection {
|
|
12
|
+
corners: [Point2, Point2, Point2, Point2];
|
|
13
|
+
text: string;
|
|
14
|
+
}
|
|
15
|
+
/** Front-agnostic detect+decode contract. */
|
|
16
|
+
interface QrFrontEnd {
|
|
17
|
+
readonly kind: 'barcode-detector';
|
|
18
|
+
/** Detect the first QR in the frame, or `null` if none. */
|
|
19
|
+
detect(image: RgbaImage): Promise<QrDetection | null>;
|
|
20
|
+
/** Free any native resources. */
|
|
21
|
+
dispose?(): void;
|
|
22
|
+
}
|
|
23
|
+
/** The native-`BarcodeDetector` result shape we consume. */
|
|
24
|
+
interface DetectedBarcodeLike {
|
|
25
|
+
rawValue: string;
|
|
26
|
+
cornerPoints: ReadonlyArray<{
|
|
27
|
+
x: number;
|
|
28
|
+
y: number;
|
|
29
|
+
}>;
|
|
30
|
+
format?: string;
|
|
31
|
+
}
|
|
32
|
+
/** The slice of `BarcodeDetector` we depend on. */
|
|
33
|
+
interface BarcodeDetectorLike {
|
|
34
|
+
detect(image: unknown): Promise<DetectedBarcodeLike[]>;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Convert our `RgbaImage` into something `BarcodeDetector.detect` accepts
|
|
38
|
+
* (`ImageData` is a valid `ImageBitmapSource`). Injected so tests need no DOM.
|
|
39
|
+
*/
|
|
40
|
+
type ToImageBitmapSource = (image: RgbaImage) => unknown;
|
|
41
|
+
declare class BarcodeDetectorFrontEnd implements QrFrontEnd {
|
|
42
|
+
readonly kind = "barcode-detector";
|
|
43
|
+
private readonly detector;
|
|
44
|
+
private readonly toSource;
|
|
45
|
+
constructor(detector: BarcodeDetectorLike, toSource?: ToImageBitmapSource);
|
|
46
|
+
detect(image: RgbaImage): Promise<QrDetection | null>;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Build a {@link BarcodeDetectorFrontEnd} if the runtime exposes a
|
|
50
|
+
* `BarcodeDetector` constructor; otherwise `null` (→ the caller must handle the
|
|
51
|
+
* unsupported-browser case; there is no OpenCV fallback). `ctor` is injectable
|
|
52
|
+
* for tests.
|
|
53
|
+
*/
|
|
54
|
+
declare function createBarcodeDetectorFrontEnd(ctor?: new (opts: {
|
|
55
|
+
formats: string[];
|
|
56
|
+
}) => BarcodeDetectorLike): BarcodeDetectorFrontEnd | null;
|
|
57
|
+
//#endregion
|
|
58
|
+
export { QrFrontEnd as a, createBarcodeDetectorFrontEnd as c, QrDetection as i, BarcodeDetectorLike as n, RgbaImage as o, DetectedBarcodeLike as r, ToImageBitmapSource as s, BarcodeDetectorFrontEnd as t };
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { Matrix4, Quaternion, Vector3 } from "gps-plus-slam-js";
|
|
2
|
+
|
|
3
|
+
//#region ../src/ar/qr-pose.d.ts
|
|
4
|
+
/** A 2D point in image/pixel space (top-left origin, x right, y down). */
|
|
5
|
+
interface Point2 {
|
|
6
|
+
x: number;
|
|
7
|
+
y: number;
|
|
8
|
+
}
|
|
9
|
+
/** Pinhole intrinsics in pixels for the exact buffer fed to the detector. */
|
|
10
|
+
interface CameraIntrinsics {
|
|
11
|
+
fx: number;
|
|
12
|
+
fy: number;
|
|
13
|
+
cx: number;
|
|
14
|
+
cy: number;
|
|
15
|
+
}
|
|
16
|
+
/** A rigid pose: rotation quaternion [x,y,z,w] + position [x,y,z]. */
|
|
17
|
+
interface Pose {
|
|
18
|
+
position: Vector3;
|
|
19
|
+
rotation: Quaternion;
|
|
20
|
+
}
|
|
21
|
+
/** Raw OpenCV `solvePnP` output: Rodrigues rotation vector + translation. */
|
|
22
|
+
interface OpenCvPnpResult {
|
|
23
|
+
/** Rodrigues rotation vector (axis · angle), OpenCV camera frame. */
|
|
24
|
+
rvec: Vector3;
|
|
25
|
+
/** Translation (object origin in OpenCV camera frame), meters. */
|
|
26
|
+
tvec: Vector3;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Injected planar-PnP solver. The production implementation is the pure-JS
|
|
30
|
+
* {@link PlanarPnpSquare} (planar-pnp.ts, IPPE); tests inject a stub. Returns `null`
|
|
31
|
+
* when the solve fails (degenerate input, no convergence).
|
|
32
|
+
*/
|
|
33
|
+
interface SolvePnpSquare {
|
|
34
|
+
solve(objectPoints: readonly Vector3[], imagePoints: readonly Point2[], intrinsics: CameraIntrinsics): OpenCvPnpResult | null;
|
|
35
|
+
}
|
|
36
|
+
/** Inputs for {@link solveQrPose}. */
|
|
37
|
+
interface SolveQrPoseInput {
|
|
38
|
+
/** The 4 detected corners, ordered TL, TR, BR, BL (symbol reading order). */
|
|
39
|
+
imagePoints: readonly Point2[];
|
|
40
|
+
/** Printed physical side length of the QR, meters (from the level file). */
|
|
41
|
+
sizeM: number;
|
|
42
|
+
/** Intrinsics for the exact pixel buffer the corners came from. */
|
|
43
|
+
intrinsics: CameraIntrinsics;
|
|
44
|
+
/** Capturing camera pose in raw-WebXR/odom space (`DepthSample.cameraPos/Rot`). */
|
|
45
|
+
cameraPose: Pose;
|
|
46
|
+
/** The PnP backend. */
|
|
47
|
+
solver: SolvePnpSquare;
|
|
48
|
+
/**
|
|
49
|
+
* Reject the detection when the RMS reprojection error exceeds this many
|
|
50
|
+
* pixels. Default 4 px (research2's runtime-stability heuristic).
|
|
51
|
+
*/
|
|
52
|
+
maxReprojectionErrorPx?: number;
|
|
53
|
+
}
|
|
54
|
+
/** Result of {@link solveQrPose}. */
|
|
55
|
+
interface QrPoseSolution {
|
|
56
|
+
/** QR pose in raw-WebXR/odom space, ready to hang off `arWorldGroup`. */
|
|
57
|
+
qrPoseWorld: Pose;
|
|
58
|
+
/** QR pose relative to the WebXR camera (pre-composition). */
|
|
59
|
+
qrPoseInCamera: Pose;
|
|
60
|
+
/** RMS reprojection error in pixels (lower = better intrinsics/pose fit). */
|
|
61
|
+
reprojectionErrorPx: number;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Build the 4 object points of a centered planar square of side `sizeM`, in
|
|
65
|
+
* the QR-local frame (+x right, +y up, z = 0), ordered TL, TR, BR, BL to match
|
|
66
|
+
* the detector's symbol reading order.
|
|
67
|
+
*/
|
|
68
|
+
declare function buildObjectPoints(sizeM: number): [Vector3, Vector3, Vector3, Vector3];
|
|
69
|
+
/**
|
|
70
|
+
* Derive pinhole intrinsics (pixels) from a column-major GL projection matrix
|
|
71
|
+
* for a detector frame of `width`×`height`. Valid for an axis-aligned frustum
|
|
72
|
+
* (no skew); principal point uses the TOP-LEFT-origin pixel convention.
|
|
73
|
+
*
|
|
74
|
+
* Derivation (see qr-pose.ts.md): for clip = P·v_view and pixel
|
|
75
|
+
* px = (ndc.x+1)·W/2, py = (1−ndc.y)·H/2 with ndc = clip.xy / (−Z):
|
|
76
|
+
* fx = P[0]·W/2, fy = P[5]·H/2, cx = (1−P[8])·W/2, cy = (1+P[9])·H/2.
|
|
77
|
+
*/
|
|
78
|
+
declare function intrinsicsFromProjection(projection: Matrix4, width: number, height: number): CameraIntrinsics;
|
|
79
|
+
/**
|
|
80
|
+
* Project a point given in the WebXR camera frame (+x right, +y up, −z forward)
|
|
81
|
+
* to a pixel (top-left origin). Returns `null` for points not strictly in front
|
|
82
|
+
* of the camera (z ≥ 0) or non-finite results.
|
|
83
|
+
*/
|
|
84
|
+
declare function projectViewPoint(pointInCamera: Vector3, intrinsics: CameraIntrinsics): Point2 | null;
|
|
85
|
+
/**
|
|
86
|
+
* Convert a solved OpenCV pose (object→OpenCV-camera) into the QR pose in the
|
|
87
|
+
* WebXR camera frame. Left-multiply by Rx(π): rotation = Rx(π)·R, position =
|
|
88
|
+
* Rx(π)·t = (t0, −t1, −t2).
|
|
89
|
+
*/
|
|
90
|
+
declare function qrInCameraFromOpenCv(pnp: OpenCvPnpResult): Pose;
|
|
91
|
+
/** Compose two rigid poses: result = parent ∘ child (child expressed in parent). */
|
|
92
|
+
declare function composePose(parent: Pose, child: Pose): Pose;
|
|
93
|
+
/** Invert a rigid pose: if pose maps A→B, the result maps B→A. */
|
|
94
|
+
declare function invertPose(pose: Pose): Pose;
|
|
95
|
+
/**
|
|
96
|
+
* Signed area of the quad TL→TR→BR→BL via the shoelace formula, in image
|
|
97
|
+
* pixels (top-left origin, y down). A front-facing QR imaged in symbol order is
|
|
98
|
+
* clockwise on screen → POSITIVE signed area in this y-down convention. A
|
|
99
|
+
* negative value means the winding is reversed (mirrored / wrong corner order)
|
|
100
|
+
* — the #1 pose bug the briefs warn about.
|
|
101
|
+
*/
|
|
102
|
+
declare function signedQuadArea(corners: readonly Point2[]): number;
|
|
103
|
+
/** Result of {@link validateQuad}. */
|
|
104
|
+
interface QuadValidation {
|
|
105
|
+
ok: boolean;
|
|
106
|
+
/** True when the quad winding is reversed (mirrored corner order). */
|
|
107
|
+
mirrored: boolean;
|
|
108
|
+
/** True when the quad is too small or too thin to trust. */
|
|
109
|
+
degenerate: boolean;
|
|
110
|
+
/** |signed area| in px² — useful for a "too small" threshold. */
|
|
111
|
+
areaPx2: number;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Validate the 4 detected corners (assumed ordered TL, TR, BR, BL). Rejects a
|
|
115
|
+
* mirrored winding and degenerate (tiny / collinear) quads. This does NOT
|
|
116
|
+
* re-sort by image position — that would destroy the reading orientation the
|
|
117
|
+
* detector encodes; it only guards the known failure modes.
|
|
118
|
+
*/
|
|
119
|
+
declare function validateQuad(corners: readonly Point2[], options?: {
|
|
120
|
+
minAreaPx2?: number;
|
|
121
|
+
}): QuadValidation;
|
|
122
|
+
/**
|
|
123
|
+
* RMS reprojection error (pixels): project the object points through
|
|
124
|
+
* `qrPoseInCamera` (WebXR frame) and the pinhole model, compare to the detected
|
|
125
|
+
* image points. `Infinity` if any point falls behind the camera.
|
|
126
|
+
*/
|
|
127
|
+
declare function reprojectionErrorPx(objectPoints: readonly Vector3[], imagePoints: readonly Point2[], qrPoseInCamera: Pose, intrinsics: CameraIntrinsics): number;
|
|
128
|
+
/** Apply a rigid pose to a point: result = rotation·p + position. */
|
|
129
|
+
declare function transformPoint(point: Vector3, pose: Pose): Vector3;
|
|
130
|
+
/**
|
|
131
|
+
* Full pipeline: detected corners → QR pose in raw-WebXR/odom space. Validates
|
|
132
|
+
* the quad, solves PnP (injected), converts OpenCV→WebXR, composes with the
|
|
133
|
+
* camera pose, and gates on reprojection error. Returns `null` when the
|
|
134
|
+
* detection is rejected at any stage.
|
|
135
|
+
*/
|
|
136
|
+
declare function solveQrPose(input: SolveQrPoseInput): QrPoseSolution | null;
|
|
137
|
+
//#endregion
|
|
138
|
+
export { solveQrPose as _, QrPoseSolution as a, SolveQrPoseInput as c, intrinsicsFromProjection as d, invertPose as f, signedQuadArea as g, reprojectionErrorPx as h, Pose as i, buildObjectPoints as l, qrInCameraFromOpenCv as m, OpenCvPnpResult as n, QuadValidation as o, projectViewPoint as p, Point2 as r, SolvePnpSquare as s, CameraIntrinsics as t, composePose as u, transformPoint as v, validateQuad as y };
|