gps-plus-slam-app-framework 1.4.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/absolute-orientation-CAEh840t.d.ts +54 -0
- package/dist/{app-selectors-CrOucL00.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 +1 -1
- 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 +1 -1
- package/dist/ar/depth-sampler.d.ts +2 -2
- 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 +1 -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 -30
- package/dist/ar/index.js +6 -5
- package/dist/ar/occupancy-grid.d.ts +1 -1
- package/dist/ar/planar-pnp.d.ts +1 -1
- package/dist/ar/qr-debug-view.d.ts +1 -1
- package/dist/ar/qr-derived-pose.d.ts +1 -1
- package/dist/ar/qr-detection-controller.d.ts +1 -1
- package/dist/ar/qr-frontend.d.ts +1 -1
- package/dist/ar/qr-pose.d.ts +1 -1
- package/dist/ar/qr-size-depth-context.d.ts +2 -2
- package/dist/ar/qr-size-measurer.d.ts +1 -1
- 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-Dc72Ksiu.js → ar-MFpFlV7j.js} +3 -3
- package/dist/{ar-world-group-alignment-DDLkgNf_.d.ts → ar-world-group-alignment-69fgnB39.d.ts} +1 -1
- package/dist/capture-motion-gate-0Yl2NpQG.d.ts +24 -0
- package/dist/capture-motion-gate-4EscNZCw.js +94 -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-Ce2FMKCp.d.ts → create-slam-app-store-n_VF63D5.d.ts} +50 -40
- package/dist/{depth-grid-lookup-vMDrGTk_.d.ts → depth-grid-lookup-DQ-IYhO3.d.ts} +1 -1
- package/dist/{depth-sampler-DwfPYzS7.d.ts → depth-sampler-asc8exHt.d.ts} +1 -1
- package/dist/{depth-unprojection-CK3b9htH.d.ts → depth-unprojection-DjsbJW9L.d.ts} +1 -1
- package/dist/{enable-gps-ar-CjZ1nEdu.d.ts → enable-gps-ar-DkNhFv4Y.d.ts} +4 -4
- package/dist/{frame-conversions-D_Af8Dt3.d.ts → frame-conversions-DObhj8Wf.d.ts} +1 -1
- package/dist/{fused-path-C0FHe67t.d.ts → fused-path-v6eOmLl3.d.ts} +1 -1
- package/dist/geo/h3-proximity.d.ts +1 -1
- package/dist/geo/index.d.ts +1 -1
- package/dist/geodesic-angle-BTldfFgs.js +39 -0
- package/dist/{gps-anchor-C6I9NDHA.d.ts → gps-anchor-BeFO2RYa.d.ts} +1 -1
- package/dist/{gps-ar-pose-sampler-DaHg2aPZ.d.ts → gps-ar-pose-sampler-DHMZmYAc.d.ts} +1 -1
- package/dist/{gps-event-coordinator-C7hwHyLA.d.ts → gps-event-coordinator-CjWW636x.d.ts} +21 -11
- package/dist/image-capture-B2-b6tKK.js +374 -0
- package/dist/{image-capture-C2Feywet.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-m_LneUSi.d.ts → index-BODDUTvD.d.ts} +1 -1
- package/dist/{index-BvLo2poy.d.ts → index-BneSj4_5.d.ts} +3 -3
- package/dist/index-_Rhgoqkh.d.ts +2 -0
- package/dist/index.d.ts +80 -77
- package/dist/index.js +11 -8
- package/dist/{leaflet-map-overlay-bnb2U1NQ.d.ts → leaflet-map-overlay-CmIyiE8n.d.ts} +1 -1
- package/dist/{map-data-DdjPiSav.d.ts → map-data-B4oEehUh.d.ts} +1 -1
- package/dist/{map-overlay-draw-CPVdAsFq.d.ts → map-overlay-draw-ymz1s7fG.d.ts} +1 -1
- package/dist/{null-storage-backend-CuSdYFsM.d.ts → null-storage-backend-B9rtyDfG.d.ts} +2 -2
- package/dist/{occupancy-grid-Bvi2Gw1U.d.ts → occupancy-grid-DnOc-6PU.d.ts} +2 -2
- package/dist/{opfs-storage-backend-CeQAb0SD.d.ts → opfs-storage-backend-JJiUJo-3.d.ts} +2 -2
- package/dist/{persistence-middleware-5eearhSV.d.ts → persistence-middleware-DAPTTwB2.d.ts} +1 -1
- package/dist/{planar-pnp-cjthe7Be.d.ts → planar-pnp-my9bqULS.d.ts} +1 -1
- package/dist/{qr-debug-view-B8gTzjMm.d.ts → qr-debug-view-Brl73HGO.d.ts} +1 -1
- package/dist/{qr-derived-pose-DinKE600.d.ts → qr-derived-pose-CmxTP2m4.d.ts} +2 -2
- package/dist/{qr-detected-slice-Cpv-dg82.d.ts → qr-detected-slice-VQssZmQJ.d.ts} +4 -4
- package/dist/{qr-detection-controller-Cd2Ih4cC.d.ts → qr-detection-controller-CDurHv8E.d.ts} +3 -3
- package/dist/{qr-frontend-DufZp2xS.d.ts → qr-frontend--fFX25mV.d.ts} +1 -1
- package/dist/{qr-pose-aggregation-CFlmLm6w.d.ts → qr-pose-aggregation-B7tWJq7j.d.ts} +1 -1
- package/dist/{qr-pose-aggregation-DXR9STHs.js → qr-pose-aggregation-DBiaLW0O.js} +1 -10
- package/dist/{qr-size-depth-context-onu36E9s.d.ts → qr-size-depth-context-CeAG53fy.d.ts} +2 -2
- package/dist/{qr-size-measurer-D7739ZpB.d.ts → qr-size-measurer-DQdAsid1.d.ts} +3 -3
- package/dist/{recording-options-D93kksJU.d.ts → recording-options-BPCpMHLK.d.ts} +154 -9
- package/dist/{recording-replayer-CUEK82tM.d.ts → recording-replayer-DQKpAGyQ.d.ts} +2 -2
- package/dist/{recording-slice-B-uCgGoh.d.ts → recording-slice-DaxHZvNu.d.ts} +2 -2
- package/dist/{replay-engine-BSVd7Zzb.d.ts → replay-engine-CmKh_4wf.d.ts} +1 -1
- package/dist/{replay-scene-CPt56pps.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/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 -20
- package/dist/state/index.js +4 -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 +3 -3
- package/dist/state/qr-detected-slice.js +1 -1
- package/dist/state/recording-options.d.ts +2 -2
- package/dist/state/recording-options.js +155 -13
- 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/replay-engine.d.ts +1 -1
- 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-IEw0ZsBu.js → state-E51QT_-j.js} +1 -1
- package/dist/storage/file-system-utils.d.ts +1 -1
- package/dist/storage/index.d.ts +8 -8
- 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.d.ts +1 -1
- package/dist/storage/storage-backend.d.ts +1 -1
- package/dist/storage/zip-coverage-embed.d.ts +1 -1
- package/dist/storage/zip-export.d.ts +1 -1
- package/dist/storage/zip-reader.d.ts +1 -1
- package/dist/{storage-backend-CTJHNdMp.d.ts → storage-backend-BdVLD0dU.d.ts} +1 -1
- package/dist/{store-subscribers-BZP81hjh.d.ts → store-subscribers-CA6AJH_8.d.ts} +2 -2
- package/dist/{subscribe-to-selector-Bp3sLx8L.d.ts → subscribe-to-selector-B6EUmQ3I.d.ts} +1 -1
- package/dist/test-utils/browser-mocks.d.ts +1 -1
- package/dist/test-utils/zip-round-trip-helpers.d.ts +1 -1
- package/dist/{tracking-slice-DJZ9qeGg.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 +1 -1
- 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 +1 -1
- package/dist/visualization/hit-test-reticle.d.ts +1 -1
- package/dist/visualization/index.d.ts +18 -18
- package/dist/visualization/leaflet-map-overlay.d.ts +1 -1
- 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-CJF9Nijp.js → webxr-session-DG-eNfny.js} +25 -4
- package/dist/{webxr-session-COsOjJwn.d.ts → webxr-session-Do5L6G3B.d.ts} +19 -9
- package/package.json +2 -2
- package/dist/index-eaI1Z1KR.d.ts +0 -2
- /package/dist/{accuracy-circles-Pi2YeCrM.d.ts → accuracy-circles-BtqOP_v1.d.ts} +0 -0
- /package/dist/{alignment-lerper-BUTsQ2yu.d.ts → alignment-lerper-Bil6vbVw.d.ts} +0 -0
- /package/dist/{ar-types-BckI1E-O.d.ts → ar-types-BFX2r1wk.d.ts} +0 -0
- /package/dist/{bresenham3d-ByjcmEgM.d.ts → bresenham3d-CU1Gv2W4.d.ts} +0 -0
- /package/dist/{camera-blit-capture-CqWe1G7s.d.ts → camera-blit-capture-NKT9H5-e.d.ts} +0 -0
- /package/dist/{camera-follower-BTb2Yp12.d.ts → camera-follower-CqJJ7zRj.d.ts} +0 -0
- /package/dist/{capability-checker-CXqoJYtb.d.ts → capability-checker-DUQLg6gq.d.ts} +0 -0
- /package/dist/{capture-failure-tracker-BL5SoG2a.d.ts → capture-failure-tracker-gsm8q7yi.d.ts} +0 -0
- /package/dist/{chromium-camera-access-workaround-BNK189P0.d.ts → chromium-camera-access-workaround-D4K2uwwQ.d.ts} +0 -0
- /package/dist/{concurrency-DqAlKX4j.d.ts → concurrency-By0oOTZt.d.ts} +0 -0
- /package/dist/{css3d-renderer-manager-m-ab19vp.d.ts → css3d-renderer-manager-B3kGNtcn.d.ts} +0 -0
- /package/dist/{failure-tracker-Cnc_xWUw.d.ts → failure-tracker-BO7M7jF_.d.ts} +0 -0
- /package/dist/{file-system-utils-Bs7lpeVP.d.ts → file-system-utils-DOajnMm-.d.ts} +0 -0
- /package/dist/{format-file-size-BFUWvir1.d.ts → format-file-size-Dc98ulzB.d.ts} +0 -0
- /package/dist/{frame-loop-BEO9_q3k.d.ts → frame-loop-CpVjQ1dH.d.ts} +0 -0
- /package/dist/{frustum-visibility-D_RGmH98.d.ts → frustum-visibility-CeoCZchO.d.ts} +0 -0
- /package/dist/{geo-types-DqiBzPO9.d.ts → geo-types-3Ip1NzVR.d.ts} +0 -0
- /package/dist/{gps-C5inWeNV.d.ts → gps-BBO3hKfT.d.ts} +0 -0
- /package/dist/{gps-compass-cubes-BExxrWJ-.d.ts → gps-compass-cubes-DSN87L8p.d.ts} +0 -0
- /package/dist/{gps-error-handler-C2bS69oJ.d.ts → gps-error-handler-BbPsfw-_.d.ts} +0 -0
- /package/dist/{gps-event-markers-Dd0G2bD0.d.ts → gps-event-markers-Cz9RMA2k.d.ts} +0 -0
- /package/dist/{h3-proximity-B57C6rYL.d.ts → h3-proximity-C-j0pw1j.d.ts} +0 -0
- /package/dist/{hit-test-reticle-Dqak9hiR.d.ts → hit-test-reticle-DV-2TpB9.d.ts} +0 -0
- /package/dist/{lerp-utils-kLxMJ2Xj.d.ts → lerp-utils-BLmHhkVe.d.ts} +0 -0
- /package/dist/{list-formatter-ct8txwvR.d.ts → list-formatter-KNMS0ay2.d.ts} +0 -0
- /package/dist/{logger-CzQrCe6C.d.ts → logger-CLnPrHiD.d.ts} +0 -0
- /package/dist/{map-overlay-Dci4yvm3.d.ts → map-overlay-Dh2B97MP.d.ts} +0 -0
- /package/dist/{opfs-storage-DMtlt57S.d.ts → opfs-storage-3VZh-qH6.d.ts} +0 -0
- /package/dist/{permission-checker-CtPtPQRq.d.ts → permission-checker-CDuI8BwC.d.ts} +0 -0
- /package/dist/{qr-pose-Bt2reSw7.d.ts → qr-pose-D2rfu5DA.d.ts} +0 -0
- /package/dist/{scene-node-names-CdNeQjOE.d.ts → scene-node-names-BiShOQDB.d.ts} +0 -0
- /package/dist/{session-disposers-D8wnO-uW.js → session-disposers-zv9OGXPB.js} +0 -0
- /package/dist/{three-dispose-BCZkSJp2.d.ts → three-dispose-Bt1k7WKh.d.ts} +0 -0
- /package/dist/{vis-colors-BZsSLI4i.d.ts → vis-colors-Obw2pLqT.d.ts} +0 -0
- /package/dist/{webxr-nue-basis-CW1wsbm6.d.ts → webxr-nue-basis-3nfjDJJp.d.ts} +0 -0
- /package/dist/{xr-camera-texture-BmrP7iNZ.d.ts → xr-camera-texture-Cva1q3Qj.d.ts} +0 -0
- /package/dist/{xr-error-handler-BEjRgZjt.d.ts → xr-error-handler-CtvHpjfm.d.ts} +0 -0
- /package/dist/{xr-frame-loop-K8ebdjLw.d.ts → xr-frame-loop-DIYtRcUa.d.ts} +0 -0
- /package/dist/{zip-coverage-embed-tFohI2J4.d.ts → zip-coverage-embed-DCAREVmw.d.ts} +0 -0
- /package/dist/{zip-export-DDWXInTR.d.ts → zip-export-ol0S_Ofd.d.ts} +0 -0
- /package/dist/{zip-reader-Q2ue6zlC.d.ts → zip-reader-D9iPQwdQ.d.ts} +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as findNearbyGeoAnchor, c as h3CellsMatch, i as clusterCellsByZoom, l as isH3Index, n as KnownGeoAnchor, o as gpsPathToCoverageCells, r as approxDistanceMetres, s as gpsToH3, t as H3_RESOLUTION } from "../h3-proximity-
|
|
1
|
+
import { a as findNearbyGeoAnchor, c as h3CellsMatch, i as clusterCellsByZoom, l as isH3Index, n as KnownGeoAnchor, o as gpsPathToCoverageCells, r as approxDistanceMetres, s as gpsToH3, t as H3_RESOLUTION } from "../h3-proximity-C-j0pw1j.js";
|
|
2
2
|
export { H3_RESOLUTION, KnownGeoAnchor, approxDistanceMetres, clusterCellsByZoom, findNearbyGeoAnchor, gpsPathToCoverageCells, gpsToH3, h3CellsMatch, isH3Index };
|
package/dist/geo/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as findNearbyGeoAnchor, c as h3CellsMatch, i as clusterCellsByZoom, l as isH3Index, n as KnownGeoAnchor, o as gpsPathToCoverageCells, r as approxDistanceMetres, s as gpsToH3, t as H3_RESOLUTION } from "../h3-proximity-
|
|
1
|
+
import { a as findNearbyGeoAnchor, c as h3CellsMatch, i as clusterCellsByZoom, l as isH3Index, n as KnownGeoAnchor, o as gpsPathToCoverageCells, r as approxDistanceMetres, s as gpsToH3, t as H3_RESOLUTION } from "../h3-proximity-C-j0pw1j.js";
|
|
2
2
|
export { H3_RESOLUTION, type KnownGeoAnchor, approxDistanceMetres, clusterCellsByZoom, findNearbyGeoAnchor, gpsPathToCoverageCells, gpsToH3, h3CellsMatch, isH3Index };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { quat } from "gl-matrix";
|
|
2
|
+
//#region ../src/utils/geodesic-angle.ts
|
|
3
|
+
/**
|
|
4
|
+
* Geodesic-angle kernel — the single shared definition of the shortest-arc
|
|
5
|
+
* rotation angle between two unit quaternions.
|
|
6
|
+
*
|
|
7
|
+
* This used to be hand-rolled in (at least) two places:
|
|
8
|
+
* - `geodesicAngleRad` (private) in `ar/qr-pose-aggregation.ts`, and
|
|
9
|
+
* - `quat.getAngle(...)` + a NaN guard inside `state/tracking-quality.ts`'s
|
|
10
|
+
* `matrixDelta`.
|
|
11
|
+
*
|
|
12
|
+
* Both compute the same number — `acos(2·dot² − 1)` — so they are consolidated
|
|
13
|
+
* here so a third consumer (the capture motion gate's angular-velocity helper,
|
|
14
|
+
* `ar/pose-motion.ts`) reuses one definition instead of adding a fourth copy.
|
|
15
|
+
* See `GpsPlusSlamJs_Docs/docs/2026-06-23-followup-consolidate-geodesic-angle-kernel.md`.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Geodesic (shortest-arc) rotation angle in radians between two **unit**
|
|
19
|
+
* quaternions.
|
|
20
|
+
*
|
|
21
|
+
* Uses `cos θ = 2·dot² − 1`, which is invariant to the sign of either input
|
|
22
|
+
* (`q` ≡ `−q`, the quaternion double cover) because it depends only on `dot²`.
|
|
23
|
+
* The argument is clamped to `[-1, 1]` before `acos` so float error on a
|
|
24
|
+
* near-identical pair (where `2·dot² − 1` can drift slightly above 1) returns
|
|
25
|
+
* `0` rather than `NaN` — this is the guard `matrixDelta` previously applied
|
|
26
|
+
* explicitly around the raw `quat.getAngle`.
|
|
27
|
+
*
|
|
28
|
+
* Precondition: both inputs are (approximately) unit length. Callers that hold
|
|
29
|
+
* un-normalized quaternions must normalize first (gl-matrix `quat.normalize`).
|
|
30
|
+
*
|
|
31
|
+
* @returns angle in radians, in `[0, π]`. Never `NaN` for finite unit inputs.
|
|
32
|
+
*/
|
|
33
|
+
function geodesicAngleRad(a, b) {
|
|
34
|
+
const d = quat.dot(a, b);
|
|
35
|
+
const c = Math.min(1, Math.max(-1, 2 * d * d - 1));
|
|
36
|
+
return Math.acos(c);
|
|
37
|
+
}
|
|
38
|
+
//#endregion
|
|
39
|
+
export { geodesicAngleRad as t };
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { t as ARPose } from "./ar-types-
|
|
2
|
-
import { n as RawDeviceOrientation, t as GpsPosition } from "./gps-
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import { t as ARPose } from "./ar-types-BFX2r1wk.js";
|
|
2
|
+
import { n as RawDeviceOrientation, t as GpsPosition } from "./gps-BBO3hKfT.js";
|
|
3
|
+
import { t as AbsoluteOrientationReading } from "./absolute-orientation-CAEh840t.js";
|
|
4
|
+
import { i as SlamAppStore } from "./create-slam-app-store-n_VF63D5.js";
|
|
5
|
+
import { Quaternion, RawAbsoluteOrientation, RawGpsPoint, RecordGpsEventPayload, Vector3, eulerToQuaternion } from "gps-plus-slam-js";
|
|
5
6
|
import { ReducersMapObject } from "@reduxjs/toolkit";
|
|
6
7
|
|
|
7
8
|
//#region ../src/state/gps-event-coordinator.d.ts
|
|
@@ -44,28 +45,37 @@ declare function extractOdomRotation(arPose: ARPose): Quaternion;
|
|
|
44
45
|
/**
|
|
45
46
|
* Build a RawGpsPoint from GPS position data.
|
|
46
47
|
* Returns only raw sensor fields — no derived fields (coordinates, weight,
|
|
47
|
-
* zeroRef
|
|
48
|
-
*
|
|
48
|
+
* zeroRef). Derived fields are computed by the library reducer when the
|
|
49
|
+
* action is dispatched (raw-storage pattern).
|
|
49
50
|
*
|
|
50
51
|
* @param gpsPosition - GPS position from Geolocation API
|
|
51
|
-
* @param deviceOrientation -
|
|
52
|
+
* @param deviceOrientation - Accepted for signature stability; no longer
|
|
53
|
+
* read (the legacy `compassAbsolute` field it fed is no longer recorded)
|
|
52
54
|
* @returns RawGpsPoint ready for the action payload
|
|
53
55
|
*/
|
|
54
56
|
declare function buildRawGpsPoint(gpsPosition: GpsPosition, deviceOrientation: RawDeviceOrientation | null): RawGpsPoint;
|
|
57
|
+
/**
|
|
58
|
+
* Map a capture-module {@link AbsoluteOrientationReading} to the library's
|
|
59
|
+
* {@link RawAbsoluteOrientation} payload shape (the only difference is the
|
|
60
|
+
* timestamp field name: capture `timestamp` → payload `sampleTimestamp`).
|
|
61
|
+
* Returns `undefined` when the sensor produced no reading (most non-Chrome).
|
|
62
|
+
*/
|
|
63
|
+
declare function toRawAbsoluteOrientation(reading: AbsoluteOrientationReading | null): RawAbsoluteOrientation | undefined;
|
|
55
64
|
/**
|
|
56
65
|
* Build a RecordGpsEventPayload from GPS position and AR pose.
|
|
57
66
|
* This is a pure function for testability.
|
|
58
67
|
*
|
|
59
68
|
* Stores only raw sensor data in the payload. The library reducer
|
|
60
|
-
* computes derived fields (coordinates, weight
|
|
61
|
-
*
|
|
69
|
+
* computes derived fields (coordinates, weight) when the action is
|
|
70
|
+
* processed (raw-storage pattern).
|
|
62
71
|
*
|
|
63
72
|
* @param gpsPosition - GPS position from Geolocation API
|
|
64
73
|
* @param arPose - AR pose from WebXR
|
|
65
74
|
* @param deviceOrientation - Optional device orientation from sensors
|
|
75
|
+
* @param absoluteOrientation - Optional AbsoluteOrientationSensor reading
|
|
66
76
|
* @returns RecordGpsEventPayload ready for dispatch
|
|
67
77
|
*/
|
|
68
|
-
declare function buildRecordGpsEventPayload(gpsPosition: GpsPosition, arPose: ARPose, deviceOrientation: RawDeviceOrientation | null): RecordGpsEventPayload;
|
|
78
|
+
declare function buildRecordGpsEventPayload(gpsPosition: GpsPosition, arPose: ARPose, deviceOrientation: RawDeviceOrientation | null, absoluteOrientation?: AbsoluteOrientationReading | null): RecordGpsEventPayload;
|
|
69
79
|
/**
|
|
70
80
|
* Create a GPS position handler that dispatches combined GPS+AR events.
|
|
71
81
|
*
|
|
@@ -79,4 +89,4 @@ declare function buildRecordGpsEventPayload(gpsPosition: GpsPosition, arPose: AR
|
|
|
79
89
|
*/
|
|
80
90
|
declare function createGpsPositionHandler(config: RecordingCoordinatorConfig): (position: GpsPosition) => void;
|
|
81
91
|
//#endregion
|
|
82
|
-
export { eulerToQuaternion as a, getLastDeviceOrientation as c, createGpsPositionHandler as i, resetCoordinatorState as l, buildRawGpsPoint as n, extractOdomPosition as o, buildRecordGpsEventPayload as r, extractOdomRotation as s, RecordingCoordinatorConfig as t,
|
|
92
|
+
export { eulerToQuaternion as a, getLastDeviceOrientation as c, updateDeviceOrientation as d, createGpsPositionHandler as i, resetCoordinatorState as l, buildRawGpsPoint as n, extractOdomPosition as o, buildRecordGpsEventPayload as r, extractOdomRotation as s, RecordingCoordinatorConfig as t, toRawAbsoluteOrientation as u };
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
import { createLogger } from "./utils/logger.js";
|
|
2
|
+
import { t as geodesicAngleRad } from "./geodesic-angle-BTldfFgs.js";
|
|
3
|
+
import { n as MotionWindow, r as decideCapture, t as DEFAULT_MOTION_FILTER } from "./capture-motion-gate-4EscNZCw.js";
|
|
4
|
+
import { DEFAULT_QUALITY_FILTER } from "./ar/image-quality.js";
|
|
5
|
+
import { quat } from "gl-matrix";
|
|
6
|
+
//#region ../src/ar/pose-motion.ts
|
|
7
|
+
/**
|
|
8
|
+
* Pose-motion helpers — stateless angular/linear velocity from two AR poses.
|
|
9
|
+
*
|
|
10
|
+
* The capture motion gate (blurry-frame skipping, see
|
|
11
|
+
* `GpsPlusSlamJs_Docs/docs/2026-06-23-blurry-frame-motion-gating-plan.md` §4.1)
|
|
12
|
+
* rejects frames whose device motion is too fast to yield a sharp image. Motion
|
|
13
|
+
* is derived from consecutive WebXR poses — cheaper than any image-content
|
|
14
|
+
* metric because it needs no GPU readback.
|
|
15
|
+
*
|
|
16
|
+
* This module holds ONLY the pure, stateless math (angle ÷ dt, distance ÷ dt).
|
|
17
|
+
* The stateful policy (sliding window, glitch rejection, the capture decision)
|
|
18
|
+
* lives in `ImageCaptureManager` and `capture-motion-gate.ts` — keeping the
|
|
19
|
+
* reusable atom separate from the capture-specific policy (plan §4.1 "Scope").
|
|
20
|
+
*
|
|
21
|
+
* Angular velocity reuses the shared `geodesicAngleRad` kernel rather than a
|
|
22
|
+
* fresh `acos` (CLAUDE.md: search first, prefer refactor over re-implementation).
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* Convert a WebXR object-form quaternion to a normalized gl-matrix quaternion.
|
|
26
|
+
* Normalizing defends against slightly non-unit inputs (the gate must not throw
|
|
27
|
+
* on imperfect tracking data).
|
|
28
|
+
*/
|
|
29
|
+
function toGlQuat(q) {
|
|
30
|
+
const g = quat.fromValues(q.x, q.y, q.z, q.w);
|
|
31
|
+
return quat.normalize(g, g);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Angular velocity in radians/second between two orientations.
|
|
35
|
+
*
|
|
36
|
+
* @param qPrev - orientation at the previous frame.
|
|
37
|
+
* @param qCur - orientation at the current frame.
|
|
38
|
+
* @param dtSeconds - elapsed time in seconds (must be > 0).
|
|
39
|
+
* @returns geodesic angle between the orientations divided by `dtSeconds`.
|
|
40
|
+
* Returns `0` when `dtSeconds <= 0` (degenerate/duplicate frame timestamp) so
|
|
41
|
+
* a bad delta can never produce `Infinity`/`NaN` and spuriously flip the
|
|
42
|
+
* motion gate. Double-cover safe (`q` ≡ `−q`) via the shared kernel.
|
|
43
|
+
*/
|
|
44
|
+
function angularVelocity(qPrev, qCur, dtSeconds) {
|
|
45
|
+
if (!(dtSeconds > 0)) return 0;
|
|
46
|
+
return geodesicAngleRad(toGlQuat(qPrev), toGlQuat(qCur)) / dtSeconds;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Linear velocity in metres/second between two positions.
|
|
50
|
+
*
|
|
51
|
+
* @param pPrev - position at the previous frame.
|
|
52
|
+
* @param pCur - position at the current frame.
|
|
53
|
+
* @param dtSeconds - elapsed time in seconds (must be > 0).
|
|
54
|
+
* @returns straight-line distance divided by `dtSeconds`. Returns `0` when
|
|
55
|
+
* `dtSeconds <= 0` (same guard rationale as {@link angularVelocity}).
|
|
56
|
+
*/
|
|
57
|
+
function linearVelocity(pPrev, pCur, dtSeconds) {
|
|
58
|
+
if (!(dtSeconds > 0)) return 0;
|
|
59
|
+
const dx = pCur.x - pPrev.x;
|
|
60
|
+
const dy = pCur.y - pPrev.y;
|
|
61
|
+
const dz = pCur.z - pPrev.z;
|
|
62
|
+
return Math.sqrt(dx * dx + dy * dy + dz * dz) / dtSeconds;
|
|
63
|
+
}
|
|
64
|
+
//#endregion
|
|
65
|
+
//#region ../src/ar/image-capture.ts
|
|
66
|
+
/**
|
|
67
|
+
* Image Capture Module
|
|
68
|
+
*
|
|
69
|
+
* Captures periodic JPEG screenshots from the WebGL canvas during AR recording.
|
|
70
|
+
* Uses async toBlob() for better performance (non-blocking).
|
|
71
|
+
*
|
|
72
|
+
* Includes validation to detect suspiciously small images (likely black/empty)
|
|
73
|
+
* which can occur on mobile devices when the WebGL context hasn't composited yet.
|
|
74
|
+
*/
|
|
75
|
+
const log = createLogger("ImageCapture");
|
|
76
|
+
/**
|
|
77
|
+
* Minimum expected blob size in bytes for a valid JPEG image.
|
|
78
|
+
* A black/empty 1920x1080 JPEG still compresses to ~2-3KB due to headers.
|
|
79
|
+
* A real camera frame at 0.7 quality should be 50KB+.
|
|
80
|
+
* We use a conservative threshold to catch obviously broken captures.
|
|
81
|
+
*/
|
|
82
|
+
const MIN_VALID_IMAGE_BYTES = 5e3;
|
|
83
|
+
/**
|
|
84
|
+
* Default capture configuration
|
|
85
|
+
*/
|
|
86
|
+
const DEFAULT_CAPTURE_CONFIG = {
|
|
87
|
+
intervalMs: 2e3,
|
|
88
|
+
quality: .7,
|
|
89
|
+
captureTimeoutMs: 5e3,
|
|
90
|
+
resolutionDivisor: 1,
|
|
91
|
+
motionFilter: DEFAULT_MOTION_FILTER,
|
|
92
|
+
qualityFilter: DEFAULT_QUALITY_FILTER
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* Manages periodic image capture from a canvas.
|
|
96
|
+
*
|
|
97
|
+
* Usage:
|
|
98
|
+
* 1. Create with canvas and callbacks
|
|
99
|
+
* 2. Call start() when recording begins
|
|
100
|
+
* 3. Call onFrame(time) each XR frame
|
|
101
|
+
* 4. Call stop() when recording ends
|
|
102
|
+
*/
|
|
103
|
+
var ImageCaptureManager = class {
|
|
104
|
+
canvas;
|
|
105
|
+
callbacks;
|
|
106
|
+
config;
|
|
107
|
+
capturing = false;
|
|
108
|
+
lastCaptureTime = 0;
|
|
109
|
+
frameCount = 0;
|
|
110
|
+
captureInProgress = false;
|
|
111
|
+
captureTimeoutId = null;
|
|
112
|
+
/** True while an `analyzeFrame` verdict is in flight; blocks a second capture
|
|
113
|
+
* for the same interval without throttling normal cadence (captureInProgress
|
|
114
|
+
* is released after encode, NOT held across the round-trip). */
|
|
115
|
+
awaitingVerdict = false;
|
|
116
|
+
/** The current interval's capture was rejected by the quality gate; bypass the
|
|
117
|
+
* interval guard so the next acceptable frame is grabbed immediately. */
|
|
118
|
+
retryPending = false;
|
|
119
|
+
/** Frame time (ms) of the FIRST quality attempt for the current interval; the
|
|
120
|
+
* quality `maxWaitMs` fallback measures from here, independent of the motion
|
|
121
|
+
* gate's own due clock. `null` between intervals. */
|
|
122
|
+
qualityDeadlineBase = null;
|
|
123
|
+
/** Safety timeout for a hung `analyzeFrame` (fail-open). */
|
|
124
|
+
verdictTimeoutId = null;
|
|
125
|
+
/** Sliding window of recent per-frame velocities for the motion gate. */
|
|
126
|
+
motionWindow = new MotionWindow();
|
|
127
|
+
/** Previous frame's pose, for the per-frame velocity delta. */
|
|
128
|
+
prevPose = null;
|
|
129
|
+
/** Previous frame's timestamp (ms), for the per-frame velocity delta. */
|
|
130
|
+
prevTime = 0;
|
|
131
|
+
/** Whether THIS frame's velocity sample was rejected as a tracking glitch. */
|
|
132
|
+
lastSampleWasGlitch = false;
|
|
133
|
+
/** When the current capture first became due (ms); null while not due. The
|
|
134
|
+
* `maxWaitMs` fallback measures from here, not from `lastCaptureTime`. */
|
|
135
|
+
dueTime = null;
|
|
136
|
+
constructor(canvas, callbacks, config = DEFAULT_CAPTURE_CONFIG) {
|
|
137
|
+
this.canvas = canvas;
|
|
138
|
+
this.callbacks = callbacks;
|
|
139
|
+
this.config = config;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Start capturing images.
|
|
143
|
+
* Resets frame counter and last capture time.
|
|
144
|
+
*/
|
|
145
|
+
start() {
|
|
146
|
+
this.capturing = true;
|
|
147
|
+
this.lastCaptureTime = 0;
|
|
148
|
+
this.frameCount = 0;
|
|
149
|
+
this.motionWindow.reset();
|
|
150
|
+
this.prevPose = null;
|
|
151
|
+
this.prevTime = 0;
|
|
152
|
+
this.lastSampleWasGlitch = false;
|
|
153
|
+
this.dueTime = null;
|
|
154
|
+
this.clearVerdictTimeout();
|
|
155
|
+
this.awaitingVerdict = false;
|
|
156
|
+
this.retryPending = false;
|
|
157
|
+
this.qualityDeadlineBase = null;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Stop capturing images.
|
|
161
|
+
* Clears any pending safety timeout and resets in-flight capture state.
|
|
162
|
+
*/
|
|
163
|
+
stop() {
|
|
164
|
+
this.capturing = false;
|
|
165
|
+
this.clearCaptureTimeout();
|
|
166
|
+
this.captureInProgress = false;
|
|
167
|
+
this.clearVerdictTimeout();
|
|
168
|
+
this.awaitingVerdict = false;
|
|
169
|
+
this.retryPending = false;
|
|
170
|
+
this.qualityDeadlineBase = null;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Check if capture manager is active.
|
|
174
|
+
*/
|
|
175
|
+
isCapturing() {
|
|
176
|
+
return this.capturing;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Get the current frame count.
|
|
180
|
+
*/
|
|
181
|
+
getFrameCount() {
|
|
182
|
+
return this.frameCount;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Called each XR frame to check if a capture is needed.
|
|
186
|
+
*
|
|
187
|
+
* @param time - Frame timestamp in milliseconds (from requestAnimationFrame)
|
|
188
|
+
*/
|
|
189
|
+
onFrame(time) {
|
|
190
|
+
if (!this.capturing) return;
|
|
191
|
+
const pose = this.callbacks.getCurrentPose();
|
|
192
|
+
this.updateMotionWindow(time, pose);
|
|
193
|
+
if (this.captureInProgress || this.awaitingVerdict) return;
|
|
194
|
+
const elapsed = time - this.lastCaptureTime;
|
|
195
|
+
if (!this.retryPending && this.lastCaptureTime > 0 && elapsed < this.config.intervalMs) {
|
|
196
|
+
this.dueTime = null;
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (!pose) return;
|
|
200
|
+
if (this.dueTime === null) this.dueTime = time;
|
|
201
|
+
if (!this.shouldCaptureNow(time)) return;
|
|
202
|
+
this.captureInProgress = true;
|
|
203
|
+
this.lastCaptureTime = time;
|
|
204
|
+
this.dueTime = null;
|
|
205
|
+
const timestamp = performance.timeOrigin + time;
|
|
206
|
+
const screenRotation = this.callbacks.getScreenRotation();
|
|
207
|
+
this.captureTimeoutId = setTimeout(() => {
|
|
208
|
+
if (this.captureInProgress) {
|
|
209
|
+
log.error(`Capture timeout after ${this.config.captureTimeoutMs}ms — force-resetting captureInProgress`);
|
|
210
|
+
this.captureInProgress = false;
|
|
211
|
+
this.captureTimeoutId = null;
|
|
212
|
+
}
|
|
213
|
+
}, this.config.captureTimeoutMs);
|
|
214
|
+
if (this.callbacks.captureFrame) this.callbacks.captureFrame(this.config.quality).then((frame) => {
|
|
215
|
+
this.handleCapturedBlob(frame?.blob ?? null, frame?.width, frame?.height, timestamp, time, pose, screenRotation);
|
|
216
|
+
}).catch(() => {
|
|
217
|
+
this.clearCaptureTimeout();
|
|
218
|
+
this.captureInProgress = false;
|
|
219
|
+
this.callbacks.onCaptureFailed?.();
|
|
220
|
+
});
|
|
221
|
+
else this.canvas.toBlob((blob) => {
|
|
222
|
+
this.handleCapturedBlob(blob, this.canvas.width, this.canvas.height, timestamp, time, pose, screenRotation);
|
|
223
|
+
}, "image/jpeg", this.config.quality);
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Sample per-frame device motion into the sliding window. Called every frame
|
|
227
|
+
* (even during an in-flight capture or before the interval elapses) so the
|
|
228
|
+
* window reflects truly instantaneous motion the moment a capture becomes
|
|
229
|
+
* due. A frame with no pose, or a non-positive dt (duplicate timestamp),
|
|
230
|
+
* records no sample and is treated as "not a glitch".
|
|
231
|
+
*/
|
|
232
|
+
updateMotionWindow(time, pose) {
|
|
233
|
+
if (!pose) {
|
|
234
|
+
this.lastSampleWasGlitch = false;
|
|
235
|
+
this.motionWindow.reset();
|
|
236
|
+
this.prevPose = null;
|
|
237
|
+
this.prevTime = 0;
|
|
238
|
+
this.retryPending = false;
|
|
239
|
+
this.qualityDeadlineBase = null;
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (this.prevPose && this.prevTime > 0) {
|
|
243
|
+
const dt = (time - this.prevTime) / 1e3;
|
|
244
|
+
if (dt > 0) {
|
|
245
|
+
const angVel = angularVelocity(this.prevPose.orientation, pose.orientation, dt);
|
|
246
|
+
const linVel = linearVelocity(this.prevPose.position, pose.position, dt);
|
|
247
|
+
this.lastSampleWasGlitch = !this.motionWindow.push(angVel, linVel);
|
|
248
|
+
} else this.lastSampleWasGlitch = false;
|
|
249
|
+
} else this.lastSampleWasGlitch = false;
|
|
250
|
+
this.prevPose = pose;
|
|
251
|
+
this.prevTime = time;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Apply the motion gate to a due capture. Returns `true` to capture now,
|
|
255
|
+
* `false` to defer to a later frame.
|
|
256
|
+
*
|
|
257
|
+
* - Filter disabled → always `true` (legacy behavior).
|
|
258
|
+
* - `maxWaitMs` elapsed → always `true` (never-calm safety fallback; an
|
|
259
|
+
* interval is never silently lost).
|
|
260
|
+
* - Current frame's sample was a tracking glitch → `false` (don't grab a
|
|
261
|
+
* relocalization-teleport frame; the fallback still guarantees progress).
|
|
262
|
+
* - No measurable motion yet (first capture / all-glitch) → `true` (the very
|
|
263
|
+
* first capture is never blocked; only measurable motion defers).
|
|
264
|
+
* - Otherwise → defer to {@link decideCapture} over the windowed maxima.
|
|
265
|
+
*/
|
|
266
|
+
shouldCaptureNow(time) {
|
|
267
|
+
const mf = this.config.motionFilter;
|
|
268
|
+
if (!mf || !mf.enabled) return true;
|
|
269
|
+
const msSinceDue = this.dueTime === null ? 0 : time - this.dueTime;
|
|
270
|
+
if (msSinceDue >= mf.maxWaitMs) return true;
|
|
271
|
+
if (this.lastSampleWasGlitch) return false;
|
|
272
|
+
if (!this.motionWindow.hasSamples()) return true;
|
|
273
|
+
return decideCapture({
|
|
274
|
+
windowMaxAngular: this.motionWindow.maxAngular(),
|
|
275
|
+
windowMaxLinear: this.motionWindow.maxLinear(),
|
|
276
|
+
maxAngularVelocity: mf.maxAngularVelocity,
|
|
277
|
+
maxLinearVelocity: mf.maxLinearVelocity,
|
|
278
|
+
msSinceDue,
|
|
279
|
+
maxWaitMs: mf.maxWaitMs
|
|
280
|
+
}) === "capture";
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Clear the safety timeout for captureInProgress.
|
|
284
|
+
* Called when capture completes (success or failure) before the timeout fires.
|
|
285
|
+
*/
|
|
286
|
+
clearCaptureTimeout() {
|
|
287
|
+
if (this.captureTimeoutId !== null) {
|
|
288
|
+
clearTimeout(this.captureTimeoutId);
|
|
289
|
+
this.captureTimeoutId = null;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
/** Clear the safety timeout for a hung `analyzeFrame` verdict. */
|
|
293
|
+
clearVerdictTimeout() {
|
|
294
|
+
if (this.verdictTimeoutId !== null) {
|
|
295
|
+
clearTimeout(this.verdictTimeoutId);
|
|
296
|
+
this.verdictTimeoutId = null;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Common handler for captured blobs (from either canvas.toBlob or captureFrame).
|
|
301
|
+
* Releases `captureInProgress` after the synchronous encode, then either saves
|
|
302
|
+
* immediately (legacy / gate-off) or runs the off-thread image-quality gate.
|
|
303
|
+
*
|
|
304
|
+
* @param time - the XR frame time the capture was dispatched at; the quality
|
|
305
|
+
* retry-deadline clock and `lastCaptureTime` are measured from it.
|
|
306
|
+
*/
|
|
307
|
+
handleCapturedBlob(blob, width, height, timestamp, time, pose, screenRotation) {
|
|
308
|
+
this.clearCaptureTimeout();
|
|
309
|
+
this.captureInProgress = false;
|
|
310
|
+
if (!blob) {
|
|
311
|
+
this.callbacks.onCaptureFailed?.();
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
const qf = this.config.qualityFilter;
|
|
315
|
+
const analyze = this.callbacks.analyzeFrame;
|
|
316
|
+
if (!analyze || !qf || !qf.enabled) {
|
|
317
|
+
this.saveCapture(blob, width, height, timestamp, pose, screenRotation);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
if (this.qualityDeadlineBase === null) this.qualityDeadlineBase = time;
|
|
321
|
+
if (time - this.qualityDeadlineBase >= qf.maxWaitMs) {
|
|
322
|
+
this.saveCapture(blob, width, height, timestamp, pose, screenRotation);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
this.awaitingVerdict = true;
|
|
326
|
+
let settled = false;
|
|
327
|
+
const finish = (accept, viaTimeout) => {
|
|
328
|
+
if (settled || !this.capturing) return;
|
|
329
|
+
settled = true;
|
|
330
|
+
this.clearVerdictTimeout();
|
|
331
|
+
this.awaitingVerdict = false;
|
|
332
|
+
if (viaTimeout) log.error("analyzeFrame verdict timed out — saving frame fail-open to keep the pipeline live");
|
|
333
|
+
if (accept) this.saveCapture(blob, width, height, timestamp, pose, screenRotation);
|
|
334
|
+
else this.retryPending = true;
|
|
335
|
+
};
|
|
336
|
+
this.verdictTimeoutId = setTimeout(() => finish(true, true), this.config.captureTimeoutMs);
|
|
337
|
+
analyze({
|
|
338
|
+
blob,
|
|
339
|
+
width: width ?? 0,
|
|
340
|
+
height: height ?? 0
|
|
341
|
+
}).then((verdict) => finish(verdict.accept, false)).catch((err) => {
|
|
342
|
+
log.error("analyzeFrame failed — saving frame without quality gate", err);
|
|
343
|
+
finish(true, false);
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Persist an accepted frame: allocate its 1-based index, run the suspicious-
|
|
348
|
+
* size belt-and-suspenders check, reset the per-interval gate clocks, and fire
|
|
349
|
+
* `onCaptured`. Called from the legacy path, the quality fallbacks, and an
|
|
350
|
+
* accepting verdict — the single place a frame becomes durable.
|
|
351
|
+
*/
|
|
352
|
+
saveCapture(blob, width, height, timestamp, pose, screenRotation) {
|
|
353
|
+
if (!this.capturing) return;
|
|
354
|
+
this.retryPending = false;
|
|
355
|
+
this.qualityDeadlineBase = null;
|
|
356
|
+
const frameIndex = ++this.frameCount;
|
|
357
|
+
if (blob.size < 5e3) {
|
|
358
|
+
log.error(`Suspicious image at frame ${frameIndex}: blob size ${blob.size} bytes is below minimum ${MIN_VALID_IMAGE_BYTES} bytes. Image may be black/empty.`);
|
|
359
|
+
this.callbacks.onSuspiciousImage?.(blob.size, frameIndex);
|
|
360
|
+
}
|
|
361
|
+
this.callbacks.onCaptured({
|
|
362
|
+
blob,
|
|
363
|
+
timestamp,
|
|
364
|
+
frameIndex,
|
|
365
|
+
position: pose.position,
|
|
366
|
+
rotation: pose.orientation,
|
|
367
|
+
screenRotation,
|
|
368
|
+
...width && width > 0 ? { width } : {},
|
|
369
|
+
...height && height > 0 ? { height } : {}
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
//#endregion
|
|
374
|
+
export { ImageCaptureManager as n, MIN_VALID_IMAGE_BYTES as r, DEFAULT_CAPTURE_CONFIG as t };
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { o as WebXRQuaternion, s as WebXRVec3, t as ARPose } from "./ar-types-
|
|
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 };
|