gps-plus-slam-app-framework 1.0.0 → 1.0.7

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 (216) hide show
  1. package/LICENSE +4 -4
  2. package/README.md +249 -108
  3. package/dist/accuracy-circles-CsxE_7x1.d.ts +43 -0
  4. package/dist/{app-selectors-DyMzxCEC.d.ts → app-selectors-BeU0_ab8.d.ts} +14 -15
  5. package/dist/ar/camera-blit-capture.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-sampler.d.ts +2 -2
  9. package/dist/ar/depth-sampler.js +1 -1
  10. package/dist/ar/frame-loop.d.ts +2 -0
  11. package/dist/ar/frame-loop.js +40 -0
  12. package/dist/ar/image-capture.d.ts +1 -1
  13. package/dist/ar/image-capture.js +1 -1
  14. package/dist/ar/index.d.ts +12 -12
  15. package/dist/ar/index.js +3 -3
  16. package/dist/ar/replay-scene.d.ts +1 -1
  17. package/dist/ar/scene-node-names.d.ts +1 -1
  18. package/dist/ar/webxr-session.d.ts +4 -4
  19. package/dist/ar/webxr-session.js +151 -56
  20. package/dist/ar/xr-camera-texture.d.ts +1 -1
  21. package/dist/ar/xr-error-handler.d.ts +1 -1
  22. package/dist/core/index.d.ts +2 -0
  23. package/dist/core/index.js +2 -0
  24. package/dist/create-slam-app-store-CNsZ3rcX.d.ts +371 -0
  25. package/dist/{depth-sampler-BNCtnEoP.d.ts → depth-sampler-xg8vna6c.d.ts} +1 -1
  26. package/dist/file-system-DTApEv_Q.d.ts +114 -0
  27. package/dist/frame-loop-QJIxLin0.d.ts +42 -0
  28. package/dist/frustum-visibility-BHnFrSKQ.d.ts +58 -0
  29. package/dist/{fused-path-DtqCSqJh.d.ts → fused-path-CB4DwWOG.d.ts} +1 -1
  30. package/dist/geo/h3-proximity.d.ts +2 -0
  31. package/dist/{ref-points/h3-ref-point.js → geo/h3-proximity.js} +16 -12
  32. package/dist/geo/index.d.ts +2 -0
  33. package/dist/geo/index.js +2 -0
  34. package/dist/{geo-types-Xx-LVcnw.d.ts → geo-types-CTuF23gG.d.ts} +16 -1
  35. package/dist/{gps-Ddzr2KlX.d.ts → gps-DXoQb9Kf.d.ts} +1 -1
  36. package/dist/gps-anchor-CnWY61XK.d.ts +48 -0
  37. package/dist/gps-anchor-DjC_3MoT.js +257 -0
  38. package/dist/gps-ar-pose-sampler-DsM1Kb2N.d.ts +43 -0
  39. package/dist/{recording-coordinator-DzflP07_.d.ts → gps-event-coordinator-DBx9I1YX.d.ts} +7 -6
  40. package/dist/gps-event-markers-BGr-iLvi.d.ts +103 -0
  41. package/dist/{h3-ref-point-CFhayNSZ.d.ts → h3-proximity-BhfgNbdW.d.ts} +16 -12
  42. package/dist/{image-capture-2pKjYUS6.d.ts → image-capture-DZLxHDN9.d.ts} +1 -1
  43. package/dist/index-gzWJMVpo.d.ts +2 -0
  44. package/dist/index-pOjUCKEP.d.ts +24 -0
  45. package/dist/index.d.ts +59 -56
  46. package/dist/index.js +22 -21
  47. package/dist/{leaflet-map-overlay-CvfQXTye.d.ts → leaflet-map-overlay-Cew7XAoO.d.ts} +32 -17
  48. package/dist/licensing/index.d.ts +1 -1
  49. package/dist/licensing/index.js +1 -1
  50. package/dist/{logger-B81iwxx0.d.ts → logger-C9vugiQb.d.ts} +7 -1
  51. package/dist/map-data-sih4g64v.d.ts +49 -0
  52. package/dist/map-overlay-draw-B3R1c9Xp.d.ts +42 -0
  53. package/dist/{null-storage-backend-CsWvQwN8.d.ts → null-storage-backend-BKOS7JpL.d.ts} +4 -2
  54. package/dist/{opfs-storage-ChOzF_Wp.d.ts → opfs-storage-DOPkzgeT.d.ts} +54 -51
  55. package/dist/opfs-storage-backend-Cj_7PWoq.d.ts +13 -0
  56. package/dist/{permission-checker-jO2ziCWC.d.ts → permission-checker-DPJl_RU6.d.ts} +41 -1
  57. package/dist/persistence-middleware-BkHIqvaA.d.ts +55 -0
  58. package/dist/{recording-replayer-oOzPITrd.d.ts → recording-replayer-CmCIIZ02.d.ts} +2 -2
  59. package/dist/recording-slice-d6OtCrza.d.ts +28 -0
  60. package/dist/{replay-engine-DuX316ae.d.ts → replay-engine-BGBRQyXs.d.ts} +4 -1
  61. package/dist/{replay-scene-DtUm6bsr.d.ts → replay-scene-DpAGcAKY.d.ts} +3 -3
  62. package/dist/sensors/gps-error-handler.d.ts +1 -1
  63. package/dist/sensors/gps.d.ts +1 -1
  64. package/dist/sensors/gps.js +2 -2
  65. package/dist/sensors/index.d.ts +3 -3
  66. package/dist/sensors/index.js +1 -1
  67. package/dist/sensors/permission-checker.d.ts +2 -2
  68. package/dist/sensors/permission-checker.js +79 -1
  69. package/dist/state/app-selectors.d.ts +2 -2
  70. package/dist/state/app-selectors.js +33 -3
  71. package/dist/state/combined-root-state.d.ts +2 -0
  72. package/dist/state/combined-root-state.js +1 -0
  73. package/dist/state/create-slam-app-store.d.ts +2 -0
  74. package/dist/state/create-slam-app-store.js +75 -0
  75. package/dist/state/gps-ar-pose-sampler.d.ts +4 -0
  76. package/dist/state/gps-ar-pose-sampler.js +19 -0
  77. package/dist/state/{recording-coordinator.d.ts → gps-event-coordinator.d.ts} +1 -1
  78. package/dist/state/{recording-coordinator.js → gps-event-coordinator.js} +3 -4
  79. package/dist/state/index.d.ts +17 -17
  80. package/dist/state/index.js +10 -8
  81. package/dist/state/persistence-middleware.d.ts +2 -2
  82. package/dist/state/persistence-middleware.js +39 -11
  83. package/dist/state/recording-options.d.ts +1 -1
  84. package/dist/state/recording-replayer.d.ts +3 -2
  85. package/dist/state/recording-replayer.js +2 -2
  86. package/dist/state/recording-slice.d.ts +2 -0
  87. package/dist/state/{recorder-slice.js → recording-slice.js} +7 -15
  88. package/dist/state/replay-engine.d.ts +1 -1
  89. package/dist/state/replay-engine.js +3 -3
  90. package/dist/state/store-subscribers.d.ts +2 -2
  91. package/dist/state/store-subscribers.js +38 -20
  92. package/dist/state/subscribe-to-selector.d.ts +1 -1
  93. package/dist/state/tracking-quality.d.ts +2 -0
  94. package/dist/state/tracking-quality.js +723 -0
  95. package/dist/state/tracking-slice.d.ts +2 -0
  96. package/dist/state/tracking-slice.js +147 -0
  97. package/dist/storage/file-system-utils.d.ts +1 -1
  98. package/dist/storage/file-system.d.ts +2 -2
  99. package/dist/storage/file-system.js +154 -94
  100. package/dist/storage/index.d.ts +9 -11
  101. package/dist/storage/index.js +5 -7
  102. package/dist/storage/null-storage-backend.d.ts +1 -1
  103. package/dist/storage/null-storage-backend.js +7 -0
  104. package/dist/storage/opfs-storage-backend.d.ts +1 -1
  105. package/dist/storage/opfs-storage-backend.js +7 -1
  106. package/dist/storage/opfs-storage.d.ts +2 -2
  107. package/dist/storage/opfs-storage.js +69 -85
  108. package/dist/storage/storage-backend.d.ts +2 -2
  109. package/dist/storage/zip-export.d.ts +2 -2
  110. package/dist/storage/zip-export.js +55 -62
  111. package/dist/storage/zip-reader.d.ts +2 -2
  112. package/dist/storage/zip-reader.js +32 -5
  113. package/dist/storage-backend-CT0Il_AB.d.ts +28 -0
  114. package/dist/{store-subscribers-CuoIzB5T.d.ts → store-subscribers-Dce_CoJF.d.ts} +31 -7
  115. package/dist/{subscribe-to-selector-UTZ0KkAc.d.ts → subscribe-to-selector-DIHm3BRZ.d.ts} +1 -1
  116. package/dist/test-utils/browser-mocks.d.ts +1 -1
  117. package/dist/test-utils/zip-round-trip-helpers.d.ts +7 -1
  118. package/dist/test-utils/zip-round-trip-helpers.js +6 -5
  119. package/dist/tracking-slice-utCNND8t.d.ts +86 -0
  120. package/dist/types/ar-types.d.ts +1 -1
  121. package/dist/types/geo-types.d.ts +2 -2
  122. package/dist/types/index.d.ts +3 -3
  123. package/dist/utils/concurrency.d.ts +1 -1
  124. package/dist/utils/failure-tracker.d.ts +1 -1
  125. package/dist/utils/format-file-size.d.ts +1 -1
  126. package/dist/utils/fused-path.d.ts +1 -1
  127. package/dist/utils/index.d.ts +6 -6
  128. package/dist/utils/list-formatter.d.ts +1 -1
  129. package/dist/utils/logger.d.ts +1 -1
  130. package/dist/utils/logger.js +68 -8
  131. package/dist/visualization/accuracy-circles.d.ts +2 -0
  132. package/dist/visualization/accuracy-circles.js +57 -0
  133. package/dist/visualization/alignment-lerper.d.ts +1 -1
  134. package/dist/visualization/camera-follower.d.ts +1 -1
  135. package/dist/visualization/css3d-renderer-manager.d.ts +1 -1
  136. package/dist/visualization/frustum-visibility.d.ts +2 -0
  137. package/dist/visualization/frustum-visibility.js +139 -0
  138. package/dist/visualization/gps-anchor.d.ts +2 -0
  139. package/dist/visualization/gps-anchor.js +2 -0
  140. package/dist/visualization/gps-compass-cubes.d.ts +1 -1
  141. package/dist/visualization/gps-event-markers.d.ts +2 -2
  142. package/dist/visualization/gps-event-markers.js +69 -2
  143. package/dist/visualization/index.d.ts +17 -12
  144. package/dist/visualization/index.js +6 -2
  145. package/dist/visualization/leaflet-map-overlay.d.ts +1 -1
  146. package/dist/visualization/leaflet-map-overlay.js +56 -98
  147. package/dist/visualization/lerp-utils.d.ts +1 -1
  148. package/dist/visualization/map-data.d.ts +2 -0
  149. package/dist/visualization/map-data.js +33 -0
  150. package/dist/visualization/map-overlay-draw.d.ts +2 -0
  151. package/dist/visualization/map-overlay-draw.js +107 -0
  152. package/dist/visualization/map-overlay.d.ts +1 -1
  153. package/dist/visualization/three-dispose.d.ts +1 -1
  154. package/dist/visualization/vis-colors.d.ts +1 -1
  155. package/dist/{webxr-session-COlSrXdL.d.ts → webxr-session-DB5cIZnN.d.ts} +53 -12
  156. package/dist/{zip-export-Rtpi75JJ.d.ts → zip-export-QO4iLIi7.d.ts} +58 -7
  157. package/dist/{zip-reader-D7pkpX8K.d.ts → zip-reader-D1105idz.d.ts} +38 -1
  158. package/package.json +29 -18
  159. package/dist/ar/tracking-state.d.ts +0 -2
  160. package/dist/ar/tracking-state.js +0 -164
  161. package/dist/community-license-key-ChZLCJyO.d.ts +0 -21
  162. package/dist/file-system-BsQgYCU3.d.ts +0 -128
  163. package/dist/gps-event-markers-DpEy2qRd.d.ts +0 -67
  164. package/dist/licensing/community-license-key.d.ts +0 -2
  165. package/dist/licensing/community-license-key.js +0 -21
  166. package/dist/opfs-storage-backend-CVcmfJHN.d.ts +0 -11
  167. package/dist/persistence-middleware-DrptY02i.d.ts +0 -31
  168. package/dist/recorder-slice-fcwAXrsh.d.ts +0 -35
  169. package/dist/ref-point-importer-B_wn5ABk.d.ts +0 -62
  170. package/dist/ref-point-loader-Pt1smgHS.d.ts +0 -117
  171. package/dist/ref-points/h3-ref-point.d.ts +0 -2
  172. package/dist/ref-points/index.d.ts +0 -2
  173. package/dist/ref-points/index.js +0 -2
  174. package/dist/ref-points-slice-tC-Wd6XJ.d.ts +0 -48
  175. package/dist/reference-points-CFWy8_wj.d.ts +0 -68
  176. package/dist/routing-slice-BlHTM8eh.d.ts +0 -23
  177. package/dist/state/recorder-slice.d.ts +0 -2
  178. package/dist/state/ref-points-slice.d.ts +0 -2
  179. package/dist/state/ref-points-slice.js +0 -44
  180. package/dist/state/routing-slice.d.ts +0 -2
  181. package/dist/state/routing-slice.js +0 -13
  182. package/dist/state/store.d.ts +0 -9
  183. package/dist/state/store.js +0 -71
  184. package/dist/storage/ref-point-importer.d.ts +0 -2
  185. package/dist/storage/ref-point-importer.js +0 -163
  186. package/dist/storage/ref-point-loader.d.ts +0 -2
  187. package/dist/storage/ref-point-loader.js +0 -248
  188. package/dist/storage/ref-point-recovery.d.ts +0 -28
  189. package/dist/storage/ref-point-recovery.js +0 -159
  190. package/dist/storage-backend-BcEItnFr.d.ts +0 -20
  191. package/dist/store-C7PKOj8S.d.ts +0 -96
  192. package/dist/tracking-state-B9QfuZCg.d.ts +0 -101
  193. package/dist/visualization/reference-points.d.ts +0 -2
  194. package/dist/visualization/reference-points.js +0 -160
  195. /package/dist/{alignment-lerper-D9BeDwZX.d.ts → alignment-lerper-L1RbWHQ4.d.ts} +0 -0
  196. /package/dist/{ar-types-B-ORgk6Z.d.ts → ar-types-Ck8LEq8k.d.ts} +0 -0
  197. /package/dist/{camera-blit-capture-DHuyrrxr.d.ts → camera-blit-capture-C1V9I1Z6.d.ts} +0 -0
  198. /package/dist/{camera-follower-BMqPddxx.d.ts → camera-follower-1k8mnw5J.d.ts} +0 -0
  199. /package/dist/{capture-failure-tracker-2Q6oI2uQ.d.ts → capture-failure-tracker-JKb1ST8E.d.ts} +0 -0
  200. /package/dist/{chromium-camera-access-workaround-CU5zSKNr.d.ts → chromium-camera-access-workaround-NvChXsi8.d.ts} +0 -0
  201. /package/dist/{concurrency-p6gB30fT.d.ts → concurrency-CMw8pSaG.d.ts} +0 -0
  202. /package/dist/{css3d-renderer-manager-rgLMVAvd.d.ts → css3d-renderer-manager-CkjCb2HW.d.ts} +0 -0
  203. /package/dist/{failure-tracker-1sQ_Cgyw.d.ts → failure-tracker-DnHXOIRa.d.ts} +0 -0
  204. /package/dist/{file-system-utils-5cWO8Xyv.d.ts → file-system-utils-CtKiiw27.d.ts} +0 -0
  205. /package/dist/{format-file-size-onGq0k1Q.d.ts → format-file-size-BTS5H62-.d.ts} +0 -0
  206. /package/dist/{gps-compass-cubes-C-6JEnpR.d.ts → gps-compass-cubes-CsLtFv4B.d.ts} +0 -0
  207. /package/dist/{gps-error-handler-VPuNOuf8.d.ts → gps-error-handler-Dg3ziRVY.d.ts} +0 -0
  208. /package/dist/{lerp-utils-87NWjUOD.d.ts → lerp-utils-B8MM8zCR.d.ts} +0 -0
  209. /package/dist/{list-formatter-DKQQVLcl.d.ts → list-formatter-CNqi-E2j.d.ts} +0 -0
  210. /package/dist/{map-overlay-DuiZuCTx.d.ts → map-overlay-DAmreyA3.d.ts} +0 -0
  211. /package/dist/{recording-options-BYUFX8iU.d.ts → recording-options-Dq6IsKAv.d.ts} +0 -0
  212. /package/dist/{scene-node-names-DPsaE2z3.d.ts → scene-node-names-6LBjPmwz.d.ts} +0 -0
  213. /package/dist/{three-dispose-CCBHjE_a.d.ts → three-dispose-C2drkp6f.d.ts} +0 -0
  214. /package/dist/{vis-colors-Qi2vlRGt.d.ts → vis-colors-CHQhjal3.d.ts} +0 -0
  215. /package/dist/{xr-camera-texture-BbukNE79.d.ts → xr-camera-texture-BU5YvKAM.d.ts} +0 -0
  216. /package/dist/{xr-error-handler-DTHfmvrl.d.ts → xr-error-handler-CDY5QxGb.d.ts} +0 -0
