gps-plus-slam-app-framework 1.7.0 → 1.9.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.
Files changed (238) hide show
  1. package/dist/{accuracy-circles-BtqOP_v1.d.ts → accuracy-circles-DkWBkF5g.d.ts} +16 -6
  2. package/dist/{app-selectors-ikTUQZpe.d.ts → app-selectors-BbAiCRUT.d.ts} +1 -1
  3. package/dist/ar/bresenham3d.d.ts +1 -1
  4. package/dist/ar/camera-blit-capture.d.ts +1 -1
  5. package/dist/ar/capability-checker.d.ts +1 -1
  6. package/dist/ar/capture-failure-tracker.d.ts +1 -1
  7. package/dist/ar/chromium-camera-access-workaround.d.ts +1 -1
  8. package/dist/ar/depth-grid-lookup.d.ts +1 -1
  9. package/dist/ar/depth-occluder.d.ts +127 -0
  10. package/dist/ar/depth-occluder.js +299 -0
  11. package/dist/ar/depth-sampler.d.ts +2 -2
  12. package/dist/ar/depth-sampler.js +24 -4
  13. package/dist/ar/depth-unprojection.d.ts +1 -1
  14. package/dist/ar/depth-unprojection.js +52 -21
  15. package/dist/ar/enable-gps-ar.d.ts +1 -1
  16. package/dist/ar/enable-gps-ar.js +1 -1
  17. package/dist/ar/frame-loop.d.ts +1 -1
  18. package/dist/ar/frame-loop.js +12 -2
  19. package/dist/ar/image-capture.d.ts +1 -1
  20. package/dist/ar/image-capture.js +1 -1
  21. package/dist/ar/image-quality.d.ts +1 -1
  22. package/dist/ar/image-quality.js +2 -1
  23. package/dist/ar/index.d.ts +32 -31
  24. package/dist/ar/index.js +5 -4
  25. package/dist/ar/occlusion-mesh-worker.d.ts +58 -0
  26. package/dist/ar/occlusion-mesh-worker.js +129 -0
  27. package/dist/ar/occupancy-grid.d.ts +2 -2
  28. package/dist/ar/occupancy-grid.js +287 -31
  29. package/dist/ar/occupancy-mesher.d.ts +2 -0
  30. package/dist/ar/occupancy-mesher.js +638 -0
  31. package/dist/ar/planar-pnp.d.ts +1 -1
  32. package/dist/ar/qr-debug-view.d.ts +1 -1
  33. package/dist/ar/qr-derived-pose.d.ts +1 -1
  34. package/dist/ar/qr-detection-controller.d.ts +1 -1
  35. package/dist/ar/qr-frontend.d.ts +1 -1
  36. package/dist/ar/qr-pose.d.ts +1 -1
  37. package/dist/ar/qr-size-depth-context.d.ts +2 -2
  38. package/dist/ar/qr-size-measurer.d.ts +1 -1
  39. package/dist/ar/replay-scene.d.ts +1 -1
  40. package/dist/ar/replay-scene.js +1 -1
  41. package/dist/ar/scene-node-names.d.ts +1 -1
  42. package/dist/ar/webxr-nue-basis.d.ts +1 -1
  43. package/dist/ar/webxr-session.d.ts +4 -4
  44. package/dist/ar/webxr-session.js +2 -2
  45. package/dist/ar/xr-camera-texture.d.ts +1 -1
  46. package/dist/ar/xr-error-handler.d.ts +1 -1
  47. package/dist/ar/xr-frame-loop.d.ts +1 -1
  48. package/dist/ar/xr-frame-loop.js +12 -2
  49. package/dist/{ar-MFpFlV7j.js → ar-BqCudtqE.js} +3 -2
  50. package/dist/{ar-world-group-alignment-69fgnB39.d.ts → ar-world-group-alignment-Cfg0trC3.d.ts} +1 -1
  51. package/dist/{capture-motion-gate-4EscNZCw.js → capture-motion-gate-_NdTSwKi.js} +17 -5
  52. package/dist/cell-key-DIB4rOTV.js +51 -0
  53. package/dist/core/index.d.ts +2 -2
  54. package/dist/core/index.js +2 -2
  55. package/dist/{create-slam-app-store-n_VF63D5.d.ts → create-slam-app-store-D1QSMuKu.d.ts} +8 -9
  56. package/dist/{create-slam-app-store-DkCHOpvb.js → create-slam-app-store-DXiAnJwR.js} +9 -2
  57. package/dist/{depth-grid-lookup-DQ-IYhO3.d.ts → depth-grid-lookup-D5atpNZ3.d.ts} +1 -1
  58. package/dist/{depth-sampler-asc8exHt.d.ts → depth-sampler-CwObAwEA.d.ts} +33 -1
  59. package/dist/{depth-unprojection-DjsbJW9L.d.ts → depth-unprojection-DRdBU90S.d.ts} +1 -1
  60. package/dist/{enable-gps-ar-DkNhFv4Y.d.ts → enable-gps-ar-BBtRO2FJ.d.ts} +4 -4
  61. package/dist/{frame-conversions-DObhj8Wf.d.ts → frame-conversions-DDcNHkTb.d.ts} +1 -1
  62. package/dist/{fused-path-v6eOmLl3.d.ts → fused-path-C5pp0PJU.d.ts} +1 -1
  63. package/dist/geo/h3-proximity.d.ts +1 -1
  64. package/dist/geo/index.d.ts +1 -1
  65. package/dist/{gps-anchor-BeFO2RYa.d.ts → gps-anchor-CEjGqEVD.d.ts} +1 -1
  66. package/dist/{gps-ar-pose-sampler-DHMZmYAc.d.ts → gps-ar-pose-sampler-DEGNk2CI.d.ts} +1 -1
  67. package/dist/{gps-event-coordinator-CjWW636x.d.ts → gps-event-coordinator-BWpgBrHq.d.ts} +4 -4
  68. package/dist/{image-capture-CNZDrQXG.d.ts → image-capture-Bp6Pd4Md.d.ts} +3 -3
  69. package/dist/{image-capture-B2-b6tKK.js → image-capture-DVcuqMtX.js} +1 -1
  70. package/dist/{index-BODDUTvD.d.ts → index-B6iBnO_K.d.ts} +1 -1
  71. package/dist/index-DXPVzOpP.d.ts +2 -0
  72. package/dist/{index-BneSj4_5.d.ts → index-cCQWuYqy.d.ts} +3 -3
  73. package/dist/index.d.ts +82 -80
  74. package/dist/index.js +10 -8
  75. package/dist/{leaflet-map-overlay-CmIyiE8n.d.ts → leaflet-map-overlay-C8GS4cUy.d.ts} +64 -10
  76. package/dist/leaflet-map-overlay-CBWPvaX9.js +452 -0
  77. package/dist/lerp-utils-Qs8OhumS.d.ts +42 -0
  78. package/dist/map-data-1tF5gcYV.js +124 -0
  79. package/dist/map-data-CUnTzRIC.d.ts +78 -0
  80. package/dist/{map-overlay-draw-ymz1s7fG.d.ts → map-overlay-draw-6WJeq0Kg.d.ts} +1 -1
  81. package/dist/{null-storage-backend-B9rtyDfG.d.ts → null-storage-backend-B1-jlpin.d.ts} +2 -2
  82. package/dist/occlusion-mesh-kXCQ2FM1.d.ts +194 -0
  83. package/dist/occupancy-grid-BV7RhB7I.d.ts +194 -0
  84. package/dist/occupancy-mesher-C2ecWhNp.d.ts +100 -0
  85. package/dist/{opfs-storage-3VZh-qH6.d.ts → opfs-storage-CTkxvxN4.d.ts} +5 -0
  86. package/dist/{opfs-storage-backend-JJiUJo-3.d.ts → opfs-storage-backend-D6Z03fML.d.ts} +2 -2
  87. package/dist/{persistence-middleware-DAPTTwB2.d.ts → persistence-middleware-CG-t08X4.d.ts} +1 -1
  88. package/dist/{planar-pnp-my9bqULS.d.ts → planar-pnp-D7bc_rLX.d.ts} +1 -1
  89. package/dist/{qr-debug-view-Brl73HGO.d.ts → qr-debug-view-BJooqbiP.d.ts} +1 -1
  90. package/dist/{qr-derived-pose-CmxTP2m4.d.ts → qr-derived-pose-BwNWn8oV.d.ts} +2 -2
  91. package/dist/{qr-detected-slice-VQssZmQJ.d.ts → qr-detected-slice-TzVb8bij.d.ts} +4 -4
  92. package/dist/{qr-detection-controller-CDurHv8E.d.ts → qr-detection-controller-Bo4XfQTP.d.ts} +3 -3
  93. package/dist/{qr-frontend--fFX25mV.d.ts → qr-frontend-BPURlh3S.d.ts} +1 -1
  94. package/dist/{qr-pose-aggregation-B7tWJq7j.d.ts → qr-pose-aggregation-CypnmjA-.d.ts} +1 -1
  95. package/dist/{qr-size-depth-context-CeAG53fy.d.ts → qr-size-depth-context-apAOmbrZ.d.ts} +2 -2
  96. package/dist/{qr-size-measurer-DQdAsid1.d.ts → qr-size-measurer-DxiMqRUw.d.ts} +3 -3
  97. package/dist/{recording-options-BPCpMHLK.d.ts → recording-options-DYKWVI93.d.ts} +203 -24
  98. package/dist/{recording-replayer-DQKpAGyQ.d.ts → recording-replayer-BJ8Lf0QY.d.ts} +2 -2
  99. package/dist/{recording-slice-DaxHZvNu.d.ts → recording-slice-BW8SiLo1.d.ts} +2 -2
  100. package/dist/{replay-engine-CmKh_4wf.d.ts → replay-engine-DmCAc7WM.d.ts} +1 -1
  101. package/dist/{replay-scene-DBlDrV_x.d.ts → replay-scene-zJ8SFDjE.d.ts} +3 -3
  102. package/dist/sensors/absolute-orientation.d.ts +1 -1
  103. package/dist/sensors/absolute-orientation.js +23 -6
  104. package/dist/sensors/gps-error-handler.d.ts +1 -1
  105. package/dist/sensors/gps.d.ts +1 -1
  106. package/dist/sensors/index.d.ts +4 -4
  107. package/dist/sensors/permission-checker.d.ts +1 -1
  108. package/dist/state/app-selectors.d.ts +1 -1
  109. package/dist/state/combined-root-state.d.ts +1 -1
  110. package/dist/state/create-slam-app-store.d.ts +1 -1
  111. package/dist/state/create-slam-app-store.js +1 -1
  112. package/dist/state/gps-ar-pose-sampler.d.ts +3 -3
  113. package/dist/state/gps-event-coordinator.d.ts +1 -1
  114. package/dist/state/index.d.ts +19 -19
  115. package/dist/state/index.js +2 -2
  116. package/dist/state/persistence-middleware.d.ts +1 -1
  117. package/dist/state/qr-detected-slice.d.ts +3 -3
  118. package/dist/state/recording-options.d.ts +2 -2
  119. package/dist/state/recording-options.js +150 -19
  120. package/dist/state/recording-replayer.d.ts +2 -2
  121. package/dist/state/recording-replayer.js +1 -1
  122. package/dist/state/recording-slice.d.ts +1 -1
  123. package/dist/state/replay-engine.d.ts +1 -1
  124. package/dist/state/store-subscribers.d.ts +2 -2
  125. package/dist/state/store-subscribers.js +26 -12
  126. package/dist/state/subscribe-to-selector.d.ts +1 -1
  127. package/dist/state/tracking-quality.d.ts +1 -1
  128. package/dist/state/tracking-quality.js +14 -6
  129. package/dist/state/tracking-slice.d.ts +1 -1
  130. package/dist/{state-E51QT_-j.js → state-BeKvn4B-.js} +1 -1
  131. package/dist/storage/file-system-utils.d.ts +1 -1
  132. package/dist/storage/index.d.ts +8 -8
  133. package/dist/storage/null-storage-backend.d.ts +1 -1
  134. package/dist/storage/opfs-storage-backend.d.ts +1 -1
  135. package/dist/storage/opfs-storage.d.ts +1 -1
  136. package/dist/storage/opfs-storage.js +30 -1
  137. package/dist/storage/storage-backend.d.ts +1 -1
  138. package/dist/storage/zip-coverage-embed.d.ts +1 -1
  139. package/dist/storage/zip-export.d.ts +1 -1
  140. package/dist/storage/zip-reader.d.ts +2 -2
  141. package/dist/storage/zip-reader.js +20 -7
  142. package/dist/{storage-backend-BdVLD0dU.d.ts → storage-backend-sEtmM4yW.d.ts} +1 -1
  143. package/dist/{store-subscribers-CA6AJH_8.d.ts → store-subscribers-DJUErW-o.d.ts} +6 -3
  144. package/dist/{subscribe-to-selector-B6EUmQ3I.d.ts → subscribe-to-selector-dQV8SpsO.d.ts} +1 -1
  145. package/dist/test-utils/browser-mocks.d.ts +1 -1
  146. package/dist/test-utils/browser-mocks.js +2 -0
  147. package/dist/test-utils/zip-round-trip-helpers.d.ts +1 -1
  148. package/dist/{tracking-slice-BuAZxXZ5.d.ts → tracking-slice-DMkHlbny.d.ts} +1 -1
  149. package/dist/types/ar-types.d.ts +1 -1
  150. package/dist/types/geo-types.d.ts +1 -1
  151. package/dist/types/index.d.ts +2 -2
  152. package/dist/utils/concurrency.d.ts +1 -1
  153. package/dist/utils/failure-tracker.d.ts +1 -1
  154. package/dist/utils/format-file-size.d.ts +1 -1
  155. package/dist/utils/fused-path.d.ts +1 -1
  156. package/dist/utils/index.d.ts +7 -7
  157. package/dist/utils/list-formatter.d.ts +1 -1
  158. package/dist/utils/logger.d.ts +1 -1
  159. package/dist/visualization/accuracy-circles.d.ts +1 -1
  160. package/dist/visualization/accuracy-circles.js +17 -8
  161. package/dist/visualization/alignment-lerper.d.ts +1 -1
  162. package/dist/visualization/ar-world-group-alignment.d.ts +1 -1
  163. package/dist/visualization/ar-world-group-alignment.js +1 -1
  164. package/dist/visualization/camera-follower.d.ts +1 -1
  165. package/dist/visualization/css3d-renderer-manager.d.ts +1 -1
  166. package/dist/visualization/frame-conversions.d.ts +1 -1
  167. package/dist/visualization/frustum-visibility.d.ts +1 -1
  168. package/dist/visualization/gps-anchor.d.ts +1 -1
  169. package/dist/visualization/gps-compass-cubes.d.ts +1 -1
  170. package/dist/visualization/gps-event-markers.d.ts +1 -1
  171. package/dist/visualization/gps-event-markers.js +1 -1
  172. package/dist/visualization/hit-test-reticle.d.ts +1 -1
  173. package/dist/visualization/index.d.ts +20 -19
  174. package/dist/visualization/index.js +4 -3
  175. package/dist/visualization/leaflet-map-overlay.d.ts +1 -1
  176. package/dist/visualization/leaflet-map-overlay.js +1 -282
  177. package/dist/visualization/lerp-utils.d.ts +2 -2
  178. package/dist/visualization/lerp-utils.js +27 -3
  179. package/dist/visualization/map-data.d.ts +1 -1
  180. package/dist/visualization/map-data.js +1 -32
  181. package/dist/visualization/map-overlay-draw.d.ts +1 -1
  182. package/dist/visualization/map-overlay-draw.js +24 -2
  183. package/dist/visualization/map-overlay.d.ts +1 -1
  184. package/dist/visualization/occluder-mesh-driver.d.ts +130 -0
  185. package/dist/visualization/occluder-mesh-driver.js +219 -0
  186. package/dist/visualization/occlusion-mesh.d.ts +2 -0
  187. package/dist/visualization/occlusion-mesh.js +437 -0
  188. package/dist/visualization/three-dispose.d.ts +1 -1
  189. package/dist/visualization/vis-colors.d.ts +1 -1
  190. package/dist/{webxr-session-DG-eNfny.js → webxr-session-BCP2XK7k.js} +88 -13
  191. package/dist/{webxr-session-Do5L6G3B.d.ts → webxr-session-BWCpGd-2.d.ts} +73 -10
  192. package/dist/{zip-reader-D9iPQwdQ.d.ts → zip-reader-BzENzMH6.d.ts} +36 -10
  193. package/package.json +9 -5
  194. package/dist/index-_Rhgoqkh.d.ts +0 -2
  195. package/dist/lerp-utils-BLmHhkVe.d.ts +0 -20
  196. package/dist/map-data-B4oEehUh.d.ts +0 -49
  197. package/dist/occupancy-grid-DnOc-6PU.d.ts +0 -82
  198. /package/dist/{absolute-orientation-CAEh840t.d.ts → absolute-orientation-BAxc15J6.d.ts} +0 -0
  199. /package/dist/{alignment-lerper-Bil6vbVw.d.ts → alignment-lerper-gD70k7Fq.d.ts} +0 -0
  200. /package/dist/{ar-types-BFX2r1wk.d.ts → ar-types-Cc5yvBwn.d.ts} +0 -0
  201. /package/dist/{bresenham3d-CU1Gv2W4.d.ts → bresenham3d-DGc4nIb-.d.ts} +0 -0
  202. /package/dist/{camera-blit-capture-NKT9H5-e.d.ts → camera-blit-capture-BimSdyih.d.ts} +0 -0
  203. /package/dist/{camera-follower-CqJJ7zRj.d.ts → camera-follower-DfBm2J5i.d.ts} +0 -0
  204. /package/dist/{capability-checker-DUQLg6gq.d.ts → capability-checker-BTTpygvZ.d.ts} +0 -0
  205. /package/dist/{capture-failure-tracker-gsm8q7yi.d.ts → capture-failure-tracker-_BVfhpxv.d.ts} +0 -0
  206. /package/dist/{capture-motion-gate-0Yl2NpQG.d.ts → capture-motion-gate-BGoAMkmf.d.ts} +0 -0
  207. /package/dist/{chromium-camera-access-workaround-D4K2uwwQ.d.ts → chromium-camera-access-workaround-C6yJLXz9.d.ts} +0 -0
  208. /package/dist/{concurrency-By0oOTZt.d.ts → concurrency-DPeGBH0X.d.ts} +0 -0
  209. /package/dist/{css3d-renderer-manager-B3kGNtcn.d.ts → css3d-renderer-manager-CPpVtvgs.d.ts} +0 -0
  210. /package/dist/{failure-tracker-BO7M7jF_.d.ts → failure-tracker-CeKf5HIc.d.ts} +0 -0
  211. /package/dist/{file-system-utils-DOajnMm-.d.ts → file-system-utils-DueXHxo9.d.ts} +0 -0
  212. /package/dist/{format-file-size-Dc98ulzB.d.ts → format-file-size-DxN-EBLc.d.ts} +0 -0
  213. /package/dist/{frame-loop-CpVjQ1dH.d.ts → frame-loop-7GBMBu68.d.ts} +0 -0
  214. /package/dist/{frustum-visibility-CeoCZchO.d.ts → frustum-visibility-CokkgS_i.d.ts} +0 -0
  215. /package/dist/{geo-types-3Ip1NzVR.d.ts → geo-types-DrGVu8nS.d.ts} +0 -0
  216. /package/dist/{gps-BBO3hKfT.d.ts → gps-CDKDQokE.d.ts} +0 -0
  217. /package/dist/{gps-compass-cubes-DSN87L8p.d.ts → gps-compass-cubes-1mooS3BS.d.ts} +0 -0
  218. /package/dist/{gps-error-handler-BbPsfw-_.d.ts → gps-error-handler-C34GQuTu.d.ts} +0 -0
  219. /package/dist/{gps-event-markers-Cz9RMA2k.d.ts → gps-event-markers-Di2nBzx-.d.ts} +0 -0
  220. /package/dist/{h3-proximity-C-j0pw1j.d.ts → h3-proximity-D0GTHQmr.d.ts} +0 -0
  221. /package/dist/{hit-test-reticle-DV-2TpB9.d.ts → hit-test-reticle-CaQbrhbk.d.ts} +0 -0
  222. /package/dist/{image-quality-smCBoFj1.d.ts → image-quality-BMx2ko9-.d.ts} +0 -0
  223. /package/dist/{index-BAPokxMb.d.ts → index-D74JTX4I.d.ts} +0 -0
  224. /package/dist/{list-formatter-KNMS0ay2.d.ts → list-formatter-DLFDnS70.d.ts} +0 -0
  225. /package/dist/{logger-CLnPrHiD.d.ts → logger-CqTd5Kpk.d.ts} +0 -0
  226. /package/dist/{map-overlay-Dh2B97MP.d.ts → map-overlay-BCQgvIgU.d.ts} +0 -0
  227. /package/dist/{permission-checker-CDuI8BwC.d.ts → permission-checker-B-VtjnBA.d.ts} +0 -0
  228. /package/dist/{qr-pose-D2rfu5DA.d.ts → qr-pose-y3vaEd6x.d.ts} +0 -0
  229. /package/dist/{scene-node-names-BiShOQDB.d.ts → scene-node-names-CGb-ylBd.d.ts} +0 -0
  230. /package/dist/{session-disposers-zv9OGXPB.js → session-disposers-BPrMplIC.js} +0 -0
  231. /package/dist/{three-dispose-Bt1k7WKh.d.ts → three-dispose-BMOAmPhb.d.ts} +0 -0
  232. /package/dist/{vis-colors-Obw2pLqT.d.ts → vis-colors-C7HI0u9_.d.ts} +0 -0
  233. /package/dist/{webxr-nue-basis-3nfjDJJp.d.ts → webxr-nue-basis-CaCnYxuq.d.ts} +0 -0
  234. /package/dist/{xr-camera-texture-Cva1q3Qj.d.ts → xr-camera-texture-q0eIDSRg.d.ts} +0 -0
  235. /package/dist/{xr-error-handler-CtvHpjfm.d.ts → xr-error-handler-uVGDu9IX.d.ts} +0 -0
  236. /package/dist/{xr-frame-loop-DIYtRcUa.d.ts → xr-frame-loop-DX3JPpAU.d.ts} +0 -0
  237. /package/dist/{zip-coverage-embed-DCAREVmw.d.ts → zip-coverage-embed-ncVbaZAz.d.ts} +0 -0
  238. /package/dist/{zip-export-ol0S_Ofd.d.ts → zip-export-CtiUnAlJ.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,124 @@
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 lastFused = fusedPath[fusedPath.length - 1];
103
+ const lastRaw = rawGpsPath[rawGpsPath.length - 1];
104
+ const userPosition = input.userPosition !== void 0 ? input.userPosition : lastFused ? {
105
+ lat: lastFused.lat,
106
+ lng: lastFused.lng
107
+ } : lastRaw ? {
108
+ lat: lastRaw.lat,
109
+ lng: lastRaw.lng
110
+ } : null;
111
+ const rotations = input.odometryRotations ?? [];
112
+ return {
113
+ userPosition,
114
+ rawGpsPath,
115
+ fusedPath,
116
+ alignmentSnapshots,
117
+ userHeadingDeg: computeUserHeadingDeg({
118
+ odometryRotation: rotations[rotations.length - 1] ?? null,
119
+ alignmentMatrix: input.alignmentMatrix ?? null
120
+ })
121
+ };
122
+ }
123
+ //#endregion
124
+ export { buildMapData as t };
@@ -0,0 +1,78 @@
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
+
4
+ //#region ../src/visualization/map-data.d.ts
5
+ /** Fully-resolved trajectory data ready to be drawn onto a Leaflet map. */
6
+ interface MapData {
7
+ /**
8
+ * User position for the blue dot, or null when unknown/hidden. Unless the
9
+ * caller provided an explicit value, this is the latest FUSED pose
10
+ * (GPS-converted tip of `fusedPath`), so the dot keeps moving indoors where
11
+ * the raw fix freezes; the last raw GPS fix is only the pre-alignment
12
+ * fallback. See
13
+ * gps-plus-slam/GpsPlusSlamJs_Docs/docs/2026-07-06-recorder-live-map-user-dot-fused-pose-user-feedback.md.
14
+ */
15
+ userPosition: GpsCoord | null;
16
+ /** Raw GPS samples (yellow polyline + per-event accuracy circles). */
17
+ rawGpsPath: RawGpsSample[];
18
+ /** Fused SLAM+GPS positions (cyan polyline), recomputed from latest matrix. */
19
+ fusedPath: GpsCoord[];
20
+ /** Alignment-snapshot GPS positions (red). */
21
+ alignmentSnapshots: GpsCoord[];
22
+ /**
23
+ * Absolute view-direction bearing (degrees clockwise from true geographic
24
+ * north, `[0, 360)`) for the user-position heading line, or null when
25
+ * undefined (no rotation/alignment yet, or camera near-vertical). See
26
+ * Finding 2 of
27
+ * gps-plus-slam/GpsPlusSlamJs_Docs/docs/2026-06-28-map-rings-transparency-and-view-direction-user-feedback.md.
28
+ *
29
+ * Optional only so pre-existing direct `MapData` literals (e.g. the static
30
+ * summary map, which is out of scope and draws no heading line) keep
31
+ * compiling. `buildMapData` ALWAYS sets it (to a bearing or null); a missing
32
+ * value is treated by `drawMapData` exactly like null (no line).
33
+ */
34
+ userHeadingDeg?: number | null;
35
+ }
36
+ /**
37
+ * Inputs to {@link buildMapData}, all optional so callers can supply only the
38
+ * slices they have. Array inputs are read-only and defensively copied.
39
+ */
40
+ interface MapDataInput {
41
+ /** Raw GPS samples in chronological order. */
42
+ rawGpsPath?: readonly RawGpsSample[];
43
+ /** Odometry positions (AR-local) used to derive the fused path. */
44
+ odometryPositions?: ReadonlyArray<Vector3>;
45
+ /**
46
+ * Odometry rotations (NUE quaternions, as stored in
47
+ * `gpsEvents.odometryRotations`). The LATEST entry drives the user heading
48
+ * line; earlier entries are ignored here. Pass the `selectOdometryRotations`
49
+ * value directly.
50
+ */
51
+ odometryRotations?: ReadonlyArray<Quaternion>;
52
+ /** Latest alignment matrix from the solver (null until first solve). */
53
+ alignmentMatrix?: Matrix4 | null;
54
+ /** GPS origin for ENU→GPS conversion (null when no GPS yet). */
55
+ zeroRef?: LatLong | null;
56
+ /** Alignment-snapshot GPS positions. */
57
+ alignmentSnapshots?: readonly GpsCoord[];
58
+ /**
59
+ * Explicit user position — including `null` to hide the dot (summary map).
60
+ * When omitted, defaults to the last `fusedPath` point when the fused path
61
+ * is non-empty, else the last `rawGpsPath` entry, else null (2026-07-06
62
+ * fused-dot feedback).
63
+ */
64
+ userPosition?: GpsCoord | null;
65
+ }
66
+ /**
67
+ * Build a {@link MapData} snapshot from store-derived inputs.
68
+ *
69
+ * Pure and free of Leaflet/Three dependencies. The fused path is always
70
+ * derived via {@link computeFusedPath} (D2); when the matrix or zero reference
71
+ * is missing it is an empty array.
72
+ *
73
+ * @param input - Optional trajectory slices (see {@link MapDataInput}).
74
+ * @returns A fully-resolved {@link MapData} with all arrays defensively copied.
75
+ */
76
+ declare function buildMapData(input: MapDataInput): MapData;
77
+ //#endregion
78
+ export { MapDataInput as n, buildMapData as r, MapData as t };