gps-plus-slam-app-framework 1.3.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/absolute-orientation-CAEh840t.d.ts +54 -0
- package/dist/{app-selectors-B8VQoMDY.d.ts → app-selectors-ikTUQZpe.d.ts} +1 -1
- package/dist/ar/bresenham3d.d.ts +1 -1
- package/dist/ar/camera-blit-capture.d.ts +2 -2
- package/dist/ar/camera-blit-capture.js +74 -9
- package/dist/ar/capability-checker.d.ts +1 -1
- package/dist/ar/capture-failure-tracker.d.ts +1 -1
- package/dist/ar/chromium-camera-access-workaround.d.ts +1 -1
- package/dist/ar/depth-grid-lookup.d.ts +2 -0
- package/dist/ar/depth-grid-lookup.js +43 -0
- package/dist/ar/depth-sampler.d.ts +2 -2
- package/dist/ar/depth-sampler.js +2 -4
- package/dist/ar/depth-unprojection.d.ts +1 -1
- package/dist/ar/enable-gps-ar.d.ts +1 -1
- package/dist/ar/enable-gps-ar.js +26 -1
- package/dist/ar/frame-loop.d.ts +1 -1
- package/dist/ar/image-capture.d.ts +2 -2
- package/dist/ar/image-capture.js +1 -154
- package/dist/ar/image-quality.d.ts +2 -0
- package/dist/ar/image-quality.js +168 -0
- package/dist/ar/index.d.ts +31 -19
- package/dist/ar/index.js +17 -5
- package/dist/ar/occupancy-grid.d.ts +1 -1
- package/dist/ar/occupancy-grid.js +29 -2
- package/dist/ar/planar-pnp.d.ts +2 -0
- package/dist/ar/planar-pnp.js +425 -0
- package/dist/ar/qr-debug-view.d.ts +2 -0
- package/dist/ar/qr-debug-view.js +89 -0
- package/dist/ar/qr-derived-pose.d.ts +2 -0
- package/dist/ar/qr-derived-pose.js +114 -0
- package/dist/ar/qr-detection-controller.d.ts +2 -0
- package/dist/ar/qr-detection-controller.js +2 -0
- package/dist/ar/qr-frontend.d.ts +2 -0
- package/dist/ar/qr-frontend.js +60 -0
- package/dist/ar/qr-pose.d.ts +2 -0
- package/dist/ar/qr-pose.js +267 -0
- package/dist/ar/qr-size-depth-context.d.ts +3 -0
- package/dist/ar/qr-size-depth-context.js +42 -0
- package/dist/ar/qr-size-measurer.d.ts +2 -0
- package/dist/ar/qr-size-measurer.js +2 -0
- package/dist/ar/replay-scene.d.ts +1 -1
- package/dist/ar/replay-scene.js +1 -1
- package/dist/ar/scene-node-names.d.ts +1 -1
- package/dist/ar/webxr-nue-basis.d.ts +1 -1
- package/dist/ar/webxr-session.d.ts +4 -4
- package/dist/ar/webxr-session.js +2 -2
- package/dist/ar/xr-camera-texture.d.ts +1 -1
- package/dist/ar/xr-error-handler.d.ts +1 -1
- package/dist/ar/xr-frame-loop.d.ts +1 -1
- package/dist/ar-MFpFlV7j.js +382 -0
- package/dist/{ar-world-group-alignment-C9lwwIyI.d.ts → ar-world-group-alignment-69fgnB39.d.ts} +1 -1
- package/dist/{camera-blit-capture-CnxUDbKx.d.ts → camera-blit-capture-NKT9H5-e.d.ts} +57 -4
- package/dist/capture-motion-gate-0Yl2NpQG.d.ts +24 -0
- package/dist/capture-motion-gate-4EscNZCw.js +94 -0
- package/dist/concurrency-By0oOTZt.d.ts +50 -0
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.js +2 -2
- package/dist/create-slam-app-store-DkCHOpvb.js +149 -0
- package/dist/{create-slam-app-store-BljusOua.d.ts → create-slam-app-store-n_VF63D5.d.ts} +50 -40
- package/dist/depth-grid-lookup-DQ-IYhO3.d.ts +21 -0
- package/dist/{depth-sampler-Bhanfwla.d.ts → depth-sampler-asc8exHt.d.ts} +1 -1
- package/dist/{depth-unprojection-Y6i_mJuT.d.ts → depth-unprojection-DjsbJW9L.d.ts} +1 -1
- package/dist/{enable-gps-ar-BfUJOgpW.d.ts → enable-gps-ar-DkNhFv4Y.d.ts} +4 -4
- package/dist/{frame-conversions-BfsafGWR.d.ts → frame-conversions-DObhj8Wf.d.ts} +1 -1
- package/dist/{fused-path-DJoVu98b.d.ts → fused-path-v6eOmLl3.d.ts} +1 -1
- package/dist/geo/h3-proximity.d.ts +2 -2
- package/dist/geo/h3-proximity.js +61 -2
- package/dist/geo/index.d.ts +2 -2
- package/dist/geo/index.js +2 -2
- package/dist/geodesic-angle-BTldfFgs.js +39 -0
- package/dist/{gps-anchor-dYeCvvp5.d.ts → gps-anchor-BeFO2RYa.d.ts} +1 -1
- package/dist/{gps-ar-pose-sampler-DVb96iOy.d.ts → gps-ar-pose-sampler-DHMZmYAc.d.ts} +1 -1
- package/dist/{gps-event-coordinator-CWIDOk4U.d.ts → gps-event-coordinator-CjWW636x.d.ts} +21 -11
- package/dist/{gps-event-markers-Dc9FT-q1.d.ts → gps-event-markers-Cz9RMA2k.d.ts} +18 -0
- package/dist/{h3-proximity-I1msGPgE.d.ts → h3-proximity-C-j0pw1j.d.ts} +39 -1
- package/dist/image-capture-B2-b6tKK.js +374 -0
- package/dist/{image-capture-qAZ5Fh5X.d.ts → image-capture-CNZDrQXG.d.ts} +103 -3
- package/dist/image-quality-smCBoFj1.d.ts +158 -0
- package/dist/index-BAPokxMb.d.ts +22 -0
- package/dist/{index-BYYUdf9v.d.ts → index-BODDUTvD.d.ts} +1 -1
- package/dist/index-BneSj4_5.d.ts +399 -0
- package/dist/index-_Rhgoqkh.d.ts +2 -0
- package/dist/index.d.ts +80 -67
- package/dist/index.js +24 -12
- package/dist/{leaflet-map-overlay-6CJC-E1l.d.ts → leaflet-map-overlay-CmIyiE8n.d.ts} +1 -1
- package/dist/{map-data-CkloBxIl.d.ts → map-data-B4oEehUh.d.ts} +1 -1
- package/dist/{map-overlay-draw-krOKwDAq.d.ts → map-overlay-draw-ymz1s7fG.d.ts} +1 -1
- package/dist/{null-storage-backend-uKQE8tl7.d.ts → null-storage-backend-B9rtyDfG.d.ts} +2 -2
- package/dist/{occupancy-grid-BRNRaPyT.d.ts → occupancy-grid-DnOc-6PU.d.ts} +12 -2
- package/dist/{opfs-storage-D5zAn_lx.d.ts → opfs-storage-3VZh-qH6.d.ts} +23 -4
- package/dist/{opfs-storage-backend-6aYMUZlJ.d.ts → opfs-storage-backend-JJiUJo-3.d.ts} +3 -3
- package/dist/{persistence-middleware-B9ei4Nhx.d.ts → persistence-middleware-DAPTTwB2.d.ts} +1 -1
- package/dist/planar-pnp-my9bqULS.d.ts +67 -0
- package/dist/qr-debug-view-Brl73HGO.d.ts +29 -0
- package/dist/qr-derived-pose-CmxTP2m4.d.ts +122 -0
- package/dist/qr-detected-slice-VQssZmQJ.d.ts +166 -0
- package/dist/qr-detection-controller-B-tLVd-S.js +119 -0
- package/dist/qr-detection-controller-CDurHv8E.d.ts +48 -0
- package/dist/qr-frontend--fFX25mV.d.ts +58 -0
- package/dist/qr-pose-D2rfu5DA.d.ts +138 -0
- package/dist/qr-pose-aggregation-B7tWJq7j.d.ts +90 -0
- package/dist/qr-pose-aggregation-DBiaLW0O.js +191 -0
- package/dist/qr-size-depth-context-CeAG53fy.d.ts +13 -0
- package/dist/qr-size-measurer-DLb33geH.js +596 -0
- package/dist/qr-size-measurer-DQdAsid1.d.ts +165 -0
- package/dist/recording-options-BPCpMHLK.d.ts +514 -0
- package/dist/{recording-replayer-r0DZ9ALp.d.ts → recording-replayer-DQKpAGyQ.d.ts} +2 -2
- package/dist/recording-slice-DaxHZvNu.d.ts +55 -0
- package/dist/{replay-engine-XpDa0dy1.d.ts → replay-engine-CmKh_4wf.d.ts} +7 -4
- package/dist/{replay-scene-Bw58VL6p.d.ts → replay-scene-DBlDrV_x.d.ts} +3 -3
- package/dist/sensors/absolute-orientation.d.ts +2 -0
- package/dist/sensors/absolute-orientation.js +145 -0
- package/dist/sensors/gps-error-handler.d.ts +1 -1
- package/dist/sensors/gps.d.ts +1 -1
- package/dist/sensors/index.d.ts +5 -4
- package/dist/sensors/index.js +2 -1
- package/dist/sensors/permission-checker.d.ts +1 -1
- package/dist/sensors/permission-checker.js +1 -1
- package/dist/state/app-selectors.d.ts +1 -1
- package/dist/state/combined-root-state.d.ts +1 -1
- package/dist/state/create-slam-app-store.d.ts +1 -1
- package/dist/state/create-slam-app-store.js +1 -74
- package/dist/state/gps-ar-pose-sampler.d.ts +3 -3
- package/dist/state/gps-event-coordinator.d.ts +2 -2
- package/dist/state/gps-event-coordinator.js +27 -17
- package/dist/state/index.d.ts +20 -17
- package/dist/state/index.js +5 -4
- package/dist/state/persistence-middleware.d.ts +1 -1
- package/dist/state/persistence-middleware.js +30 -19
- package/dist/state/qr-detected-slice.d.ts +4 -0
- package/dist/state/qr-detected-slice.js +311 -0
- package/dist/state/recording-options.d.ts +2 -2
- package/dist/state/recording-options.js +273 -7
- package/dist/state/recording-replayer.d.ts +2 -2
- package/dist/state/recording-replayer.js +1 -1
- package/dist/state/recording-slice.d.ts +1 -1
- package/dist/state/recording-slice.js +5 -1
- package/dist/state/replay-engine.d.ts +1 -1
- package/dist/state/replay-engine.js +7 -3
- package/dist/state/store-subscribers.d.ts +2 -2
- package/dist/state/subscribe-to-selector.d.ts +1 -1
- package/dist/state/tracking-quality.d.ts +2 -2
- package/dist/state/tracking-quality.js +40 -128
- package/dist/state/tracking-slice.d.ts +1 -1
- package/dist/{state-BkIfpiyA.js → state-E51QT_-j.js} +2 -1
- package/dist/storage/file-system-utils.d.ts +1 -1
- package/dist/storage/index.d.ts +9 -9
- package/dist/storage/index.js +4 -4
- package/dist/storage/null-storage-backend.d.ts +1 -1
- package/dist/storage/opfs-storage-backend.d.ts +1 -1
- package/dist/storage/opfs-storage-backend.js +3 -3
- package/dist/storage/opfs-storage.d.ts +1 -1
- package/dist/storage/opfs-storage.js +5 -4
- package/dist/storage/storage-backend.d.ts +1 -1
- package/dist/storage/zip-coverage-embed.d.ts +2 -0
- package/dist/storage/zip-coverage-embed.js +88 -0
- package/dist/storage/zip-export.d.ts +2 -2
- package/dist/storage/zip-export.js +37 -59
- package/dist/storage/zip-reader.d.ts +1 -1
- package/dist/{storage-backend-Do6YDpNo.d.ts → storage-backend-BdVLD0dU.d.ts} +1 -1
- package/dist/{store-subscribers-0YWyOLvV.d.ts → store-subscribers-CA6AJH_8.d.ts} +2 -2
- package/dist/{subscribe-to-selector-CZ6k1NFP.d.ts → subscribe-to-selector-B6EUmQ3I.d.ts} +1 -1
- package/dist/test-utils/browser-mocks.d.ts +7 -1
- package/dist/test-utils/browser-mocks.js +6 -0
- package/dist/test-utils/zip-round-trip-helpers.d.ts +9 -1
- package/dist/test-utils/zip-round-trip-helpers.js +5 -1
- package/dist/{tracking-slice-CdK8c-8P.d.ts → tracking-slice-BuAZxXZ5.d.ts} +1 -1
- package/dist/types/ar-types.d.ts +1 -1
- package/dist/types/geo-types.d.ts +1 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/utils/concurrency.d.ts +2 -2
- package/dist/utils/concurrency.js +66 -2
- package/dist/utils/failure-tracker.d.ts +1 -1
- package/dist/utils/format-file-size.d.ts +1 -1
- package/dist/utils/fused-path.d.ts +1 -1
- package/dist/utils/index.d.ts +8 -7
- package/dist/utils/index.js +2 -1
- package/dist/utils/list-formatter.d.ts +1 -1
- package/dist/utils/logger.d.ts +1 -1
- package/dist/visualization/accuracy-circles.d.ts +1 -1
- package/dist/visualization/alignment-lerper.d.ts +1 -1
- package/dist/visualization/ar-world-group-alignment.d.ts +1 -1
- package/dist/visualization/ar-world-group-alignment.js +1 -1
- package/dist/visualization/camera-follower.d.ts +1 -1
- package/dist/visualization/css3d-renderer-manager.d.ts +1 -1
- package/dist/visualization/frame-conversions.d.ts +1 -1
- package/dist/visualization/frustum-visibility.d.ts +1 -1
- package/dist/visualization/gps-anchor.d.ts +1 -1
- package/dist/visualization/gps-compass-cubes.d.ts +1 -1
- package/dist/visualization/gps-event-markers.d.ts +1 -1
- package/dist/visualization/gps-event-markers.js +40 -5
- package/dist/visualization/hit-test-reticle.d.ts +1 -1
- package/dist/visualization/hit-test-reticle.js +1 -0
- package/dist/visualization/index.d.ts +18 -18
- package/dist/visualization/leaflet-map-overlay.d.ts +1 -1
- package/dist/visualization/leaflet-map-overlay.js +15 -3
- package/dist/visualization/lerp-utils.d.ts +1 -1
- package/dist/visualization/map-data.d.ts +1 -1
- package/dist/visualization/map-overlay-draw.d.ts +1 -1
- package/dist/visualization/map-overlay.d.ts +1 -1
- package/dist/visualization/three-dispose.d.ts +1 -1
- package/dist/visualization/vis-colors.d.ts +1 -1
- package/dist/{webxr-session-TIaD883f.js → webxr-session-DG-eNfny.js} +244 -11
- package/dist/{webxr-session-GjZ2BIpS.d.ts → webxr-session-Do5L6G3B.d.ts} +86 -8
- package/dist/zip-coverage-embed-DCAREVmw.d.ts +41 -0
- package/dist/{zip-export-G14hkSPG.d.ts → zip-export-ol0S_Ofd.d.ts} +27 -23
- package/package.json +2 -2
- package/dist/concurrency-CO1FQvs3.d.ts +0 -24
- package/dist/file-system-CwXTNHmB.d.ts +0 -114
- package/dist/index-CF05QV2m.d.ts +0 -2
- package/dist/recording-options-DJBXuMxV.d.ts +0 -182
- package/dist/recording-slice-89O07ebs.d.ts +0 -36
- package/dist/storage/file-system.d.ts +0 -2
- package/dist/storage/file-system.js +0 -289
- /package/dist/{accuracy-circles-BpwWAs8G.d.ts → accuracy-circles-BtqOP_v1.d.ts} +0 -0
- /package/dist/{alignment-lerper-Dm9G7JJO.d.ts → alignment-lerper-Bil6vbVw.d.ts} +0 -0
- /package/dist/{ar-types-Dan1MVTi.d.ts → ar-types-BFX2r1wk.d.ts} +0 -0
- /package/dist/{bresenham3d-CFfd7n6B.d.ts → bresenham3d-CU1Gv2W4.d.ts} +0 -0
- /package/dist/{camera-follower-C2mwEn7c.d.ts → camera-follower-CqJJ7zRj.d.ts} +0 -0
- /package/dist/{capability-checker-CQXJHxXg.d.ts → capability-checker-DUQLg6gq.d.ts} +0 -0
- /package/dist/{capture-failure-tracker-CUeU3L_M.d.ts → capture-failure-tracker-gsm8q7yi.d.ts} +0 -0
- /package/dist/{chromium-camera-access-workaround-Jqbn0apH.d.ts → chromium-camera-access-workaround-D4K2uwwQ.d.ts} +0 -0
- /package/dist/{css3d-renderer-manager-ptu_t3B3.d.ts → css3d-renderer-manager-B3kGNtcn.d.ts} +0 -0
- /package/dist/{failure-tracker-Cn8Fo9D4.d.ts → failure-tracker-BO7M7jF_.d.ts} +0 -0
- /package/dist/{file-system-utils-CXIoL-DM.d.ts → file-system-utils-DOajnMm-.d.ts} +0 -0
- /package/dist/{format-file-size-CLCZz3lC.d.ts → format-file-size-Dc98ulzB.d.ts} +0 -0
- /package/dist/{frame-loop-CMewS8cY.d.ts → frame-loop-CpVjQ1dH.d.ts} +0 -0
- /package/dist/{frustum-visibility-CmI96ESS.d.ts → frustum-visibility-CeoCZchO.d.ts} +0 -0
- /package/dist/{geo-types-SaPnIEUM.d.ts → geo-types-3Ip1NzVR.d.ts} +0 -0
- /package/dist/{gps-VuezJWK3.d.ts → gps-BBO3hKfT.d.ts} +0 -0
- /package/dist/{gps-compass-cubes-BM1uGiYV.d.ts → gps-compass-cubes-DSN87L8p.d.ts} +0 -0
- /package/dist/{gps-error-handler-CJl_2MVm.d.ts → gps-error-handler-BbPsfw-_.d.ts} +0 -0
- /package/dist/{hit-test-reticle-BEzEybxZ.d.ts → hit-test-reticle-DV-2TpB9.d.ts} +0 -0
- /package/dist/{lerp-utils-BpV-0Dtk.d.ts → lerp-utils-BLmHhkVe.d.ts} +0 -0
- /package/dist/{list-formatter-RRcBe8Vg.d.ts → list-formatter-KNMS0ay2.d.ts} +0 -0
- /package/dist/{logger-BEHXsnnq.d.ts → logger-CLnPrHiD.d.ts} +0 -0
- /package/dist/{map-overlay-bcogY5fK.d.ts → map-overlay-Dh2B97MP.d.ts} +0 -0
- /package/dist/{permission-checker-GpAiSCwd.d.ts → permission-checker-CDuI8BwC.d.ts} +0 -0
- /package/dist/{scene-node-names-Bu0S66H1.d.ts → scene-node-names-BiShOQDB.d.ts} +0 -0
- /package/dist/{session-disposers-M-oashRH.js → session-disposers-zv9OGXPB.js} +0 -0
- /package/dist/{three-dispose-D950t71B.d.ts → three-dispose-Bt1k7WKh.d.ts} +0 -0
- /package/dist/{vis-colors-D6omC64K.d.ts → vis-colors-Obw2pLqT.d.ts} +0 -0
- /package/dist/{webxr-nue-basis-CqKQmZzT.d.ts → webxr-nue-basis-3nfjDJJp.d.ts} +0 -0
- /package/dist/{xr-camera-texture-DBwCaKTd.d.ts → xr-camera-texture-Cva1q3Qj.d.ts} +0 -0
- /package/dist/{xr-error-handler-C0AQRmxs.d.ts → xr-error-handler-CtvHpjfm.d.ts} +0 -0
- /package/dist/{xr-frame-loop-BtYytZJg.d.ts → xr-frame-loop-DIYtRcUa.d.ts} +0 -0
- /package/dist/{zip-reader-DSEXt7K6.d.ts → zip-reader-D9iPQwdQ.d.ts} +0 -0
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
import "./ar/camera-blit-capture.js";
|
|
2
|
+
import "./ar/chromium-camera-access-workaround.js";
|
|
3
|
+
import "./ar/capture-failure-tracker.js";
|
|
4
|
+
import "./ar/depth-sampler.js";
|
|
5
|
+
import "./ar/depth-unprojection.js";
|
|
6
|
+
import "./qr-size-measurer-DLb33geH.js";
|
|
7
|
+
import "./ar/qr-size-depth-context.js";
|
|
8
|
+
import { buildObjectPoints, transformPoint } from "./ar/qr-pose.js";
|
|
9
|
+
import "./ar/qr-derived-pose.js";
|
|
10
|
+
import "./ar/webxr-nue-basis.js";
|
|
11
|
+
import "./ar/qr-debug-view.js";
|
|
12
|
+
import { n as createDetectionScheduler } from "./qr-detection-controller-B-tLVd-S.js";
|
|
13
|
+
import "./qr-pose-aggregation-DBiaLW0O.js";
|
|
14
|
+
import "./webxr-session-DG-eNfny.js";
|
|
15
|
+
import "./ar/occupancy-grid.js";
|
|
16
|
+
import "./ar/frame-loop.js";
|
|
17
|
+
import "./ar/xr-frame-loop.js";
|
|
18
|
+
import "./image-capture-B2-b6tKK.js";
|
|
19
|
+
import "./ar/xr-camera-texture.js";
|
|
20
|
+
import "./ar/enable-gps-ar.js";
|
|
21
|
+
import "./ar/replay-scene.js";
|
|
22
|
+
//#region ../src/ar/qr-level.ts
|
|
23
|
+
/** Thrown when a fetched level file fails validation. */
|
|
24
|
+
var QrLevelValidationError = class extends Error {
|
|
25
|
+
constructor(message) {
|
|
26
|
+
super(`qr-level: ${message}`);
|
|
27
|
+
this.name = "QrLevelValidationError";
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
function isFiniteNumber(v) {
|
|
31
|
+
return typeof v === "number" && Number.isFinite(v);
|
|
32
|
+
}
|
|
33
|
+
function isRecord(v) {
|
|
34
|
+
return typeof v === "object" && v !== null;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Validate the optional `qr.physicalSizeM`. When present it MUST be a positive
|
|
38
|
+
* number (a `0`/negative authored size is a bug, not a "measure it instead"
|
|
39
|
+
* signal). Returns `undefined` when omitted.
|
|
40
|
+
*/
|
|
41
|
+
function parsePhysicalSize(value) {
|
|
42
|
+
if (value === void 0) return void 0;
|
|
43
|
+
if (!isFiniteNumber(value) || value <= 0) throw new QrLevelValidationError("\"qr.physicalSizeM\" must be a positive number when present");
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Validate the optional `qr.geo`. When present every field is validated (a
|
|
48
|
+
* partial geo is a bug — it would silently place the vote wrong). Returns
|
|
49
|
+
* `undefined` when omitted; heading is normalized into `[0, 360)`.
|
|
50
|
+
*/
|
|
51
|
+
function parseGeo(value) {
|
|
52
|
+
if (value === void 0) return void 0;
|
|
53
|
+
if (!isRecord(value)) throw new QrLevelValidationError("\"qr.geo\" must be an object when present");
|
|
54
|
+
const { lat, lon, alt, headingDeg } = value;
|
|
55
|
+
if (!isFiniteNumber(lat) || lat < -90 || lat > 90) throw new QrLevelValidationError("\"qr.geo.lat\" must be a number in [-90, 90]");
|
|
56
|
+
if (!isFiniteNumber(lon) || lon < -180 || lon > 180) throw new QrLevelValidationError("\"qr.geo.lon\" must be a number in [-180, 180]");
|
|
57
|
+
if (!isFiniteNumber(alt)) throw new QrLevelValidationError("\"qr.geo.alt\" must be a finite number");
|
|
58
|
+
if (!isFiniteNumber(headingDeg)) throw new QrLevelValidationError("\"qr.geo.headingDeg\" must be a finite number");
|
|
59
|
+
return {
|
|
60
|
+
lat,
|
|
61
|
+
lon,
|
|
62
|
+
alt,
|
|
63
|
+
headingDeg: (headingDeg % 360 + 360) % 360
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Validate an already-parsed value as a {@link QrLevel}. Throws
|
|
68
|
+
* {@link QrLevelValidationError} with a descriptive message on any violation.
|
|
69
|
+
*/
|
|
70
|
+
function parseQrLevel(data) {
|
|
71
|
+
if (!isRecord(data)) throw new QrLevelValidationError("level file must be a JSON object");
|
|
72
|
+
if (!isFiniteNumber(data.version)) throw new QrLevelValidationError("missing/invalid \"version\"");
|
|
73
|
+
if (!isRecord(data.qr)) throw new QrLevelValidationError("missing/invalid \"qr\"");
|
|
74
|
+
const physicalSizeM = parsePhysicalSize(data.qr.physicalSizeM);
|
|
75
|
+
const geo = parseGeo(data.qr.geo);
|
|
76
|
+
return {
|
|
77
|
+
version: data.version,
|
|
78
|
+
qr: {
|
|
79
|
+
...physicalSizeM !== void 0 ? { physicalSizeM } : {},
|
|
80
|
+
...geo !== void 0 ? { geo } : {}
|
|
81
|
+
},
|
|
82
|
+
content: "content" in data ? data.content : void 0
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Fetch and validate a level file from `url`. Rejects with
|
|
87
|
+
* {@link QrLevelValidationError} on a non-OK response, non-JSON body, or a
|
|
88
|
+
* schema violation.
|
|
89
|
+
*/
|
|
90
|
+
async function fetchQrLevel(url, options = {}) {
|
|
91
|
+
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
92
|
+
if (!fetchImpl) throw new QrLevelValidationError("no fetch implementation available");
|
|
93
|
+
let response;
|
|
94
|
+
try {
|
|
95
|
+
response = await fetchImpl(url, { signal: options.signal });
|
|
96
|
+
} catch (err) {
|
|
97
|
+
throw new QrLevelValidationError(`fetch failed for ${url}: ${err instanceof Error ? err.message : String(err)}`);
|
|
98
|
+
}
|
|
99
|
+
if (!response.ok) throw new QrLevelValidationError(`fetch ${url} returned status ${response.status}`);
|
|
100
|
+
let body;
|
|
101
|
+
try {
|
|
102
|
+
body = await response.json();
|
|
103
|
+
} catch {
|
|
104
|
+
throw new QrLevelValidationError(`response for ${url} was not valid JSON`);
|
|
105
|
+
}
|
|
106
|
+
return parseQrLevel(body);
|
|
107
|
+
}
|
|
108
|
+
//#endregion
|
|
109
|
+
//#region ../src/ar/qr-gps-vote.ts
|
|
110
|
+
/** Mean meters per degree of latitude (WGS-84 average). */
|
|
111
|
+
const METERS_PER_DEG_LAT = 111320;
|
|
112
|
+
/**
|
|
113
|
+
* `count` points evenly spaced on a circle of radius `baselineM` in the QR
|
|
114
|
+
* plane (z = 0), starting at local +x. The symmetric ring's centroid is the QR
|
|
115
|
+
* center, so translation stays anchored there while the wide radius stiffens
|
|
116
|
+
* the rotation fit. Non-collinear for `count ≥ 3`.
|
|
117
|
+
*/
|
|
118
|
+
function widePlanePoints(baselineM, count) {
|
|
119
|
+
const points = [];
|
|
120
|
+
for (let i = 0; i < count; i++) {
|
|
121
|
+
const theta = 2 * Math.PI * i / count;
|
|
122
|
+
points.push([
|
|
123
|
+
baselineM * Math.cos(theta),
|
|
124
|
+
baselineM * Math.sin(theta),
|
|
125
|
+
0
|
|
126
|
+
]);
|
|
127
|
+
}
|
|
128
|
+
return points;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Map a QR-local plane offset (meters; +x right, +y up on the printed face) to
|
|
132
|
+
* an East/North/Up offset, given the QR's compass heading. Vertical-QR
|
|
133
|
+
* convention: +y is world up; +x is horizontal at `headingDeg` (clockwise from
|
|
134
|
+
* North), so east = x·sin(h), north = x·cos(h).
|
|
135
|
+
*/
|
|
136
|
+
function localPlaneToEnu(localX, localY, headingDeg) {
|
|
137
|
+
const h = headingDeg * Math.PI / 180;
|
|
138
|
+
return {
|
|
139
|
+
east: localX * Math.sin(h),
|
|
140
|
+
north: localX * Math.cos(h),
|
|
141
|
+
up: localY
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
/** Apply an ENU meter offset to a geo pose (equirectangular; exact enough for sub-meter QR corners). */
|
|
145
|
+
function offsetGeo(center, enu) {
|
|
146
|
+
const dLat = enu.north / METERS_PER_DEG_LAT;
|
|
147
|
+
const metersPerDegLon = METERS_PER_DEG_LAT * Math.cos(center.lat * Math.PI / 180);
|
|
148
|
+
const dLon = Math.abs(metersPerDegLon) < 1e-9 ? 0 : enu.east / metersPerDegLon;
|
|
149
|
+
return {
|
|
150
|
+
latitude: center.lat + dLat,
|
|
151
|
+
longitude: center.lon + dLon,
|
|
152
|
+
altitude: center.alt + enu.up
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Build the synthetic `recordGpsEvent` payload(s) for one QR detection. Returns
|
|
157
|
+
* 4 corner correspondences by default, or 1 center correspondence when
|
|
158
|
+
* `multiCorrespondence` is false. Each payload pairs the corner's odom position
|
|
159
|
+
* (from the solved pose) with its absolute geo position (from the level file),
|
|
160
|
+
* stamped with the tiny synthetic accuracy.
|
|
161
|
+
*
|
|
162
|
+
* @throws RangeError on a non-positive `sizeM` or `syntheticAccuracyM`.
|
|
163
|
+
*/
|
|
164
|
+
function buildQrGpsVotes(input) {
|
|
165
|
+
const { qrPoseWorld, sizeM, qrGeo, syntheticAccuracyM, odomRotation = qrPoseWorld.rotation, multiCorrespondence = true, baselineM, count = 4, timestamp = Date.now(), idPrefix = "qr" } = input;
|
|
166
|
+
if (!(syntheticAccuracyM > 0) || !Number.isFinite(syntheticAccuracyM)) throw new RangeError(`qr-gps-vote: syntheticAccuracyM must be positive, got ${syntheticAccuracyM}`);
|
|
167
|
+
const useWideBaseline = baselineM !== void 0 && Number.isFinite(baselineM) && baselineM > 0;
|
|
168
|
+
if (useWideBaseline && count < 3) throw new RangeError(`qr-gps-vote: wide-baseline count must be >= 3 (collinear otherwise), got ${count}`);
|
|
169
|
+
return (useWideBaseline ? widePlanePoints(baselineM, count) : multiCorrespondence ? buildObjectPoints(sizeM) : [[
|
|
170
|
+
0,
|
|
171
|
+
0,
|
|
172
|
+
0
|
|
173
|
+
]]).map((local, i) => {
|
|
174
|
+
const odomPosition = transformPoint(local, qrPoseWorld);
|
|
175
|
+
const geo = offsetGeo(qrGeo, localPlaneToEnu(local[0], local[1], qrGeo.headingDeg));
|
|
176
|
+
return {
|
|
177
|
+
odomPosition,
|
|
178
|
+
odomRotation,
|
|
179
|
+
rawGpsPoint: {
|
|
180
|
+
id: `${idPrefix}-${timestamp}-${i}`,
|
|
181
|
+
latitude: geo.latitude,
|
|
182
|
+
longitude: geo.longitude,
|
|
183
|
+
altitude: geo.altitude,
|
|
184
|
+
latLongAccuracy: syntheticAccuracyM,
|
|
185
|
+
timestamp
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
//#endregion
|
|
191
|
+
//#region ../src/ar/qr-tracking-controller.ts
|
|
192
|
+
function createQrTrackingController(config) {
|
|
193
|
+
const { frontEnd, solvePose, fetchLevel, dispatchVotes, onDetection, resolveSizeM, resolveStablePose, getCameraPose, getIntrinsics, syntheticAccuracyM, voteBaselineM, voteCount, isPlausible, onStatus, onLocked, onError, minIntervalMs = 150, requiredLockCount = 3, now } = config;
|
|
194
|
+
const timestampNow = now ?? (() => Date.now());
|
|
195
|
+
let status = "idle";
|
|
196
|
+
const levelCache = /* @__PURE__ */ new Map();
|
|
197
|
+
let active = null;
|
|
198
|
+
function setStatus(next) {
|
|
199
|
+
if (status === next) return;
|
|
200
|
+
status = next;
|
|
201
|
+
onStatus?.(next);
|
|
202
|
+
}
|
|
203
|
+
async function ensureLevel(url) {
|
|
204
|
+
const cached = levelCache.get(url);
|
|
205
|
+
if (cached) return cached;
|
|
206
|
+
setStatus("loading-level");
|
|
207
|
+
const level = await fetchLevel(url);
|
|
208
|
+
levelCache.set(url, level);
|
|
209
|
+
return level;
|
|
210
|
+
}
|
|
211
|
+
async function detect(image) {
|
|
212
|
+
if (status === "idle" || status === "error") setStatus("scanning");
|
|
213
|
+
const detection = await frontEnd.detect(image);
|
|
214
|
+
if (!detection) {
|
|
215
|
+
active = null;
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
const level = await ensureLevel(detection.text);
|
|
219
|
+
const sizeM = level.qr.physicalSizeM ?? resolveSizeM?.(detection.text, level) ?? null;
|
|
220
|
+
if (sizeM === null || !(sizeM > 0) || !Number.isFinite(sizeM)) {
|
|
221
|
+
active = null;
|
|
222
|
+
if (status === "loading-level") setStatus("scanning");
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
const cameraPose = getCameraPose();
|
|
226
|
+
const intrinsics = getIntrinsics(image);
|
|
227
|
+
if (!cameraPose || !intrinsics) {
|
|
228
|
+
active = null;
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
const solution = solvePose({
|
|
232
|
+
imagePoints: detection.corners,
|
|
233
|
+
sizeM,
|
|
234
|
+
intrinsics,
|
|
235
|
+
cameraPose
|
|
236
|
+
});
|
|
237
|
+
if (!solution) {
|
|
238
|
+
active = null;
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
if (isPlausible && !isPlausible(solution, cameraPose)) {
|
|
242
|
+
active = null;
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
active = {
|
|
246
|
+
level,
|
|
247
|
+
text: detection.text,
|
|
248
|
+
sizeM
|
|
249
|
+
};
|
|
250
|
+
return solution;
|
|
251
|
+
}
|
|
252
|
+
const scheduler = createDetectionScheduler({
|
|
253
|
+
detect,
|
|
254
|
+
minIntervalMs,
|
|
255
|
+
requiredLockCount,
|
|
256
|
+
now,
|
|
257
|
+
onLocked: (solution) => {
|
|
258
|
+
const current = active;
|
|
259
|
+
if (!current) return;
|
|
260
|
+
const { level, text, sizeM } = current;
|
|
261
|
+
onDetection?.({
|
|
262
|
+
text,
|
|
263
|
+
qrPoseWorld: solution.qrPoseWorld,
|
|
264
|
+
qrPoseInCamera: solution.qrPoseInCamera,
|
|
265
|
+
reprojectionErrorPx: solution.reprojectionErrorPx,
|
|
266
|
+
timestamp: timestampNow()
|
|
267
|
+
});
|
|
268
|
+
if (level.qr.geo) {
|
|
269
|
+
const votePose = resolveStablePose ? resolveStablePose(text) : solution.qrPoseWorld;
|
|
270
|
+
if (votePose) dispatchVotes(buildQrGpsVotes({
|
|
271
|
+
qrPoseWorld: votePose,
|
|
272
|
+
sizeM,
|
|
273
|
+
qrGeo: level.qr.geo,
|
|
274
|
+
syntheticAccuracyM,
|
|
275
|
+
...voteBaselineM !== void 0 ? { baselineM: voteBaselineM } : {},
|
|
276
|
+
...voteCount !== void 0 ? { count: voteCount } : {}
|
|
277
|
+
}));
|
|
278
|
+
}
|
|
279
|
+
setStatus("tracking");
|
|
280
|
+
onLocked?.(solution, level);
|
|
281
|
+
},
|
|
282
|
+
onMiss: () => {
|
|
283
|
+
if (status === "tracking") setStatus("scanning");
|
|
284
|
+
},
|
|
285
|
+
onError: (err) => {
|
|
286
|
+
active = null;
|
|
287
|
+
setStatus("error");
|
|
288
|
+
onError?.(err);
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
return {
|
|
292
|
+
offerFrame(image) {
|
|
293
|
+
scheduler.offerFrame(image);
|
|
294
|
+
},
|
|
295
|
+
get status() {
|
|
296
|
+
return status;
|
|
297
|
+
},
|
|
298
|
+
reset() {
|
|
299
|
+
levelCache.clear();
|
|
300
|
+
active = null;
|
|
301
|
+
setStatus("idle");
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
//#endregion
|
|
306
|
+
//#region ../src/ar/qr-occupancy-check.ts
|
|
307
|
+
const DEFAULTS = {
|
|
308
|
+
surfaceToleranceCells: 1.5,
|
|
309
|
+
minObservations: 1,
|
|
310
|
+
rayCorridorCells: 1,
|
|
311
|
+
occlusionMarginCells: 2
|
|
312
|
+
};
|
|
313
|
+
function distance(a, b) {
|
|
314
|
+
return Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]);
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Classify a solved QR center against the occupancy grid.
|
|
318
|
+
*
|
|
319
|
+
* - `no-grid` (ok): the grid has no occupied voxels — nothing to check against.
|
|
320
|
+
* - `floating` (not ok): nearest occupied voxel is farther than the tolerance.
|
|
321
|
+
* - `behind-surface` (not ok): an occupied voxel lies on the camera→QR ray
|
|
322
|
+
* clearly in front of the QR (the QR is inside / beyond geometry).
|
|
323
|
+
* - `plausible` (ok): on a surface with empty space toward the camera.
|
|
324
|
+
*
|
|
325
|
+
* All positions are raw-WebXR/odom space (the same frame as `qrPoseWorld`).
|
|
326
|
+
*/
|
|
327
|
+
function checkQrPlausibility(qrCenterWorld, cameraPosWorld, grid, options = {}) {
|
|
328
|
+
const surfaceToleranceCells = options.surfaceToleranceCells ?? DEFAULTS.surfaceToleranceCells;
|
|
329
|
+
const minObservations = options.minObservations ?? DEFAULTS.minObservations;
|
|
330
|
+
const rayCorridorCells = options.rayCorridorCells ?? DEFAULTS.rayCorridorCells;
|
|
331
|
+
const occlusionMarginCells = options.occlusionMarginCells ?? DEFAULTS.occlusionMarginCells;
|
|
332
|
+
const cells = grid.getOccupiedCells(minObservations);
|
|
333
|
+
if (cells.length === 0) return {
|
|
334
|
+
verdict: "no-grid",
|
|
335
|
+
ok: true,
|
|
336
|
+
nearestSurfaceDistanceM: Infinity
|
|
337
|
+
};
|
|
338
|
+
const cellSizeM = grid.cellSizeM;
|
|
339
|
+
const centers = cells.map((c) => grid.getCellCenter(c));
|
|
340
|
+
let nearest = Infinity;
|
|
341
|
+
for (const center of centers) {
|
|
342
|
+
const d = distance(center, qrCenterWorld);
|
|
343
|
+
if (d < nearest) nearest = d;
|
|
344
|
+
}
|
|
345
|
+
if (nearest > surfaceToleranceCells * cellSizeM) return {
|
|
346
|
+
verdict: "floating",
|
|
347
|
+
ok: false,
|
|
348
|
+
nearestSurfaceDistanceM: nearest
|
|
349
|
+
};
|
|
350
|
+
const dir = [
|
|
351
|
+
qrCenterWorld[0] - cameraPosWorld[0],
|
|
352
|
+
qrCenterWorld[1] - cameraPosWorld[1],
|
|
353
|
+
qrCenterWorld[2] - cameraPosWorld[2]
|
|
354
|
+
];
|
|
355
|
+
const rayLength = Math.hypot(dir[0], dir[1], dir[2]);
|
|
356
|
+
if (rayLength > cellSizeM) {
|
|
357
|
+
const nx = dir[0] / rayLength;
|
|
358
|
+
const ny = dir[1] / rayLength;
|
|
359
|
+
const nz = dir[2] / rayLength;
|
|
360
|
+
const corridor = rayCorridorCells * cellSizeM;
|
|
361
|
+
const tMax = rayLength - occlusionMarginCells * cellSizeM;
|
|
362
|
+
for (const center of centers) {
|
|
363
|
+
const ox = center[0] - cameraPosWorld[0];
|
|
364
|
+
const oy = center[1] - cameraPosWorld[1];
|
|
365
|
+
const oz = center[2] - cameraPosWorld[2];
|
|
366
|
+
const t = ox * nx + oy * ny + oz * nz;
|
|
367
|
+
if (t <= cellSizeM || t >= tMax) continue;
|
|
368
|
+
if (Math.hypot(ox - t * nx, oy - t * ny, oz - t * nz) < corridor) return {
|
|
369
|
+
verdict: "behind-surface",
|
|
370
|
+
ok: false,
|
|
371
|
+
nearestSurfaceDistanceM: nearest
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return {
|
|
376
|
+
verdict: "plausible",
|
|
377
|
+
ok: true,
|
|
378
|
+
nearestSurfaceDistanceM: nearest
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
//#endregion
|
|
382
|
+
export { localPlaneToEnu as a, fetchQrLevel as c, buildQrGpsVotes as i, parseQrLevel as l, createQrTrackingController as n, offsetGeo as o, METERS_PER_DEG_LAT as r, QrLevelValidationError as s, checkQrPlausibility as t };
|
|
@@ -31,6 +31,28 @@ declare function computeCaptureSize(cameraWidth: number, cameraHeight: number, d
|
|
|
31
31
|
width: number;
|
|
32
32
|
height: number;
|
|
33
33
|
};
|
|
34
|
+
/**
|
|
35
|
+
* Compute blit dimensions that PRESERVE the camera aspect ratio with the
|
|
36
|
+
* **longer edge fixed at `maxEdge`**. Unlike {@link computeCaptureSize} (which
|
|
37
|
+
* divides the native resolution by a user divisor), this fits the frame into a
|
|
38
|
+
* fixed pixel budget, so a 4:3 camera becomes e.g. 512×384 — NOT a stretched
|
|
39
|
+
* 512×512. Used by the QR-detection blit (B2) so the detector sees an
|
|
40
|
+
* undistorted code while the readback cost stays bounded by `maxEdge²`.
|
|
41
|
+
*
|
|
42
|
+
* @param cameraWidth - Native camera width in pixels (from XRCamera)
|
|
43
|
+
* @param cameraHeight - Native camera height in pixels (from XRCamera)
|
|
44
|
+
* @param maxEdge - Target length (px) of the longer output edge.
|
|
45
|
+
* @returns Integer dimensions, longer edge == `maxEdge`, aspect preserved
|
|
46
|
+
* (within rounding), each clamped to ≥ 1. When `maxEdge` itself is invalid
|
|
47
|
+
* (< 1 / NaN / Infinity) the longer edge falls back to
|
|
48
|
+
* `DEFAULT_BLIT_CONFIG.width` (still aspect-preserving). When the camera
|
|
49
|
+
* dimensions are invalid (≤ 0 / NaN / Infinity) the aspect is unknown, so it
|
|
50
|
+
* returns a square at the (possibly defaulted) edge.
|
|
51
|
+
*/
|
|
52
|
+
declare function computeAspectFitSize(cameraWidth: number, cameraHeight: number, maxEdge: number): {
|
|
53
|
+
width: number;
|
|
54
|
+
height: number;
|
|
55
|
+
};
|
|
34
56
|
/**
|
|
35
57
|
* Captures readable pixel data from WebXR opaque camera textures
|
|
36
58
|
* using the intermediate render target "blit" technique.
|
|
@@ -83,6 +105,29 @@ declare class CameraBlitCapture {
|
|
|
83
105
|
width: number;
|
|
84
106
|
height: number;
|
|
85
107
|
} | null;
|
|
108
|
+
/**
|
|
109
|
+
* Capture the camera texture as **top-left-origin** RGBA — the orientation
|
|
110
|
+
* QR detection (and `BarcodeDetector`) expects — returning a FRESH copy that
|
|
111
|
+
* is safe to retain beyond the next capture.
|
|
112
|
+
*
|
|
113
|
+
* This is the efficient replacement for the demo's old JPEG round-trip: blit
|
|
114
|
+
* + readback (shared with {@link captureToBlob} / {@link captureToPixels}),
|
|
115
|
+
* then the same vertical flip the JPEG encoder applies, but with no encode →
|
|
116
|
+
* decode. Unlike {@link captureToPixels} (which returns the reusable internal
|
|
117
|
+
* buffer in WebGL bottom-row-first order), this returns an owned,
|
|
118
|
+
* correctly-oriented `Uint8ClampedArray`.
|
|
119
|
+
*
|
|
120
|
+
* IMPORTANT: Must be called within the XR animation frame callback, while the
|
|
121
|
+
* camera texture is valid.
|
|
122
|
+
*
|
|
123
|
+
* @returns `{ data, width, height }` (top-left RGBA), or null on
|
|
124
|
+
* failure/dispose.
|
|
125
|
+
*/
|
|
126
|
+
captureToRgba(renderer: THREE.WebGLRenderer, cameraTexture: THREE.Texture): {
|
|
127
|
+
data: Uint8ClampedArray;
|
|
128
|
+
width: number;
|
|
129
|
+
height: number;
|
|
130
|
+
} | null;
|
|
86
131
|
/**
|
|
87
132
|
* Check if a pixel buffer is entirely black (all zeros).
|
|
88
133
|
* Uses sampling for performance (checks BLACK_CHECK_SAMPLE_COUNT evenly-spaced pixels).
|
|
@@ -100,13 +145,21 @@ declare class CameraBlitCapture {
|
|
|
100
145
|
*/
|
|
101
146
|
private pixelsToJpegBlob;
|
|
102
147
|
private getFlippedImageData;
|
|
148
|
+
/**
|
|
149
|
+
* Produce a FRESH, top-left-origin RGBA copy of the internal readback buffer.
|
|
150
|
+
* WebGL `readPixels` returns bottom-row-first; this copies (so the internal
|
|
151
|
+
* buffer stays y-flipped for a subsequent capture) and swaps rows. Shared by
|
|
152
|
+
* the JPEG encode path ({@link getFlippedImageData}) and {@link captureToRgba}.
|
|
153
|
+
*/
|
|
154
|
+
private flippedPixelCopy;
|
|
103
155
|
private pixelsToJpegViaOffscreenCanvas;
|
|
104
156
|
private pixelsToJpegViaCanvas;
|
|
105
157
|
/**
|
|
106
|
-
* Flip
|
|
107
|
-
* WebGL readPixels returns bottom-row-first; canvas
|
|
158
|
+
* Flip an RGBA buffer vertically in-place (row swap).
|
|
159
|
+
* WebGL readPixels returns bottom-row-first; canvas / detectors expect
|
|
160
|
+
* top-row-first.
|
|
108
161
|
*/
|
|
109
|
-
private
|
|
162
|
+
private flipRowsVertically;
|
|
110
163
|
/**
|
|
111
164
|
* Resize the render target and pixel buffer to match new dimensions.
|
|
112
165
|
* Call this when the camera resolution is first known or changes.
|
|
@@ -134,4 +187,4 @@ declare class CameraBlitCapture {
|
|
|
134
187
|
dispose(): void;
|
|
135
188
|
}
|
|
136
189
|
//#endregion
|
|
137
|
-
export { computeCaptureSize as i, CameraBlitCaptureConfig as n, DEFAULT_BLIT_CONFIG as r, CameraBlitCapture as t };
|
|
190
|
+
export { computeCaptureSize as a, computeAspectFitSize as i, CameraBlitCaptureConfig as n, DEFAULT_BLIT_CONFIG as r, CameraBlitCapture as t };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
//#region ../src/ar/capture-motion-gate.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* User-/consumer-facing motion-filter configuration. Shared by both config
|
|
4
|
+
* shapes that carry it — `ImageCaptureConfig` (what `ImageCaptureManager`
|
|
5
|
+
* consumes) and `ImageCaptureOptions` (the persisted recorder options) — so the
|
|
6
|
+
* one definition cannot drift between them. The window size and glitch ceilings
|
|
7
|
+
* are deliberately NOT here: they are internal tuning constants, not exposed.
|
|
8
|
+
*/
|
|
9
|
+
interface MotionFilterConfig {
|
|
10
|
+
/** Master switch for the motion gate. Default `true`. */
|
|
11
|
+
enabled: boolean;
|
|
12
|
+
/** Angular-velocity threshold (rad/s); at/below this a frame is "calm". */
|
|
13
|
+
maxAngularVelocity: number;
|
|
14
|
+
/** Linear-velocity threshold (m/s); at/below this a frame is "calm". */
|
|
15
|
+
maxLinearVelocity: number;
|
|
16
|
+
/**
|
|
17
|
+
* Never-calm safety fallback: once a due capture has waited this many ms it
|
|
18
|
+
* fires regardless of motion, so an interval is never silently lost. A
|
|
19
|
+
* sensible value is ~2× the capture interval.
|
|
20
|
+
*/
|
|
21
|
+
maxWaitMs: number;
|
|
22
|
+
}
|
|
23
|
+
//#endregion
|
|
24
|
+
export { MotionFilterConfig as t };
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Angular velocity (rad/s) above which a sample is treated as a tracking glitch
|
|
3
|
+
* (e.g. relocalization teleport), not real motion. A genuine hand/device turn
|
|
4
|
+
* tops out well below this; a teleport is far above it. Module-internal tuning
|
|
5
|
+
* constant (not exported — the gate's user surface is only the four
|
|
6
|
+
* `MotionFilterConfig` fields); overridable per-instance via the `MotionWindow`
|
|
7
|
+
* constructor for tests.
|
|
8
|
+
*/
|
|
9
|
+
const ANGULAR_GLITCH_CEILING_RAD_S = 50;
|
|
10
|
+
/**
|
|
11
|
+
* Linear velocity (m/s) above which a sample is treated as a tracking glitch.
|
|
12
|
+
* ~20 m/s (72 km/h) is unreachable by handheld scanning but trivially exceeded
|
|
13
|
+
* by a relocalization origin jump. Module-internal tuning constant (see
|
|
14
|
+
* {@link ANGULAR_GLITCH_CEILING_RAD_S}).
|
|
15
|
+
*/
|
|
16
|
+
const LINEAR_GLITCH_CEILING_M_S = 20;
|
|
17
|
+
/**
|
|
18
|
+
* Default motion-filter configuration. Enabled by default (plan §1). Thresholds
|
|
19
|
+
* are PLACEHOLDERS pending on-device field tuning (plan §7) — record measured
|
|
20
|
+
* values in implementation-progress.md once known. `maxWaitMs` of 4000 ms is
|
|
21
|
+
* 2× the default 2000 ms image interval.
|
|
22
|
+
*/
|
|
23
|
+
const DEFAULT_MOTION_FILTER = {
|
|
24
|
+
enabled: true,
|
|
25
|
+
maxAngularVelocity: .6,
|
|
26
|
+
maxLinearVelocity: .5,
|
|
27
|
+
maxWaitMs: 4e3
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Decide whether a due capture should fire now or defer to a later frame.
|
|
31
|
+
*
|
|
32
|
+
* A frame is "calm" when BOTH windowed maxima are at/below their thresholds —
|
|
33
|
+
* an empty window reports `Infinity`, so it is never calm (no capture before a
|
|
34
|
+
* valid sample exists). If not calm, the capture is deferred UNTIL `msSinceDue`
|
|
35
|
+
* reaches `maxWaitMs`, at which point it fires regardless to guarantee an
|
|
36
|
+
* interval is never silently lost (worst case: one blurry frame, not a gap).
|
|
37
|
+
*/
|
|
38
|
+
function decideCapture(input) {
|
|
39
|
+
if (input.windowMaxAngular <= input.maxAngularVelocity && input.windowMaxLinear <= input.maxLinearVelocity) return "capture";
|
|
40
|
+
if (input.msSinceDue >= input.maxWaitMs) return "capture";
|
|
41
|
+
return "defer";
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Fixed-size ring of the most recent VALID (non-glitch) per-frame velocities.
|
|
45
|
+
*
|
|
46
|
+
* `maxAngular()`/`maxLinear()` return the max over the retained samples, or
|
|
47
|
+
* `Infinity` when empty so "no data yet" reads as not-calm in
|
|
48
|
+
* {@link decideCapture}. A sample exceeding either glitch ceiling is rejected
|
|
49
|
+
* outright (`push` returns `false`) and never stored, so a relocalization spike
|
|
50
|
+
* neither pollutes the window (no spurious defer) nor is treated as motion.
|
|
51
|
+
*/
|
|
52
|
+
var MotionWindow = class {
|
|
53
|
+
angularGlitchCeiling;
|
|
54
|
+
linearGlitchCeiling;
|
|
55
|
+
size;
|
|
56
|
+
angular = [];
|
|
57
|
+
linear = [];
|
|
58
|
+
constructor(size = 3, angularGlitchCeiling = ANGULAR_GLITCH_CEILING_RAD_S, linearGlitchCeiling = LINEAR_GLITCH_CEILING_M_S) {
|
|
59
|
+
this.angularGlitchCeiling = angularGlitchCeiling;
|
|
60
|
+
this.linearGlitchCeiling = linearGlitchCeiling;
|
|
61
|
+
this.size = Number.isFinite(size) && size >= 1 ? Math.floor(size) : 1;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Record a per-frame velocity pair. Returns `false` (and stores nothing) when
|
|
65
|
+
* the sample exceeds either glitch ceiling, `true` otherwise.
|
|
66
|
+
*/
|
|
67
|
+
push(angularVel, linearVel) {
|
|
68
|
+
if (!Number.isFinite(angularVel) || !Number.isFinite(linearVel) || angularVel > this.angularGlitchCeiling || linearVel > this.linearGlitchCeiling) return false;
|
|
69
|
+
this.angular.push(angularVel);
|
|
70
|
+
this.linear.push(linearVel);
|
|
71
|
+
if (this.angular.length > this.size) this.angular.shift();
|
|
72
|
+
if (this.linear.length > this.size) this.linear.shift();
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
/** Whether the window holds at least one valid (non-glitch) sample yet. */
|
|
76
|
+
hasSamples() {
|
|
77
|
+
return this.angular.length > 0;
|
|
78
|
+
}
|
|
79
|
+
/** Max angular velocity over the retained window, or `Infinity` if empty. */
|
|
80
|
+
maxAngular() {
|
|
81
|
+
return this.angular.length === 0 ? Number.POSITIVE_INFINITY : Math.max(...this.angular);
|
|
82
|
+
}
|
|
83
|
+
/** Max linear velocity over the retained window, or `Infinity` if empty. */
|
|
84
|
+
maxLinear() {
|
|
85
|
+
return this.linear.length === 0 ? Number.POSITIVE_INFINITY : Math.max(...this.linear);
|
|
86
|
+
}
|
|
87
|
+
/** Clear all samples (e.g. on tracking loss / capture restart). */
|
|
88
|
+
reset() {
|
|
89
|
+
this.angular.length = 0;
|
|
90
|
+
this.linear.length = 0;
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
//#endregion
|
|
94
|
+
export { MotionWindow as n, decideCapture as r, DEFAULT_MOTION_FILTER as t };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
//#region ../src/utils/concurrency.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Concurrency Utilities
|
|
4
|
+
*
|
|
5
|
+
* Provides helpers for limiting the number of concurrent async operations.
|
|
6
|
+
* Used to prevent excessive memory consumption when scanning many files
|
|
7
|
+
* in parallel (e.g., reading zip metadata during scenario discovery).
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Map over an array with a concurrency limit on the async mapper function.
|
|
11
|
+
*
|
|
12
|
+
* Behaves like `Promise.all(items.map(fn))` but limits how many mapper
|
|
13
|
+
* invocations run simultaneously. Results are returned in the same order
|
|
14
|
+
* as the input items.
|
|
15
|
+
*
|
|
16
|
+
* @param items - Array of items to process
|
|
17
|
+
* @param limit - Maximum number of concurrent mapper invocations
|
|
18
|
+
* @param mapper - Async function to apply to each item
|
|
19
|
+
* @returns Array of results in the same order as input items
|
|
20
|
+
* @throws Re-throws the first error from any mapper invocation (fail-fast)
|
|
21
|
+
*/
|
|
22
|
+
declare function mapWithConcurrencyLimit<T, R>(items: T[], limit: number, mapper: (item: T, index: number) => Promise<R>): Promise<R[]>;
|
|
23
|
+
/**
|
|
24
|
+
* Run an async worker over each item with a concurrency cap, invoking the worker
|
|
25
|
+
* per item as the pool pulls it (rather than collecting results into an array).
|
|
26
|
+
*
|
|
27
|
+
* Unlike {@link mapWithConcurrencyLimit}, this returns no values — the worker is
|
|
28
|
+
* expected to produce its side effect (e.g. emit a streaming callback) itself.
|
|
29
|
+
* This is the shape progressive consumers need: each item is handled and can be
|
|
30
|
+
* reported as soon as it settles, instead of waiting for the whole batch.
|
|
31
|
+
*
|
|
32
|
+
* Completion order is **not** guaranteed (it depends on which worker finishes
|
|
33
|
+
* first). Each item's worker is invoked exactly once unless the run is aborted.
|
|
34
|
+
*
|
|
35
|
+
* When an `AbortSignal` is supplied, workers stop pulling **new** items as soon
|
|
36
|
+
* as the signal is aborted (checked before each pull). Items already in flight
|
|
37
|
+
* run to completion — the File System Access reads underneath cannot be torn
|
|
38
|
+
* mid-read — but no further items are started, so a destroyed consumer stops
|
|
39
|
+
* accumulating work. If the signal is already aborted, no work starts at all.
|
|
40
|
+
*
|
|
41
|
+
* @param items - Items to process
|
|
42
|
+
* @param limit - Maximum number of concurrent worker invocations (`>= 1`)
|
|
43
|
+
* @param worker - Async side-effecting function applied to each item
|
|
44
|
+
* @param signal - Optional abort signal that halts pulling new items
|
|
45
|
+
* @throws RangeError if `limit < 1`
|
|
46
|
+
* @throws Re-throws the first error from any worker invocation (fail-fast)
|
|
47
|
+
*/
|
|
48
|
+
declare function forEachWithConcurrencyLimit<T>(items: readonly T[], limit: number, worker: (item: T, index: number) => Promise<void>, signal?: AbortSignal): Promise<void>;
|
|
49
|
+
//#endregion
|
|
50
|
+
export { mapWithConcurrencyLimit as n, forEachWithConcurrencyLimit as t };
|
package/dist/core/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as LibraryRootState, c as RootState, d as
|
|
2
|
-
export { ArImageCapture, GpsPoint, LatLong, LatLongAlt, LibraryRootState, Matrix4, Quaternion, RootState, Vector3, calcGpsCoords, calcRelativeCoordsInMeters, createGpsSlamStore, isIdentityMatrix4, odometryTrackingRestarted, validateLicenseKey, webxrToNUE };
|
|
1
|
+
import { _ as odometryTrackingRestarted, a as LibraryRootState, c as RootState, d as bearingDeltaDeg, f as calcGpsCoords, g as magneticHeadingFromEnuQuat, h as isIdentityMatrix4, i as LatLongAlt, l as Vector3, m as createGpsSlamStore, n as GpsPoint, o as Matrix4, p as calcRelativeCoordsInMeters, r as LatLong, s as Quaternion, t as ArImageCapture, u as arNorthBearingDeg, v as validateLicenseKey, y as webxrToNUE } from "../index-_Rhgoqkh.js";
|
|
2
|
+
export { ArImageCapture, GpsPoint, LatLong, LatLongAlt, LibraryRootState, Matrix4, Quaternion, RootState, Vector3, arNorthBearingDeg, bearingDeltaDeg, calcGpsCoords, calcRelativeCoordsInMeters, createGpsSlamStore, isIdentityMatrix4, magneticHeadingFromEnuQuat, odometryTrackingRestarted, validateLicenseKey, webxrToNUE };
|
package/dist/core/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { calcGpsCoords, calcRelativeCoordsInMeters, createGpsSlamStore, isIdentityMatrix4, odometryTrackingRestarted, validateLicenseKey, webxrToNUE } from "gps-plus-slam-js";
|
|
2
|
-
export { calcGpsCoords, calcRelativeCoordsInMeters, createGpsSlamStore, isIdentityMatrix4, odometryTrackingRestarted, validateLicenseKey, webxrToNUE };
|
|
1
|
+
import { arNorthBearingDeg, bearingDeltaDeg, calcGpsCoords, calcRelativeCoordsInMeters, createGpsSlamStore, isIdentityMatrix4, magneticHeadingFromEnuQuat, odometryTrackingRestarted, validateLicenseKey, webxrToNUE } from "gps-plus-slam-js";
|
|
2
|
+
export { arNorthBearingDeg, bearingDeltaDeg, calcGpsCoords, calcRelativeCoordsInMeters, createGpsSlamStore, isIdentityMatrix4, magneticHeadingFromEnuQuat, odometryTrackingRestarted, validateLicenseKey, webxrToNUE };
|