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,230 @@
|
|
|
1
|
+
//#region ../src/state/replay-engine.ts
|
|
2
|
+
/** Default maximum delay between actions in ms (30 seconds real-time) */
|
|
3
|
+
const DEFAULT_MAX_DELAY_MS = 3e4;
|
|
4
|
+
/** Max consecutive dispatch errors before auto-pause (Risk R7) */
|
|
5
|
+
const MAX_CONSECUTIVE_ERRORS = 10;
|
|
6
|
+
/**
|
|
7
|
+
* Extract an absolute epoch-ms timestamp from a Redux action, or null
|
|
8
|
+
* if the action type doesn't carry one.
|
|
9
|
+
*
|
|
10
|
+
* IMPORTANT: depthSample uses performance.now() (relative to page load),
|
|
11
|
+
* NOT epoch ms. Returning it here would mix clock domains and produce
|
|
12
|
+
* garbage delays. It must return null. (Risk R4)
|
|
13
|
+
*
|
|
14
|
+
* @param action - A Redux action with type and optional payload
|
|
15
|
+
* @returns Epoch milliseconds, or null if unavailable/unreliable
|
|
16
|
+
*/
|
|
17
|
+
function extractActionTimestamp(action) {
|
|
18
|
+
if (!action.payload || typeof action.payload !== "object") return null;
|
|
19
|
+
const payload = action.payload;
|
|
20
|
+
switch (action.type) {
|
|
21
|
+
case "gpsData/recordGpsEvent": {
|
|
22
|
+
const rawGpsPoint = payload.rawGpsPoint;
|
|
23
|
+
if (rawGpsPoint && typeof rawGpsPoint.timestamp === "number") return rawGpsPoint.timestamp;
|
|
24
|
+
const gpsPoint = payload.gpsPoint;
|
|
25
|
+
if (gpsPoint && typeof gpsPoint.timestamp === "number") return gpsPoint.timestamp;
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
case "recorder/startSession":
|
|
29
|
+
if (typeof payload.startTime === "number") return payload.startTime;
|
|
30
|
+
return null;
|
|
31
|
+
case "gpsData/markReferencePoint": {
|
|
32
|
+
if (typeof payload.timestamp === "number") return payload.timestamp;
|
|
33
|
+
const rawGpsPoint = payload.rawGpsPoint;
|
|
34
|
+
if (rawGpsPoint && typeof rawGpsPoint.timestamp === "number") return rawGpsPoint.timestamp;
|
|
35
|
+
const gpsPoint = payload.gpsPoint;
|
|
36
|
+
if (gpsPoint && typeof gpsPoint.timestamp === "number") return gpsPoint.timestamp;
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
case "recorder/recordDepthSample": return null;
|
|
40
|
+
case "recorder/endSession": return null;
|
|
41
|
+
default: return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Compute the delay in ms between two consecutive actions for replay.
|
|
46
|
+
*
|
|
47
|
+
* @param currentTs - Epoch ms of the current (just-dispatched) action, or null
|
|
48
|
+
* @param nextTs - Epoch ms of the next action, or null
|
|
49
|
+
* @param speedFactor - Playback speed multiplier (1 = real-time)
|
|
50
|
+
* @param maxDelay - Maximum delay in ms (default: DEFAULT_MAX_DELAY_MS)
|
|
51
|
+
* @returns Delay in ms, clamped to [0, maxDelay]
|
|
52
|
+
*/
|
|
53
|
+
function computeInterActionDelay(currentTs, nextTs, speedFactor, maxDelay = DEFAULT_MAX_DELAY_MS) {
|
|
54
|
+
if (currentTs === null || nextTs === null) return 0;
|
|
55
|
+
const rawDelay = (nextTs - currentTs) / speedFactor;
|
|
56
|
+
return Math.min(Math.max(0, rawDelay), maxDelay);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Async controller that dispatches recorded actions with timed delays.
|
|
60
|
+
*
|
|
61
|
+
* Uses AbortController for pause/cancel:
|
|
62
|
+
* - play() starts the async loop
|
|
63
|
+
* - pause() aborts the current controller → loop exits
|
|
64
|
+
* - resume() creates a new controller and restarts from current index
|
|
65
|
+
* - setSpeed() updates a closure variable, picked up on next delay calc
|
|
66
|
+
* - dispose() stops everything and resets state
|
|
67
|
+
*/
|
|
68
|
+
var ReplayEngine = class {
|
|
69
|
+
state = "idle";
|
|
70
|
+
currentIndex = 0;
|
|
71
|
+
actions = [];
|
|
72
|
+
store = null;
|
|
73
|
+
speedFactor = 1;
|
|
74
|
+
abortController = null;
|
|
75
|
+
progressCallbacks = [];
|
|
76
|
+
completeCallbacks = [];
|
|
77
|
+
errorCallbacks = [];
|
|
78
|
+
currentPlayPromise = null;
|
|
79
|
+
/** Get the current engine state */
|
|
80
|
+
getState() {
|
|
81
|
+
return this.state;
|
|
82
|
+
}
|
|
83
|
+
/** Get the current action index (1-based after dispatch, 0 before play) */
|
|
84
|
+
getCurrentActionIndex() {
|
|
85
|
+
return this.currentIndex;
|
|
86
|
+
}
|
|
87
|
+
/** Register a progress callback: (currentIndex, totalCount) */
|
|
88
|
+
onProgress(callback) {
|
|
89
|
+
this.progressCallbacks.push(callback);
|
|
90
|
+
}
|
|
91
|
+
/** Register a completion callback */
|
|
92
|
+
onComplete(callback) {
|
|
93
|
+
this.completeCallbacks.push(callback);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Register an error callback: (actionIndex, error).
|
|
97
|
+
* Called when a dispatch throws. Replay continues past the error.
|
|
98
|
+
* After MAX_CONSECUTIVE_ERRORS consecutive failures, engine auto-pauses (R7).
|
|
99
|
+
*/
|
|
100
|
+
onError(callback) {
|
|
101
|
+
this.errorCallbacks.push(callback);
|
|
102
|
+
}
|
|
103
|
+
/** Update playback speed (takes effect on next delay calculation) */
|
|
104
|
+
setSpeed(factor) {
|
|
105
|
+
if (!Number.isFinite(factor) || factor <= 0) throw new RangeError(`Speed factor must be a positive finite number, got ${factor}`);
|
|
106
|
+
this.speedFactor = factor;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Start replaying actions with timed delays.
|
|
110
|
+
*
|
|
111
|
+
* @param actions - Array of Redux actions to dispatch
|
|
112
|
+
* @param store - RecorderStore to dispatch into
|
|
113
|
+
* @param speedFactor - Initial playback speed (1 = real-time)
|
|
114
|
+
* @returns Promise that resolves when replay completes or is paused
|
|
115
|
+
*/
|
|
116
|
+
play(actions, store, speedFactor) {
|
|
117
|
+
this.abortController?.abort();
|
|
118
|
+
this.actions = actions;
|
|
119
|
+
this.store = store;
|
|
120
|
+
this.speedFactor = speedFactor;
|
|
121
|
+
this.currentIndex = 0;
|
|
122
|
+
this.state = "playing";
|
|
123
|
+
this.abortController = new AbortController();
|
|
124
|
+
this.currentPlayPromise = this.runLoop(this.abortController.signal);
|
|
125
|
+
return this.currentPlayPromise;
|
|
126
|
+
}
|
|
127
|
+
/** Pause the replay. Can be resumed later with resume(). */
|
|
128
|
+
pause() {
|
|
129
|
+
if (this.state !== "playing") return;
|
|
130
|
+
this.state = "paused";
|
|
131
|
+
this.abortController?.abort();
|
|
132
|
+
this.abortController = null;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Resume from the current action index after a pause.
|
|
136
|
+
* @returns Promise that resolves when replay completes or is paused again
|
|
137
|
+
*/
|
|
138
|
+
resume() {
|
|
139
|
+
if (this.state !== "paused" || !this.store) return Promise.resolve();
|
|
140
|
+
this.state = "playing";
|
|
141
|
+
this.abortController = new AbortController();
|
|
142
|
+
this.currentPlayPromise = this.runLoop(this.abortController.signal);
|
|
143
|
+
return this.currentPlayPromise;
|
|
144
|
+
}
|
|
145
|
+
/** Stop playback and reset to idle. */
|
|
146
|
+
dispose() {
|
|
147
|
+
this.abortController?.abort();
|
|
148
|
+
this.abortController = null;
|
|
149
|
+
this.state = "idle";
|
|
150
|
+
this.currentIndex = 0;
|
|
151
|
+
this.actions = [];
|
|
152
|
+
this.store = null;
|
|
153
|
+
this.currentPlayPromise = null;
|
|
154
|
+
this.progressCallbacks = [];
|
|
155
|
+
this.completeCallbacks = [];
|
|
156
|
+
this.errorCallbacks = [];
|
|
157
|
+
}
|
|
158
|
+
async runLoop(signal) {
|
|
159
|
+
const { actions, store } = this;
|
|
160
|
+
if (!store) return;
|
|
161
|
+
const total = actions.length;
|
|
162
|
+
if (total === 0) {
|
|
163
|
+
this.state = "completed";
|
|
164
|
+
this.notifyComplete();
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
let consecutiveErrors = 0;
|
|
168
|
+
while (this.currentIndex < total) {
|
|
169
|
+
if (signal.aborted) return;
|
|
170
|
+
const action = actions[this.currentIndex];
|
|
171
|
+
try {
|
|
172
|
+
store.dispatch(action);
|
|
173
|
+
consecutiveErrors = 0;
|
|
174
|
+
} catch (err) {
|
|
175
|
+
consecutiveErrors++;
|
|
176
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
177
|
+
this.notifyError(this.currentIndex + 1, error);
|
|
178
|
+
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
|
|
179
|
+
this.state = "paused";
|
|
180
|
+
this.abortController?.abort();
|
|
181
|
+
this.abortController = null;
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
this.currentIndex++;
|
|
186
|
+
this.notifyProgress(this.currentIndex, total);
|
|
187
|
+
if (this.currentIndex >= total) {
|
|
188
|
+
this.state = "completed";
|
|
189
|
+
this.notifyComplete();
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const delay = computeInterActionDelay(extractActionTimestamp(action), extractActionTimestamp(actions[this.currentIndex]), this.speedFactor);
|
|
193
|
+
if (delay > 0) {
|
|
194
|
+
if (await this.abortableDelay(delay, signal)) return;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Promise-based delay that resolves on timeout or rejects on abort.
|
|
200
|
+
* Returns true if aborted, false if delay completed normally.
|
|
201
|
+
*/
|
|
202
|
+
abortableDelay(ms, signal) {
|
|
203
|
+
return new Promise((resolve) => {
|
|
204
|
+
if (signal.aborted) {
|
|
205
|
+
resolve(true);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const timer = setTimeout(() => {
|
|
209
|
+
signal.removeEventListener("abort", onAbort);
|
|
210
|
+
resolve(false);
|
|
211
|
+
}, ms);
|
|
212
|
+
const onAbort = () => {
|
|
213
|
+
clearTimeout(timer);
|
|
214
|
+
resolve(true);
|
|
215
|
+
};
|
|
216
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
notifyProgress(current, total) {
|
|
220
|
+
for (const cb of this.progressCallbacks) cb(current, total);
|
|
221
|
+
}
|
|
222
|
+
notifyComplete() {
|
|
223
|
+
for (const cb of this.completeCallbacks) cb();
|
|
224
|
+
}
|
|
225
|
+
notifyError(actionIndex, error) {
|
|
226
|
+
for (const cb of this.errorCallbacks) cb(actionIndex, error);
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
//#endregion
|
|
230
|
+
export { DEFAULT_MAX_DELAY_MS, ReplayEngine, computeInterActionDelay, extractActionTimestamp };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { createSlice } from "@reduxjs/toolkit";
|
|
2
|
+
//#region ../src/state/routing-slice.ts
|
|
3
|
+
const routingSlice = createSlice({
|
|
4
|
+
name: "routing",
|
|
5
|
+
initialState: { currentScreen: "setup" },
|
|
6
|
+
reducers: { navigateTo(state, action) {
|
|
7
|
+
state.currentScreen = action.payload;
|
|
8
|
+
} }
|
|
9
|
+
});
|
|
10
|
+
const { navigateTo } = routingSlice.actions;
|
|
11
|
+
const routingReducer = routingSlice.reducer;
|
|
12
|
+
//#endregion
|
|
13
|
+
export { navigateTo, routingReducer };
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { fusedGpsFromOdom } from "../utils/fused-path.js";
|
|
2
|
+
import { subscribeToSelector } from "./subscribe-to-selector.js";
|
|
3
|
+
import { selectAlignmentMatrix, selectGpsPositions, selectOdometryPositions, selectOdometryRotations, selectReferencePoints, selectZeroReference } from "./app-selectors.js";
|
|
4
|
+
import { calcGpsCoords } from "gps-plus-slam-js";
|
|
5
|
+
import { mat4, vec3 } from "gl-matrix";
|
|
6
|
+
//#region ../src/state/store-subscribers.ts
|
|
7
|
+
/**
|
|
8
|
+
* Wire store state-change subscriptions to visualization dependencies.
|
|
9
|
+
*
|
|
10
|
+
* Uses subscribeToSelector to decompose the monolithic subscriber into
|
|
11
|
+
* focused, change-gated callbacks:
|
|
12
|
+
* 1. Alignment matrix changes → apply matrix + capture snapshots
|
|
13
|
+
* 2. GPS position changes → incremental event visualization + map overlay
|
|
14
|
+
* 3. Reference point changes → forward to map overlay
|
|
15
|
+
*
|
|
16
|
+
* Each subscription uses reference equality to detect changes — callbacks
|
|
17
|
+
* only fire when the selector output actually differs between dispatches.
|
|
18
|
+
*
|
|
19
|
+
* Each call creates **fresh subscriptions** — no manual reset needed between
|
|
20
|
+
* sessions. Call the returned unsubscribe function to tear down all.
|
|
21
|
+
*
|
|
22
|
+
* @param store — any object satisfying SubscribableStore (RecorderStore, replay store, mock)
|
|
23
|
+
* @param deps — visualization dependencies (real singletons or test mocks)
|
|
24
|
+
* @returns An unsubscribe function that removes all listeners from the store.
|
|
25
|
+
*/
|
|
26
|
+
function wireStoreSubscribers(store, deps) {
|
|
27
|
+
const _odomVec = vec3.create();
|
|
28
|
+
const _alignedVec = vec3.create();
|
|
29
|
+
const unsubs = [];
|
|
30
|
+
unsubs.push(subscribeToSelector(store, selectAlignmentMatrix, (matrix) => {
|
|
31
|
+
if (!matrix) return;
|
|
32
|
+
deps.applyAlignmentMatrix(matrix);
|
|
33
|
+
const state = store.getState();
|
|
34
|
+
const odomPositions = selectOdometryPositions(state);
|
|
35
|
+
if (odomPositions.length > 0) {
|
|
36
|
+
const latestOdom = odomPositions[odomPositions.length - 1];
|
|
37
|
+
const glMat = mat4.fromValues(...matrix);
|
|
38
|
+
vec3.set(_odomVec, latestOdom[0], latestOdom[1], latestOdom[2]);
|
|
39
|
+
vec3.transformMat4(_alignedVec, _odomVec, glMat);
|
|
40
|
+
const snapshotPos = [
|
|
41
|
+
_alignedVec[0],
|
|
42
|
+
_alignedVec[1],
|
|
43
|
+
_alignedVec[2]
|
|
44
|
+
];
|
|
45
|
+
deps.gpsEventVisualizer.addAlignmentSnapshot(snapshotPos);
|
|
46
|
+
deps.onAlignmentSnapshot?.(snapshotPos);
|
|
47
|
+
const zeroRef = selectZeroReference(state);
|
|
48
|
+
if (zeroRef && deps.mapOverlay?.addAlignmentSnapshot) {
|
|
49
|
+
const gps = calcGpsCoords(zeroRef, _alignedVec);
|
|
50
|
+
deps.mapOverlay.addAlignmentSnapshot(gps.lat, gps.lon);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}));
|
|
54
|
+
unsubs.push(subscribeToSelector(store, selectGpsPositions, (gpsPositions, prevPositions) => {
|
|
55
|
+
const prevCount = prevPositions?.length ?? 0;
|
|
56
|
+
const state = store.getState();
|
|
57
|
+
const zeroRef = selectZeroReference(state);
|
|
58
|
+
if (zeroRef && !deps.gpsEventVisualizer.getZeroRef()) deps.gpsEventVisualizer.setZeroRef(zeroRef);
|
|
59
|
+
const odomPositions = selectOdometryPositions(state);
|
|
60
|
+
const odomRotations = selectOdometryRotations(state);
|
|
61
|
+
for (let i = prevCount; i < gpsPositions.length; i++) {
|
|
62
|
+
const gpsPoint = gpsPositions[i];
|
|
63
|
+
const odomPos = odomPositions[i];
|
|
64
|
+
if (gpsPoint && odomPos) {
|
|
65
|
+
deps.gpsEventVisualizer.addGpsEvent(gpsPoint.coordinates, odomPos);
|
|
66
|
+
deps.onNewGpsPosition?.(gpsPoint.coordinates);
|
|
67
|
+
deps.onNewGpsLatLng?.(gpsPoint.latitude, gpsPoint.longitude);
|
|
68
|
+
deps.mapOverlay?.addRawGpsPoint?.(gpsPoint.latitude, gpsPoint.longitude);
|
|
69
|
+
const alignmentMatrix = selectAlignmentMatrix(state);
|
|
70
|
+
const zero = selectZeroReference(state);
|
|
71
|
+
if (alignmentMatrix && zero && deps.mapOverlay?.addFusedPoint) {
|
|
72
|
+
const fusedGps = fusedGpsFromOdom(alignmentMatrix, odomPos, zero);
|
|
73
|
+
deps.mapOverlay.addFusedPoint(fusedGps.lat, fusedGps.lon);
|
|
74
|
+
}
|
|
75
|
+
const odomRot = odomRotations[i];
|
|
76
|
+
if (odomRot) deps.onNewOdomPose?.(odomPos, odomRot);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (deps.mapOverlay && gpsPositions.length > 0) {
|
|
80
|
+
const lastGpsPoint = gpsPositions[gpsPositions.length - 1];
|
|
81
|
+
deps.mapOverlay.setGpsPosition(lastGpsPoint.latitude, lastGpsPoint.longitude);
|
|
82
|
+
}
|
|
83
|
+
}));
|
|
84
|
+
unsubs.push(subscribeToSelector(store, selectReferencePoints, (refPoints, prevPoints) => {
|
|
85
|
+
const prevCount = prevPoints?.length ?? 0;
|
|
86
|
+
if (!deps.mapOverlay?.addRefPoint || refPoints.length <= prevCount) return;
|
|
87
|
+
for (let i = prevCount; i < refPoints.length; i++) {
|
|
88
|
+
const rp = refPoints[i];
|
|
89
|
+
deps.mapOverlay.addRefPoint(rp.gpsPoint.latitude, rp.gpsPoint.longitude, rp.id);
|
|
90
|
+
}
|
|
91
|
+
}));
|
|
92
|
+
return () => {
|
|
93
|
+
for (const unsub of unsubs) unsub();
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
//#endregion
|
|
97
|
+
export { wireStoreSubscribers };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { i as DepthSample, r as DepthPoint } from "../ar-types-B-ORgk6Z.js";
|
|
2
|
+
import { s as RecordingOptions } from "../recording-options-BYUFX8iU.js";
|
|
3
|
+
import { n as SessionMetadata } from "../opfs-storage-ChOzF_Wp.js";
|
|
4
|
+
import { t as StorageBackend } from "../storage-backend-BcEItnFr.js";
|
|
5
|
+
import { a as recordWriteFailure, c as startSession, i as recordDepthSample, n as SessionMetadata$1, r as endSession, s as setCurrentScenarioName, t as RecorderState } from "../recorder-slice-fcwAXrsh.js";
|
|
6
|
+
import { a as resetRefPointsState, n as clearSessionRefPointUsage, o as selectCachedKnownRefPoints, r as incrementRefPointUsage, s as setImportedRefPoints, t as RefPointsState } from "../ref-points-slice-tC-Wd6XJ.js";
|
|
7
|
+
import { n as RefPointMark } from "../ref-point-loader-Pt1smgHS.js";
|
|
8
|
+
import { _ as setZeroPos, a as MarkReferencePointPayload, c as RecordGpsEventPayload, d as RootState, f as add2dImage, g as recordGpsEvent, h as markReferencePoint, i as LatLong, l as RecorderStore, m as createRecorderStore, n as CombinedRootState, o as RawDeviceOrientation, p as calcRelativeCoordsInMeters, r as GpsPoint, s as RawGpsPoint, t as AppDispatch, u as RecorderStoreOptions } from "../store-C7PKOj8S.js";
|
|
9
|
+
export { AppDispatch, CombinedRootState, DepthPoint, DepthSample, GpsPoint, LatLong, MarkReferencePointPayload, SessionMetadata as OpfsSessionMetadata, RawDeviceOrientation, RawGpsPoint, RecordGpsEventPayload, RecorderState, RecorderStore, RecorderStoreOptions, RecordingOptions, RefPointMark, RefPointsState, RootState, SessionMetadata$1 as SessionMetadata, StorageBackend, add2dImage, calcRelativeCoordsInMeters, clearSessionRefPointUsage, createRecorderStore, endSession, incrementRefPointUsage, markReferencePoint, recordDepthSample, recordGpsEvent, recordWriteFailure, resetRefPointsState, selectCachedKnownRefPoints, setCurrentScenarioName, setImportedRefPoints, setZeroPos, startSession };
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { endSession, recordDepthSample, recordWriteFailure, recorderReducer, setCurrentScenarioName, startSession } from "./recorder-slice.js";
|
|
2
|
+
import { clearSessionRefPointUsage, incrementRefPointUsage, refPointsReducer, resetRefPointsState, selectCachedKnownRefPoints, setImportedRefPoints } from "./ref-points-slice.js";
|
|
3
|
+
import { routingReducer } from "./routing-slice.js";
|
|
4
|
+
import { OpfsStorageBackend } from "../storage/opfs-storage-backend.js";
|
|
5
|
+
import { createPersistenceMiddleware } from "./persistence-middleware.js";
|
|
6
|
+
import "../licensing/community-license-key.js";
|
|
7
|
+
import { configureStore } from "@reduxjs/toolkit";
|
|
8
|
+
import { add2dImage, arElementsReducer, calcRelativeCoordsInMeters, gpsDataReducer, gpsElementsReducer, markReferencePoint, recordGpsEvent, sanitizeForDevTools, setZeroPos, validateLicenseKey } from "gps-plus-slam-js";
|
|
9
|
+
//#region ../src/state/store.ts
|
|
10
|
+
/**
|
|
11
|
+
* Redux Store for Recorder App
|
|
12
|
+
*
|
|
13
|
+
* Uses Redux Toolkit's configureStore to combine all reducers (library +
|
|
14
|
+
* app-level) into a single store. Action persistence during recording is
|
|
15
|
+
* handled by the persistence middleware.
|
|
16
|
+
*
|
|
17
|
+
* ARCHITECTURE NOTE: See docs/architecture-ar-gps-pose-separation.md
|
|
18
|
+
* The library's recordGpsEvent action captures paired AR+GPS data
|
|
19
|
+
* that feeds directly into the alignment algorithm.
|
|
20
|
+
*
|
|
21
|
+
* Migration note (§4 — architecture-observations-consolidated.md):
|
|
22
|
+
* Previously the store used a manual wrapper pattern with action.type.startsWith()
|
|
23
|
+
* prefix matching to route actions between a library store and local state
|
|
24
|
+
* variables. This has been replaced with a single configureStore call that
|
|
25
|
+
* combines all 6 reducers declaratively.
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* Create the recorder store using Redux Toolkit's configureStore.
|
|
29
|
+
*
|
|
30
|
+
* Combines all 6 reducers (3 library: gpsData, gpsElements, arElements;
|
|
31
|
+
* 3 app: recorder, refPoints, routing) into a single store with the
|
|
32
|
+
* persistence middleware for action recording.
|
|
33
|
+
*
|
|
34
|
+
* @param options - Optional configuration including callbacks
|
|
35
|
+
*/
|
|
36
|
+
function createRecorderStore(options = {}) {
|
|
37
|
+
const { onWriteFailure } = options;
|
|
38
|
+
const storageBackend = options.storageBackend ?? new OpfsStorageBackend();
|
|
39
|
+
const enableDevChecks = options.enableDevChecks ?? true;
|
|
40
|
+
validateLicenseKey(options.licenseKey ?? "eyJ0eXBlIjoiY29tbXVuaXR5IiwiZXhwIjoxODA4NzcyNDI3fQ.YYNf0IdqniZdMzRXCuvewxsQQ01XNywrKsfL5IpiJotfFJfqtrGg4u9z0QFhXeszPtUhnvgC8_oOnbOkbXPuAw");
|
|
41
|
+
const store = configureStore({
|
|
42
|
+
reducer: {
|
|
43
|
+
gpsData: gpsDataReducer,
|
|
44
|
+
gpsElements: gpsElementsReducer,
|
|
45
|
+
arElements: arElementsReducer,
|
|
46
|
+
recorder: recorderReducer,
|
|
47
|
+
refPoints: refPointsReducer,
|
|
48
|
+
routing: routingReducer
|
|
49
|
+
},
|
|
50
|
+
middleware: (getDefaultMiddleware) => getDefaultMiddleware({
|
|
51
|
+
serializableCheck: enableDevChecks,
|
|
52
|
+
immutableCheck: enableDevChecks
|
|
53
|
+
}).concat(createPersistenceMiddleware({
|
|
54
|
+
storageBackend,
|
|
55
|
+
onWriteFailure
|
|
56
|
+
})),
|
|
57
|
+
devTools: {
|
|
58
|
+
actionSanitizer: sanitizeForDevTools,
|
|
59
|
+
stateSanitizer: sanitizeForDevTools
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
return {
|
|
63
|
+
getState: () => store.getState(),
|
|
64
|
+
dispatch: store.dispatch,
|
|
65
|
+
subscribe: (listener) => store.subscribe(listener),
|
|
66
|
+
writeFrame: (blob, index) => storageBackend.writeFrame(blob, index),
|
|
67
|
+
writeSessionMetadata: (metadata) => storageBackend.writeSessionMetadata(metadata)
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
//#endregion
|
|
71
|
+
export { add2dImage, calcRelativeCoordsInMeters, clearSessionRefPointUsage, createRecorderStore, endSession, incrementRefPointUsage, markReferencePoint, recordDepthSample, recordGpsEvent, recordWriteFailure, resetRefPointsState, selectCachedKnownRefPoints, setCurrentScenarioName, setImportedRefPoints, setZeroPos, startSession };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
//#region ../src/state/subscribe-to-selector.ts
|
|
2
|
+
/**
|
|
3
|
+
* Subscribe to a specific derived value from the store, invoking `onChange`
|
|
4
|
+
* only when the selector result changes by strict reference equality (`!==`).
|
|
5
|
+
*
|
|
6
|
+
* This replaces the `let lastX = …; if (current !== lastX) { … }` pattern
|
|
7
|
+
* previously used in store-subscribers.ts.
|
|
8
|
+
*
|
|
9
|
+
* @param store — subscribable store instance
|
|
10
|
+
* @param selector — pure function extracting a value from state
|
|
11
|
+
* @param onChange — callback receiving (current, previous) when the value changes
|
|
12
|
+
* @returns unsubscribe function
|
|
13
|
+
*/
|
|
14
|
+
function subscribeToSelector(store, selector, onChange) {
|
|
15
|
+
let previous;
|
|
16
|
+
return store.subscribe(() => {
|
|
17
|
+
const current = selector(store.getState());
|
|
18
|
+
if (current !== previous) {
|
|
19
|
+
const prev = previous;
|
|
20
|
+
previous = current;
|
|
21
|
+
onChange(current, prev);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
//#endregion
|
|
26
|
+
export { subscribeToSelector };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
//#region ../src/storage/file-system-utils.ts
|
|
2
|
+
/**
|
|
3
|
+
* File System Utilities - Pure Functions
|
|
4
|
+
*
|
|
5
|
+
* These are extracted from file-system.ts so they can be tested
|
|
6
|
+
* without mocking browser APIs.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Format a Date as a timestamp string for folder names
|
|
10
|
+
* @example "2025-02-28_14-30-11utc"
|
|
11
|
+
*/
|
|
12
|
+
function formatTimestamp(date) {
|
|
13
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
14
|
+
return `${date.getUTCFullYear()}-${pad(date.getUTCMonth() + 1)}-${pad(date.getUTCDate())}_${pad(date.getUTCHours())}-${pad(date.getUTCMinutes())}-${pad(date.getUTCSeconds())}utc`;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Format an action file index as a zero-padded filename
|
|
18
|
+
* @example 42 -> "000042.json"
|
|
19
|
+
*/
|
|
20
|
+
function formatActionFilename(index) {
|
|
21
|
+
return `${String(index).padStart(6, "0")}.json`;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Format a frame index as a zero-padded filename
|
|
25
|
+
* @example 42 -> "frame-000042.jpg"
|
|
26
|
+
*/
|
|
27
|
+
function formatFrameFilename(index) {
|
|
28
|
+
return `frame-${String(index).padStart(6, "0")}.jpg`;
|
|
29
|
+
}
|
|
30
|
+
//#endregion
|
|
31
|
+
export { formatActionFilename, formatFrameFilename, formatTimestamp };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { a as initStorage, c as resetForNewSession, d as startSession, f as verifyWriteAccess, h as writeSessionMetadata, i as getCurrentScenarioHandle, l as resetStorageState, m as writeFrame, n as WriteAccessResult, o as isRefPointAction, p as writeAction, r as ensureScenarioDirectory, s as loadScenarioRefPoints, t as RefPointRecord, u as setCurrentScenario } from "../file-system-BsQgYCU3.js";
|
|
2
|
+
export { RefPointRecord, WriteAccessResult, ensureScenarioDirectory, getCurrentScenarioHandle, initStorage, isRefPointAction, loadScenarioRefPoints, resetForNewSession, resetStorageState, setCurrentScenario, startSession, verifyWriteAccess, writeAction, writeFrame, writeSessionMetadata };
|