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.
- package/LICENSE +201 -0
- package/README.md +222 -0
- package/dist/alignment-lerper-D9BeDwZX.d.ts +29 -0
- package/dist/app-selectors-DyMzxCEC.d.ts +140 -0
- package/dist/ar/camera-blit-capture.d.ts +2 -0
- package/dist/ar/camera-blit-capture.js +253 -0
- package/dist/ar/capture-failure-tracker.d.ts +2 -0
- package/dist/ar/capture-failure-tracker.js +45 -0
- package/dist/ar/chromium-camera-access-workaround.d.ts +2 -0
- package/dist/ar/chromium-camera-access-workaround.js +32 -0
- package/dist/ar/depth-sampler.d.ts +3 -0
- package/dist/ar/depth-sampler.js +147 -0
- package/dist/ar/image-capture.d.ts +2 -0
- package/dist/ar/image-capture.js +152 -0
- package/dist/ar/index.d.ts +12 -0
- package/dist/ar/index.js +12 -0
- package/dist/ar/replay-scene.d.ts +2 -0
- package/dist/ar/replay-scene.js +325 -0
- package/dist/ar/scene-node-names.d.ts +2 -0
- package/dist/ar/scene-node-names.js +16 -0
- package/dist/ar/tracking-state.d.ts +2 -0
- package/dist/ar/tracking-state.js +164 -0
- package/dist/ar/webxr-session.d.ts +4 -0
- package/dist/ar/webxr-session.js +890 -0
- package/dist/ar/xr-camera-texture.d.ts +2 -0
- package/dist/ar/xr-camera-texture.js +39 -0
- package/dist/ar/xr-error-handler.d.ts +2 -0
- package/dist/ar/xr-error-handler.js +37 -0
- package/dist/ar-types-B-ORgk6Z.d.ts +82 -0
- package/dist/camera-blit-capture-DHuyrrxr.d.ts +108 -0
- package/dist/camera-follower-BMqPddxx.d.ts +29 -0
- package/dist/capture-failure-tracker-2Q6oI2uQ.d.ts +52 -0
- package/dist/chromium-camera-access-workaround-CU5zSKNr.d.ts +61 -0
- package/dist/community-license-key-ChZLCJyO.d.ts +21 -0
- package/dist/concurrency-p6gB30fT.d.ts +24 -0
- package/dist/css3d-renderer-manager-rgLMVAvd.d.ts +23 -0
- package/dist/depth-sampler-BNCtnEoP.d.ts +108 -0
- package/dist/failure-tracker-1sQ_Cgyw.d.ts +49 -0
- package/dist/file-system-BsQgYCU3.d.ts +128 -0
- package/dist/file-system-utils-5cWO8Xyv.d.ts +24 -0
- package/dist/format-file-size-onGq0k1Q.d.ts +24 -0
- package/dist/fused-path-DtqCSqJh.d.ts +25 -0
- package/dist/geo-types-Xx-LVcnw.d.ts +29 -0
- package/dist/gps-Ddzr2KlX.d.ts +54 -0
- package/dist/gps-compass-cubes-C-6JEnpR.d.ts +22 -0
- package/dist/gps-error-handler-VPuNOuf8.d.ts +40 -0
- package/dist/gps-event-markers-DpEy2qRd.d.ts +67 -0
- package/dist/h3-ref-point-CFhayNSZ.d.ts +59 -0
- package/dist/image-capture-2pKjYUS6.d.ts +130 -0
- package/dist/index.d.ts +56 -0
- package/dist/index.js +61 -0
- package/dist/leaflet-map-overlay-CvfQXTye.d.ts +114 -0
- package/dist/lerp-utils-87NWjUOD.d.ts +20 -0
- package/dist/licensing/community-license-key.d.ts +2 -0
- package/dist/licensing/community-license-key.js +21 -0
- package/dist/licensing/index.d.ts +2 -0
- package/dist/licensing/index.js +2 -0
- package/dist/list-formatter-DKQQVLcl.d.ts +14 -0
- package/dist/logger-B81iwxx0.d.ts +74 -0
- package/dist/map-overlay-DuiZuCTx.d.ts +153 -0
- package/dist/null-storage-backend-CsWvQwN8.d.ts +11 -0
- package/dist/opfs-storage-ChOzF_Wp.d.ts +181 -0
- package/dist/opfs-storage-backend-CVcmfJHN.d.ts +11 -0
- package/dist/permission-checker-jO2ziCWC.d.ts +146 -0
- package/dist/persistence-middleware-DrptY02i.d.ts +31 -0
- package/dist/recorder-slice-fcwAXrsh.d.ts +35 -0
- package/dist/recording-coordinator-DzflP07_.d.ts +81 -0
- package/dist/recording-options-BYUFX8iU.d.ts +166 -0
- package/dist/recording-replayer-oOzPITrd.d.ts +32 -0
- package/dist/ref-point-importer-B_wn5ABk.d.ts +62 -0
- package/dist/ref-point-loader-Pt1smgHS.d.ts +117 -0
- package/dist/ref-points/h3-ref-point.d.ts +2 -0
- package/dist/ref-points/h3-ref-point.js +74 -0
- package/dist/ref-points/index.d.ts +2 -0
- package/dist/ref-points/index.js +2 -0
- package/dist/ref-points-slice-tC-Wd6XJ.d.ts +48 -0
- package/dist/reference-points-CFWy8_wj.d.ts +68 -0
- package/dist/replay-engine-DuX316ae.d.ts +103 -0
- package/dist/replay-scene-DtUm6bsr.d.ts +109 -0
- package/dist/routing-slice-BlHTM8eh.d.ts +23 -0
- package/dist/scene-node-names-DPsaE2z3.d.ts +14 -0
- package/dist/sensors/gps-error-handler.d.ts +2 -0
- package/dist/sensors/gps-error-handler.js +53 -0
- package/dist/sensors/gps.d.ts +2 -0
- package/dist/sensors/gps.js +93 -0
- package/dist/sensors/index.d.ts +4 -0
- package/dist/sensors/index.js +4 -0
- package/dist/sensors/permission-checker.d.ts +2 -0
- package/dist/sensors/permission-checker.js +397 -0
- package/dist/state/app-selectors.d.ts +2 -0
- package/dist/state/app-selectors.js +35 -0
- package/dist/state/index.d.ts +17 -0
- package/dist/state/index.js +13 -0
- package/dist/state/persistence-middleware.d.ts +2 -0
- package/dist/state/persistence-middleware.js +71 -0
- package/dist/state/recorder-slice.d.ts +2 -0
- package/dist/state/recorder-slice.js +47 -0
- package/dist/state/recording-coordinator.d.ts +2 -0
- package/dist/state/recording-coordinator.js +150 -0
- package/dist/state/recording-options.d.ts +2 -0
- package/dist/state/recording-options.js +192 -0
- package/dist/state/recording-replayer.d.ts +2 -0
- package/dist/state/recording-replayer.js +39 -0
- package/dist/state/ref-points-slice.d.ts +2 -0
- package/dist/state/ref-points-slice.js +44 -0
- package/dist/state/replay-engine.d.ts +2 -0
- package/dist/state/replay-engine.js +230 -0
- package/dist/state/routing-slice.d.ts +2 -0
- package/dist/state/routing-slice.js +13 -0
- package/dist/state/store-subscribers.d.ts +3 -0
- package/dist/state/store-subscribers.js +97 -0
- package/dist/state/store.d.ts +9 -0
- package/dist/state/store.js +71 -0
- package/dist/state/subscribe-to-selector.d.ts +2 -0
- package/dist/state/subscribe-to-selector.js +26 -0
- package/dist/storage/file-system-utils.d.ts +2 -0
- package/dist/storage/file-system-utils.js +31 -0
- package/dist/storage/file-system.d.ts +2 -0
- package/dist/storage/file-system.js +229 -0
- package/dist/storage/index.d.ts +11 -0
- package/dist/storage/index.js +10 -0
- package/dist/storage/null-storage-backend.d.ts +2 -0
- package/dist/storage/null-storage-backend.js +8 -0
- package/dist/storage/opfs-storage-backend.d.ts +2 -0
- package/dist/storage/opfs-storage-backend.js +15 -0
- package/dist/storage/opfs-storage.d.ts +2 -0
- package/dist/storage/opfs-storage.js +242 -0
- package/dist/storage/ref-point-importer.d.ts +2 -0
- package/dist/storage/ref-point-importer.js +163 -0
- package/dist/storage/ref-point-loader.d.ts +2 -0
- package/dist/storage/ref-point-loader.js +248 -0
- package/dist/storage/ref-point-recovery.d.ts +28 -0
- package/dist/storage/ref-point-recovery.js +159 -0
- package/dist/storage/storage-backend.d.ts +2 -0
- package/dist/storage/storage-backend.js +1 -0
- package/dist/storage/zip-export.d.ts +2 -0
- package/dist/storage/zip-export.js +214 -0
- package/dist/storage/zip-reader.d.ts +2 -0
- package/dist/storage/zip-reader.js +190 -0
- package/dist/storage-backend-BcEItnFr.d.ts +20 -0
- package/dist/store-C7PKOj8S.d.ts +96 -0
- package/dist/store-subscribers-CuoIzB5T.d.ts +77 -0
- package/dist/subscribe-to-selector-UTZ0KkAc.d.ts +26 -0
- package/dist/test-utils/browser-mocks.d.ts +294 -0
- package/dist/test-utils/browser-mocks.js +568 -0
- package/dist/test-utils/zip-round-trip-helpers.d.ts +60 -0
- package/dist/test-utils/zip-round-trip-helpers.js +207 -0
- package/dist/three-dispose-CCBHjE_a.d.ts +30 -0
- package/dist/tracking-state-B9QfuZCg.d.ts +101 -0
- package/dist/types/ar-types.d.ts +2 -0
- package/dist/types/ar-types.js +1 -0
- package/dist/types/geo-types.d.ts +2 -0
- package/dist/types/geo-types.js +1 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.js +1 -0
- package/dist/utils/concurrency.d.ts +2 -0
- package/dist/utils/concurrency.js +40 -0
- package/dist/utils/failure-tracker.d.ts +2 -0
- package/dist/utils/failure-tracker.js +48 -0
- package/dist/utils/format-file-size.d.ts +2 -0
- package/dist/utils/format-file-size.js +41 -0
- package/dist/utils/fused-path.d.ts +2 -0
- package/dist/utils/fused-path.js +44 -0
- package/dist/utils/index.d.ts +7 -0
- package/dist/utils/index.js +7 -0
- package/dist/utils/list-formatter.d.ts +2 -0
- package/dist/utils/list-formatter.js +17 -0
- package/dist/utils/logger.d.ts +2 -0
- package/dist/utils/logger.js +226 -0
- package/dist/vis-colors-Qi2vlRGt.d.ts +63 -0
- package/dist/visualization/alignment-lerper.d.ts +2 -0
- package/dist/visualization/alignment-lerper.js +68 -0
- package/dist/visualization/camera-follower.d.ts +2 -0
- package/dist/visualization/camera-follower.js +51 -0
- package/dist/visualization/css3d-renderer-manager.d.ts +2 -0
- package/dist/visualization/css3d-renderer-manager.js +41 -0
- package/dist/visualization/gps-compass-cubes.d.ts +2 -0
- package/dist/visualization/gps-compass-cubes.js +129 -0
- package/dist/visualization/gps-event-markers.d.ts +2 -0
- package/dist/visualization/gps-event-markers.js +159 -0
- package/dist/visualization/index.d.ts +12 -0
- package/dist/visualization/index.js +12 -0
- package/dist/visualization/leaflet-map-overlay.d.ts +2 -0
- package/dist/visualization/leaflet-map-overlay.js +313 -0
- package/dist/visualization/lerp-utils.d.ts +2 -0
- package/dist/visualization/lerp-utils.js +22 -0
- package/dist/visualization/map-overlay.d.ts +2 -0
- package/dist/visualization/map-overlay.js +241 -0
- package/dist/visualization/reference-points.d.ts +2 -0
- package/dist/visualization/reference-points.js +160 -0
- package/dist/visualization/three-dispose.d.ts +2 -0
- package/dist/visualization/three-dispose.js +55 -0
- package/dist/visualization/vis-colors.d.ts +2 -0
- package/dist/visualization/vis-colors.js +74 -0
- package/dist/webxr-session-COlSrXdL.d.ts +316 -0
- package/dist/xr-camera-texture-BbukNE79.d.ts +53 -0
- package/dist/xr-error-handler-DTHfmvrl.d.ts +24 -0
- package/dist/zip-export-Rtpi75JJ.d.ts +87 -0
- package/dist/zip-reader-D7pkpX8K.d.ts +97 -0
- 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 };
|