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
|
@@ -29,6 +29,44 @@ interface KnownGeoAnchor {
|
|
|
29
29
|
* Compute the H3 resolution-11 index for a GPS position.
|
|
30
30
|
*/
|
|
31
31
|
declare function gpsToH3(lat: number, lng: number): string;
|
|
32
|
+
/** Minimal GPS coordinate shape (Leaflet convention: `lng`, not `lon`). */
|
|
33
|
+
interface LatLngLike {
|
|
34
|
+
readonly lat: number;
|
|
35
|
+
readonly lng: number;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Reduce a recorded GPS path to the deduplicated set of res-11 H3 cells it
|
|
39
|
+
* crossed — the per-tour "coverage index" stored in `SessionMetadata.h3Cells`.
|
|
40
|
+
*
|
|
41
|
+
* Cells are returned in first-seen (chronological) order and deduplicated, so a
|
|
42
|
+
* tour that dwells in one ~25 m cell contributes a single cell. This keeps the
|
|
43
|
+
* stored index small (see the metadata-size risk in the map-browser feedback
|
|
44
|
+
* doc, §7). Non-finite coordinates are skipped defensively — bad sensor data
|
|
45
|
+
* must not poison the index or throw inside `latLngToCell`.
|
|
46
|
+
*/
|
|
47
|
+
declare function gpsPathToCoverageCells(path: readonly LatLngLike[]): string[];
|
|
48
|
+
/**
|
|
49
|
+
* Coarsen res-11 coverage cells to a target resolution for map zoom-clustering.
|
|
50
|
+
*
|
|
51
|
+
* Uses h3-js `cellToParent` — the ONLY correct way to coarsen an H3 cell. NEVER
|
|
52
|
+
* truncate the hex-string id: resolution is encoded in the high bits with
|
|
53
|
+
* trailing `f` padding, so slicing yields INVALID cells, not parents (verified;
|
|
54
|
+
* see the D1 gotcha in
|
|
55
|
+
* docs/2026-06-14-map-centric-recording-browser-and-h3-index-user-feedback.md).
|
|
56
|
+
*
|
|
57
|
+
* `targetRes` is clamped to `[0, H3_RESOLUTION]` because `cellToParent` throws
|
|
58
|
+
* when asked for a FINER resolution than the cell, and res 11 is the finest
|
|
59
|
+
* data stored — so the highest map zooms render the stored cells unclustered
|
|
60
|
+
* (`cellToParent(cell, 11) === cell`). A non-finite `targetRes` (e.g. a bad
|
|
61
|
+
* zoom→res mapping) degrades to unclustered output rather than throwing.
|
|
62
|
+
* Invalid input cells (corrupt/legacy metadata) are skipped defensively, as are
|
|
63
|
+
* valid cells coarser than `res` (which have no parent at the finer target res
|
|
64
|
+
* and would otherwise throw "incompatible resolutions").
|
|
65
|
+
*
|
|
66
|
+
* Output is deduplicated in first-seen order: sibling cells under one parent
|
|
67
|
+
* collapse to a single tile.
|
|
68
|
+
*/
|
|
69
|
+
declare function clusterCellsByZoom(cells: readonly string[], targetRes: number): string[];
|
|
32
70
|
/**
|
|
33
71
|
* Approximate distance in metres using equirectangular projection.
|
|
34
72
|
* Accurate to < 0.1% for distances under 1 km at mid-latitudes,
|
|
@@ -60,4 +98,4 @@ declare function h3CellsMatch(h3a: string, h3b: string): boolean;
|
|
|
60
98
|
*/
|
|
61
99
|
declare function isH3Index(id: string): boolean;
|
|
62
100
|
//#endregion
|
|
63
|
-
export {
|
|
101
|
+
export { findNearbyGeoAnchor as a, h3CellsMatch as c, clusterCellsByZoom as i, isH3Index as l, KnownGeoAnchor as n, gpsPathToCoverageCells as o, approxDistanceMetres as r, gpsToH3 as s, H3_RESOLUTION as t };
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
import { createLogger } from "./utils/logger.js";
|
|
2
|
+
import { t as geodesicAngleRad } from "./geodesic-angle-BTldfFgs.js";
|
|
3
|
+
import { n as MotionWindow, r as decideCapture, t as DEFAULT_MOTION_FILTER } from "./capture-motion-gate-4EscNZCw.js";
|
|
4
|
+
import { DEFAULT_QUALITY_FILTER } from "./ar/image-quality.js";
|
|
5
|
+
import { quat } from "gl-matrix";
|
|
6
|
+
//#region ../src/ar/pose-motion.ts
|
|
7
|
+
/**
|
|
8
|
+
* Pose-motion helpers — stateless angular/linear velocity from two AR poses.
|
|
9
|
+
*
|
|
10
|
+
* The capture motion gate (blurry-frame skipping, see
|
|
11
|
+
* `GpsPlusSlamJs_Docs/docs/2026-06-23-blurry-frame-motion-gating-plan.md` §4.1)
|
|
12
|
+
* rejects frames whose device motion is too fast to yield a sharp image. Motion
|
|
13
|
+
* is derived from consecutive WebXR poses — cheaper than any image-content
|
|
14
|
+
* metric because it needs no GPU readback.
|
|
15
|
+
*
|
|
16
|
+
* This module holds ONLY the pure, stateless math (angle ÷ dt, distance ÷ dt).
|
|
17
|
+
* The stateful policy (sliding window, glitch rejection, the capture decision)
|
|
18
|
+
* lives in `ImageCaptureManager` and `capture-motion-gate.ts` — keeping the
|
|
19
|
+
* reusable atom separate from the capture-specific policy (plan §4.1 "Scope").
|
|
20
|
+
*
|
|
21
|
+
* Angular velocity reuses the shared `geodesicAngleRad` kernel rather than a
|
|
22
|
+
* fresh `acos` (CLAUDE.md: search first, prefer refactor over re-implementation).
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* Convert a WebXR object-form quaternion to a normalized gl-matrix quaternion.
|
|
26
|
+
* Normalizing defends against slightly non-unit inputs (the gate must not throw
|
|
27
|
+
* on imperfect tracking data).
|
|
28
|
+
*/
|
|
29
|
+
function toGlQuat(q) {
|
|
30
|
+
const g = quat.fromValues(q.x, q.y, q.z, q.w);
|
|
31
|
+
return quat.normalize(g, g);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Angular velocity in radians/second between two orientations.
|
|
35
|
+
*
|
|
36
|
+
* @param qPrev - orientation at the previous frame.
|
|
37
|
+
* @param qCur - orientation at the current frame.
|
|
38
|
+
* @param dtSeconds - elapsed time in seconds (must be > 0).
|
|
39
|
+
* @returns geodesic angle between the orientations divided by `dtSeconds`.
|
|
40
|
+
* Returns `0` when `dtSeconds <= 0` (degenerate/duplicate frame timestamp) so
|
|
41
|
+
* a bad delta can never produce `Infinity`/`NaN` and spuriously flip the
|
|
42
|
+
* motion gate. Double-cover safe (`q` ≡ `−q`) via the shared kernel.
|
|
43
|
+
*/
|
|
44
|
+
function angularVelocity(qPrev, qCur, dtSeconds) {
|
|
45
|
+
if (!(dtSeconds > 0)) return 0;
|
|
46
|
+
return geodesicAngleRad(toGlQuat(qPrev), toGlQuat(qCur)) / dtSeconds;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Linear velocity in metres/second between two positions.
|
|
50
|
+
*
|
|
51
|
+
* @param pPrev - position at the previous frame.
|
|
52
|
+
* @param pCur - position at the current frame.
|
|
53
|
+
* @param dtSeconds - elapsed time in seconds (must be > 0).
|
|
54
|
+
* @returns straight-line distance divided by `dtSeconds`. Returns `0` when
|
|
55
|
+
* `dtSeconds <= 0` (same guard rationale as {@link angularVelocity}).
|
|
56
|
+
*/
|
|
57
|
+
function linearVelocity(pPrev, pCur, dtSeconds) {
|
|
58
|
+
if (!(dtSeconds > 0)) return 0;
|
|
59
|
+
const dx = pCur.x - pPrev.x;
|
|
60
|
+
const dy = pCur.y - pPrev.y;
|
|
61
|
+
const dz = pCur.z - pPrev.z;
|
|
62
|
+
return Math.sqrt(dx * dx + dy * dy + dz * dz) / dtSeconds;
|
|
63
|
+
}
|
|
64
|
+
//#endregion
|
|
65
|
+
//#region ../src/ar/image-capture.ts
|
|
66
|
+
/**
|
|
67
|
+
* Image Capture Module
|
|
68
|
+
*
|
|
69
|
+
* Captures periodic JPEG screenshots from the WebGL canvas during AR recording.
|
|
70
|
+
* Uses async toBlob() for better performance (non-blocking).
|
|
71
|
+
*
|
|
72
|
+
* Includes validation to detect suspiciously small images (likely black/empty)
|
|
73
|
+
* which can occur on mobile devices when the WebGL context hasn't composited yet.
|
|
74
|
+
*/
|
|
75
|
+
const log = createLogger("ImageCapture");
|
|
76
|
+
/**
|
|
77
|
+
* Minimum expected blob size in bytes for a valid JPEG image.
|
|
78
|
+
* A black/empty 1920x1080 JPEG still compresses to ~2-3KB due to headers.
|
|
79
|
+
* A real camera frame at 0.7 quality should be 50KB+.
|
|
80
|
+
* We use a conservative threshold to catch obviously broken captures.
|
|
81
|
+
*/
|
|
82
|
+
const MIN_VALID_IMAGE_BYTES = 5e3;
|
|
83
|
+
/**
|
|
84
|
+
* Default capture configuration
|
|
85
|
+
*/
|
|
86
|
+
const DEFAULT_CAPTURE_CONFIG = {
|
|
87
|
+
intervalMs: 2e3,
|
|
88
|
+
quality: .7,
|
|
89
|
+
captureTimeoutMs: 5e3,
|
|
90
|
+
resolutionDivisor: 1,
|
|
91
|
+
motionFilter: DEFAULT_MOTION_FILTER,
|
|
92
|
+
qualityFilter: DEFAULT_QUALITY_FILTER
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* Manages periodic image capture from a canvas.
|
|
96
|
+
*
|
|
97
|
+
* Usage:
|
|
98
|
+
* 1. Create with canvas and callbacks
|
|
99
|
+
* 2. Call start() when recording begins
|
|
100
|
+
* 3. Call onFrame(time) each XR frame
|
|
101
|
+
* 4. Call stop() when recording ends
|
|
102
|
+
*/
|
|
103
|
+
var ImageCaptureManager = class {
|
|
104
|
+
canvas;
|
|
105
|
+
callbacks;
|
|
106
|
+
config;
|
|
107
|
+
capturing = false;
|
|
108
|
+
lastCaptureTime = 0;
|
|
109
|
+
frameCount = 0;
|
|
110
|
+
captureInProgress = false;
|
|
111
|
+
captureTimeoutId = null;
|
|
112
|
+
/** True while an `analyzeFrame` verdict is in flight; blocks a second capture
|
|
113
|
+
* for the same interval without throttling normal cadence (captureInProgress
|
|
114
|
+
* is released after encode, NOT held across the round-trip). */
|
|
115
|
+
awaitingVerdict = false;
|
|
116
|
+
/** The current interval's capture was rejected by the quality gate; bypass the
|
|
117
|
+
* interval guard so the next acceptable frame is grabbed immediately. */
|
|
118
|
+
retryPending = false;
|
|
119
|
+
/** Frame time (ms) of the FIRST quality attempt for the current interval; the
|
|
120
|
+
* quality `maxWaitMs` fallback measures from here, independent of the motion
|
|
121
|
+
* gate's own due clock. `null` between intervals. */
|
|
122
|
+
qualityDeadlineBase = null;
|
|
123
|
+
/** Safety timeout for a hung `analyzeFrame` (fail-open). */
|
|
124
|
+
verdictTimeoutId = null;
|
|
125
|
+
/** Sliding window of recent per-frame velocities for the motion gate. */
|
|
126
|
+
motionWindow = new MotionWindow();
|
|
127
|
+
/** Previous frame's pose, for the per-frame velocity delta. */
|
|
128
|
+
prevPose = null;
|
|
129
|
+
/** Previous frame's timestamp (ms), for the per-frame velocity delta. */
|
|
130
|
+
prevTime = 0;
|
|
131
|
+
/** Whether THIS frame's velocity sample was rejected as a tracking glitch. */
|
|
132
|
+
lastSampleWasGlitch = false;
|
|
133
|
+
/** When the current capture first became due (ms); null while not due. The
|
|
134
|
+
* `maxWaitMs` fallback measures from here, not from `lastCaptureTime`. */
|
|
135
|
+
dueTime = null;
|
|
136
|
+
constructor(canvas, callbacks, config = DEFAULT_CAPTURE_CONFIG) {
|
|
137
|
+
this.canvas = canvas;
|
|
138
|
+
this.callbacks = callbacks;
|
|
139
|
+
this.config = config;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Start capturing images.
|
|
143
|
+
* Resets frame counter and last capture time.
|
|
144
|
+
*/
|
|
145
|
+
start() {
|
|
146
|
+
this.capturing = true;
|
|
147
|
+
this.lastCaptureTime = 0;
|
|
148
|
+
this.frameCount = 0;
|
|
149
|
+
this.motionWindow.reset();
|
|
150
|
+
this.prevPose = null;
|
|
151
|
+
this.prevTime = 0;
|
|
152
|
+
this.lastSampleWasGlitch = false;
|
|
153
|
+
this.dueTime = null;
|
|
154
|
+
this.clearVerdictTimeout();
|
|
155
|
+
this.awaitingVerdict = false;
|
|
156
|
+
this.retryPending = false;
|
|
157
|
+
this.qualityDeadlineBase = null;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Stop capturing images.
|
|
161
|
+
* Clears any pending safety timeout and resets in-flight capture state.
|
|
162
|
+
*/
|
|
163
|
+
stop() {
|
|
164
|
+
this.capturing = false;
|
|
165
|
+
this.clearCaptureTimeout();
|
|
166
|
+
this.captureInProgress = false;
|
|
167
|
+
this.clearVerdictTimeout();
|
|
168
|
+
this.awaitingVerdict = false;
|
|
169
|
+
this.retryPending = false;
|
|
170
|
+
this.qualityDeadlineBase = null;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Check if capture manager is active.
|
|
174
|
+
*/
|
|
175
|
+
isCapturing() {
|
|
176
|
+
return this.capturing;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Get the current frame count.
|
|
180
|
+
*/
|
|
181
|
+
getFrameCount() {
|
|
182
|
+
return this.frameCount;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Called each XR frame to check if a capture is needed.
|
|
186
|
+
*
|
|
187
|
+
* @param time - Frame timestamp in milliseconds (from requestAnimationFrame)
|
|
188
|
+
*/
|
|
189
|
+
onFrame(time) {
|
|
190
|
+
if (!this.capturing) return;
|
|
191
|
+
const pose = this.callbacks.getCurrentPose();
|
|
192
|
+
this.updateMotionWindow(time, pose);
|
|
193
|
+
if (this.captureInProgress || this.awaitingVerdict) return;
|
|
194
|
+
const elapsed = time - this.lastCaptureTime;
|
|
195
|
+
if (!this.retryPending && this.lastCaptureTime > 0 && elapsed < this.config.intervalMs) {
|
|
196
|
+
this.dueTime = null;
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (!pose) return;
|
|
200
|
+
if (this.dueTime === null) this.dueTime = time;
|
|
201
|
+
if (!this.shouldCaptureNow(time)) return;
|
|
202
|
+
this.captureInProgress = true;
|
|
203
|
+
this.lastCaptureTime = time;
|
|
204
|
+
this.dueTime = null;
|
|
205
|
+
const timestamp = performance.timeOrigin + time;
|
|
206
|
+
const screenRotation = this.callbacks.getScreenRotation();
|
|
207
|
+
this.captureTimeoutId = setTimeout(() => {
|
|
208
|
+
if (this.captureInProgress) {
|
|
209
|
+
log.error(`Capture timeout after ${this.config.captureTimeoutMs}ms — force-resetting captureInProgress`);
|
|
210
|
+
this.captureInProgress = false;
|
|
211
|
+
this.captureTimeoutId = null;
|
|
212
|
+
}
|
|
213
|
+
}, this.config.captureTimeoutMs);
|
|
214
|
+
if (this.callbacks.captureFrame) this.callbacks.captureFrame(this.config.quality).then((frame) => {
|
|
215
|
+
this.handleCapturedBlob(frame?.blob ?? null, frame?.width, frame?.height, timestamp, time, pose, screenRotation);
|
|
216
|
+
}).catch(() => {
|
|
217
|
+
this.clearCaptureTimeout();
|
|
218
|
+
this.captureInProgress = false;
|
|
219
|
+
this.callbacks.onCaptureFailed?.();
|
|
220
|
+
});
|
|
221
|
+
else this.canvas.toBlob((blob) => {
|
|
222
|
+
this.handleCapturedBlob(blob, this.canvas.width, this.canvas.height, timestamp, time, pose, screenRotation);
|
|
223
|
+
}, "image/jpeg", this.config.quality);
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Sample per-frame device motion into the sliding window. Called every frame
|
|
227
|
+
* (even during an in-flight capture or before the interval elapses) so the
|
|
228
|
+
* window reflects truly instantaneous motion the moment a capture becomes
|
|
229
|
+
* due. A frame with no pose, or a non-positive dt (duplicate timestamp),
|
|
230
|
+
* records no sample and is treated as "not a glitch".
|
|
231
|
+
*/
|
|
232
|
+
updateMotionWindow(time, pose) {
|
|
233
|
+
if (!pose) {
|
|
234
|
+
this.lastSampleWasGlitch = false;
|
|
235
|
+
this.motionWindow.reset();
|
|
236
|
+
this.prevPose = null;
|
|
237
|
+
this.prevTime = 0;
|
|
238
|
+
this.retryPending = false;
|
|
239
|
+
this.qualityDeadlineBase = null;
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (this.prevPose && this.prevTime > 0) {
|
|
243
|
+
const dt = (time - this.prevTime) / 1e3;
|
|
244
|
+
if (dt > 0) {
|
|
245
|
+
const angVel = angularVelocity(this.prevPose.orientation, pose.orientation, dt);
|
|
246
|
+
const linVel = linearVelocity(this.prevPose.position, pose.position, dt);
|
|
247
|
+
this.lastSampleWasGlitch = !this.motionWindow.push(angVel, linVel);
|
|
248
|
+
} else this.lastSampleWasGlitch = false;
|
|
249
|
+
} else this.lastSampleWasGlitch = false;
|
|
250
|
+
this.prevPose = pose;
|
|
251
|
+
this.prevTime = time;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Apply the motion gate to a due capture. Returns `true` to capture now,
|
|
255
|
+
* `false` to defer to a later frame.
|
|
256
|
+
*
|
|
257
|
+
* - Filter disabled → always `true` (legacy behavior).
|
|
258
|
+
* - `maxWaitMs` elapsed → always `true` (never-calm safety fallback; an
|
|
259
|
+
* interval is never silently lost).
|
|
260
|
+
* - Current frame's sample was a tracking glitch → `false` (don't grab a
|
|
261
|
+
* relocalization-teleport frame; the fallback still guarantees progress).
|
|
262
|
+
* - No measurable motion yet (first capture / all-glitch) → `true` (the very
|
|
263
|
+
* first capture is never blocked; only measurable motion defers).
|
|
264
|
+
* - Otherwise → defer to {@link decideCapture} over the windowed maxima.
|
|
265
|
+
*/
|
|
266
|
+
shouldCaptureNow(time) {
|
|
267
|
+
const mf = this.config.motionFilter;
|
|
268
|
+
if (!mf || !mf.enabled) return true;
|
|
269
|
+
const msSinceDue = this.dueTime === null ? 0 : time - this.dueTime;
|
|
270
|
+
if (msSinceDue >= mf.maxWaitMs) return true;
|
|
271
|
+
if (this.lastSampleWasGlitch) return false;
|
|
272
|
+
if (!this.motionWindow.hasSamples()) return true;
|
|
273
|
+
return decideCapture({
|
|
274
|
+
windowMaxAngular: this.motionWindow.maxAngular(),
|
|
275
|
+
windowMaxLinear: this.motionWindow.maxLinear(),
|
|
276
|
+
maxAngularVelocity: mf.maxAngularVelocity,
|
|
277
|
+
maxLinearVelocity: mf.maxLinearVelocity,
|
|
278
|
+
msSinceDue,
|
|
279
|
+
maxWaitMs: mf.maxWaitMs
|
|
280
|
+
}) === "capture";
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Clear the safety timeout for captureInProgress.
|
|
284
|
+
* Called when capture completes (success or failure) before the timeout fires.
|
|
285
|
+
*/
|
|
286
|
+
clearCaptureTimeout() {
|
|
287
|
+
if (this.captureTimeoutId !== null) {
|
|
288
|
+
clearTimeout(this.captureTimeoutId);
|
|
289
|
+
this.captureTimeoutId = null;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
/** Clear the safety timeout for a hung `analyzeFrame` verdict. */
|
|
293
|
+
clearVerdictTimeout() {
|
|
294
|
+
if (this.verdictTimeoutId !== null) {
|
|
295
|
+
clearTimeout(this.verdictTimeoutId);
|
|
296
|
+
this.verdictTimeoutId = null;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Common handler for captured blobs (from either canvas.toBlob or captureFrame).
|
|
301
|
+
* Releases `captureInProgress` after the synchronous encode, then either saves
|
|
302
|
+
* immediately (legacy / gate-off) or runs the off-thread image-quality gate.
|
|
303
|
+
*
|
|
304
|
+
* @param time - the XR frame time the capture was dispatched at; the quality
|
|
305
|
+
* retry-deadline clock and `lastCaptureTime` are measured from it.
|
|
306
|
+
*/
|
|
307
|
+
handleCapturedBlob(blob, width, height, timestamp, time, pose, screenRotation) {
|
|
308
|
+
this.clearCaptureTimeout();
|
|
309
|
+
this.captureInProgress = false;
|
|
310
|
+
if (!blob) {
|
|
311
|
+
this.callbacks.onCaptureFailed?.();
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
const qf = this.config.qualityFilter;
|
|
315
|
+
const analyze = this.callbacks.analyzeFrame;
|
|
316
|
+
if (!analyze || !qf || !qf.enabled) {
|
|
317
|
+
this.saveCapture(blob, width, height, timestamp, pose, screenRotation);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
if (this.qualityDeadlineBase === null) this.qualityDeadlineBase = time;
|
|
321
|
+
if (time - this.qualityDeadlineBase >= qf.maxWaitMs) {
|
|
322
|
+
this.saveCapture(blob, width, height, timestamp, pose, screenRotation);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
this.awaitingVerdict = true;
|
|
326
|
+
let settled = false;
|
|
327
|
+
const finish = (accept, viaTimeout) => {
|
|
328
|
+
if (settled || !this.capturing) return;
|
|
329
|
+
settled = true;
|
|
330
|
+
this.clearVerdictTimeout();
|
|
331
|
+
this.awaitingVerdict = false;
|
|
332
|
+
if (viaTimeout) log.error("analyzeFrame verdict timed out — saving frame fail-open to keep the pipeline live");
|
|
333
|
+
if (accept) this.saveCapture(blob, width, height, timestamp, pose, screenRotation);
|
|
334
|
+
else this.retryPending = true;
|
|
335
|
+
};
|
|
336
|
+
this.verdictTimeoutId = setTimeout(() => finish(true, true), this.config.captureTimeoutMs);
|
|
337
|
+
analyze({
|
|
338
|
+
blob,
|
|
339
|
+
width: width ?? 0,
|
|
340
|
+
height: height ?? 0
|
|
341
|
+
}).then((verdict) => finish(verdict.accept, false)).catch((err) => {
|
|
342
|
+
log.error("analyzeFrame failed — saving frame without quality gate", err);
|
|
343
|
+
finish(true, false);
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Persist an accepted frame: allocate its 1-based index, run the suspicious-
|
|
348
|
+
* size belt-and-suspenders check, reset the per-interval gate clocks, and fire
|
|
349
|
+
* `onCaptured`. Called from the legacy path, the quality fallbacks, and an
|
|
350
|
+
* accepting verdict — the single place a frame becomes durable.
|
|
351
|
+
*/
|
|
352
|
+
saveCapture(blob, width, height, timestamp, pose, screenRotation) {
|
|
353
|
+
if (!this.capturing) return;
|
|
354
|
+
this.retryPending = false;
|
|
355
|
+
this.qualityDeadlineBase = null;
|
|
356
|
+
const frameIndex = ++this.frameCount;
|
|
357
|
+
if (blob.size < 5e3) {
|
|
358
|
+
log.error(`Suspicious image at frame ${frameIndex}: blob size ${blob.size} bytes is below minimum ${MIN_VALID_IMAGE_BYTES} bytes. Image may be black/empty.`);
|
|
359
|
+
this.callbacks.onSuspiciousImage?.(blob.size, frameIndex);
|
|
360
|
+
}
|
|
361
|
+
this.callbacks.onCaptured({
|
|
362
|
+
blob,
|
|
363
|
+
timestamp,
|
|
364
|
+
frameIndex,
|
|
365
|
+
position: pose.position,
|
|
366
|
+
rotation: pose.orientation,
|
|
367
|
+
screenRotation,
|
|
368
|
+
...width && width > 0 ? { width } : {},
|
|
369
|
+
...height && height > 0 ? { height } : {}
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
//#endregion
|
|
374
|
+
export { ImageCaptureManager as n, MIN_VALID_IMAGE_BYTES as r, DEFAULT_CAPTURE_CONFIG as t };
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { o as WebXRQuaternion, s as WebXRVec3, t as ARPose } from "./ar-types-
|
|
1
|
+
import { o as WebXRQuaternion, s as WebXRVec3, t as ARPose } from "./ar-types-BFX2r1wk.js";
|
|
2
|
+
import { t as MotionFilterConfig } from "./capture-motion-gate-0Yl2NpQG.js";
|
|
3
|
+
import { a as QualityFilterConfig } from "./image-quality-smCBoFj1.js";
|
|
2
4
|
|
|
3
5
|
//#region ../src/ar/image-capture.d.ts
|
|
4
6
|
/**
|
|
@@ -26,6 +28,17 @@ interface ImageCaptureConfig {
|
|
|
26
28
|
* into this config so the whole user options section can flow through the
|
|
27
29
|
* capture seam as one object (see the field-drop audit, F3). */
|
|
28
30
|
resolutionDivisor: number;
|
|
31
|
+
/** Motion gate: skip motion-blurred frames by deferring a due capture until
|
|
32
|
+
* device motion settles (or `maxWaitMs` elapses). Mirrored in the persisted
|
|
33
|
+
* `ImageCaptureOptions.motionFilter` and flowed through the same capture
|
|
34
|
+
* seam as `resolutionDivisor`. See `capture-motion-gate.ts`. */
|
|
35
|
+
motionFilter: MotionFilterConfig;
|
|
36
|
+
/** Image-quality gate: drop a blurry/black frame (judged off-thread by the
|
|
37
|
+
* injected `analyzeFrame` callback) and retry the next acceptable frame,
|
|
38
|
+
* bounded by `maxWaitMs`. The increment ON TOP of the motion gate — only ever
|
|
39
|
+
* runs on frames that already passed motion gating. Mirrored in the persisted
|
|
40
|
+
* `ImageCaptureOptions.qualityFilter`. See `image-quality.ts`. */
|
|
41
|
+
qualityFilter: QualityFilterConfig;
|
|
29
42
|
}
|
|
30
43
|
/**
|
|
31
44
|
* Default capture configuration
|
|
@@ -79,6 +92,18 @@ interface CapturedFrame {
|
|
|
79
92
|
readonly width: number;
|
|
80
93
|
readonly height: number;
|
|
81
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* Verdict returned by the injected off-thread {@link ImageCaptureCallbacks.analyzeFrame}
|
|
97
|
+
* analyzer. The manager only needs `accept`; `reason` is carried for logging/
|
|
98
|
+
* tuning. Structurally compatible with `image-quality.ts`'s `QualityVerdict`, so
|
|
99
|
+
* the recorder worker can return that type directly.
|
|
100
|
+
*/
|
|
101
|
+
interface FrameQualityVerdict {
|
|
102
|
+
/** `true` to save the frame, `false` to drop it and retry. */
|
|
103
|
+
readonly accept: boolean;
|
|
104
|
+
/** Optional debug reason (e.g. `'black'` | `'blurry'`); for logging only. */
|
|
105
|
+
readonly reason?: string | null;
|
|
106
|
+
}
|
|
82
107
|
/**
|
|
83
108
|
* Callbacks for image capture integration
|
|
84
109
|
*/
|
|
@@ -110,6 +135,22 @@ interface ImageCaptureCallbacks {
|
|
|
110
135
|
* @see docs/2026-02-06-bug-camera-frames-black.md
|
|
111
136
|
*/
|
|
112
137
|
captureFrame?: (quality: number) => Promise<CapturedFrame | null>;
|
|
138
|
+
/**
|
|
139
|
+
* Optional off-thread image-quality analyzer (blur/blackness gate). When
|
|
140
|
+
* provided AND `config.qualityFilter.enabled`, a freshly-encoded blob is NOT
|
|
141
|
+
* saved immediately: it is handed to this async callback (a Web Worker
|
|
142
|
+
* round-trip in the recorder) and saved only if the verdict accepts. A reject
|
|
143
|
+
* drops the blob and re-arms capture so the next acceptable (motion-calm)
|
|
144
|
+
* frame fills the slot, bounded by `qualityFilter.maxWaitMs` (after which the
|
|
145
|
+
* next frame is saved regardless, so an interval is never silently lost).
|
|
146
|
+
*
|
|
147
|
+
* The manager itself NEVER touches pixels — the off-main-thread guarantee. The
|
|
148
|
+
* callback MUST settle its promise; if it rejects, or does not settle within
|
|
149
|
+
* `captureTimeoutMs`, the manager fails open (saves the frame) so a hung
|
|
150
|
+
* analyzer cannot deadlock the pipeline. See `image-quality.ts` and
|
|
151
|
+
* `GpsPlusSlamJs_Docs/docs/2026-06-24-image-quality-gate-plan.md`.
|
|
152
|
+
*/
|
|
153
|
+
analyzeFrame?: (frame: CapturedFrame) => Promise<FrameQualityVerdict>;
|
|
113
154
|
}
|
|
114
155
|
/**
|
|
115
156
|
* Manages periodic image capture from a canvas.
|
|
@@ -129,6 +170,30 @@ declare class ImageCaptureManager {
|
|
|
129
170
|
private frameCount;
|
|
130
171
|
private captureInProgress;
|
|
131
172
|
private captureTimeoutId;
|
|
173
|
+
/** True while an `analyzeFrame` verdict is in flight; blocks a second capture
|
|
174
|
+
* for the same interval without throttling normal cadence (captureInProgress
|
|
175
|
+
* is released after encode, NOT held across the round-trip). */
|
|
176
|
+
private awaitingVerdict;
|
|
177
|
+
/** The current interval's capture was rejected by the quality gate; bypass the
|
|
178
|
+
* interval guard so the next acceptable frame is grabbed immediately. */
|
|
179
|
+
private retryPending;
|
|
180
|
+
/** Frame time (ms) of the FIRST quality attempt for the current interval; the
|
|
181
|
+
* quality `maxWaitMs` fallback measures from here, independent of the motion
|
|
182
|
+
* gate's own due clock. `null` between intervals. */
|
|
183
|
+
private qualityDeadlineBase;
|
|
184
|
+
/** Safety timeout for a hung `analyzeFrame` (fail-open). */
|
|
185
|
+
private verdictTimeoutId;
|
|
186
|
+
/** Sliding window of recent per-frame velocities for the motion gate. */
|
|
187
|
+
private readonly motionWindow;
|
|
188
|
+
/** Previous frame's pose, for the per-frame velocity delta. */
|
|
189
|
+
private prevPose;
|
|
190
|
+
/** Previous frame's timestamp (ms), for the per-frame velocity delta. */
|
|
191
|
+
private prevTime;
|
|
192
|
+
/** Whether THIS frame's velocity sample was rejected as a tracking glitch. */
|
|
193
|
+
private lastSampleWasGlitch;
|
|
194
|
+
/** When the current capture first became due (ms); null while not due. The
|
|
195
|
+
* `maxWaitMs` fallback measures from here, not from `lastCaptureTime`. */
|
|
196
|
+
private dueTime;
|
|
132
197
|
constructor(canvas: HTMLCanvasElement, callbacks: ImageCaptureCallbacks, config?: ImageCaptureConfig);
|
|
133
198
|
/**
|
|
134
199
|
* Start capturing images.
|
|
@@ -154,16 +219,51 @@ declare class ImageCaptureManager {
|
|
|
154
219
|
* @param time - Frame timestamp in milliseconds (from requestAnimationFrame)
|
|
155
220
|
*/
|
|
156
221
|
onFrame(time: number): void;
|
|
222
|
+
/**
|
|
223
|
+
* Sample per-frame device motion into the sliding window. Called every frame
|
|
224
|
+
* (even during an in-flight capture or before the interval elapses) so the
|
|
225
|
+
* window reflects truly instantaneous motion the moment a capture becomes
|
|
226
|
+
* due. A frame with no pose, or a non-positive dt (duplicate timestamp),
|
|
227
|
+
* records no sample and is treated as "not a glitch".
|
|
228
|
+
*/
|
|
229
|
+
private updateMotionWindow;
|
|
230
|
+
/**
|
|
231
|
+
* Apply the motion gate to a due capture. Returns `true` to capture now,
|
|
232
|
+
* `false` to defer to a later frame.
|
|
233
|
+
*
|
|
234
|
+
* - Filter disabled → always `true` (legacy behavior).
|
|
235
|
+
* - `maxWaitMs` elapsed → always `true` (never-calm safety fallback; an
|
|
236
|
+
* interval is never silently lost).
|
|
237
|
+
* - Current frame's sample was a tracking glitch → `false` (don't grab a
|
|
238
|
+
* relocalization-teleport frame; the fallback still guarantees progress).
|
|
239
|
+
* - No measurable motion yet (first capture / all-glitch) → `true` (the very
|
|
240
|
+
* first capture is never blocked; only measurable motion defers).
|
|
241
|
+
* - Otherwise → defer to {@link decideCapture} over the windowed maxima.
|
|
242
|
+
*/
|
|
243
|
+
private shouldCaptureNow;
|
|
157
244
|
/**
|
|
158
245
|
* Clear the safety timeout for captureInProgress.
|
|
159
246
|
* Called when capture completes (success or failure) before the timeout fires.
|
|
160
247
|
*/
|
|
161
248
|
private clearCaptureTimeout;
|
|
249
|
+
/** Clear the safety timeout for a hung `analyzeFrame` verdict. */
|
|
250
|
+
private clearVerdictTimeout;
|
|
162
251
|
/**
|
|
163
252
|
* Common handler for captured blobs (from either canvas.toBlob or captureFrame).
|
|
164
|
-
*
|
|
253
|
+
* Releases `captureInProgress` after the synchronous encode, then either saves
|
|
254
|
+
* immediately (legacy / gate-off) or runs the off-thread image-quality gate.
|
|
255
|
+
*
|
|
256
|
+
* @param time - the XR frame time the capture was dispatched at; the quality
|
|
257
|
+
* retry-deadline clock and `lastCaptureTime` are measured from it.
|
|
165
258
|
*/
|
|
166
259
|
private handleCapturedBlob;
|
|
260
|
+
/**
|
|
261
|
+
* Persist an accepted frame: allocate its 1-based index, run the suspicious-
|
|
262
|
+
* size belt-and-suspenders check, reset the per-interval gate clocks, and fire
|
|
263
|
+
* `onCaptured`. Called from the legacy path, the quality fallbacks, and an
|
|
264
|
+
* accepting verdict — the single place a frame becomes durable.
|
|
265
|
+
*/
|
|
266
|
+
private saveCapture;
|
|
167
267
|
}
|
|
168
268
|
//#endregion
|
|
169
|
-
export {
|
|
269
|
+
export { ImageCaptureCallbacks as a, MIN_VALID_IMAGE_BYTES as c, FrameQualityVerdict as i, CapturedImage as n, ImageCaptureConfig as o, DEFAULT_CAPTURE_CONFIG as r, ImageCaptureManager as s, CapturedFrame as t };
|