gps-plus-slam-app-framework 1.0.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 (200) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +222 -0
  3. package/dist/alignment-lerper-D9BeDwZX.d.ts +29 -0
  4. package/dist/app-selectors-DyMzxCEC.d.ts +140 -0
  5. package/dist/ar/camera-blit-capture.d.ts +2 -0
  6. package/dist/ar/camera-blit-capture.js +253 -0
  7. package/dist/ar/capture-failure-tracker.d.ts +2 -0
  8. package/dist/ar/capture-failure-tracker.js +45 -0
  9. package/dist/ar/chromium-camera-access-workaround.d.ts +2 -0
  10. package/dist/ar/chromium-camera-access-workaround.js +32 -0
  11. package/dist/ar/depth-sampler.d.ts +3 -0
  12. package/dist/ar/depth-sampler.js +147 -0
  13. package/dist/ar/image-capture.d.ts +2 -0
  14. package/dist/ar/image-capture.js +152 -0
  15. package/dist/ar/index.d.ts +12 -0
  16. package/dist/ar/index.js +12 -0
  17. package/dist/ar/replay-scene.d.ts +2 -0
  18. package/dist/ar/replay-scene.js +325 -0
  19. package/dist/ar/scene-node-names.d.ts +2 -0
  20. package/dist/ar/scene-node-names.js +16 -0
  21. package/dist/ar/tracking-state.d.ts +2 -0
  22. package/dist/ar/tracking-state.js +164 -0
  23. package/dist/ar/webxr-session.d.ts +4 -0
  24. package/dist/ar/webxr-session.js +890 -0
  25. package/dist/ar/xr-camera-texture.d.ts +2 -0
  26. package/dist/ar/xr-camera-texture.js +39 -0
  27. package/dist/ar/xr-error-handler.d.ts +2 -0
  28. package/dist/ar/xr-error-handler.js +37 -0
  29. package/dist/ar-types-B-ORgk6Z.d.ts +82 -0
  30. package/dist/camera-blit-capture-DHuyrrxr.d.ts +108 -0
  31. package/dist/camera-follower-BMqPddxx.d.ts +29 -0
  32. package/dist/capture-failure-tracker-2Q6oI2uQ.d.ts +52 -0
  33. package/dist/chromium-camera-access-workaround-CU5zSKNr.d.ts +61 -0
  34. package/dist/community-license-key-ChZLCJyO.d.ts +21 -0
  35. package/dist/concurrency-p6gB30fT.d.ts +24 -0
  36. package/dist/css3d-renderer-manager-rgLMVAvd.d.ts +23 -0
  37. package/dist/depth-sampler-BNCtnEoP.d.ts +108 -0
  38. package/dist/failure-tracker-1sQ_Cgyw.d.ts +49 -0
  39. package/dist/file-system-BsQgYCU3.d.ts +128 -0
  40. package/dist/file-system-utils-5cWO8Xyv.d.ts +24 -0
  41. package/dist/format-file-size-onGq0k1Q.d.ts +24 -0
  42. package/dist/fused-path-DtqCSqJh.d.ts +25 -0
  43. package/dist/geo-types-Xx-LVcnw.d.ts +29 -0
  44. package/dist/gps-Ddzr2KlX.d.ts +54 -0
  45. package/dist/gps-compass-cubes-C-6JEnpR.d.ts +22 -0
  46. package/dist/gps-error-handler-VPuNOuf8.d.ts +40 -0
  47. package/dist/gps-event-markers-DpEy2qRd.d.ts +67 -0
  48. package/dist/h3-ref-point-CFhayNSZ.d.ts +59 -0
  49. package/dist/image-capture-2pKjYUS6.d.ts +130 -0
  50. package/dist/index.d.ts +56 -0
  51. package/dist/index.js +61 -0
  52. package/dist/leaflet-map-overlay-CvfQXTye.d.ts +114 -0
  53. package/dist/lerp-utils-87NWjUOD.d.ts +20 -0
  54. package/dist/licensing/community-license-key.d.ts +2 -0
  55. package/dist/licensing/community-license-key.js +21 -0
  56. package/dist/licensing/index.d.ts +2 -0
  57. package/dist/licensing/index.js +2 -0
  58. package/dist/list-formatter-DKQQVLcl.d.ts +14 -0
  59. package/dist/logger-B81iwxx0.d.ts +74 -0
  60. package/dist/map-overlay-DuiZuCTx.d.ts +153 -0
  61. package/dist/null-storage-backend-CsWvQwN8.d.ts +11 -0
  62. package/dist/opfs-storage-ChOzF_Wp.d.ts +181 -0
  63. package/dist/opfs-storage-backend-CVcmfJHN.d.ts +11 -0
  64. package/dist/permission-checker-jO2ziCWC.d.ts +146 -0
  65. package/dist/persistence-middleware-DrptY02i.d.ts +31 -0
  66. package/dist/recorder-slice-fcwAXrsh.d.ts +35 -0
  67. package/dist/recording-coordinator-DzflP07_.d.ts +81 -0
  68. package/dist/recording-options-BYUFX8iU.d.ts +166 -0
  69. package/dist/recording-replayer-oOzPITrd.d.ts +32 -0
  70. package/dist/ref-point-importer-B_wn5ABk.d.ts +62 -0
  71. package/dist/ref-point-loader-Pt1smgHS.d.ts +117 -0
  72. package/dist/ref-points/h3-ref-point.d.ts +2 -0
  73. package/dist/ref-points/h3-ref-point.js +74 -0
  74. package/dist/ref-points/index.d.ts +2 -0
  75. package/dist/ref-points/index.js +2 -0
  76. package/dist/ref-points-slice-tC-Wd6XJ.d.ts +48 -0
  77. package/dist/reference-points-CFWy8_wj.d.ts +68 -0
  78. package/dist/replay-engine-DuX316ae.d.ts +103 -0
  79. package/dist/replay-scene-DtUm6bsr.d.ts +109 -0
  80. package/dist/routing-slice-BlHTM8eh.d.ts +23 -0
  81. package/dist/scene-node-names-DPsaE2z3.d.ts +14 -0
  82. package/dist/sensors/gps-error-handler.d.ts +2 -0
  83. package/dist/sensors/gps-error-handler.js +53 -0
  84. package/dist/sensors/gps.d.ts +2 -0
  85. package/dist/sensors/gps.js +93 -0
  86. package/dist/sensors/index.d.ts +4 -0
  87. package/dist/sensors/index.js +4 -0
  88. package/dist/sensors/permission-checker.d.ts +2 -0
  89. package/dist/sensors/permission-checker.js +397 -0
  90. package/dist/state/app-selectors.d.ts +2 -0
  91. package/dist/state/app-selectors.js +35 -0
  92. package/dist/state/index.d.ts +17 -0
  93. package/dist/state/index.js +13 -0
  94. package/dist/state/persistence-middleware.d.ts +2 -0
  95. package/dist/state/persistence-middleware.js +71 -0
  96. package/dist/state/recorder-slice.d.ts +2 -0
  97. package/dist/state/recorder-slice.js +47 -0
  98. package/dist/state/recording-coordinator.d.ts +2 -0
  99. package/dist/state/recording-coordinator.js +150 -0
  100. package/dist/state/recording-options.d.ts +2 -0
  101. package/dist/state/recording-options.js +192 -0
  102. package/dist/state/recording-replayer.d.ts +2 -0
  103. package/dist/state/recording-replayer.js +39 -0
  104. package/dist/state/ref-points-slice.d.ts +2 -0
  105. package/dist/state/ref-points-slice.js +44 -0
  106. package/dist/state/replay-engine.d.ts +2 -0
  107. package/dist/state/replay-engine.js +230 -0
  108. package/dist/state/routing-slice.d.ts +2 -0
  109. package/dist/state/routing-slice.js +13 -0
  110. package/dist/state/store-subscribers.d.ts +3 -0
  111. package/dist/state/store-subscribers.js +97 -0
  112. package/dist/state/store.d.ts +9 -0
  113. package/dist/state/store.js +71 -0
  114. package/dist/state/subscribe-to-selector.d.ts +2 -0
  115. package/dist/state/subscribe-to-selector.js +26 -0
  116. package/dist/storage/file-system-utils.d.ts +2 -0
  117. package/dist/storage/file-system-utils.js +31 -0
  118. package/dist/storage/file-system.d.ts +2 -0
  119. package/dist/storage/file-system.js +229 -0
  120. package/dist/storage/index.d.ts +11 -0
  121. package/dist/storage/index.js +10 -0
  122. package/dist/storage/null-storage-backend.d.ts +2 -0
  123. package/dist/storage/null-storage-backend.js +8 -0
  124. package/dist/storage/opfs-storage-backend.d.ts +2 -0
  125. package/dist/storage/opfs-storage-backend.js +15 -0
  126. package/dist/storage/opfs-storage.d.ts +2 -0
  127. package/dist/storage/opfs-storage.js +242 -0
  128. package/dist/storage/ref-point-importer.d.ts +2 -0
  129. package/dist/storage/ref-point-importer.js +163 -0
  130. package/dist/storage/ref-point-loader.d.ts +2 -0
  131. package/dist/storage/ref-point-loader.js +248 -0
  132. package/dist/storage/ref-point-recovery.d.ts +28 -0
  133. package/dist/storage/ref-point-recovery.js +159 -0
  134. package/dist/storage/storage-backend.d.ts +2 -0
  135. package/dist/storage/storage-backend.js +1 -0
  136. package/dist/storage/zip-export.d.ts +2 -0
  137. package/dist/storage/zip-export.js +214 -0
  138. package/dist/storage/zip-reader.d.ts +2 -0
  139. package/dist/storage/zip-reader.js +190 -0
  140. package/dist/storage-backend-BcEItnFr.d.ts +20 -0
  141. package/dist/store-C7PKOj8S.d.ts +96 -0
  142. package/dist/store-subscribers-CuoIzB5T.d.ts +77 -0
  143. package/dist/subscribe-to-selector-UTZ0KkAc.d.ts +26 -0
  144. package/dist/test-utils/browser-mocks.d.ts +294 -0
  145. package/dist/test-utils/browser-mocks.js +568 -0
  146. package/dist/test-utils/zip-round-trip-helpers.d.ts +60 -0
  147. package/dist/test-utils/zip-round-trip-helpers.js +207 -0
  148. package/dist/three-dispose-CCBHjE_a.d.ts +30 -0
  149. package/dist/tracking-state-B9QfuZCg.d.ts +101 -0
  150. package/dist/types/ar-types.d.ts +2 -0
  151. package/dist/types/ar-types.js +1 -0
  152. package/dist/types/geo-types.d.ts +2 -0
  153. package/dist/types/geo-types.js +1 -0
  154. package/dist/types/index.d.ts +3 -0
  155. package/dist/types/index.js +1 -0
  156. package/dist/utils/concurrency.d.ts +2 -0
  157. package/dist/utils/concurrency.js +40 -0
  158. package/dist/utils/failure-tracker.d.ts +2 -0
  159. package/dist/utils/failure-tracker.js +48 -0
  160. package/dist/utils/format-file-size.d.ts +2 -0
  161. package/dist/utils/format-file-size.js +41 -0
  162. package/dist/utils/fused-path.d.ts +2 -0
  163. package/dist/utils/fused-path.js +44 -0
  164. package/dist/utils/index.d.ts +7 -0
  165. package/dist/utils/index.js +7 -0
  166. package/dist/utils/list-formatter.d.ts +2 -0
  167. package/dist/utils/list-formatter.js +17 -0
  168. package/dist/utils/logger.d.ts +2 -0
  169. package/dist/utils/logger.js +226 -0
  170. package/dist/vis-colors-Qi2vlRGt.d.ts +63 -0
  171. package/dist/visualization/alignment-lerper.d.ts +2 -0
  172. package/dist/visualization/alignment-lerper.js +68 -0
  173. package/dist/visualization/camera-follower.d.ts +2 -0
  174. package/dist/visualization/camera-follower.js +51 -0
  175. package/dist/visualization/css3d-renderer-manager.d.ts +2 -0
  176. package/dist/visualization/css3d-renderer-manager.js +41 -0
  177. package/dist/visualization/gps-compass-cubes.d.ts +2 -0
  178. package/dist/visualization/gps-compass-cubes.js +129 -0
  179. package/dist/visualization/gps-event-markers.d.ts +2 -0
  180. package/dist/visualization/gps-event-markers.js +159 -0
  181. package/dist/visualization/index.d.ts +12 -0
  182. package/dist/visualization/index.js +12 -0
  183. package/dist/visualization/leaflet-map-overlay.d.ts +2 -0
  184. package/dist/visualization/leaflet-map-overlay.js +313 -0
  185. package/dist/visualization/lerp-utils.d.ts +2 -0
  186. package/dist/visualization/lerp-utils.js +22 -0
  187. package/dist/visualization/map-overlay.d.ts +2 -0
  188. package/dist/visualization/map-overlay.js +241 -0
  189. package/dist/visualization/reference-points.d.ts +2 -0
  190. package/dist/visualization/reference-points.js +160 -0
  191. package/dist/visualization/three-dispose.d.ts +2 -0
  192. package/dist/visualization/three-dispose.js +55 -0
  193. package/dist/visualization/vis-colors.d.ts +2 -0
  194. package/dist/visualization/vis-colors.js +74 -0
  195. package/dist/webxr-session-COlSrXdL.d.ts +316 -0
  196. package/dist/xr-camera-texture-BbukNE79.d.ts +53 -0
  197. package/dist/xr-error-handler-DTHfmvrl.d.ts +24 -0
  198. package/dist/zip-export-Rtpi75JJ.d.ts +87 -0
  199. package/dist/zip-reader-D7pkpX8K.d.ts +97 -0
  200. package/package.json +104 -0
