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,452 @@
|
|
|
1
|
+
import { createLogger } from "./utils/logger.js";
|
|
2
|
+
import { clampedAlpha, lerpAngleDeg } from "./visualization/lerp-utils.js";
|
|
3
|
+
import { VIS_COLORS } from "./visualization/vis-colors.js";
|
|
4
|
+
import { drawMapData } from "./visualization/map-overlay-draw.js";
|
|
5
|
+
import { quat } from "gl-matrix";
|
|
6
|
+
import { CSS3DObject } from "three/addons/renderers/CSS3DRenderer.js";
|
|
7
|
+
import L from "leaflet";
|
|
8
|
+
//#region ../src/visualization/heading-up-rotation.ts
|
|
9
|
+
/**
|
|
10
|
+
* Heading-up minimap rotation helper.
|
|
11
|
+
*
|
|
12
|
+
* The live in-AR minimap is a Leaflet map wrapped in a three.js `CSS3DObject`
|
|
13
|
+
* lying flat below the camera. Its baseline orientation is a single −π/2
|
|
14
|
+
* rotation about the local +X axis (`tilt`), which lays the DOM plane flat so
|
|
15
|
+
* the user looks down on a NORTH-UP map (its plane normal points world +Y).
|
|
16
|
+
*
|
|
17
|
+
* "Heading-up" mode spins that flat map in its own plane so the user's view
|
|
18
|
+
* direction always points up/forward. We do this by composing a yaw about the
|
|
19
|
+
* world up axis (+Y) with the baseline tilt:
|
|
20
|
+
*
|
|
21
|
+
* quaternion = yaw(−headingDeg about +Y) · tilt(−π/2 about +X)
|
|
22
|
+
*
|
|
23
|
+
* Because the yaw is about +Y and the tilted plane's normal is already +Y, the
|
|
24
|
+
* normal is invariant under the yaw — the map stays flat and merely rotates
|
|
25
|
+
* in-plane. The map's parent (`CameraFollower`) is rotation-identity, so the
|
|
26
|
+
* object-local axes equal world axes; the yaw is therefore about true world up.
|
|
27
|
+
*
|
|
28
|
+
* Sign: the *magnitude* and *axis* of the rotation are pinned by
|
|
29
|
+
* `heading-up-rotation.test.ts`; the *perceived* turn direction was
|
|
30
|
+
* device-verified correct with `YAW_SIGN = -1` (2026-06-29, see
|
|
31
|
+
* gps-plus-slam/GpsPlusSlamJs_Docs/docs/2026-06-29-heading-up-minimap-rotation-plan.md).
|
|
32
|
+
* `YAW_SIGN` remains the one place to flip if a future frame/convention change
|
|
33
|
+
* ever inverts it.
|
|
34
|
+
*/
|
|
35
|
+
/** Baseline tilt: −π/2 about +X (lays the map flat, normal → world +Y). */
|
|
36
|
+
const TILT_X_RAD = -Math.PI / 2;
|
|
37
|
+
/**
|
|
38
|
+
* Sign of the yaw applied for a positive (clockwise-from-north) heading.
|
|
39
|
+
* Isolated as the single knob the on-device sign check would flip.
|
|
40
|
+
*/
|
|
41
|
+
const YAW_SIGN = -1;
|
|
42
|
+
const DEG_TO_RAD = Math.PI / 180;
|
|
43
|
+
const RAD_TO_DEG = 180 / Math.PI;
|
|
44
|
+
const TILT = quat.setAxisAngle(quat.create(), [
|
|
45
|
+
1,
|
|
46
|
+
0,
|
|
47
|
+
0
|
|
48
|
+
], TILT_X_RAD);
|
|
49
|
+
/** World-up yaw axis, hoisted so the per-frame call allocates nothing
|
|
50
|
+
* (`setAxisAngle` only reads it). */
|
|
51
|
+
const UP_AXIS = [
|
|
52
|
+
0,
|
|
53
|
+
1,
|
|
54
|
+
0
|
|
55
|
+
];
|
|
56
|
+
const _yaw = quat.create();
|
|
57
|
+
const _out = quat.create();
|
|
58
|
+
/**
|
|
59
|
+
* Quaternion (`[x, y, z, w]`) for the `CSS3DObject`: the baseline −π/2 tilt plus
|
|
60
|
+
* an in-plane yaw of `azimuthDeg` about world-up.
|
|
61
|
+
*
|
|
62
|
+
* `azimuthDeg` is the yaw to apply, NOT a heading: the consumer passes
|
|
63
|
+
* `viewAzimuth(camera) − userHeading` (camera-relative — see the file header and
|
|
64
|
+
* `viewAzimuthDeg`). `azimuthDeg = 0` returns the baseline tilt-only orientation
|
|
65
|
+
* (north-up); the map's north edge ends up pointing along scene azimuth
|
|
66
|
+
* `azimuthDeg` (atan2(x, −z) convention).
|
|
67
|
+
*
|
|
68
|
+
* @param azimuthDeg In-plane yaw to apply, degrees, atan2(x, −z) convention.
|
|
69
|
+
* @returns A unit quaternion as a 4-tuple `[x, y, z, w]`.
|
|
70
|
+
*/
|
|
71
|
+
function headingUpQuat(azimuthDeg) {
|
|
72
|
+
quat.setAxisAngle(_yaw, UP_AXIS, YAW_SIGN * azimuthDeg * DEG_TO_RAD);
|
|
73
|
+
quat.multiply(_out, _yaw, TILT);
|
|
74
|
+
return [
|
|
75
|
+
_out[0],
|
|
76
|
+
_out[1],
|
|
77
|
+
_out[2],
|
|
78
|
+
_out[3]
|
|
79
|
+
];
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Azimuth (degrees clockwise, `[0, 360)`) that the camera is looking, measured
|
|
83
|
+
* in the same scene frame and convention as {@link headingUpQuat}'s input
|
|
84
|
+
* (0° = looking along world −Z, +90° = looking along world +X).
|
|
85
|
+
*
|
|
86
|
+
* Why this is needed for heading-up: the minimap is world-locked (its parent is
|
|
87
|
+
* rotation-identity) but it is composited through the **live head-tracked
|
|
88
|
+
* camera**, so the camera already rotates the map's on-screen appearance as the
|
|
89
|
+
* user turns. The map's local heading-up yaw must therefore be expressed
|
|
90
|
+
* RELATIVE to the camera, not in absolute GPS-north terms — otherwise the
|
|
91
|
+
* camera's rotation is double-counted and the result is only correct at a single
|
|
92
|
+
* heading (the difference being the GPS↔scene alignment yaw). The consumer rolls
|
|
93
|
+
* the alignment offset out by feeding `headingUpQuat(viewAzimuth − userHeading)`.
|
|
94
|
+
*
|
|
95
|
+
* @param matrixWorldElements The camera's `matrixWorld.elements` (column-major).
|
|
96
|
+
* The camera looks down its local −Z, so world-forward = −(3rd column).
|
|
97
|
+
* @returns The camera's horizontal viewing azimuth in `[0, 360)` degrees.
|
|
98
|
+
*/
|
|
99
|
+
function viewAzimuthDeg(matrixWorldElements) {
|
|
100
|
+
const m8 = matrixWorldElements[8] ?? 0;
|
|
101
|
+
const m10 = matrixWorldElements[10] ?? 0;
|
|
102
|
+
return (Math.atan2(-m8, m10) * RAD_TO_DEG % 360 + 360) % 360;
|
|
103
|
+
}
|
|
104
|
+
//#endregion
|
|
105
|
+
//#region ../src/visualization/leaflet-map-overlay.ts
|
|
106
|
+
const log = createLogger("LeafletMapOverlay");
|
|
107
|
+
/** Default pixel size of the Leaflet map container */
|
|
108
|
+
const DEFAULT_LEAFLET_MAP_SIZE_PX = 600;
|
|
109
|
+
/**
|
|
110
|
+
* Diameter (px) of the ref-point markers on the in-AR minimap.
|
|
111
|
+
*
|
|
112
|
+
* Enlarged 12 → 20 px (F5-A, 2026-06-16 user feedback): the field tester
|
|
113
|
+
* reported the minimap "showed the user but not the marker". The markers WERE
|
|
114
|
+
* rendered — they were just too small to notice on the small CSS3D minimap. A
|
|
115
|
+
* bigger dot plus a thicker white halo and a drop shadow makes them the most
|
|
116
|
+
* prominent feature on the minimap (the user-position marker stays 14 px).
|
|
117
|
+
*/
|
|
118
|
+
const REF_POINT_MARKER_SIZE_PX = 20;
|
|
119
|
+
/**
|
|
120
|
+
* Map-plane placement defaults (re-fit 2026-07-04, F1 user feedback).
|
|
121
|
+
*
|
|
122
|
+
* The plane's parent is the CameraFollower — camera POSITION is followed,
|
|
123
|
+
* rotation stays IDENTITY (GPS-world-aligned). CSS3D content crossing the
|
|
124
|
+
* viewer plane (camera-space z ≥ 0) is cut off by the browser (an effective
|
|
125
|
+
* near plane at 0 that camera.near cannot move), so the defaults must keep
|
|
126
|
+
* every plane corner in front of the viewer plane for ALL camera yaws and
|
|
127
|
+
* heading-up rotations at map-viewing pitches. Invariant (tests pin it):
|
|
128
|
+
*
|
|
129
|
+
* |DEFAULT_Z_OFFSET| + DEFAULT_WORLD_SIZE/√2 + lag ≤ |h|·tan(θ*)
|
|
130
|
+
*
|
|
131
|
+
* with lag = 0.5 m (follower lerp) and design pitch θ* = 51°. Below θ* only
|
|
132
|
+
* the region farther than |h|·tan(pitch) behind the camera is cut, shrinking
|
|
133
|
+
* to nothing at θ*. Values 8 / −5 / 0 give θ* ≈ 51°; zOffset MUST stay 0 —
|
|
134
|
+
* with a world-yaw-locked parent a non-zero offset is a fixed compass
|
|
135
|
+
* direction, not "ahead of the user".
|
|
136
|
+
*/
|
|
137
|
+
/** Default world-space size in meters */
|
|
138
|
+
const DEFAULT_WORLD_SIZE = 8;
|
|
139
|
+
/** Default height offset below camera (negative = below) */
|
|
140
|
+
const DEFAULT_HEIGHT_OFFSET = -5;
|
|
141
|
+
/** Default offset from parent along world Z (keep 0 — see block comment) */
|
|
142
|
+
const DEFAULT_Z_OFFSET = 0;
|
|
143
|
+
/** Default zoom level for the Leaflet map */
|
|
144
|
+
const DEFAULT_ZOOM = 17;
|
|
145
|
+
/** Minimum allowed zoom level */
|
|
146
|
+
const MIN_ZOOM = 0;
|
|
147
|
+
/** Maximum allowed zoom level */
|
|
148
|
+
const MAX_ZOOM = 19;
|
|
149
|
+
var LeafletMapOverlay = class {
|
|
150
|
+
scene;
|
|
151
|
+
camera;
|
|
152
|
+
mapParent;
|
|
153
|
+
worldSize;
|
|
154
|
+
heightOffset;
|
|
155
|
+
mapSizePx;
|
|
156
|
+
tileServerUrl;
|
|
157
|
+
onTileError;
|
|
158
|
+
offscreenRoot;
|
|
159
|
+
zoomLevel;
|
|
160
|
+
gpsPosition = null;
|
|
161
|
+
visible = false;
|
|
162
|
+
headingUp;
|
|
163
|
+
targetHeadingDeg = null;
|
|
164
|
+
currentHeadingDeg = null;
|
|
165
|
+
mapContainer = null;
|
|
166
|
+
leafletMap = null;
|
|
167
|
+
tileLayer = null;
|
|
168
|
+
cssObject = null;
|
|
169
|
+
latestMapData = null;
|
|
170
|
+
trajectoryLayers = [];
|
|
171
|
+
namedMarkers = [];
|
|
172
|
+
constructor(scene, camera, options = {}) {
|
|
173
|
+
this.scene = scene;
|
|
174
|
+
this.camera = camera;
|
|
175
|
+
this.mapParent = options.mapParent ?? camera;
|
|
176
|
+
this.worldSize = options.worldSize ?? 8;
|
|
177
|
+
this.heightOffset = options.heightOffset ?? -5;
|
|
178
|
+
this.mapSizePx = options.mapSizePx ?? 600;
|
|
179
|
+
this.zoomLevel = options.zoomLevel ?? 17;
|
|
180
|
+
this.tileServerUrl = options.tileServerUrl ?? "https://tile.openstreetmap.org/{z}/{x}/{y}.png";
|
|
181
|
+
this.onTileError = options.onTileError;
|
|
182
|
+
this.offscreenRoot = options.offscreenRoot ?? document.body;
|
|
183
|
+
this.headingUp = options.headingUp ?? false;
|
|
184
|
+
}
|
|
185
|
+
setGpsPosition(lat, lon) {
|
|
186
|
+
this.gpsPosition = {
|
|
187
|
+
lat,
|
|
188
|
+
lon
|
|
189
|
+
};
|
|
190
|
+
if (this.leafletMap) this.leafletMap.setView([lat, lon], this.zoomLevel);
|
|
191
|
+
}
|
|
192
|
+
getGpsPosition() {
|
|
193
|
+
return this.gpsPosition;
|
|
194
|
+
}
|
|
195
|
+
show() {
|
|
196
|
+
if (!this.gpsPosition) {
|
|
197
|
+
log.warn("Cannot show map — no GPS position set");
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (this.visible) return;
|
|
201
|
+
this.createLeafletMap();
|
|
202
|
+
this.createCssObject();
|
|
203
|
+
this.applyBufferedOverlays();
|
|
204
|
+
this.visible = true;
|
|
205
|
+
log.info("Leaflet map overlay shown");
|
|
206
|
+
}
|
|
207
|
+
hide() {
|
|
208
|
+
if (!this.visible) return;
|
|
209
|
+
this.removeCssObject();
|
|
210
|
+
this.visible = false;
|
|
211
|
+
log.info("Leaflet map overlay hidden");
|
|
212
|
+
}
|
|
213
|
+
toggle() {
|
|
214
|
+
if (this.visible) this.hide();
|
|
215
|
+
else this.show();
|
|
216
|
+
}
|
|
217
|
+
isVisible() {
|
|
218
|
+
return this.visible;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Render a full trajectory snapshot.
|
|
222
|
+
*
|
|
223
|
+
* Replaces the previous incremental `addRawGpsPoint` / `addFusedPoint` /
|
|
224
|
+
* `addAlignmentSnapshot` API with a single full-snapshot path: the caller
|
|
225
|
+
* builds a {@link MapData} (via the shared `buildMapData`) from the latest
|
|
226
|
+
* store slices and hands it here. The trajectory layers are redrawn
|
|
227
|
+
* wholesale through the shared {@link drawMapData} routine, so the live map
|
|
228
|
+
* stays pixel-identical to the 2D session-summary map and the fused path
|
|
229
|
+
* recomputes (D2) as the alignment matrix improves.
|
|
230
|
+
*
|
|
231
|
+
* Buffered when the map is not yet shown; applied on `show()`.
|
|
232
|
+
*/
|
|
233
|
+
render(data) {
|
|
234
|
+
this.latestMapData = data;
|
|
235
|
+
this.targetHeadingDeg = typeof data.userHeadingDeg === "number" && Number.isFinite(data.userHeadingDeg) ? data.userHeadingDeg : null;
|
|
236
|
+
if (this.leafletMap) this.drawTrajectory();
|
|
237
|
+
}
|
|
238
|
+
/** Add a "current" named marker (red). Buffered if map is not yet shown. */
|
|
239
|
+
addCurrentMarker(lat, lon, name) {
|
|
240
|
+
const marker = this.leafletMap ? this.createNamedMarker(lat, lon, name, false) : null;
|
|
241
|
+
this.namedMarkers.push({
|
|
242
|
+
lat,
|
|
243
|
+
lng: lon,
|
|
244
|
+
name,
|
|
245
|
+
isPrior: false,
|
|
246
|
+
marker
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
/** Add a "prior" named marker (green, decorated). Buffered if not shown. */
|
|
250
|
+
addPriorMarker(lat, lon, name) {
|
|
251
|
+
const marker = this.leafletMap ? this.createNamedMarker(lat, lon, name, true) : null;
|
|
252
|
+
this.namedMarkers.push({
|
|
253
|
+
lat,
|
|
254
|
+
lng: lon,
|
|
255
|
+
name,
|
|
256
|
+
isPrior: true,
|
|
257
|
+
marker
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
/** Bulk add prior markers. */
|
|
261
|
+
addPriorMarkers(markers) {
|
|
262
|
+
for (const m of markers) this.addPriorMarker(m.lat, m.lon, m.name);
|
|
263
|
+
}
|
|
264
|
+
/** Remove all prior markers; current markers are unaffected. */
|
|
265
|
+
clearPriorMarkers() {
|
|
266
|
+
this.namedMarkers = this.namedMarkers.filter((m) => {
|
|
267
|
+
if (m.isPrior) {
|
|
268
|
+
m.marker?.remove();
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
return true;
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
setZoomLevel(zoom) {
|
|
275
|
+
this.zoomLevel = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, zoom));
|
|
276
|
+
if (this.leafletMap) this.leafletMap.setZoom(this.zoomLevel);
|
|
277
|
+
}
|
|
278
|
+
getZoomLevel() {
|
|
279
|
+
return this.zoomLevel;
|
|
280
|
+
}
|
|
281
|
+
/** Increment zoom level by 1 (clamped at MAX_ZOOM). */
|
|
282
|
+
zoomIn() {
|
|
283
|
+
this.setZoomLevel(this.zoomLevel + 1);
|
|
284
|
+
}
|
|
285
|
+
/** Decrement zoom level by 1 (clamped at MIN_ZOOM). */
|
|
286
|
+
zoomOut() {
|
|
287
|
+
this.setZoomLevel(this.zoomLevel - 1);
|
|
288
|
+
}
|
|
289
|
+
getWorldSize() {
|
|
290
|
+
return this.worldSize;
|
|
291
|
+
}
|
|
292
|
+
getHeightOffset() {
|
|
293
|
+
return this.heightOffset;
|
|
294
|
+
}
|
|
295
|
+
getMapSizePx() {
|
|
296
|
+
return this.mapSizePx;
|
|
297
|
+
}
|
|
298
|
+
/** Expose the Leaflet map instance for testing and advanced usage. */
|
|
299
|
+
getLeafletMap() {
|
|
300
|
+
return this.leafletMap;
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Enable/disable heading-up rotation at runtime (e.g. from a settings flag
|
|
304
|
+
* read at Enter-AR). When disabled, the map snaps back to the north-up
|
|
305
|
+
* baseline immediately. Position follows the parent via the scene graph, so
|
|
306
|
+
* this only governs the in-plane yaw.
|
|
307
|
+
*/
|
|
308
|
+
setHeadingUpEnabled(enabled) {
|
|
309
|
+
this.headingUp = enabled;
|
|
310
|
+
if (!enabled) {
|
|
311
|
+
this.currentHeadingDeg = null;
|
|
312
|
+
this.cssObject?.rotation.set(-Math.PI / 2, 0, 0);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
/** Whether heading-up rotation is currently enabled. */
|
|
316
|
+
isHeadingUpEnabled() {
|
|
317
|
+
return this.headingUp;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Per-frame update driving the heading-up rotation. Positioning is handled by
|
|
321
|
+
* Three.js scene-graph propagation (the CSS3DObject is a child of the parent),
|
|
322
|
+
* so this only sets the in-plane yaw.
|
|
323
|
+
*
|
|
324
|
+
* The minimap is world-locked but composited through the **live head-tracked
|
|
325
|
+
* camera**, so the camera already rotates the map's on-screen appearance as
|
|
326
|
+
* the user turns. The local yaw must therefore be expressed RELATIVE to the
|
|
327
|
+
* camera — `headingUpQuat(viewAzimuth − userHeading)` — otherwise the camera's
|
|
328
|
+
* rotation is double-counted and the GPS↔scene alignment offset leaks in (the
|
|
329
|
+
* map then only points forward at a single heading). `userHeading` is smoothed
|
|
330
|
+
* toward the latest ~1 Hz target; `viewAzimuth` is read live from the camera
|
|
331
|
+
* each frame so the projection cancels exactly (no lag).
|
|
332
|
+
*
|
|
333
|
+
* A no-op when heading-up is disabled, the map is not shown, the camera is
|
|
334
|
+
* absent, or the heading is undefined (the last orientation is held).
|
|
335
|
+
*
|
|
336
|
+
* @param dtSeconds Seconds since the previous frame (drives heading smoothing).
|
|
337
|
+
* @param camera The live render camera (the same one compositing the CSS3D
|
|
338
|
+
* overlay). Required for heading-up; omitted callers leave the map as-is.
|
|
339
|
+
*/
|
|
340
|
+
updatePosition(dtSeconds = 0, camera) {
|
|
341
|
+
if (!this.headingUp || !this.visible || !this.cssObject || !camera) return;
|
|
342
|
+
const target = this.targetHeadingDeg;
|
|
343
|
+
if (target === null) return;
|
|
344
|
+
if (this.currentHeadingDeg === null) this.currentHeadingDeg = target;
|
|
345
|
+
else {
|
|
346
|
+
const alpha = clampedAlpha(8, dtSeconds);
|
|
347
|
+
this.currentHeadingDeg = lerpAngleDeg(this.currentHeadingDeg, target, alpha);
|
|
348
|
+
}
|
|
349
|
+
camera.updateMatrixWorld();
|
|
350
|
+
const viewAz = viewAzimuthDeg(camera.matrixWorld.elements);
|
|
351
|
+
this.cssObject.quaternion.set(...headingUpQuat(viewAz - this.currentHeadingDeg));
|
|
352
|
+
}
|
|
353
|
+
dispose() {
|
|
354
|
+
this.hide();
|
|
355
|
+
this.destroyLeafletMap();
|
|
356
|
+
this.latestMapData = null;
|
|
357
|
+
this.trajectoryLayers = [];
|
|
358
|
+
this.namedMarkers = [];
|
|
359
|
+
this.targetHeadingDeg = null;
|
|
360
|
+
this.currentHeadingDeg = null;
|
|
361
|
+
this.cssObject = null;
|
|
362
|
+
log.info("Leaflet map overlay disposed");
|
|
363
|
+
}
|
|
364
|
+
createLeafletMap() {
|
|
365
|
+
if (this.leafletMap) return;
|
|
366
|
+
this.mapContainer = document.createElement("div");
|
|
367
|
+
this.mapContainer.style.width = `${this.mapSizePx}px`;
|
|
368
|
+
this.mapContainer.style.height = `${this.mapSizePx}px`;
|
|
369
|
+
this.mapContainer.style.background = "white";
|
|
370
|
+
this.mapContainer.style.position = "fixed";
|
|
371
|
+
this.mapContainer.style.left = "-9999px";
|
|
372
|
+
this.mapContainer.style.top = "-9999px";
|
|
373
|
+
this.mapContainer.style.pointerEvents = "none";
|
|
374
|
+
this.offscreenRoot.appendChild(this.mapContainer);
|
|
375
|
+
const pos = this.gpsPosition;
|
|
376
|
+
this.leafletMap = L.map(this.mapContainer, {
|
|
377
|
+
center: [pos.lat, pos.lon],
|
|
378
|
+
zoom: this.zoomLevel,
|
|
379
|
+
zoomControl: false,
|
|
380
|
+
attributionControl: false
|
|
381
|
+
});
|
|
382
|
+
this.tileLayer = L.tileLayer(this.tileServerUrl, { maxZoom: MAX_ZOOM });
|
|
383
|
+
this.tileLayer.on("tileerror", (e) => {
|
|
384
|
+
log.error("Failed to load map tile:", e.error);
|
|
385
|
+
this.onTileError?.(e.error);
|
|
386
|
+
});
|
|
387
|
+
this.tileLayer.addTo(this.leafletMap);
|
|
388
|
+
}
|
|
389
|
+
destroyLeafletMap() {
|
|
390
|
+
for (const layer of this.trajectoryLayers) layer.remove();
|
|
391
|
+
this.trajectoryLayers = [];
|
|
392
|
+
for (const m of this.namedMarkers) {
|
|
393
|
+
m.marker?.remove();
|
|
394
|
+
m.marker = null;
|
|
395
|
+
}
|
|
396
|
+
if (this.leafletMap) {
|
|
397
|
+
this.leafletMap.remove();
|
|
398
|
+
this.leafletMap = null;
|
|
399
|
+
}
|
|
400
|
+
if (this.mapContainer?.parentElement) this.mapContainer.parentElement.removeChild(this.mapContainer);
|
|
401
|
+
this.mapContainer = null;
|
|
402
|
+
this.tileLayer = null;
|
|
403
|
+
}
|
|
404
|
+
createCssObject() {
|
|
405
|
+
if (!this.mapContainer) return;
|
|
406
|
+
if (this.cssObject) {
|
|
407
|
+
this.mapParent.add(this.cssObject);
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
this.mapContainer.style.position = "";
|
|
411
|
+
this.mapContainer.style.left = "";
|
|
412
|
+
this.mapContainer.style.top = "";
|
|
413
|
+
this.cssObject = new CSS3DObject(this.mapContainer);
|
|
414
|
+
const scale = this.worldSize / this.mapSizePx;
|
|
415
|
+
this.cssObject.scale.set(scale, scale, scale);
|
|
416
|
+
this.cssObject.position.set(0, this.heightOffset, 0);
|
|
417
|
+
this.cssObject.rotation.x = -Math.PI / 2;
|
|
418
|
+
this.mapParent.add(this.cssObject);
|
|
419
|
+
}
|
|
420
|
+
removeCssObject() {
|
|
421
|
+
if (this.cssObject) this.mapParent.remove(this.cssObject);
|
|
422
|
+
}
|
|
423
|
+
applyBufferedOverlays() {
|
|
424
|
+
if (!this.leafletMap) return;
|
|
425
|
+
this.drawTrajectory();
|
|
426
|
+
for (const m of this.namedMarkers) if (!m.marker) m.marker = this.createNamedMarker(m.lat, m.lng, m.name, m.isPrior);
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Redraw the trajectory layers from {@link latestMapData} via the shared
|
|
430
|
+
* {@link drawMapData} routine, removing any layers from the previous draw.
|
|
431
|
+
*/
|
|
432
|
+
drawTrajectory() {
|
|
433
|
+
if (!this.leafletMap || !this.latestMapData) return;
|
|
434
|
+
for (const layer of this.trajectoryLayers) layer.remove();
|
|
435
|
+
this.trajectoryLayers = drawMapData(this.leafletMap, this.latestMapData, { showUserPosition: true }).layers;
|
|
436
|
+
}
|
|
437
|
+
createNamedMarker(lat, lon, name, isPrior) {
|
|
438
|
+
const color = isPrior ? VIS_COLORS.PRIOR_REF_POINT.css : VIS_COLORS.CURRENT_REF_POINT.css;
|
|
439
|
+
const opacity = isPrior ? "opacity:0.8;" : "";
|
|
440
|
+
const label = isPrior ? `📌 ${name} (prior)` : name;
|
|
441
|
+
const size = REF_POINT_MARKER_SIZE_PX;
|
|
442
|
+
const anchor = size / 2;
|
|
443
|
+
return L.marker([lat, lon], { icon: L.divIcon({
|
|
444
|
+
className: "",
|
|
445
|
+
html: `<div style="background:${color};width:${size}px;height:${size}px;border-radius:50%;border:3px solid white;box-shadow:0 0 4px rgba(0,0,0,0.7);${opacity}"></div>`,
|
|
446
|
+
iconSize: [size, size],
|
|
447
|
+
iconAnchor: [anchor, anchor]
|
|
448
|
+
}) }).bindPopup(label).addTo(this.leafletMap);
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
//#endregion
|
|
452
|
+
export { DEFAULT_Z_OFFSET as a, DEFAULT_ZOOM as i, DEFAULT_LEAFLET_MAP_SIZE_PX as n, LeafletMapOverlay as o, DEFAULT_WORLD_SIZE as r, DEFAULT_HEIGHT_OFFSET as t };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
//#region ../src/visualization/lerp-utils.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Shared lerp utilities for visualization modules.
|
|
4
|
+
*
|
|
5
|
+
* Both alignment-lerper and camera-follower need the same smoothing
|
|
6
|
+
* rate constant and clamped-alpha formula. Centralised here (R3) to
|
|
7
|
+
* keep the two modules in sync.
|
|
8
|
+
*/
|
|
9
|
+
/** Default speed multiplier — ~90 % convergence in ~0.3 s at 60 fps. */
|
|
10
|
+
declare const DEFAULT_LERP_RATE = 8;
|
|
11
|
+
/**
|
|
12
|
+
* Compute a frame-rate-independent lerp/slerp factor clamped to [0, 1].
|
|
13
|
+
*
|
|
14
|
+
* Both bounds are clamped: the upper bound caps an overlong frame at a full
|
|
15
|
+
* (instant) step, and the lower bound rejects a negative `dt` (system clock
|
|
16
|
+
* adjustment, a frame loop whose timestamp runs backward, or a paused-then-
|
|
17
|
+
* resumed tab). Without the lower clamp a negative `dt` would yield a negative
|
|
18
|
+
* alpha and the angle/quaternion lerp would extrapolate BACKWARD, away from the
|
|
19
|
+
* target — a visible heading/alignment jump.
|
|
20
|
+
*
|
|
21
|
+
* @param lerpRate Speed multiplier (higher = faster convergence).
|
|
22
|
+
* @param dt Delta time in seconds since last frame.
|
|
23
|
+
* @returns The interpolation alpha, guaranteed within [0, 1].
|
|
24
|
+
*/
|
|
25
|
+
declare function clampedAlpha(lerpRate: number, dt: number): number;
|
|
26
|
+
/**
|
|
27
|
+
* Interpolate between two angles (degrees) along the SHORTEST arc.
|
|
28
|
+
*
|
|
29
|
+
* Used by the heading-up minimap to smooth the map's yaw toward a ~1 Hz target
|
|
30
|
+
* heading every frame without spinning the long way across the 0°/360° seam
|
|
31
|
+
* (e.g. 350° → 10° goes +20° through 0°, not −340° through 180°).
|
|
32
|
+
*
|
|
33
|
+
* @param current Current angle in degrees (any range; normalized internally).
|
|
34
|
+
* @param target Target angle in degrees (any range).
|
|
35
|
+
* @param alpha Interpolation factor — 0 returns `current`, 1 returns `target`.
|
|
36
|
+
* Values outside [0, 1] extrapolate; callers should clamp (see
|
|
37
|
+
* {@link clampedAlpha}).
|
|
38
|
+
* @returns The interpolated angle normalized to `[0, 360)`.
|
|
39
|
+
*/
|
|
40
|
+
declare function lerpAngleDeg(current: number, target: number, alpha: number): number;
|
|
41
|
+
//#endregion
|
|
42
|
+
export { clampedAlpha as n, lerpAngleDeg as r, DEFAULT_LERP_RATE as t };
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { computeFusedPath } from "./utils/fused-path.js";
|
|
2
|
+
import { vec3 } from "gl-matrix";
|
|
3
|
+
//#region ../src/utils/user-heading.ts
|
|
4
|
+
/**
|
|
5
|
+
* True-north user-heading kernel.
|
|
6
|
+
*
|
|
7
|
+
* Turns the latest AR camera rotation into an absolute compass bearing (degrees
|
|
8
|
+
* clockwise from geographic North) so the live/replay map overlay can draw a
|
|
9
|
+
* thin view-direction line from the user-position dot. See Finding 2 of
|
|
10
|
+
* gps-plus-slam/GpsPlusSlamJs_Docs/docs/2026-06-28-map-rings-transparency-and-view-direction-user-feedback.md.
|
|
11
|
+
*
|
|
12
|
+
* Why this lives in the app-framework (NOT the library's `orientation-heading.ts`):
|
|
13
|
+
* that library module is the MAGNETIC path — its bearings come from the
|
|
14
|
+
* AbsoluteOrientationSensor's device→ENU quaternion and are explicitly magnetic
|
|
15
|
+
* (they need WMM declination correction). Finding 2's locked source is the
|
|
16
|
+
* GPS+SLAM **alignment matrix → true geographic north**, which rejects the
|
|
17
|
+
* magnetic sensor. So the math belongs here, alongside `fused-path.ts`, which
|
|
18
|
+
* already converts AR odometry through the same alignment matrix.
|
|
19
|
+
*
|
|
20
|
+
* Frame algebra (pinned by `user-heading.test.ts`):
|
|
21
|
+
* - State stores odometry rotations as NUE quaternions
|
|
22
|
+
* (`webxrQuaternionToNUE(odomRotation)` in the library reducer). NUE means
|
|
23
|
+
* X=North, Y=Up, Z=East.
|
|
24
|
+
* - A WebXR camera looks down its own −Z. The basis-change `webxrToNUE` maps
|
|
25
|
+
* that local forward `[0,0,-1]` to `[1,0,0]`, so the camera-forward basis
|
|
26
|
+
* vector in the NUE-AR frame is **[1, 0, 0]** (an identity camera looks North).
|
|
27
|
+
* - The alignment matrix maps AR-NUE → world NUE metres (same matrix
|
|
28
|
+
* `computeFusedPath` applies to positions). Rotating the forward direction by
|
|
29
|
+
* it yields a world-NUE direction; the geographic bearing is
|
|
30
|
+
* `atan2(East, North)`.
|
|
31
|
+
*
|
|
32
|
+
* We rotate the *direction* and read the bearing directly rather than
|
|
33
|
+
* round-tripping two points through `calcGpsCoords`: lat/lon scale North and
|
|
34
|
+
* East by different metres-per-degree, so a naive GPS-coord bearing would be
|
|
35
|
+
* anisotropically distorted. The aligned NUE metres already carry the true
|
|
36
|
+
* bearing, so `atan2(East, North)` is both simpler and undistorted.
|
|
37
|
+
*/
|
|
38
|
+
/** Camera-forward basis vector in the NUE-AR frame: `webxrToNUE([0,0,-1]) = [1,0,0]`. */
|
|
39
|
+
const NUE_CAMERA_FORWARD = [
|
|
40
|
+
1,
|
|
41
|
+
0,
|
|
42
|
+
0
|
|
43
|
+
];
|
|
44
|
+
/**
|
|
45
|
+
* Minimum horizontal fraction of the (unit) forward direction below which the
|
|
46
|
+
* heading is treated as undefined. Because `horiz/len = sin(angle from
|
|
47
|
+
* vertical)`, `horiz/len < GUARD` means the camera points within
|
|
48
|
+
* `asin(0.08) ≈ 4.6°` of straight up/down (equivalently, pitched more than ~85°
|
|
49
|
+
* from horizontal), where a 2D map bearing is meaningless and jittery. Mirrors
|
|
50
|
+
* the rationale of the library's `VERTICAL_GUARD` (0.08).
|
|
51
|
+
*/
|
|
52
|
+
const VERTICAL_GUARD = .08;
|
|
53
|
+
const RAD_TO_DEG = 180 / Math.PI;
|
|
54
|
+
const _forward = vec3.create();
|
|
55
|
+
const _origin = vec3.create();
|
|
56
|
+
const _tip = vec3.create();
|
|
57
|
+
const _dir = vec3.create();
|
|
58
|
+
/**
|
|
59
|
+
* Compute the user's absolute view-direction bearing in degrees clockwise from
|
|
60
|
+
* geographic North, in `[0, 360)`, or `null` when undefined.
|
|
61
|
+
*
|
|
62
|
+
* Returns `null` when there is no rotation yet, no alignment matrix yet, or the
|
|
63
|
+
* camera points near-vertically (see {@link VERTICAL_GUARD}). Position is NOT
|
|
64
|
+
* required — the bearing is a pure direction; the consumer only draws the line
|
|
65
|
+
* when it also has a user-position dot to anchor it to.
|
|
66
|
+
*/
|
|
67
|
+
function computeUserHeadingDeg(input) {
|
|
68
|
+
const { odometryRotation, alignmentMatrix } = input;
|
|
69
|
+
if (!odometryRotation || !alignmentMatrix) return null;
|
|
70
|
+
vec3.transformQuat(_forward, NUE_CAMERA_FORWARD, odometryRotation);
|
|
71
|
+
vec3.set(_origin, 0, 0, 0);
|
|
72
|
+
vec3.transformMat4(_origin, _origin, alignmentMatrix);
|
|
73
|
+
vec3.transformMat4(_tip, _forward, alignmentMatrix);
|
|
74
|
+
vec3.subtract(_dir, _tip, _origin);
|
|
75
|
+
const north = _dir[0];
|
|
76
|
+
const east = _dir[2];
|
|
77
|
+
const len = Math.hypot(_dir[0], _dir[1], _dir[2]);
|
|
78
|
+
const horiz = Math.hypot(north, east);
|
|
79
|
+
if (!Number.isFinite(len) || !Number.isFinite(horiz) || len < 1e-9 || horiz / len < VERTICAL_GUARD) return null;
|
|
80
|
+
return (Math.atan2(east, north) * RAD_TO_DEG % 360 + 360) % 360;
|
|
81
|
+
}
|
|
82
|
+
//#endregion
|
|
83
|
+
//#region ../src/visualization/map-data.ts
|
|
84
|
+
/**
|
|
85
|
+
* Build a {@link MapData} snapshot from store-derived inputs.
|
|
86
|
+
*
|
|
87
|
+
* Pure and free of Leaflet/Three dependencies. The fused path is always
|
|
88
|
+
* derived via {@link computeFusedPath} (D2); when the matrix or zero reference
|
|
89
|
+
* is missing it is an empty array.
|
|
90
|
+
*
|
|
91
|
+
* @param input - Optional trajectory slices (see {@link MapDataInput}).
|
|
92
|
+
* @returns A fully-resolved {@link MapData} with all arrays defensively copied.
|
|
93
|
+
*/
|
|
94
|
+
function buildMapData(input) {
|
|
95
|
+
const rawGpsPath = input.rawGpsPath ? [...input.rawGpsPath] : [];
|
|
96
|
+
const alignmentSnapshots = input.alignmentSnapshots ? [...input.alignmentSnapshots] : [];
|
|
97
|
+
const fusedPath = computeFusedPath({
|
|
98
|
+
odometryPositions: input.odometryPositions ?? [],
|
|
99
|
+
alignmentMatrix: input.alignmentMatrix ?? null,
|
|
100
|
+
zeroRef: input.zeroRef ?? null
|
|
101
|
+
});
|
|
102
|
+
const lastRaw = rawGpsPath[rawGpsPath.length - 1];
|
|
103
|
+
const userPosition = input.userPosition !== void 0 ? input.userPosition : lastRaw ? {
|
|
104
|
+
lat: lastRaw.lat,
|
|
105
|
+
lng: lastRaw.lng
|
|
106
|
+
} : null;
|
|
107
|
+
const rotations = input.odometryRotations ?? [];
|
|
108
|
+
return {
|
|
109
|
+
userPosition,
|
|
110
|
+
rawGpsPath,
|
|
111
|
+
fusedPath,
|
|
112
|
+
alignmentSnapshots,
|
|
113
|
+
userHeadingDeg: computeUserHeadingDeg({
|
|
114
|
+
odometryRotation: rotations[rotations.length - 1] ?? null,
|
|
115
|
+
alignmentMatrix: input.alignmentMatrix ?? null
|
|
116
|
+
})
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
//#endregion
|
|
120
|
+
export { buildMapData as t };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { n as RawGpsSample, t as GpsCoord } from "./geo-types-
|
|
2
|
-
import { LatLong, Matrix4, Vector3 } from "gps-plus-slam-js";
|
|
1
|
+
import { n as RawGpsSample, t as GpsCoord } from "./geo-types-DrGVu8nS.js";
|
|
2
|
+
import { LatLong, Matrix4, Quaternion, Vector3 } from "gps-plus-slam-js";
|
|
3
3
|
|
|
4
4
|
//#region ../src/visualization/map-data.d.ts
|
|
5
5
|
/** Fully-resolved trajectory data ready to be drawn onto a Leaflet map. */
|
|
@@ -12,6 +12,19 @@ interface MapData {
|
|
|
12
12
|
fusedPath: GpsCoord[];
|
|
13
13
|
/** Alignment-snapshot GPS positions (red). */
|
|
14
14
|
alignmentSnapshots: GpsCoord[];
|
|
15
|
+
/**
|
|
16
|
+
* Absolute view-direction bearing (degrees clockwise from true geographic
|
|
17
|
+
* north, `[0, 360)`) for the user-position heading line, or null when
|
|
18
|
+
* undefined (no rotation/alignment yet, or camera near-vertical). See
|
|
19
|
+
* Finding 2 of
|
|
20
|
+
* gps-plus-slam/GpsPlusSlamJs_Docs/docs/2026-06-28-map-rings-transparency-and-view-direction-user-feedback.md.
|
|
21
|
+
*
|
|
22
|
+
* Optional only so pre-existing direct `MapData` literals (e.g. the static
|
|
23
|
+
* summary map, which is out of scope and draws no heading line) keep
|
|
24
|
+
* compiling. `buildMapData` ALWAYS sets it (to a bearing or null); a missing
|
|
25
|
+
* value is treated by `drawMapData` exactly like null (no line).
|
|
26
|
+
*/
|
|
27
|
+
userHeadingDeg?: number | null;
|
|
15
28
|
}
|
|
16
29
|
/**
|
|
17
30
|
* Inputs to {@link buildMapData}, all optional so callers can supply only the
|
|
@@ -22,6 +35,13 @@ interface MapDataInput {
|
|
|
22
35
|
rawGpsPath?: readonly RawGpsSample[];
|
|
23
36
|
/** Odometry positions (AR-local) used to derive the fused path. */
|
|
24
37
|
odometryPositions?: ReadonlyArray<Vector3>;
|
|
38
|
+
/**
|
|
39
|
+
* Odometry rotations (NUE quaternions, as stored in
|
|
40
|
+
* `gpsEvents.odometryRotations`). The LATEST entry drives the user heading
|
|
41
|
+
* line; earlier entries are ignored here. Pass the `selectOdometryRotations`
|
|
42
|
+
* value directly.
|
|
43
|
+
*/
|
|
44
|
+
odometryRotations?: ReadonlyArray<Quaternion>;
|
|
25
45
|
/** Latest alignment matrix from the solver (null until first solve). */
|
|
26
46
|
alignmentMatrix?: Matrix4 | null;
|
|
27
47
|
/** GPS origin for ENU→GPS conversion (null when no GPS yet). */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { n as SessionMetadata } from "./opfs-storage-
|
|
2
|
-
import { n as StorageBackend, t as CreateSessionResult } from "./storage-backend-
|
|
1
|
+
import { n as SessionMetadata } from "./opfs-storage-CTkxvxN4.js";
|
|
2
|
+
import { n as StorageBackend, t as CreateSessionResult } from "./storage-backend-sEtmM4yW.js";
|
|
3
3
|
|
|
4
4
|
//#region ../src/storage/null-storage-backend.d.ts
|
|
5
5
|
declare class NullStorageBackend implements StorageBackend {
|