gps-plus-slam-app-framework 1.4.0 → 1.8.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-BAxc15J6.d.ts +54 -0
- package/dist/{accuracy-circles-Pi2YeCrM.d.ts → accuracy-circles-DkWBkF5g.d.ts} +16 -6
- package/dist/{app-selectors-CrOucL00.d.ts → app-selectors-JqydaUMN.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-occluder.d.ts +127 -0
- package/dist/ar/depth-occluder.js +299 -0
- package/dist/ar/depth-sampler.d.ts +2 -2
- package/dist/ar/depth-sampler.js +24 -4
- package/dist/ar/depth-unprojection.d.ts +1 -1
- package/dist/ar/depth-unprojection.js +52 -21
- 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/frame-loop.js +12 -2
- 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 +169 -0
- package/dist/ar/index.d.ts +32 -30
- package/dist/ar/index.js +7 -5
- package/dist/ar/occlusion-mesh-worker.d.ts +58 -0
- package/dist/ar/occlusion-mesh-worker.js +129 -0
- package/dist/ar/occupancy-grid.d.ts +2 -2
- package/dist/ar/occupancy-grid.js +287 -31
- package/dist/ar/occupancy-mesher.d.ts +2 -0
- package/dist/ar/occupancy-mesher.js +638 -0
- 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/xr-frame-loop.js +12 -2
- package/dist/{ar-Dc72Ksiu.js → ar-BqCudtqE.js} +4 -3
- package/dist/{ar-world-group-alignment-DDLkgNf_.d.ts → ar-world-group-alignment-BlvGcIAk.d.ts} +1 -1
- package/dist/capture-motion-gate-BGoAMkmf.d.ts +24 -0
- package/dist/capture-motion-gate-_NdTSwKi.js +106 -0
- package/dist/cell-key-DIB4rOTV.js +51 -0
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.js +2 -2
- package/dist/{create-slam-app-store-Ce2FMKCp.d.ts → create-slam-app-store-D3nzh9Wb.d.ts} +50 -40
- package/dist/create-slam-app-store-DXiAnJwR.js +156 -0
- package/dist/{depth-grid-lookup-vMDrGTk_.d.ts → depth-grid-lookup-D5atpNZ3.d.ts} +1 -1
- package/dist/{depth-sampler-DwfPYzS7.d.ts → depth-sampler-CwObAwEA.d.ts} +33 -1
- package/dist/{depth-unprojection-CK3b9htH.d.ts → depth-unprojection-DRdBU90S.d.ts} +1 -1
- package/dist/{enable-gps-ar-CjZ1nEdu.d.ts → enable-gps-ar-DvGzBOrp.d.ts} +4 -4
- package/dist/{frame-conversions-D_Af8Dt3.d.ts → frame-conversions-C4bnNOEY.d.ts} +1 -1
- package/dist/{fused-path-C0FHe67t.d.ts → fused-path-C5pp0PJU.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-BjECcYnD.d.ts} +1 -1
- package/dist/{gps-ar-pose-sampler-DaHg2aPZ.d.ts → gps-ar-pose-sampler-DEGNk2CI.d.ts} +1 -1
- package/dist/{gps-event-coordinator-C7hwHyLA.d.ts → gps-event-coordinator-CHtIkhb7.d.ts} +21 -11
- package/dist/{image-capture-C2Feywet.d.ts → image-capture-Bp6Pd4Md.d.ts} +103 -3
- package/dist/image-capture-DVcuqMtX.js +374 -0
- package/dist/image-quality-BMx2ko9-.d.ts +158 -0
- package/dist/index-D4vrgV4b.d.ts +2 -0
- package/dist/index-D74JTX4I.d.ts +22 -0
- package/dist/{index-m_LneUSi.d.ts → index-DlsOo2IB.d.ts} +1 -1
- package/dist/{index-BvLo2poy.d.ts → index-cCQWuYqy.d.ts} +3 -3
- package/dist/index.d.ts +82 -77
- package/dist/index.js +15 -10
- package/dist/{leaflet-map-overlay-bnb2U1NQ.d.ts → leaflet-map-overlay-BOZFW5kT.d.ts} +64 -10
- package/dist/leaflet-map-overlay-CBWPvaX9.js +452 -0
- package/dist/lerp-utils-Qs8OhumS.d.ts +42 -0
- package/dist/map-data-D7rpMC6f.js +120 -0
- package/dist/{map-data-DdjPiSav.d.ts → map-data-PNfGKGWx.d.ts} +22 -2
- package/dist/{map-overlay-draw-CPVdAsFq.d.ts → map-overlay-draw-DiroaJed.d.ts} +1 -1
- package/dist/{null-storage-backend-CuSdYFsM.d.ts → null-storage-backend-B1-jlpin.d.ts} +2 -2
- package/dist/occlusion-mesh-b28lRJ6S.d.ts +194 -0
- package/dist/occupancy-grid-BV7RhB7I.d.ts +194 -0
- package/dist/occupancy-mesher-C2ecWhNp.d.ts +100 -0
- package/dist/{opfs-storage-DMtlt57S.d.ts → opfs-storage-CTkxvxN4.d.ts} +5 -0
- package/dist/{opfs-storage-backend-CeQAb0SD.d.ts → opfs-storage-backend-D6Z03fML.d.ts} +2 -2
- package/dist/{persistence-middleware-5eearhSV.d.ts → persistence-middleware-CG-t08X4.d.ts} +1 -1
- package/dist/{planar-pnp-cjthe7Be.d.ts → planar-pnp-D7bc_rLX.d.ts} +1 -1
- package/dist/{qr-debug-view-B8gTzjMm.d.ts → qr-debug-view-BJooqbiP.d.ts} +1 -1
- package/dist/{qr-derived-pose-DinKE600.d.ts → qr-derived-pose-BwNWn8oV.d.ts} +2 -2
- package/dist/{qr-detected-slice-Cpv-dg82.d.ts → qr-detected-slice-TzVb8bij.d.ts} +4 -4
- package/dist/{qr-detection-controller-Cd2Ih4cC.d.ts → qr-detection-controller-Bo4XfQTP.d.ts} +3 -3
- package/dist/{qr-frontend-DufZp2xS.d.ts → qr-frontend-BPURlh3S.d.ts} +1 -1
- package/dist/{qr-pose-aggregation-CFlmLm6w.d.ts → qr-pose-aggregation-CypnmjA-.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-apAOmbrZ.d.ts} +2 -2
- package/dist/{qr-size-measurer-D7739ZpB.d.ts → qr-size-measurer-DxiMqRUw.d.ts} +3 -3
- package/dist/recording-options-vASCFzc5.d.ts +667 -0
- package/dist/{recording-replayer-CUEK82tM.d.ts → recording-replayer-D0V9WkmY.d.ts} +2 -2
- package/dist/{recording-slice-B-uCgGoh.d.ts → recording-slice-DTUtSVgd.d.ts} +2 -2
- package/dist/{replay-engine-BSVd7Zzb.d.ts → replay-engine-xdrUgWJi.d.ts} +1 -1
- package/dist/{replay-scene-CPt56pps.d.ts → replay-scene-zJ8SFDjE.d.ts} +3 -3
- package/dist/sensors/absolute-orientation.d.ts +2 -0
- package/dist/sensors/absolute-orientation.js +162 -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 +286 -25
- 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/store-subscribers.js +2 -1
- 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 +49 -128
- package/dist/state/tracking-slice.d.ts +1 -1
- package/dist/{state-IEw0ZsBu.js → state-BeKvn4B-.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/opfs-storage.js +25 -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-sEtmM4yW.d.ts} +1 -1
- package/dist/{store-subscribers-BZP81hjh.d.ts → store-subscribers-DScA7tdT.d.ts} +2 -2
- package/dist/{subscribe-to-selector-Bp3sLx8L.d.ts → subscribe-to-selector-B9ylM5Oh.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-DMkHlbny.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/accuracy-circles.js +17 -8
- 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 +20 -19
- package/dist/visualization/index.js +4 -3
- package/dist/visualization/leaflet-map-overlay.d.ts +1 -1
- package/dist/visualization/leaflet-map-overlay.js +1 -282
- package/dist/visualization/lerp-utils.d.ts +2 -2
- package/dist/visualization/lerp-utils.js +27 -3
- package/dist/visualization/map-data.d.ts +1 -1
- package/dist/visualization/map-data.js +1 -32
- package/dist/visualization/map-overlay-draw.d.ts +1 -1
- package/dist/visualization/map-overlay-draw.js +24 -2
- package/dist/visualization/map-overlay.d.ts +1 -1
- package/dist/visualization/occluder-mesh-driver.d.ts +130 -0
- package/dist/visualization/occluder-mesh-driver.js +219 -0
- package/dist/visualization/occlusion-mesh.d.ts +2 -0
- package/dist/visualization/occlusion-mesh.js +437 -0
- 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-BCP2XK7k.js} +110 -14
- package/dist/{webxr-session-COsOjJwn.d.ts → webxr-session-cRG2SYAQ.d.ts} +83 -10
- package/package.json +4 -4
- package/dist/index-eaI1Z1KR.d.ts +0 -2
- package/dist/lerp-utils-kLxMJ2Xj.d.ts +0 -20
- package/dist/occupancy-grid-Bvi2Gw1U.d.ts +0 -82
- package/dist/recording-options-D93kksJU.d.ts +0 -369
- /package/dist/{alignment-lerper-BUTsQ2yu.d.ts → alignment-lerper-gD70k7Fq.d.ts} +0 -0
- /package/dist/{ar-types-BckI1E-O.d.ts → ar-types-Cc5yvBwn.d.ts} +0 -0
- /package/dist/{bresenham3d-ByjcmEgM.d.ts → bresenham3d-DGc4nIb-.d.ts} +0 -0
- /package/dist/{camera-blit-capture-CqWe1G7s.d.ts → camera-blit-capture-BimSdyih.d.ts} +0 -0
- /package/dist/{camera-follower-BTb2Yp12.d.ts → camera-follower-DfBm2J5i.d.ts} +0 -0
- /package/dist/{capability-checker-CXqoJYtb.d.ts → capability-checker-BTTpygvZ.d.ts} +0 -0
- /package/dist/{capture-failure-tracker-BL5SoG2a.d.ts → capture-failure-tracker-_BVfhpxv.d.ts} +0 -0
- /package/dist/{chromium-camera-access-workaround-BNK189P0.d.ts → chromium-camera-access-workaround-C6yJLXz9.d.ts} +0 -0
- /package/dist/{concurrency-DqAlKX4j.d.ts → concurrency-DPeGBH0X.d.ts} +0 -0
- /package/dist/{css3d-renderer-manager-m-ab19vp.d.ts → css3d-renderer-manager-CPpVtvgs.d.ts} +0 -0
- /package/dist/{failure-tracker-Cnc_xWUw.d.ts → failure-tracker-CeKf5HIc.d.ts} +0 -0
- /package/dist/{file-system-utils-Bs7lpeVP.d.ts → file-system-utils-DueXHxo9.d.ts} +0 -0
- /package/dist/{format-file-size-BFUWvir1.d.ts → format-file-size-DxN-EBLc.d.ts} +0 -0
- /package/dist/{frame-loop-BEO9_q3k.d.ts → frame-loop-7GBMBu68.d.ts} +0 -0
- /package/dist/{frustum-visibility-D_RGmH98.d.ts → frustum-visibility-CokkgS_i.d.ts} +0 -0
- /package/dist/{geo-types-DqiBzPO9.d.ts → geo-types-DrGVu8nS.d.ts} +0 -0
- /package/dist/{gps-C5inWeNV.d.ts → gps-CDKDQokE.d.ts} +0 -0
- /package/dist/{gps-compass-cubes-BExxrWJ-.d.ts → gps-compass-cubes-1mooS3BS.d.ts} +0 -0
- /package/dist/{gps-error-handler-C2bS69oJ.d.ts → gps-error-handler-C34GQuTu.d.ts} +0 -0
- /package/dist/{gps-event-markers-Dd0G2bD0.d.ts → gps-event-markers-Di2nBzx-.d.ts} +0 -0
- /package/dist/{h3-proximity-B57C6rYL.d.ts → h3-proximity-D0GTHQmr.d.ts} +0 -0
- /package/dist/{hit-test-reticle-Dqak9hiR.d.ts → hit-test-reticle-CaQbrhbk.d.ts} +0 -0
- /package/dist/{list-formatter-ct8txwvR.d.ts → list-formatter-DLFDnS70.d.ts} +0 -0
- /package/dist/{logger-CzQrCe6C.d.ts → logger-CqTd5Kpk.d.ts} +0 -0
- /package/dist/{map-overlay-Dci4yvm3.d.ts → map-overlay-BCQgvIgU.d.ts} +0 -0
- /package/dist/{permission-checker-CtPtPQRq.d.ts → permission-checker-B-VtjnBA.d.ts} +0 -0
- /package/dist/{qr-pose-Bt2reSw7.d.ts → qr-pose-y3vaEd6x.d.ts} +0 -0
- /package/dist/{scene-node-names-CdNeQjOE.d.ts → scene-node-names-CGb-ylBd.d.ts} +0 -0
- /package/dist/{session-disposers-D8wnO-uW.js → session-disposers-BPrMplIC.js} +0 -0
- /package/dist/{three-dispose-BCZkSJp2.d.ts → three-dispose-BMOAmPhb.d.ts} +0 -0
- /package/dist/{vis-colors-BZsSLI4i.d.ts → vis-colors-C7HI0u9_.d.ts} +0 -0
- /package/dist/{webxr-nue-basis-CW1wsbm6.d.ts → webxr-nue-basis-CaCnYxuq.d.ts} +0 -0
- /package/dist/{xr-camera-texture-BmrP7iNZ.d.ts → xr-camera-texture-q0eIDSRg.d.ts} +0 -0
- /package/dist/{xr-error-handler-BEjRgZjt.d.ts → xr-error-handler-uVGDu9IX.d.ts} +0 -0
- /package/dist/{xr-frame-loop-K8ebdjLw.d.ts → xr-frame-loop-DX3JPpAU.d.ts} +0 -0
- /package/dist/{zip-coverage-embed-tFohI2J4.d.ts → zip-coverage-embed-ncVbaZAz.d.ts} +0 -0
- /package/dist/{zip-export-DDWXInTR.d.ts → zip-export-CtiUnAlJ.d.ts} +0 -0
- /package/dist/{zip-reader-Q2ue6zlC.d.ts → zip-reader-CvruaGvD.d.ts} +0 -0
|
@@ -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-_NdTSwKi.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 };
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
//#region ../src/ar/image-quality.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Image-quality metrics + the drop/retry verdict policy for the image-content
|
|
4
|
+
* capture gate (blur + blackness), the increment layered ON TOP of the motion
|
|
5
|
+
* gate (`capture-motion-gate.ts`).
|
|
6
|
+
*
|
|
7
|
+
* Everything here is **pure and deterministic** — plain typed-array math plus a
|
|
8
|
+
* tiny rolling-history class. There is **no DOM and no Web Worker** in this
|
|
9
|
+
* module: it is fully unit-testable with synthetic buffers, and the recorder's
|
|
10
|
+
* `image-quality.worker.ts` is a thin shell that decodes a frame to pixels and
|
|
11
|
+
* calls into these functions. Keeping the verdict/history policy here (rather
|
|
12
|
+
* than inside the worker, as the original plan §8 sketched) honours the project
|
|
13
|
+
* rule that no untested logic ships: the worker is the device layer and is not
|
|
14
|
+
* unit-built, so the median/threshold decision must live where tests can pin it.
|
|
15
|
+
*
|
|
16
|
+
* Metrics:
|
|
17
|
+
* - {@link sharpnessScore} — variance of the Laplacian over a grayscale buffer
|
|
18
|
+
* (the standard Pech-Pacheco focus measure). Scene-dependent in absolute
|
|
19
|
+
* terms, so the gate compares it RELATIVE to a rolling median (§5).
|
|
20
|
+
* - {@link meanLuminance} — mean Rec. 601 luma of an RGBA buffer. Near-zero ⇒
|
|
21
|
+
* black/empty frame; an absolute cutoff is safe because "black is black"
|
|
22
|
+
* regardless of scene texture (§5).
|
|
23
|
+
*
|
|
24
|
+
* @see GpsPlusSlamJs_Docs/docs/2026-06-24-image-quality-gate-plan.md
|
|
25
|
+
* @see ./capture-motion-gate.ts — the motion gate this builds on (same shape:
|
|
26
|
+
* a shared config type + a small stateful window + a pure decision).
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* User-/consumer-facing configuration for the image-quality gate. Shared by both
|
|
30
|
+
* config shapes that carry it — `ImageCaptureConfig` (what `ImageCaptureManager`
|
|
31
|
+
* consumes) and `ImageCaptureOptions` (the persisted recorder options) — so the
|
|
32
|
+
* one definition cannot drift between them, exactly as `MotionFilterConfig` is
|
|
33
|
+
* shared (see `capture-motion-gate.ts` and the motion-gate follow-up §3).
|
|
34
|
+
*/
|
|
35
|
+
interface QualityFilterConfig {
|
|
36
|
+
/** Master switch for the image-quality gate. Default `false` (see below). */
|
|
37
|
+
enabled: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Blur cutoff as a fraction `k` of the recent sharpness median: a frame is
|
|
40
|
+
* judged blurry when its sharpness `< k · median(recent)`. ~0.5 is a sensible
|
|
41
|
+
* start. Relative (not absolute) because variance-of-Laplacian is
|
|
42
|
+
* scene-dependent — a correctly-focused blank wall scores low (§5).
|
|
43
|
+
*/
|
|
44
|
+
blurRelativeThreshold: number;
|
|
45
|
+
/**
|
|
46
|
+
* Absolute black cutoff on a 0–255 luma scale: a frame whose mean luminance is
|
|
47
|
+
* below this is judged black/empty and dropped. Absolute is safe because black
|
|
48
|
+
* is black regardless of scene (§5). Supersedes the byte-size
|
|
49
|
+
* `MIN_VALID_IMAGE_BYTES` heuristic for correctness (kept as a cheap
|
|
50
|
+
* pre-filter).
|
|
51
|
+
*/
|
|
52
|
+
minMeanLuminance: number;
|
|
53
|
+
/**
|
|
54
|
+
* Never-good safety fallback: once a due capture has been retried for this many
|
|
55
|
+
* ms without an acceptable frame, the next frame is saved regardless of the
|
|
56
|
+
* image verdict, so a recording interval is never silently lost (mirrors the
|
|
57
|
+
* motion gate's `maxWaitMs`). A sensible value is ~2× the capture interval.
|
|
58
|
+
*/
|
|
59
|
+
maxWaitMs: number;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Default image-quality configuration. **Disabled by default** (plan §10): the
|
|
63
|
+
* relative blur threshold is unvalidated, and a mis-tuned gate silently dropping
|
|
64
|
+
* good frames for every consumer app is worse than the motion gate's low-risk
|
|
65
|
+
* default-on. Flip to `true` once the thresholds are field-tuned. The numeric
|
|
66
|
+
* values are PLACEHOLDERS pending on-device tuning (record measured values in
|
|
67
|
+
* implementation-progress.md). `maxWaitMs` of 4000 ms is 2× the default 2000 ms
|
|
68
|
+
* image interval.
|
|
69
|
+
*/
|
|
70
|
+
declare const DEFAULT_QUALITY_FILTER: QualityFilterConfig;
|
|
71
|
+
/** Default number of recent (non-black) sharpness scores the gate keeps. */
|
|
72
|
+
declare const DEFAULT_SHARPNESS_HISTORY_SIZE = 15;
|
|
73
|
+
/**
|
|
74
|
+
* Default minimum samples before the relative blur check engages. Below this the
|
|
75
|
+
* gate is in "cold start" and accepts every non-black frame — the same
|
|
76
|
+
* "no baseline yet ⇒ don't block" principle the motion gate uses for an empty
|
|
77
|
+
* window.
|
|
78
|
+
*/
|
|
79
|
+
declare const DEFAULT_SHARPNESS_MIN_SAMPLES = 3;
|
|
80
|
+
/**
|
|
81
|
+
* Compute the **variance of the Laplacian** of a single-channel grayscale image
|
|
82
|
+
* — the standard focus measure (higher ⇒ sharper / more in-focus).
|
|
83
|
+
*
|
|
84
|
+
* The discrete Laplacian per interior pixel is
|
|
85
|
+
* `up + down + left + right − 4·centre` (the 4-neighbour kernel); the score is
|
|
86
|
+
* the variance of that response over all interior pixels. A flat/constant image
|
|
87
|
+
* yields 0; a focused, textured image yields a large value.
|
|
88
|
+
*
|
|
89
|
+
* Defensive: returns `0` (rather than throwing or `NaN`) for any input that has
|
|
90
|
+
* no interior pixels — non-integer or `< 3` dimensions, or a buffer shorter than
|
|
91
|
+
* `width · height`. A tiny negative variance from floating-point cancellation is
|
|
92
|
+
* clamped to 0.
|
|
93
|
+
*
|
|
94
|
+
* @param gray Row-major single-channel intensities, length ≥ `width · height`.
|
|
95
|
+
* @param width Image width in pixels.
|
|
96
|
+
* @param height Image height in pixels.
|
|
97
|
+
*/
|
|
98
|
+
declare function sharpnessScore(gray: Uint8Array | Uint8ClampedArray | readonly number[], width: number, height: number): number;
|
|
99
|
+
/**
|
|
100
|
+
* Convert an RGBA buffer to a single-channel grayscale (Rec. 601 luma) buffer,
|
|
101
|
+
* the input {@link sharpnessScore} expects. Pure; ignores the alpha channel. A
|
|
102
|
+
* partial trailing pixel (length not a multiple of 4) is ignored.
|
|
103
|
+
*/
|
|
104
|
+
declare function rgbaToGrayscale(rgba: Uint8Array | Uint8ClampedArray): Uint8ClampedArray;
|
|
105
|
+
/**
|
|
106
|
+
* Mean Rec. 601 luma (`0.299R + 0.587G + 0.114B`) over an RGBA buffer, on a
|
|
107
|
+
* 0–255 scale. Near-zero ⇒ black/empty frame. Alpha is ignored. Returns `0` for
|
|
108
|
+
* an empty buffer.
|
|
109
|
+
*/
|
|
110
|
+
declare function meanLuminance(rgba: Uint8Array | Uint8ClampedArray): number;
|
|
111
|
+
/** Reason a frame was rejected by the quality gate, or `null` when accepted. */
|
|
112
|
+
type QualityRejectReason = 'black' | 'blurry';
|
|
113
|
+
/** Outcome of evaluating one frame against the quality gate. */
|
|
114
|
+
interface QualityVerdict {
|
|
115
|
+
/** `true` to save the frame, `false` to drop + retry. */
|
|
116
|
+
readonly accept: boolean;
|
|
117
|
+
/** Why it was dropped, or `null` when accepted. */
|
|
118
|
+
readonly reason: QualityRejectReason | null;
|
|
119
|
+
/** The frame's variance-of-Laplacian sharpness (for logging/tuning). */
|
|
120
|
+
readonly sharpness: number;
|
|
121
|
+
/** The frame's mean luminance (for logging/tuning). */
|
|
122
|
+
readonly meanLuminance: number;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* The self-calibrating blur+blackness verdict, with the rolling sharpness
|
|
126
|
+
* history that makes the blur check scene-relative (§5). Stateful but pure (no
|
|
127
|
+
* DOM/worker): one instance per recording session, fed each analysed frame's
|
|
128
|
+
* metrics.
|
|
129
|
+
*
|
|
130
|
+
* Policy per frame ({@link evaluate}):
|
|
131
|
+
* 1. **Blackness — absolute.** `meanLuminance < config.minMeanLuminance` ⇒
|
|
132
|
+
* reject `'black'`. The score is NOT recorded (a black frame's ~0 sharpness
|
|
133
|
+
* must not drag the median down and disarm the blur check).
|
|
134
|
+
* 2. **Blur — relative.** Once at least `minSamples` non-black frames have been
|
|
135
|
+
* seen, reject `'blurry'` when `sharpness < blurRelativeThreshold · median`.
|
|
136
|
+
* Before that (cold start) every non-black frame is accepted.
|
|
137
|
+
* 3. The non-black frame's sharpness is recorded into the rolling window
|
|
138
|
+
* (capped at `historySize`) so the median tracks the scene's texture level —
|
|
139
|
+
* including a genuinely softening scene, which gradually lowers the bar (the
|
|
140
|
+
* §10 "retry storm" regime where the fallback then guarantees progress).
|
|
141
|
+
*/
|
|
142
|
+
declare class ImageQualityGate {
|
|
143
|
+
private readonly history;
|
|
144
|
+
private readonly historySize;
|
|
145
|
+
private readonly minSamples;
|
|
146
|
+
constructor(historySize?: number, minSamples?: number);
|
|
147
|
+
/**
|
|
148
|
+
* Judge one frame from its precomputed metrics + the current config, updating
|
|
149
|
+
* the rolling history. See the class doc for the policy.
|
|
150
|
+
*/
|
|
151
|
+
evaluate(sharpness: number, meanLum: number, config: QualityFilterConfig): QualityVerdict;
|
|
152
|
+
/** Number of (non-black) sharpness samples currently retained. */
|
|
153
|
+
historyLength(): number;
|
|
154
|
+
/** Clear the rolling history (e.g. on a new recording session). */
|
|
155
|
+
reset(): void;
|
|
156
|
+
}
|
|
157
|
+
//#endregion
|
|
158
|
+
export { QualityFilterConfig as a, meanLuminance as c, ImageQualityGate as i, rgbaToGrayscale as l, DEFAULT_SHARPNESS_HISTORY_SIZE as n, QualityRejectReason as o, DEFAULT_SHARPNESS_MIN_SAMPLES as r, QualityVerdict as s, DEFAULT_QUALITY_FILTER as t, sharpnessScore as u };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { ArImageCapture as ArImageCapture$1, GpsPoint as GpsPoint$1, LatLong as LatLong$1, LatLongAlt, Matrix4 as Matrix4$1, Quaternion as Quaternion$1, RootState as LibraryRootState, RootState as RootState$1, Vector3 as Vector3$1, arNorthBearingDeg, bearingDeltaDeg, calcGpsCoords as calcGpsCoords$1, calcRelativeCoordsInMeters, createGpsSlamStore, isIdentityMatrix4, magneticHeadingFromEnuQuat, odometryTrackingRestarted, validateLicenseKey as validateLicenseKey$1, webxrToNUE } from "gps-plus-slam-js";
|
|
2
|
+
export { odometryTrackingRestarted as _, LibraryRootState as a, RootState$1 as c, bearingDeltaDeg as d, calcGpsCoords$1 as f, magneticHeadingFromEnuQuat as g, isIdentityMatrix4 as h, LatLongAlt as i, Vector3$1 as l, createGpsSlamStore as m, GpsPoint$1 as n, Matrix4$1 as o, calcRelativeCoordsInMeters as p, LatLong$1 as r, Quaternion$1 as s, ArImageCapture$1 as t, arNorthBearingDeg as u, validateLicenseKey$1 as v, webxrToNUE as y };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ReadonlyQuat } from "gl-matrix";
|
|
2
|
+
|
|
3
|
+
//#region ../src/utils/geodesic-angle.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Geodesic (shortest-arc) rotation angle in radians between two **unit**
|
|
6
|
+
* quaternions.
|
|
7
|
+
*
|
|
8
|
+
* Uses `cos θ = 2·dot² − 1`, which is invariant to the sign of either input
|
|
9
|
+
* (`q` ≡ `−q`, the quaternion double cover) because it depends only on `dot²`.
|
|
10
|
+
* The argument is clamped to `[-1, 1]` before `acos` so float error on a
|
|
11
|
+
* near-identical pair (where `2·dot² − 1` can drift slightly above 1) returns
|
|
12
|
+
* `0` rather than `NaN` — this is the guard `matrixDelta` previously applied
|
|
13
|
+
* explicitly around the raw `quat.getAngle`.
|
|
14
|
+
*
|
|
15
|
+
* Precondition: both inputs are (approximately) unit length. Callers that hold
|
|
16
|
+
* un-normalized quaternions must normalize first (gl-matrix `quat.normalize`).
|
|
17
|
+
*
|
|
18
|
+
* @returns angle in radians, in `[0, π]`. Never `NaN` for finite unit inputs.
|
|
19
|
+
*/
|
|
20
|
+
declare function geodesicAngleRad(a: ReadonlyQuat, b: ReadonlyQuat): number;
|
|
21
|
+
//#endregion
|
|
22
|
+
export { geodesicAngleRad as t };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { N as CombinedRootState, m as TrackingQualityReport } from "./create-slam-app-store-D3nzh9Wb.js";
|
|
2
2
|
import { Add2dImagePayload, GpsPoint as GpsPoint$1, LatLong as LatLong$1, RawDeviceOrientation, RawGpsPoint as RawGpsPoint$1, RecordGpsEventPayload as RecordGpsEventPayload$1, add2dImage, calcRelativeCoordsInMeters, recordGpsEvent as recordGpsEvent$1, setZeroPos as setZeroPos$1 } from "gps-plus-slam-js";
|
|
3
3
|
|
|
4
4
|
//#region ../src/state/onboarding-guidance.d.ts
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { t as GridCell } from "./bresenham3d-
|
|
2
|
-
import { a as QrPoseSolution, i as Pose, r as Point2, t as CameraIntrinsics } from "./qr-pose-
|
|
3
|
-
import { a as QrFrontEnd, o as RgbaImage } from "./qr-frontend-
|
|
1
|
+
import { t as GridCell } from "./bresenham3d-DGc4nIb-.js";
|
|
2
|
+
import { a as QrPoseSolution, i as Pose, r as Point2, t as CameraIntrinsics } from "./qr-pose-y3vaEd6x.js";
|
|
3
|
+
import { a as QrFrontEnd, o as RgbaImage } from "./qr-frontend-BPURlh3S.js";
|
|
4
4
|
import { Quaternion, RecordGpsEventPayload, Vector3 } from "gps-plus-slam-js";
|
|
5
5
|
|
|
6
6
|
//#region ../src/ar/qr-gps-vote.d.ts
|