@@ -8,26 +8,26 @@ import { formatActionFilename, formatFrameFilename, formatTimestamp } from "./fi
8
8
  * OPFS works consistently on Desktop Chrome, Android Chrome, and iOS Safari,
9
9
  * unlike showDirectoryPicker which has write restrictions on Android.
10
10
  *
11
- * Directory structure:
12
- * /gps-recorder/
13
- * └── scenarios/
14
- * ├── {scenario-name}/
15
- * │ ├── recording-YYYY-MM-DD_HH-MM-SSutc/
16
- * │ ├── session.json
17
- * │ │ ├── actions/
18
- * │ │ │ ├── 000001.json
19
- * │ │ │ └── ...
20
- * │ │ └── frames/
21
- * │ ├── frame-000001.jpg
22
- * │ │ └── ...
23
- * │ └── ...
11
+ * Directory structure (flat sessions — no scenario hierarchy):
12
+ * /gps-plus-slam/
13
+ * └── sessions/
14
+ * ├── recording-YYYY-MM-DD_HH-MM-SSutc/
15
+ * │ ├── session.json
16
+ * │ ├── actions/
17
+ * │ │ ├── 000001.json
18
+ * │ │ └── ...
19
+ * │ └── frames/
20
+ * │ ├── frame-000001.jpg
21
+ * │ └── ...
24
22
  * └── ...