@@ -0,0 +1,214 @@
1
+ import { createLogger } from "../utils/logger.js";
2
+ import { BlobReader, BlobWriter, ZipWriter } from "@zip.js/zip.js";
3
+ //#region ../src/storage/zip-export.ts
4
+ /**
5
+ * ZIP Export Module
6
+ *
7
+ * Exports OPFS session data as ZIP files for download.
8
+ * Uses @zip.js/zip.js library for streaming ZIP creation and reading.
9
+ *
10
+ * The ZIP uses "store" mode (no compression) because:
11
+ * 1. Images are already compressed (JPEG)
12
+ * 2. Faster packaging
13
+ * 3. Simpler implementation
14
+ *
15
+ * @zip.js/zip.js was chosen over fflate because:
16
+ * - Supports streaming reads and writes (needed for periodic sync to external file)
17
+ * - Can append to existing ZIP files via prependZip
18
+ * - Actively maintained with good TypeScript support
19
+ * - Built-in Web Worker support for non-blocking compression
20
+ */
21
+ const log = createLogger("ZipExport");
22
+ /**
23
+ * Get the OPFS scenarios directory handle.
24
+ * Re-acquires from navigator.storage to avoid holding stale references.
25
+ */
26
+ async function getScenariosHandle() {
27
+ return (await (await navigator.storage.getDirectory()).getDirectoryHandle("gps-recorder")).getDirectoryHandle("scenarios");
28
+ }
29
+ /**
30
+ * Stream all files from a directory recursively into a ZipWriter.
31
+ * Files are read one at a time and immediately added to the ZIP,
32
+ * keeping memory usage proportional to a single file (not the total).
33
+ *
34
+ * @returns Number of files added
35
+ */
36
+ async function streamDirectoryToZip(dirHandle, zipWriter, basePath = "") {
37
+ let count = 0;
38
+ for await (const entry of dirHandle.values()) {
39
+ const relativePath = basePath ? `${basePath}/${entry.name}` : entry.name;
40
+ if (entry.kind === "file") {
41
+ const file = await (await dirHandle.getFileHandle(entry.name)).getFile();
42
+ await zipWriter.add(relativePath, new BlobReader(file));
43
+ count++;
44
+ } else if (entry.kind === "directory") {
45
+ const subDirHandle = await dirHandle.getDirectoryHandle(entry.name);
46
+ count += await streamDirectoryToZip(subDirHandle, zipWriter, relativePath);
47
+ }
48
+ }
49
+ return count;
50
+ }
51
+ /**
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
+ * Export a session from OPFS as a ZIP blob.
94
+ *
95
+ * The ZIP structure mirrors the OPFS session structure:
96
+ * - session.json (at root)
97
+ * - actions/000001.json, actions/000002.json, ...
98
+ * - frames/frame-000001.jpg, frames/frame-000002.jpg, ...
99
+ * - refPoints/{h3}.json (only observations from this session)
100
+ *
101
+ * Uses "store" mode (compression level 0) for fast packaging.
102
+ *
103
+ * @param scenarioName - Name of the scenario
104
+ * @param sessionName - Name of the session folder
105
+ * @returns ZIP export result with blob and file count
106
+ * @throws Error if scenario or session not found
107
+ */
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
+ }
117
+ let sessionHandle;
118
+ try {
119
+ sessionHandle = await scenarioHandle.getDirectoryHandle(sessionName);
120
+ } catch {
121
+ throw new Error(`Session "${sessionName}" not found in scenario "${scenarioName}"`);
122
+ }
123
+ const zipWriter = new ZipWriter(new BlobWriter("application/zip"), { level: 0 });
124
+ let fileCount = await streamDirectoryToZip(sessionHandle, zipWriter);
125
+ fileCount += await streamSessionRefPointsToZip(scenarioHandle, sessionName, zipWriter);
126
+ const blob = await zipWriter.close();
127
+ log.info(`ZIP created: ${blob.size} bytes, ${fileCount} files`);
128
+ return {
129
+ blob,
130
+ fileCount
131
+ };
132
+ }
133
+ /**
134
+ * Sync current OPFS session data to an external file handle.
135
+ *
136
+ * This is used for periodic sync during recording to save data to the user's
137
+ * chosen location (obtained via showSaveFilePicker). The function:
138
+ * 1. Reads all current session data from OPFS
139
+ * 2. Creates a ZIP in memory
140
+ * 3. Writes the ZIP to the external file handle
141
+ *
142
+ * This provides crash safety combined with OPFS - even if the app crashes,
143
+ * the last synced ZIP contains all data up to that point.
144
+ *
145
+ * @param fileHandle - File handle from showSaveFilePicker
146
+ * @param scenarioName - Name of the scenario
147
+ * @param sessionName - Name of the session folder
148
+ * @returns ZIP export result with blob and file count
149
+ * @throws Error if scenario or session not found
150
+ */
151
+ async function syncToExternalZip(fileHandle, scenarioName, sessionName) {
152
+ log.info(`Syncing to external ZIP: ${scenarioName}/${sessionName}`);
153
+ const result = await exportSessionAsZip(scenarioName, sessionName);
154
+ const writable = await fileHandle.createWritable();
155
+ await writable.write(result.blob);
156
+ await writable.close();
157
+ log.info(`Synced ${result.blob.size} bytes to external file`);
158
+ return result;
159
+ }
160
+ /**
161
+ * Trigger a file download in the browser.
162
+ *
163
+ * Uses showSaveFilePicker if available (for better UX on desktop),
164
+ * falls back to <a download> for broader compatibility.
165
+ *
166
+ * @param blob - The blob to download
167
+ * @param filename - Suggested filename
168
+ */
169
+ async function downloadZip(blob, filename) {
170
+ if ("showSaveFilePicker" in window && window.showSaveFilePicker) try {
171
+ const writable = await (await window.showSaveFilePicker({
172
+ suggestedName: filename,
173
+ types: [{
174
+ description: "ZIP Archive",
175
+ accept: { "application/zip": [".zip"] }
176
+ }]
177
+ })).createWritable();
178
+ await writable.write(blob);
179
+ await writable.close();
180
+ log.info(`Saved via File System Access API: ${filename}`);
181
+ return;
182
+ } catch (err) {
183
+ const error = err;
184
+ if (error.name === "AbortError") {
185
+ log.info("User cancelled save dialog");
186
+ return;
187
+ }
188
+ log.warn("showSaveFilePicker failed, using fallback:", error.message);
189
+ }
190
+ const url = URL.createObjectURL(blob);
191
+ const link = document.createElement("a");
192
+ link.href = url;
193
+ link.download = filename;
194
+ link.style.display = "none";
195
+ document.body.appendChild(link);
196
+ link.click();
197
+ document.body.removeChild(link);
198
+ setTimeout(() => URL.revokeObjectURL(url), 1e3);
199
+ log.info(`Download triggered via <a download>: ${filename}`);
200
+ }
201
+ /**
202
+ * Export and download a session in one step.
203
+ *
204
+ * Convenience function combining exportSessionAsZip and downloadZip.
205
+ *
206
+ * @param scenarioName - Name of the scenario
207
+ * @param sessionName - Name of the session folder
208
+ */
209
+ async function exportAndDownloadSession(scenarioName, sessionName) {
210
+ const { blob } = await exportSessionAsZip(scenarioName, sessionName);
211
+ await downloadZip(blob, `${scenarioName}-${sessionName}.zip`);
212
+ }
213
+ //#endregion
214
+ export { downloadZip, exportAndDownloadSession, exportSessionAsZip, syncToExternalZip };
@@ -0,0 +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 };
@@ -0,0 +1,190 @@
1
+ import { createLogger } from "../utils/logger.js";
2
+ import { BlobReader, TextWriter, Uint8ArrayReader, ZipReader } from "@zip.js/zip.js";
3
+ //#region ../src/storage/zip-reader.ts
4
+ /**
5
+ * Zip Reader — utilities for reading recording session zip files.
6
+ *
7
+ * Provides production-ready functions to enumerate zip entries, extract
8
+ * recorded Redux actions, and load session metadata from exported recording
9
+ * zip files. Works with both Node.js (Uint8Array from fs) and browser
10
+ * (Uint8Array from fetch/FileReader) environments.
11
+ *
12
+ * The recording zip format:
13
+ * actions/000001.json — Redux action JSON files (1-based, zero-padded)
14
+ * actions/000002.json
15
+ * ...
16
+ * frames/frame-000001.jpg — captured image frames (optional)
17
+ * session.json — session metadata (optional, see F1 bug)
18
+ */
19
+ const log = createLogger("ZipReader");
20
+ /**
21
+ * Maximum allowed uncompressed size (in bytes) for a single action or
22
+ * metadata JSON file inside a recording zip. Entries exceeding this limit
23
+ * are rejected before decompression to prevent out-of-memory conditions
24
+ * from malicious or corrupted zip files.
25
+ *
26
+ * 1 MB — generous for Redux action JSON; typical actions are a few KB.
27
+ */
28
+ const MAX_ACTION_FILE_SIZE = 1048576;
29
+ /**
30
+ * Read all entries from a zip file.
31
+ *
32
+ * @param data - The zip file content as a Uint8Array
33
+ * @returns Array of zip entries (directories and files)
34
+ */
35
+ async function readZipEntries(data) {
36
+ const reader = new ZipReader(new Uint8ArrayReader(data));
37
+ try {
38
+ return await reader.getEntries();
39
+ } finally {
40
+ await reader.close();
41
+ }
42
+ }
43
+ /**
44
+ * Extract the numeric index from an action filename.
45
+ * Filenames are zero-padded like "000001.json" or "actions/000001.json".
46
+ *
47
+ * @param filename - The entry filename from the zip
48
+ * @returns The numeric index, or NaN if the filename doesn't match the pattern
49
+ */
50
+ function extractActionIndex(filename) {
51
+ const match = /(\d+)\.json$/.exec(filename);
52
+ return match ? parseInt(match[1], 10) : NaN;
53
+ }
54
+ /**
55
+ * Load all recorded Redux actions from a zip file.
56
+ *
57
+ * Filters for JSON files in the actions/ directory, parses them, and returns
58
+ * them sorted by their filename index (chronological order for replay).
59
+ *
60
+ * @param data - The zip file content as a Uint8Array
61
+ * @param maxFileSize - Maximum allowed uncompressed size per entry (defaults to MAX_ACTION_FILE_SIZE)
62
+ * @returns Array of action entries sorted by index
63
+ */
64
+ async function loadActionsFromZip(data, maxFileSize = MAX_ACTION_FILE_SIZE) {
65
+ const actionEntries = (await readZipEntries(data)).filter((e) => !e.directory && e.filename.includes("actions/") && e.filename.endsWith(".json")).sort((a, b) => a.filename.localeCompare(b.filename));
66
+ const results = [];
67
+ for (const entry of actionEntries) if (!entry.directory) {
68
+ if (entry.uncompressedSize > maxFileSize) throw new Error(`Action file "${entry.filename}" (${entry.uncompressedSize} bytes) exceeds maximum allowed size of ${maxFileSize} bytes`);
69
+ const index = extractActionIndex(entry.filename);
70
+ if (Number.isNaN(index)) log.warn(`Unexpected non-numeric action filename: "${entry.filename}". Expected pattern like "actions/000001.json". The file will still be processed.`);
71
+ const text = await entry.getData(new TextWriter());
72
+ let action;
73
+ try {
74
+ const parsed = JSON.parse(text);
75
+ if (typeof parsed !== "object" || parsed === null || typeof parsed.type !== "string") {
76
+ log.warn(`Skipping "${entry.filename}": parsed JSON is not a valid action (missing "type" string)`);
77
+ continue;
78
+ }
79
+ action = parsed;
80
+ } catch {
81
+ log.warn(`Skipping malformed JSON in "${entry.filename}": parse failed`);
82
+ continue;
83
+ }
84
+ results.push({
85
+ index,
86
+ filename: entry.filename,
87
+ action
88
+ });
89
+ }
90
+ return results;
91
+ }
92
+ /**
93
+ * Extract session metadata from an already-constructed ZipReader.
94
+ *
95
+ * Finds the `session.json` entry, validates its size, extracts and parses it.
96
+ * The reader is always closed in `finally`, so callers must not reuse it.
97
+ *
98
+ * @param reader - An open ZipReader (Uint8ArrayReader or BlobReader based)
99
+ * @param maxFileSize - Maximum allowed uncompressed size for session.json
100
+ * @returns Parsed metadata object, or null if session.json is absent
101
+ */
102
+ async function extractSessionMetadataFromReader(reader, maxFileSize) {
103
+ try {
104
+ const sessionEntry = (await reader.getEntries()).find((e) => !e.directory && e.filename.endsWith("session.json"));
105
+ if (!sessionEntry) return null;
106
+ if (sessionEntry.uncompressedSize > maxFileSize) throw new Error(`Session file "${sessionEntry.filename}" (${sessionEntry.uncompressedSize} bytes) exceeds maximum allowed size of ${maxFileSize} bytes`);
107
+ const text = await sessionEntry.getData(new TextWriter());
108
+ return JSON.parse(text);
109
+ } finally {
110
+ await reader.close();
111
+ }
112
+ }
113
+ /**
114
+ * Load session metadata from session.json in the zip file.
115
+ *
116
+ * Returns null if session.json is absent (graceful degradation for
117
+ * recordings created before the metadata writing bug was fixed).
118
+ *
119
+ * @param data - The zip file content as a Uint8Array
120
+ * @param maxFileSize - Maximum allowed uncompressed size per entry (defaults to MAX_ACTION_FILE_SIZE)
121
+ * @returns Parsed session metadata, or null if not found
122
+ */
123
+ async function loadSessionMetadata(data, maxFileSize = MAX_ACTION_FILE_SIZE) {
124
+ return extractSessionMetadataFromReader(new ZipReader(new Uint8ArrayReader(data)), maxFileSize);
125
+ }
126
+ /**
127
+ * Load session metadata from session.json in a zip file provided as a Blob.
128
+ *
129
+ * Unlike `loadSessionMetadata(Uint8Array)`, this variant uses zip.js's
130
+ * BlobReader which reads only the central directory and the target entry
131
+ * from the Blob — not the entire file contents. This is critical for
132
+ * memory-efficient scanning of many large recording zips (e.g., during
133
+ * `discoverScenariosFromZipMetadata`).
134
+ *
135
+ * Since `File` extends `Blob`, this works directly with file handles:
136
+ * const file = await handle.getFile();
137
+ * const meta = await loadSessionMetadataFromBlob(file);
138
+ *
139
+ * @param blob - A Blob or File containing the zip data
140
+ * @param maxFileSize - Maximum allowed uncompressed size per entry (defaults to MAX_ACTION_FILE_SIZE)
141
+ * @returns Parsed session metadata, or null if session.json is not found
142
+ */
143
+ async function loadSessionMetadataFromBlob(blob, maxFileSize = MAX_ACTION_FILE_SIZE) {
144
+ return extractSessionMetadataFromReader(new ZipReader(new BlobReader(blob)), maxFileSize);
145
+ }
146
+ /**
147
+ * Extract GPS coordinates from a recording zip provided as a Blob.
148
+ *
149
+ * Uses BlobReader for memory-efficient reading. Reads all action JSON files,
150
+ * identifies `gpsData/recordGpsEvent` actions, and returns only the lightweight
151
+ * `{ lat, lng }` pairs — all other action data is discarded immediately.
152
+ *
153
+ * Returns coordinates in chronological order (sorted by action filename).
154
+ * Returns an empty array if the zip is invalid or contains no GPS actions.
155
+ *
156
+ * @param blob - A Blob or File containing the zip data
157
+ * @param maxFileSize - Maximum allowed uncompressed size per action entry
158
+ * @returns Array of GPS coordinates in chronological order
159
+ */
160
+ async function loadGpsPathFromBlob(blob, maxFileSize = MAX_ACTION_FILE_SIZE) {
161
+ const reader = new ZipReader(new BlobReader(blob));
162
+ try {
163
+ const actionEntries = (await reader.getEntries()).filter((e) => !e.directory && e.filename.includes("actions/") && e.filename.endsWith(".json")).sort((a, b) => a.filename.localeCompare(b.filename));
164
+ const coords = [];
165
+ for (const entry of actionEntries) {
166
+ if (entry.uncompressedSize > maxFileSize) continue;
167
+ const text = await entry.getData(new TextWriter());
168
+ let action;
169
+ try {
170
+ action = JSON.parse(text);
171
+ } catch {
172
+ continue;
173
+ }
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
+ });
179
+ }
180
+ return coords;
181
+ } catch {
182
+ return [];
183
+ } finally {
184
+ try {
185
+ await reader.close();
186
+ } catch {}
187
+ }
188
+ }
189
+ //#endregion
190
+ export { MAX_ACTION_FILE_SIZE, loadActionsFromZip, loadGpsPathFromBlob, loadSessionMetadata, loadSessionMetadataFromBlob, readZipEntries };
@@ -0,0 +1,20 @@
1
+ import { n as SessionMetadata } from "./opfs-storage-ChOzF_Wp.js";
2
+
3
+ //#region ../src/storage/storage-backend.d.ts
4
+ /**
5
+ * Contract for persisting recording data during a session.
6
+ *
7
+ * Implementations:
8
+ * - OpfsStorageBackend: production — delegates to OPFS via file-system.ts
9
+ * - NullStorageBackend: tests/replay — silent no-ops
10
+ */
11
+ interface StorageBackend {
12
+ /** Persist a Redux action as a numbered JSON file. */
13
+ writeAction(action: unknown, index: number): Promise<void>;
14
+ /** Persist a captured camera frame as a numbered JPEG. */
15
+ writeFrame(blob: Blob, index: number): Promise<void>;
16
+ /** Persist session metadata (session.json). */
17
+ writeSessionMetadata(metadata: SessionMetadata): Promise<void>;
18
+ }
19
+ //#endregion
20
+ export { StorageBackend as t };
@@ -0,0 +1,96 @@
1
+ import { n as SessionMetadata } from "./opfs-storage-ChOzF_Wp.js";
2
+ import { t as StorageBackend } from "./storage-backend-BcEItnFr.js";
3
+ import { t as RecorderState } from "./recorder-slice-fcwAXrsh.js";
4
+ import { t as RefPointsState } from "./ref-points-slice-tC-Wd6XJ.js";
5
+ import { n as RoutingState } from "./routing-slice-BlHTM8eh.js";
6
+ import { configureStore } from "@reduxjs/toolkit";
7
+ import { GpsPoint as GpsPoint$1, LatLong as LatLong$1, MarkReferencePointPayload, RawDeviceOrientation, RawGpsPoint as RawGpsPoint$1, RecordGpsEventPayload, RootState, add2dImage, calcRelativeCoordsInMeters as calcRelativeCoordsInMeters$1, markReferencePoint, recordGpsEvent, setZeroPos } from "gps-plus-slam-js";
8
+
9
+ //#region ../src/state/store.d.ts
10
+ /**
11
+ * Combined root state: library state + app-level state.
12
+ * All 6 reducers (3 library + 3 app) are managed by a single configureStore.
13
+ */
14
+ interface CombinedRootState extends RootState {
15
+ recorder: RecorderState;
16
+ refPoints: RefPointsState;
17
+ routing: RoutingState;
18
+ }
19
+ /**
20
+ * Store interface for the recorder app.
21
+ *
22
+ * Wraps the Redux Toolkit store and adds storage delegation methods
23
+ * (writeFrame, writeSessionMetadata) that route through the injected
24
+ * StorageBackend abstraction.
25
+ */
26
+ interface RecorderStore {
27
+ /** Get combined state (library + app) */
28
+ getState: () => CombinedRootState;
29
+ /** Dispatch actions to the unified store */
30
+ dispatch: ReturnType<typeof configureStore>['dispatch'];
31
+ /** Subscribe to state changes */
32
+ subscribe: (listener: () => void) => () => void;
33
+ /**
34
+ * Persist a captured camera frame via the StorageBackend.
35
+ * A1 fix: routes through the abstraction so NullStorageBackend suppresses
36
+ * writes during replay/testing.
37
+ */
38
+ writeFrame: (blob: Blob, index: number) => Promise<void>;
39
+ /**
40
+ * Persist session metadata (session.json) via the StorageBackend.
41
+ * A1 fix: routes through the abstraction so NullStorageBackend suppresses
42
+ * writes during replay/testing.
43
+ */
44
+ writeSessionMetadata: (metadata: SessionMetadata) => Promise<void>;
45
+ }
46
+ /**
47
+ * Options for creating the recorder store.
48
+ */
49
+ interface RecorderStoreOptions {
50
+ /**
51
+ * Callback invoked when a write operation fails during persistence.
52
+ * User Feedback Issue #1 Part B: Used to show toast notifications.
53
+ */
54
+ onWriteFailure?: (error: Error) => void;
55
+ /**
56
+ * StorageBackend to use for action persistence.
57
+ * Defaults to OpfsStorageBackend (production OPFS).
58
+ * Pass NullStorageBackend for tests or replay mode.
59
+ *
60
+ * Finding F2: Decouples the store from the concrete OPFS dependency.
61
+ */
62
+ storageBackend?: StorageBackend;
63
+ /**
64
+ * When false, disables expensive Redux dev-only middleware
65
+ * (SerializableStateInvariantMiddleware, ImmutableStateInvariantMiddleware).
66
+ * Set to false in high-throughput replay scenarios such as investigation tests
67
+ * to avoid excessive per-dispatch overhead.
68
+ * Default: true (dev checks enabled).
69
+ */
70
+ enableDevChecks?: boolean;
71
+ /**
72
+ * License key for the core library.
73
+ * Validated via Ed25519 signature verification and expiry check.
74
+ *
75
+ * Defaults to the bundled `COMMUNITY_LICENSE_KEY` so example apps work out
76
+ * of the box. Pass a paid license to override. The library is never usable
77
+ * without a valid key — invalid, empty, or expired keys throw.
78
+ *
79
+ * @see EULA.md §3 — License Key
80
+ */
81
+ licenseKey?: string;
82
+ }
83
+ /**
84
+ * Create the recorder store using Redux Toolkit's configureStore.
85
+ *
86
+ * Combines all 6 reducers (3 library: gpsData, gpsElements, arElements;
87
+ * 3 app: recorder, refPoints, routing) into a single store with the
88
+ * persistence middleware for action recording.
89
+ *
90
+ * @param options - Optional configuration including callbacks
91
+ */
92
+ declare function createRecorderStore(options?: RecorderStoreOptions): RecorderStore;
93
+ type RootState$1 = CombinedRootState;
94
+ type AppDispatch = RecorderStore['dispatch'];
95
+ //#endregion
96
+ export { setZeroPos as _, MarkReferencePointPayload as a, RecordGpsEventPayload as c, RootState$1 as d, add2dImage as f, recordGpsEvent as g, markReferencePoint as h, LatLong$1 as i, RecorderStore as l, createRecorderStore as m, CombinedRootState as n, RawDeviceOrientation as o, calcRelativeCoordsInMeters$1 as p, GpsPoint$1 as r, RawGpsPoint$1 as s, AppDispatch as t, RecorderStoreOptions as u };
@@ -0,0 +1,77 @@
1
+ import { t as SubscribableStore } from "./subscribe-to-selector-UTZ0KkAc.js";
2
+ import { LatLong, Matrix4, Quaternion, Vector3 } from "gps-plus-slam-js";
3
+
4
+ //#region ../src/state/store-subscribers.d.ts
5
+ /**
6
+ * Dependencies injected into wireStoreSubscribers.
7
+ * Uses minimal interfaces — callers pass the real singletons or test mocks.
8
+ */
9
+ interface StoreSubscriberDeps {
10
+ /** Apply alignment matrix to the AR world group (from webxr-session.ts). */
11
+ applyAlignmentMatrix: (matrix: Matrix4) => void;
12
+ /** GPS event visualizer — adds raw + fused marker spheres. */
13
+ gpsEventVisualizer: {
14
+ getZeroRef: () => LatLong | null;
15
+ setZeroRef: (zero: LatLong) => void;
16
+ addGpsEvent: (gpsCoords: Vector3, odomPosition: Vector3) => void;
17
+ addAlignmentSnapshot: (nuePosition: Vector3) => void;
18
+ };
19
+ /** Map overlay — updates the 2D map position. Nullable (not present in all modes). */
20
+ mapOverlay?: {
21
+ 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;
26
+ } | null;
27
+ /**
28
+ * Optional callback invoked with the GPS world-space coordinates of each
29
+ * newly visualized GPS event. Used in replay mode to auto-follow the orbit
30
+ * camera target to the latest position (Risk R9 fix).
31
+ *
32
+ * Not provided in live recording mode (camera is XR-controlled).
33
+ */
34
+ onNewGpsPosition?: (coords: Vector3) => void;
35
+ /**
36
+ * Optional callback invoked with the odom position and rotation of each
37
+ * newly dispatched GPS event. Used in replay mode to update the arpose
38
+ * Object3D so the camera follows the recorded AR trajectory.
39
+ *
40
+ * Not provided in live recording mode (arpose stays at identity).
41
+ */
42
+ onNewOdomPose?: (odomPosition: Vector3, odomRotation: Quaternion) => void;
43
+ /**
44
+ * Optional callback invoked with the transformed NUE position each time
45
+ * an alignment snapshot is captured. Used in replay mode to update the
46
+ * orbit camera target to the snapshot position (Issue #3).
47
+ */
48
+ onAlignmentSnapshot?: (nuePosition: Vector3) => void;
49
+ /**
50
+ * Optional callback invoked with raw lat/lng of each newly processed GPS
51
+ * event. Used in live recording to drive ref-point proximity detection
52
+ * for dynamic button label updates.
53
+ */
54
+ onNewGpsLatLng?: (lat: number, lng: number) => void;
55
+ }
56
+ /**
57
+ * Wire store state-change subscriptions to visualization dependencies.
58
+ *
59
+ * Uses subscribeToSelector to decompose the monolithic subscriber into
60
+ * focused, change-gated callbacks:
61
+ * 1. Alignment matrix changes → apply matrix + capture snapshots
62
+ * 2. GPS position changes → incremental event visualization + map overlay
63
+ * 3. Reference point changes → forward to map overlay
64
+ *
65
+ * Each subscription uses reference equality to detect changes — callbacks
66
+ * only fire when the selector output actually differs between dispatches.
67
+ *
68
+ * Each call creates **fresh subscriptions** — no manual reset needed between
69
+ * sessions. Call the returned unsubscribe function to tear down all.
70
+ *
71
+ * @param store — any object satisfying SubscribableStore (RecorderStore, replay store, mock)
72
+ * @param deps — visualization dependencies (real singletons or test mocks)
73
+ * @returns An unsubscribe function that removes all listeners from the store.
74
+ */
75
+ declare function wireStoreSubscribers(store: SubscribableStore, deps: StoreSubscriberDeps): () => void;
76
+ //#endregion
77
+ export { wireStoreSubscribers as n, StoreSubscriberDeps as t };
@@ -0,0 +1,26 @@
1
+ import { n as CombinedRootState } from "./store-C7PKOj8S.js";
2
+
3
+ //#region ../src/state/subscribe-to-selector.d.ts
4
+ /**
5
+ * Minimal store interface for selector subscriptions.
6
+ * Both RecorderStore (live) and replay stores satisfy this.
7
+ */
8
+ interface SubscribableStore {
9
+ getState: () => CombinedRootState;
10
+ subscribe: (listener: () => void) => () => void;
11
+ }
12
+ /**
13
+ * Subscribe to a specific derived value from the store, invoking `onChange`
14
+ * only when the selector result changes by strict reference equality (`!==`).
15
+ *
16
+ * This replaces the `let lastX = …; if (current !== lastX) { … }` pattern
17
+ * previously used in store-subscribers.ts.
18
+ *
19
+ * @param store — subscribable store instance
20
+ * @param selector — pure function extracting a value from state
21
+ * @param onChange — callback receiving (current, previous) when the value changes
22
+ * @returns unsubscribe function
23
+ */
24
+ declare function subscribeToSelector<T>(store: SubscribableStore, selector: (state: CombinedRootState) => T, onChange: (current: T, previous: T | undefined) => void): () => void;
25
+ //#endregion
26
+ export { subscribeToSelector as n, SubscribableStore as t };