23
+ *
24
+ * Scenario grouping (scenarios/{name}/recording-…) is a recorder-only concern,
25
+ * handled by ScenarioWrappingStorageBackend in the recorder app.
25
26
  */
26
27
  const log = createLogger("OPFS");
27
28
  let opfsRoot = null;
28
- let gpsRecorderDir = null;
29
- let scenariosDir = null;
30
- let currentScenarioHandle = null;
29
+ let gpsPlusSlamDir = null;
30
+ let sessionsDir = null;
31
31
  let currentSessionHandle = null;
32
32
  let actionsHandle = null;
33
33
  let framesHandle = null;
@@ -37,9 +37,8 @@ let framesHandle = null;
37
37
  */
38
38
  function resetOpfsStorage() {
39
39
  opfsRoot = null;
40
- gpsRecorderDir = null;
41
- scenariosDir = null;
42
- currentScenarioHandle = null;
40
+ gpsPlusSlamDir = null;
41
+ sessionsDir = null;
43
42
  currentSessionHandle = null;
44
43
  actionsHandle = null;
45
44
  framesHandle = null;
@@ -47,14 +46,11 @@ function resetOpfsStorage() {
47
46
  /**
48
47
  * Reset session-level handles only.
49
48
  *
50
- * Preserves opfsRoot/gpsRecorderDir/scenariosDir (OPFS stays initialized)
51
- * but clears current scenario/session/actions/frames handles so a new
52
- * session can be started fresh.
53
- *
54
- * Used during soft reset for new recordings (Issue 4, 2026-02-06 user feedback).
49
+ * Preserves opfsRoot/gpsPlusSlamDir/sessionsDir (OPFS stays initialized)
50
+ * but clears current session/actions/frames handles so a new session can
51
+ * be started fresh.
55
52
  */
56
53
  function resetSessionHandles() {
57
- currentScenarioHandle = null;
58
54
  currentSessionHandle = null;
59
55
  actionsHandle = null;
60
56
  framesHandle = null;
@@ -63,8 +59,8 @@ function resetSessionHandles() {
63
59
  * Initialize OPFS storage by creating the required directory structure.
64
60
  *
65
61
  * Creates:
66
- * - /gps-recorder/
67
- * - /gps-recorder/scenarios/
62
+ * - /gps-plus-slam/
63
+ * - /gps-plus-slam/sessions/
68
64
  *
69
65
  * This is idempotent - calling multiple times is safe.
70
66
  *
@@ -74,65 +70,74 @@ async function initOpfsStorage() {
74
70
  if (typeof navigator === "undefined" || !navigator.storage || typeof navigator.storage.getDirectory !== "function") throw new Error("OPFS (Origin Private File System) is not supported in this browser. Please use Chrome 86+, Safari 15.2+, or Firefox 111+.");
75
71
  opfsRoot = await navigator.storage.getDirectory();
76
72
  log.info("OPFS root obtained");
77
- gpsRecorderDir = await opfsRoot.getDirectoryHandle("gps-recorder", { create: true });
78
- scenariosDir = await gpsRecorderDir.getDirectoryHandle("scenarios", { create: true });
73
+ gpsPlusSlamDir = await opfsRoot.getDirectoryHandle("gps-plus-slam", { create: true });
74
+ sessionsDir = await gpsPlusSlamDir.getDirectoryHandle("sessions", { create: true });
79
75
  log.info("OPFS storage initialized");
80
76
  }
81
77
  /**
82
- * Create a new recording session within a scenario.
78
+ * Get the sessions root directory handle.
79
+ * Used by consumers that need direct OPFS access (e.g. ZIP export).
80
+ */
81
+ function getSessionsRootHandle() {
82
+ return sessionsDir;
83
+ }
84
+ /**
85
+ * Get the app root directory handle (gps-plus-slam/).
86
+ * Used by consumers that need to create additional directories alongside sessions/.
87
+ */
88
+ function getAppRootHandle() {
89
+ return gpsPlusSlamDir;
90
+ }
91
+ /**
92
+ * Create a new recording session.
83
93
  *
84
94
  * Creates the directory structure:
85
- * - /gps-recorder/scenarios/{scenarioName}/recording-{timestamp}/
86
- * - /gps-recorder/scenarios/{scenarioName}/recording-{timestamp}/actions/
87
- * - /gps-recorder/scenarios/{scenarioName}/recording-{timestamp}/frames/
95
+ * - /gps-plus-slam/sessions/recording-{timestamp}/
96
+ * - /gps-plus-slam/sessions/recording-{timestamp}/actions/
97
+ * - /gps-plus-slam/sessions/recording-{timestamp}/frames/
88
98
  *
89
- * @param scenarioName - Name of the scenario (creates if doesn't exist)
90
99
  * @param timestamp - Session start time (used for folder naming)
100
+ * @param _contextTag - Opaque tag; not used by the flat layout but accepted
101
+ * for interface compatibility with wrapping backends
91
102
  * @returns Session information
92
103
  * @throws Error if storage not initialized
93
104
  */
94
- async function createSession(scenarioName, timestamp) {
95
- if (!scenariosDir) throw new Error("OPFS storage not initialized. Call initOpfsStorage first.");
96
- currentScenarioHandle = await scenariosDir.getDirectoryHandle(scenarioName, { create: true });
105
+ async function createSession(timestamp, _contextTag) {
106
+ if (!sessionsDir) throw new Error("OPFS storage not initialized. Call initOpfsStorage first.");
97
107
  const sessionName = `recording-${formatTimestamp(timestamp)}`;
98
- currentSessionHandle = await currentScenarioHandle.getDirectoryHandle(sessionName, { create: true });
108
+ currentSessionHandle = await sessionsDir.getDirectoryHandle(sessionName, { create: true });
99
109
  actionsHandle = await currentSessionHandle.getDirectoryHandle("actions", { create: true });
100
110
  framesHandle = await currentSessionHandle.getDirectoryHandle("frames", { create: true });
101
- log.info("Session created:", `${scenarioName}/${sessionName}`);
102
- return {
103
- scenarioName,
104
- sessionName
105
- };
111
+ log.info("Session created:", sessionName);
112
+ return { sessionName };
106
113
  }
107
114
  /**
108
115
  * Get the current session directory handle.
109
- * Useful for test assertions.
110
116
  * @internal
111
117
  */
112
118
  function getSessionHandle() {
113
119
  return currentSessionHandle;
114
120
  }
115
121
  /**
116
- * Get the current scenario directory handle.
117
- */
118
- function getScenarioHandle() {
119
- return currentScenarioHandle;
120
- }
121
- /**
122
- * Get the scenarios root directory handle.
123
- * Used for loading reference points across scenarios.
122
+ * Set the active session handles externally.
123
+ *
124
+ * Used by file-system.ts's scenario-based path to reuse opfs-storage's
125
+ * write infrastructure for sessions created outside the flat layout.
126
+ * This is a temporary bridge until the recorder migrates to
127
+ * ScenarioWrappingStorageBackend.
128
+ *
129
+ * @internal
124
130
  */
125
- function getScenariosRootHandle() {
126
- return scenariosDir;
131
+ function setSessionHandles(session, actions, frames) {
132
+ currentSessionHandle = session;
133
+ actionsHandle = actions;
134
+ framesHandle = frames;
127
135
  }
128
136
  /**
129
137
  * Safely write data to a file handle with proper cleanup.
130
138
  *
131
139
  * Uses try/finally to ensure the writable stream is always cleaned up,
132
140
  * calling abort() on errors to prevent resource leaks and file locks.
133
- *
134
- * @param fileHandle - The file handle to write to
135
- * @param data - The data to write (string or Blob)
136
141
  */
137
142
  async function safeWriteToFile(fileHandle, data) {
138
143
  const writable = await fileHandle.createWritable();
@@ -188,40 +193,19 @@ async function writeSessionMetadata(metadata) {
188
193
  log.info("Session metadata written");
189
194
  }
190
195
  /**
191
- * List all scenario names in OPFS storage.
192
- *
193
- * @returns Array of scenario folder names
194
- */
195
- async function listScenarios() {
196
- if (!scenariosDir) return [];
197
- const scenarios = [];
198
- for await (const entry of scenariosDir.values()) if (entry.kind === "directory") scenarios.push(entry.name);
199
- return scenarios;
200
- }
201
- /**
202
- * List all session names within a scenario.
196
+ * List all session names in OPFS storage.
203
197
  *
204
- * @param scenarioName - Name of the scenario
205
198
  * @returns Array of session folder names
206
199
  */
207
- async function listSessions(scenarioName) {
208
- if (!scenariosDir) return [];
209
- try {
210
- const scenarioHandle = await scenariosDir.getDirectoryHandle(scenarioName);
211
- const sessions = [];
212
- for await (const entry of scenarioHandle.values()) if (entry.kind === "directory") sessions.push(entry.name);
213
- return sessions;
214
- } catch {
215
- return [];
216
- }
200
+ async function listSessions() {
201
+ if (!sessionsDir) return [];
202
+ const sessions = [];
203
+ for await (const entry of sessionsDir.values()) if (entry.kind === "directory") sessions.push(entry.name);
204
+ return sessions;
217
205
  }
218
206
  /**
219
207
  * Check storage quota.
220
208
  *
221
- * Note: Prefer calling `initOpfsStorage()` before this function to ensure
222
- * the Storage API is available. If the Storage Manager API is not supported,
223
- * returns a safe default of `{ available: 0, used: 0 }`.
224
- *
225
209
  * @returns Available and used storage in bytes
226
210
  */
227
211
  async function checkStorageQuota() {
@@ -239,4 +223,4 @@ async function checkStorageQuota() {
239
223
  };
240
224
  }
241
225
  //#endregion
242
- export { checkStorageQuota, createSession, getScenarioHandle, getScenariosRootHandle, getSessionHandle, initOpfsStorage, listScenarios, listSessions, resetOpfsStorage, resetSessionHandles, writeAction, writeFrame, writeSessionMetadata };
226
+ export { checkStorageQuota, createSession, getAppRootHandle, getSessionHandle, getSessionsRootHandle, initOpfsStorage, listSessions, resetOpfsStorage, resetSessionHandles, setSessionHandles, writeAction, writeFrame, writeSessionMetadata };
@@ -1,2 +1,2 @@
1
- import { t as StorageBackend } from "../storage-backend-BcEItnFr.js";
2
- export { StorageBackend };
1
+ import { n as StorageBackend, t as CreateSessionResult } from "../storage-backend-CT0Il_AB.js";
2
+ export { CreateSessionResult, StorageBackend };
@@ -1,2 +1,2 @@
1
- import { a as syncToExternalZip, i as exportSessionAsZip, n as downloadZip, r as exportAndDownloadSession, t as ZipExportResult } from "../zip-export-Rtpi75JJ.js";
2
- export { ZipExportResult, downloadZip, exportAndDownloadSession, exportSessionAsZip, syncToExternalZip };
1
+ import { a as downloadZip, c as syncToExternalZip, i as ZipExportResult, n as ZipContributorAddFile, o as exportAndDownloadSession, r as ZipExportContributor, s as exportSessionAsZip, t as ExportSessionAsZipOptions } from "../zip-export-QO4iLIi7.js";
2
+ export { ExportSessionAsZipOptions, ZipContributorAddFile, ZipExportContributor, ZipExportResult, downloadZip, exportAndDownloadSession, exportSessionAsZip, syncToExternalZip };
@@ -22,9 +22,16 @@ const log = createLogger("ZipExport");
22
22
  /**
23
23
  * Get the OPFS scenarios directory handle.
24
24
  * Re-acquires from navigator.storage to avoid holding stale references.
25
+ * Creates on demand so it works even when initOpfsStorage only creates sessions/.
25
26
  */
26
27
  async function getScenariosHandle() {
27
- return (await (await navigator.storage.getDirectory()).getDirectoryHandle("gps-recorder")).getDirectoryHandle("scenarios");
28
+ return (await (await navigator.storage.getDirectory()).getDirectoryHandle("gps-plus-slam")).getDirectoryHandle("scenarios", { create: true });
29
+ }
30
+ /**
31
+ * Get the OPFS sessions directory handle (flat layout).
32
+ */
33
+ async function getSessionsHandle() {
34
+ return (await (await navigator.storage.getDirectory()).getDirectoryHandle("gps-plus-slam")).getDirectoryHandle("sessions");
28
35
  }
29
36
  /**
30
37
  * Stream all files from a directory recursively into a ZipWriter.
@@ -49,80 +56,66 @@ async function streamDirectoryToZip(dirHandle, zipWriter, basePath = "") {
49
56
  return count;
50
57
  }
51
58
  /**
52
- * Filter and stream per-session ref point observations into a ZipWriter.
53
- *
54
- * Reads each ref point JSON from the scenario-level refPoints/ directory,
55
- * keeps only observations where `sessionId` matches `sessionName`, and writes
56
- * the filtered definition into the ZIP under `refPoints/{h3}.json`.
57
- *
58
- * Ref points with no observations for the current session are skipped entirely.
59
- * If the refPoints/ directory doesn't exist yet (new scenario), returns 0.
60
- *
61
- * @returns Number of ref point files added
62
- */
63
- async function streamSessionRefPointsToZip(scenarioHandle, sessionName, zipWriter) {
64
- let count = 0;
65
- let refPointsHandle;
66
- try {
67
- refPointsHandle = await scenarioHandle.getDirectoryHandle("refPoints");
68
- } catch {
69
- return 0;
70
- }
71
- for await (const [name, handle] of refPointsHandle.entries()) {
72
- if (handle.kind !== "file" || !name.endsWith(".json")) continue;
73
- try {
74
- const text = await (await handle.getFile()).text();
75
- const def = JSON.parse(text);
76
- if (!Array.isArray(def.observations)) continue;
77
- const sessionObs = def.observations.filter((o) => o.sessionId === sessionName);
78
- if (sessionObs.length === 0) continue;
79
- const filtered = {
80
- ...def,
81
- observations: sessionObs
82
- };
83
- const blob = new Blob([JSON.stringify(filtered, null, 2)], { type: "application/json" });
84
- await zipWriter.add(`refPoints/${name}`, new BlobReader(blob));
85
- count++;
86
- } catch (err) {
87
- log.warn(`Failed to process ref point ${name}:`, err);
88
- }
89
- }
90
- return count;
91
- }
92
- /**
93
59
  * Export a session from OPFS as a ZIP blob.
94
60
  *
95
61
  * The ZIP structure mirrors the OPFS session structure:
96
62
  * - session.json (at root)
97
63
  * - actions/000001.json, actions/000002.json, ...
98
64
  * - frames/frame-000001.jpg, frames/frame-000002.jpg, ...
99
- * - refPoints/{h3}.json (only observations from this session)
65
+ * - refPoints/{h3}.json (only observations from this session, scenario layout only)
100
66
  *
101
67
  * Uses "store" mode (compression level 0) for fast packaging.
102
68
  *
103
- * @param scenarioName - Name of the scenario
104
- * @param sessionName - Name of the session folder
69
+ * Supports both flat and scenario-based layouts:
70
+ * - `exportSessionAsZip(sessionName)` flat layout (sessions/)
71
+ * - `exportSessionAsZip(scenarioName, sessionName)` — scenario layout (scenarios/{name}/)
72
+ *
105
73
  * @returns ZIP export result with blob and file count
106
- * @throws Error if scenario or session not found
74
+ * @throws Error if session not found
107
75
  */
108
- async function exportSessionAsZip(scenarioName, sessionName) {
109
- log.info(`Exporting session: ${scenarioName}/${sessionName}`);
110
- const scenariosDir = await getScenariosHandle();
111
- let scenarioHandle;
112
- try {
113
- scenarioHandle = await scenariosDir.getDirectoryHandle(scenarioName);
114
- } catch {
115
- throw new Error(`Scenario "${scenarioName}" not found in OPFS storage`);
116
- }
76
+ async function exportSessionAsZip(scenarioNameOrSessionName, sessionName, options) {
117
77
  let sessionHandle;
118
- try {
119
- sessionHandle = await scenarioHandle.getDirectoryHandle(sessionName);
120
- } catch {
121
- throw new Error(`Session "${sessionName}" not found in scenario "${scenarioName}"`);
78
+ if (sessionName) {
79
+ const scenarioName = scenarioNameOrSessionName;
80
+ log.info(`Exporting session: ${scenarioName}/${sessionName}`);
81
+ const scenariosDir = await getScenariosHandle();
82
+ let scenarioHandle;
83
+ try {
84
+ scenarioHandle = await scenariosDir.getDirectoryHandle(scenarioName);
85
+ } catch {
86
+ throw new Error(`Scenario "${scenarioName}" not found in OPFS storage`);
87
+ }
88
+ try {
89
+ sessionHandle = await scenarioHandle.getDirectoryHandle(sessionName);
90
+ } catch {
91
+ throw new Error(`Session "${sessionName}" not found in scenario "${scenarioName}"`);
92
+ }
93
+ } else {
94
+ log.info(`Exporting session: ${scenarioNameOrSessionName}`);
95
+ const sessionsDir = await getSessionsHandle();
96
+ try {
97
+ sessionHandle = await sessionsDir.getDirectoryHandle(scenarioNameOrSessionName);
98
+ } catch {
99
+ throw new Error(`Session "${scenarioNameOrSessionName}" not found in OPFS storage`);
100
+ }
122
101
  }
123
102
  const zipWriter = new ZipWriter(new BlobWriter("application/zip"), { level: 0 });
124
103
  let fileCount = await streamDirectoryToZip(sessionHandle, zipWriter);
125
- fileCount += await streamSessionRefPointsToZip(scenarioHandle, sessionName, zipWriter);
104
+ const contributors = options?.contributors ?? [];
105
+ const seenSubdirs = /* @__PURE__ */ new Set();
106
+ for (const contributor of contributors) {
107
+ const subdir = contributor.subdir;
108
+ if (!subdir || subdir.includes("/") || subdir.startsWith(".")) throw new Error(`ZipExportContributor.subdir must be a non-empty single path segment, got: ${JSON.stringify(subdir)}`);
109
+ if (seenSubdirs.has(subdir)) throw new Error(`Duplicate ZipExportContributor.subdir registered: ${subdir}`);
110
+ seenSubdirs.add(subdir);
111
+ const addFile = async (relativePath, blob) => {
112
+ if (relativePath.startsWith("/")) throw new Error(`ZipExportContributor relative path must not start with '/' (got ${relativePath})`);
113
+ if (relativePath.includes("\\")) throw new Error(`ZipExportContributor relative path must not contain '\\' (got ${relativePath})`);
114
+ if (relativePath.split("/").some((s) => s === ".." || s === ".")) throw new Error(`ZipExportContributor relative path must not contain '.' or '..' segments (got ${relativePath})`);
115
+ await zipWriter.add(`${subdir}/${relativePath}`, new BlobReader(blob));
116
+ };
117
+ fileCount += await contributor.contribute(addFile);
118
+ }
126
119
  const blob = await zipWriter.close();
127
120
  log.info(`ZIP created: ${blob.size} bytes, ${fileCount} files`);
128
121
  return {
@@ -148,9 +141,9 @@ async function exportSessionAsZip(scenarioName, sessionName) {
148
141
  * @returns ZIP export result with blob and file count
149
142
  * @throws Error if scenario or session not found
150
143
  */
151
- async function syncToExternalZip(fileHandle, scenarioName, sessionName) {
144
+ async function syncToExternalZip(fileHandle, scenarioName, sessionName, options) {
152
145
  log.info(`Syncing to external ZIP: ${scenarioName}/${sessionName}`);
153
- const result = await exportSessionAsZip(scenarioName, sessionName);
146
+ const result = await exportSessionAsZip(scenarioName, sessionName, options);
154
147
  const writable = await fileHandle.createWritable();
155
148
  await writable.write(result.blob);
156
149
  await writable.close();
@@ -1,2 +1,2 @@
1
- import { a as ZipActionEntry, c as loadSessionMetadata, i as RecordedAction, l as loadSessionMetadataFromBlob, n as GpsPathCoord, o as loadActionsFromZip, r as MAX_ACTION_FILE_SIZE, s as loadGpsPathFromBlob, t as Entry, u as readZipEntries } from "../zip-reader-D7pkpX8K.js";
2
- export { Entry, GpsPathCoord, MAX_ACTION_FILE_SIZE, RecordedAction, ZipActionEntry, loadActionsFromZip, loadGpsPathFromBlob, loadSessionMetadata, loadSessionMetadataFromBlob, readZipEntries };
1
+ import { a as ZipActionEntry, c as loadEntriesFromSubdir, d as loadSessionMetadataFromBlob, f as readZipEntries, i as RecordedAction, l as loadGpsPathFromBlob, n as GpsPathCoord, o as ZipSubdirEntry, r as MAX_ACTION_FILE_SIZE, s as loadActionsFromZip, t as Entry, u as loadSessionMetadata } from "../zip-reader-D1105idz.js";
2
+ export { Entry, GpsPathCoord, MAX_ACTION_FILE_SIZE, RecordedAction, ZipActionEntry, ZipSubdirEntry, loadActionsFromZip, loadEntriesFromSubdir, loadGpsPathFromBlob, loadSessionMetadata, loadSessionMetadataFromBlob, readZipEntries };
@@ -172,10 +172,14 @@ async function loadGpsPathFromBlob(blob, maxFileSize = MAX_ACTION_FILE_SIZE) {
172
172
  continue;
173
173
  }
174
174
  const gps = action.payload?.rawGpsPoint ?? action.payload?.gpsPoint;
175
- if (action.type === "gpsData/recordGpsEvent" && gps && typeof gps.latitude === "number" && typeof gps.longitude === "number") coords.push({
176
- lat: gps.latitude,
177
- lng: gps.longitude
178
- });
175
+ if (action.type === "gpsData/recordGpsEvent" && gps && typeof gps.latitude === "number" && typeof gps.longitude === "number") {
176
+ const accuracy = typeof gps.latLongAccuracy === "number" && gps.latLongAccuracy > 0 ? gps.latLongAccuracy : void 0;
177
+ coords.push({
178
+ lat: gps.latitude,
179
+ lng: gps.longitude,
180
+ ...accuracy !== void 0 ? { accuracy } : {}
181
+ });
182
+ }
179
183
  }
180
184
  return coords;
181
185
  } catch {
@@ -186,5 +190,28 @@ async function loadGpsPathFromBlob(blob, maxFileSize = MAX_ACTION_FILE_SIZE) {
186
190
  } catch {}
187
191
  }
188
192
  }
193
+ /**
194
+ * Enumerate every file under a single top-level subdirectory of a recording
195
+ * zip. Mirrors the writer-side {@link ZipExportContributor} seam so consumers
196
+ * (typically the recorder) can read back what they wrote without
197
+ * re-implementing zip enumeration.
198
+ *
199
+ * Skips directory entries and files outside `subdir`. Sorts entries by
200
+ * `relativePath` for deterministic iteration. Returns an empty array when
201
+ * the subdir is absent (graceful degradation for older zips).
202
+ *
203
+ * @param data - The zip file content as a Uint8Array
204
+ * @param subdir - Top-level subdir to scan (no leading or trailing `/`)
205
+ */
206
+ async function loadEntriesFromSubdir(data, subdir) {
207
+ if (!subdir || subdir.includes("/") || subdir.startsWith(".")) throw new Error(`subdir must be a non-empty single path segment, got: ${JSON.stringify(subdir)}`);
208
+ const prefix = `${subdir}/`;
209
+ return (await readZipEntries(data)).filter((e) => !e.directory && e.filename.startsWith(prefix) && e.filename !== prefix).sort((a, b) => a.filename.localeCompare(b.filename)).map((entry) => ({
210
+ relativePath: entry.filename.slice(prefix.length),
211
+ fullPath: entry.filename,
212
+ uncompressedSize: entry.uncompressedSize,
213
+ getText: () => entry.getData(new TextWriter())
214
+ }));
215
+ }
189
216
  //#endregion
190
- export { MAX_ACTION_FILE_SIZE, loadActionsFromZip, loadGpsPathFromBlob, loadSessionMetadata, loadSessionMetadataFromBlob, readZipEntries };
217
+ export { MAX_ACTION_FILE_SIZE, loadActionsFromZip, loadEntriesFromSubdir, loadGpsPathFromBlob, loadSessionMetadata, loadSessionMetadataFromBlob, readZipEntries };
@@ -0,0 +1,28 @@
1
+ import { n as SessionMetadata } from "./opfs-storage-DOPkzgeT.js";
2
+
3
+ //#region ../src/storage/storage-backend.d.ts
4
+ interface CreateSessionResult {
5
+ sessionName: string;
6
+ }
7
+ /**
8
+ * Contract for session lifecycle and recording persistence.
9
+ *
10
+ * Implementations:
11
+ * - OpfsStorageBackend: production — flat sessions under OPFS
12
+ * - NullStorageBackend: tests/replay — silent no-ops
13
+ * - ScenarioWrappingStorageBackend (recorder): layers scenario hierarchy on top
14
+ */
15
+ interface StorageBackend {
16
+ /** Create a new recording session. Returns the session name for reference. */
17
+ createSession(timestamp: Date, contextTag?: string): Promise<CreateSessionResult>;
18
+ /** List all session names managed by this backend. */
19
+ listSessions(): Promise<string[]>;
20
+ /** Persist a Redux action as a numbered JSON file. */
21
+ writeAction(action: unknown, index: number): Promise<void>;
22
+ /** Persist a captured camera frame as a numbered JPEG. */
23
+ writeFrame(blob: Blob, index: number): Promise<void>;
24
+ /** Persist session metadata (session.json). */
25
+ writeSessionMetadata(metadata: SessionMetadata): Promise<void>;
26
+ }
27
+ //#endregion
28
+ export { StorageBackend as n, CreateSessionResult as t };
@@ -1,4 +1,5 @@
1
- import { t as SubscribableStore } from "./subscribe-to-selector-UTZ0KkAc.js";
1
+ import { t as MapData } from "./map-data-sih4g64v.js";
2
+ import { t as SubscribableStore } from "./subscribe-to-selector-DIHm3BRZ.js";
2
3
  import { LatLong, Matrix4, Quaternion, Vector3 } from "gps-plus-slam-js";
3
4
 
4
5
  //#region ../src/state/store-subscribers.d.ts
@@ -13,16 +14,28 @@ interface StoreSubscriberDeps {
13
14
  gpsEventVisualizer: {
14
15
  getZeroRef: () => LatLong | null;
15
16
  setZeroRef: (zero: LatLong) => void;
16
- addGpsEvent: (gpsCoords: Vector3, odomPosition: Vector3) => void;
17
+ addGpsEvent: (gpsCoords: Vector3, odomPosition: Vector3, accuracy?: {
18
+ horizontal?: number;
19
+ vertical?: number;
20
+ }) => void;
17
21
  addAlignmentSnapshot: (nuePosition: Vector3) => void;
18
22
  };
19
- /** Map overlay — updates the 2D map position. Nullable (not present in all modes). */
23
+ /**
24
+ * Map overlay — renders the shared trajectory snapshot. Nullable (not
25
+ * present in all modes).
26
+ *
27
+ * `setGpsPosition` centers the map on the latest fix; `render` draws the
28
+ * full {@link MapData} snapshot (raw GPS + accuracy circles, fused path,
29
+ * alignment snapshots, user dot) via the shared `drawMapData` routine — the
30
+ * SAME one the 2D session-summary map uses, so the live and summary maps
31
+ * stay identical (unified-trajectory-map Phase 3). The previous incremental
32
+ * `addRawGpsPoint` / `addFusedPoint` / `addAlignmentSnapshot` API is gone:
33
+ * the fused path now recomputes from the latest matrix on every rebuild
34
+ * (D2), so the live polyline "snaps" as alignment improves.
35
+ */
20
36
  mapOverlay?: {
21
37
  setGpsPosition: (lat: number, lon: number) => void;
22
- addRawGpsPoint?: (lat: number, lon: number) => void;
23
- addFusedPoint?: (lat: number, lon: number) => void;
24
- addAlignmentSnapshot?: (lat: number, lon: number) => void;
25
- addRefPoint?: (lat: number, lon: number, name: string) => void;
38
+ render?: (data: MapData) => void;
26
39
  } | null;
27
40
  /**
28
41
  * Optional callback invoked with the GPS world-space coordinates of each
@@ -52,6 +65,17 @@ interface StoreSubscriberDeps {
52
65
  * for dynamic button label updates.
53
66
  */
54
67
  onNewGpsLatLng?: (lat: number, lng: number) => void;
68
+ /**
69
+ * When `true`, the recorded GPS 1σ accuracies (`latLongAccuracy`,
70
+ * `altitudeAccuracy`) on each `GpsPoint` are forwarded to `addGpsEvent`
71
+ * so the raw-GPS marker renders as a non-uniform-scaled ellipsoid (see
72
+ * `gps-event-markers.ts` §3). When `false` (the default) the accuracies
73
+ * are dropped and the legacy fixed 8 cm sphere is used.
74
+ *
75
+ * Live recording leaves this `false` (large ellipsoids would distract the
76
+ * operator). Replay mode sets it to `true` so the diagnostic is visible.
77
+ */
78
+ showAccuracySpheres?: boolean;
55
79
  }
56
80
  /**
57
81
  * Wire store state-change subscriptions to visualization dependencies.
@@ -1,4 +1,4 @@
1
- import { n as CombinedRootState } from "./store-C7PKOj8S.js";
1
+ import { L as CombinedRootState } from "./create-slam-app-store-CNsZ3rcX.js";
2
2
 
3
3
  //#region ../src/state/subscribe-to-selector.d.ts
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { a as WebXRQuaternion, o as WebXRVec3 } from "../ar-types-B-ORgk6Z.js";
1
+ import { a as WebXRQuaternion, o as WebXRVec3 } from "../ar-types-Ck8LEq8k.js";
2
2
  import { Mock } from "vitest";
3
3
 
4
4
  //#region ../src/test-utils/browser-mocks.d.ts
@@ -1,4 +1,4 @@
1
- import { i as RecordedAction } from "../zip-reader-D7pkpX8K.js";
1
+ import { i as RecordedAction } from "../zip-reader-D1105idz.js";
2
2
  import { LatLong } from "gps-plus-slam-js";
3
3
 
4
4
  //#region ../src/test-utils/zip-round-trip-helpers.d.ts
@@ -12,6 +12,12 @@ interface ProduceTestZipOptions {
12
12
  frameCount: number;
13
13
  deviceInfo: string;
14
14
  zeroPos: LatLong;
15
+ /**
16
+ * Optional horizontal accuracy (meters) stamped onto every produced
17
+ * `recordGpsEvent` action's `rawGpsPoint.latLongAccuracy`. When omitted,
18
+ * the field is left off — matching pre-accuracy recordings.
19
+ */
20
+ gpsAccuracy?: number;
15
21
  }
16
22
  /** Result of producing a test zip, with all metadata needed for assertions. */
17
23
  interface TestZipResult {
@@ -65,7 +65,7 @@ async function produceTestZip(opts) {
65
65
  const { cleanup } = installOPFSMocks();
66
66
  try {
67
67
  await initOpfsStorage();
68
- const { scenarioName, sessionName } = await createSession(options.scenarioName, options.sessionTimestamp);
68
+ const { sessionName } = await createSession(options.sessionTimestamp, options.scenarioName);
69
69
  const allActions = [];
70
70
  let actionIndex = 0;
71
71
  /** Increment index, persist to OPFS, and record for later assertions. */
@@ -75,7 +75,7 @@ async function produceTestZip(opts) {
75
75
  allActions.push(action);
76
76
  };
77
77
  await writeAndRecordAction({
78
- type: "recorder/startSession",
78
+ type: "recording/startSession",
79
79
  payload: {
80
80
  scenarioName: options.scenarioName,
81
81
  sessionName,
@@ -131,7 +131,8 @@ async function produceTestZip(opts) {
131
131
  id: `gps-event-${i + 1}`,
132
132
  latitude: options.zeroPos.lat + (i + 1) * 1e-4,
133
133
  longitude: options.zeroPos.lon + (i + 1) * 1e-4,
134
- timestamp: t
134
+ timestamp: t,
135
+ ...typeof options.gpsAccuracy === "number" ? { latLongAccuracy: options.gpsAccuracy } : {}
135
136
  }
136
137
  }
137
138
  });
@@ -174,12 +175,12 @@ async function produceTestZip(opts) {
174
175
  version: 1,
175
176
  startedAt: options.sessionTimestamp.toISOString(),
176
177
  endedAt: new Date(options.sessionTimestamp.getTime() + actionIndex * 1e3 + 5e3).toISOString(),
177
- scenarioName: options.scenarioName,
178
+ contextTag: options.scenarioName,
178
179
  actionCount: actionIndex,
179
180
  frameCount: options.frameCount,
180
181
  userAgent: options.deviceInfo
181
182
  });
182
- const { blob } = await exportSessionAsZip(scenarioName, sessionName);
183
+ const { blob } = await exportSessionAsZip(sessionName);
183
184
  return {
184
185
  zipData: new Uint8Array(await blob.arrayBuffer()),
185
186
  totalActionCount: actionIndex,