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,71 @@
|
|
|
1
|
+
import { createLogger } from "../utils/logger.js";
|
|
2
|
+
import { recordWriteFailure } from "./recorder-slice.js";
|
|
3
|
+
//#region ../src/state/persistence-middleware.ts
|
|
4
|
+
const log = createLogger("PersistenceMiddleware");
|
|
5
|
+
/** Maximum number of concurrent OPFS write operations. */
|
|
6
|
+
const MAX_CONCURRENT_WRITES = 3;
|
|
7
|
+
/**
|
|
8
|
+
* Simple write queue that limits concurrent OPFS operations.
|
|
9
|
+
* Prevents unbounded memory growth when storage is slow.
|
|
10
|
+
*/
|
|
11
|
+
var WriteQueue = class {
|
|
12
|
+
pendingCount = 0;
|
|
13
|
+
queue = [];
|
|
14
|
+
enqueue(writeFn) {
|
|
15
|
+
this.queue.push(writeFn);
|
|
16
|
+
this.drain();
|
|
17
|
+
}
|
|
18
|
+
drain() {
|
|
19
|
+
while (this.pendingCount < MAX_CONCURRENT_WRITES && this.queue.length > 0) {
|
|
20
|
+
const fn = this.queue.shift();
|
|
21
|
+
this.pendingCount++;
|
|
22
|
+
fn().finally(() => {
|
|
23
|
+
this.pendingCount--;
|
|
24
|
+
this.drain();
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Create a Redux middleware that persists qualifying actions to storage
|
|
31
|
+
* during active recording sessions.
|
|
32
|
+
*
|
|
33
|
+
* Persistence rules:
|
|
34
|
+
* - Only persists when `state.recorder.isRecording` is true (checked AFTER
|
|
35
|
+
* the action is reduced, so `startSession` itself is included).
|
|
36
|
+
* - Persists `gpsData/*` and `recorder/*` actions.
|
|
37
|
+
* - Excludes `recorder/recordWriteFailure` to prevent recursive persistence.
|
|
38
|
+
* - Excludes `routing/*`, `refPoints/*`, and any other non-recording actions.
|
|
39
|
+
* - Uses 1-based indexing for action files (000001.json, 000002.json, …).
|
|
40
|
+
* - Each middleware instance maintains its own action index (Bug 10 fix).
|
|
41
|
+
*/
|
|
42
|
+
function createPersistenceMiddleware(options) {
|
|
43
|
+
const { storageBackend, onWriteFailure } = options;
|
|
44
|
+
let actionIndex = 0;
|
|
45
|
+
const writeQueue = new WriteQueue();
|
|
46
|
+
const middleware = (store) => (next) => (action) => {
|
|
47
|
+
const typedAction = action;
|
|
48
|
+
const actionType = typeof typedAction.type === "string" ? typedAction.type : null;
|
|
49
|
+
const wasRecording = store.getState().recorder?.isRecording ?? false;
|
|
50
|
+
const result = next(action);
|
|
51
|
+
if (!actionType) return result;
|
|
52
|
+
if (actionType === "recorder/startSession") actionIndex = 0;
|
|
53
|
+
if (!((store.getState().recorder?.isRecording ?? false) || wasRecording && actionType === "recorder/endSession")) return result;
|
|
54
|
+
if (!(actionType.startsWith("gpsData/") || actionType.startsWith("recorder/") && actionType !== "recorder/recordWriteFailure")) return result;
|
|
55
|
+
const index = ++actionIndex;
|
|
56
|
+
writeQueue.enqueue(async () => {
|
|
57
|
+
try {
|
|
58
|
+
await storageBackend.writeAction(typedAction, index);
|
|
59
|
+
} catch (err) {
|
|
60
|
+
const normalized = err instanceof Error ? err : new Error(String(err));
|
|
61
|
+
log.error("Failed to persist action:", normalized);
|
|
62
|
+
store.dispatch(recordWriteFailure(normalized.message));
|
|
63
|
+
if (onWriteFailure) onWriteFailure(normalized);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
return result;
|
|
67
|
+
};
|
|
68
|
+
return middleware;
|
|
69
|
+
}
|
|
70
|
+
//#endregion
|
|
71
|
+
export { createPersistenceMiddleware };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { a as recordWriteFailure, c as startSession, i as recordDepthSample, n as SessionMetadata, o as recorderReducer, r as endSession, s as setCurrentScenarioName, t as RecorderState } from "../recorder-slice-fcwAXrsh.js";
|
|
2
|
+
export { RecorderState, SessionMetadata, endSession, recordDepthSample, recordWriteFailure, recorderReducer, setCurrentScenarioName, startSession };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { createSlice } from "@reduxjs/toolkit";
|
|
2
|
+
//#region ../src/state/recorder-slice.ts
|
|
3
|
+
const recorderSlice = createSlice({
|
|
4
|
+
name: "recorder",
|
|
5
|
+
initialState: {
|
|
6
|
+
isRecording: false,
|
|
7
|
+
sessionMetadata: null,
|
|
8
|
+
actionCount: 0,
|
|
9
|
+
failedWriteCount: 0,
|
|
10
|
+
currentScenarioName: ""
|
|
11
|
+
},
|
|
12
|
+
reducers: {
|
|
13
|
+
startSession(state, action) {
|
|
14
|
+
state.isRecording = true;
|
|
15
|
+
state.sessionMetadata = action.payload;
|
|
16
|
+
state.actionCount = 0;
|
|
17
|
+
state.failedWriteCount = 0;
|
|
18
|
+
},
|
|
19
|
+
endSession(state) {
|
|
20
|
+
state.isRecording = false;
|
|
21
|
+
},
|
|
22
|
+
/**
|
|
23
|
+
* Record a depth sample from WebXR depth sensing.
|
|
24
|
+
* The sample includes camera pose and a grid of depth points.
|
|
25
|
+
* This action is persisted to enable replay with depth data.
|
|
26
|
+
*/
|
|
27
|
+
recordDepthSample(_state, _action) {},
|
|
28
|
+
/**
|
|
29
|
+
* Record a write failure during persistence.
|
|
30
|
+
* User Feedback Issue #1 Part B: Track write failures for session summary.
|
|
31
|
+
*/
|
|
32
|
+
recordWriteFailure(state, _action) {
|
|
33
|
+
state.failedWriteCount += 1;
|
|
34
|
+
},
|
|
35
|
+
/**
|
|
36
|
+
* Set the currently selected scenario name.
|
|
37
|
+
* Replaces the closure variable threading through folder-manager deps.
|
|
38
|
+
*/
|
|
39
|
+
setCurrentScenarioName(state, action) {
|
|
40
|
+
state.currentScenarioName = action.payload;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
const { startSession, endSession, recordDepthSample, recordWriteFailure, setCurrentScenarioName } = recorderSlice.actions;
|
|
45
|
+
const recorderReducer = recorderSlice.reducer;
|
|
46
|
+
//#endregion
|
|
47
|
+
export { endSession, recordDepthSample, recordWriteFailure, recorderReducer, setCurrentScenarioName, startSession };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { a as eulerToQuaternion, c as getLastDeviceOrientation, i as createGpsPositionHandler, l as resetCoordinatorState, n as buildRawGpsPoint, o as extractOdomPosition, r as buildRecordGpsEventPayload, s as extractOdomRotation, t as RecordingCoordinatorConfig, u as updateDeviceOrientation } from "../recording-coordinator-DzflP07_.js";
|
|
2
|
+
export { RecordingCoordinatorConfig, buildRawGpsPoint, buildRecordGpsEventPayload, createGpsPositionHandler, eulerToQuaternion, extractOdomPosition, extractOdomRotation, getLastDeviceOrientation, resetCoordinatorState, updateDeviceOrientation };
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { createLogger } from "../utils/logger.js";
|
|
2
|
+
import { recordGpsEvent, setZeroPos } from "./store.js";
|
|
3
|
+
import { eulerToQuaternion, getOdometryPositions, getZeroReference } from "gps-plus-slam-js";
|
|
4
|
+
//#region ../src/state/recording-coordinator.ts
|
|
5
|
+
const log = createLogger("RecordingCoordinator");
|
|
6
|
+
/** Counter for generating unique GPS point IDs */
|
|
7
|
+
let gpsEventCounter = 0;
|
|
8
|
+
/**
|
|
9
|
+
* State for device orientation (captured separately from GPS)
|
|
10
|
+
*/
|
|
11
|
+
let lastDeviceOrientation = null;
|
|
12
|
+
/**
|
|
13
|
+
* Update the cached device orientation.
|
|
14
|
+
* Called by orientation watch, used when GPS event arrives.
|
|
15
|
+
*/
|
|
16
|
+
function updateDeviceOrientation(orientation) {
|
|
17
|
+
lastDeviceOrientation = orientation;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Get the current cached device orientation.
|
|
21
|
+
*/
|
|
22
|
+
function getLastDeviceOrientation() {
|
|
23
|
+
return lastDeviceOrientation;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Clear cached state (for testing or session reset)
|
|
27
|
+
*/
|
|
28
|
+
function resetCoordinatorState() {
|
|
29
|
+
lastDeviceOrientation = null;
|
|
30
|
+
gpsEventCounter = 0;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Extract raw odometry position tuple from ARPose.
|
|
34
|
+
*
|
|
35
|
+
* Returns the raw WebXR position without coordinate conversion.
|
|
36
|
+
* The reducer applies the WebXR→NUE transform when storing into state
|
|
37
|
+
* (raw-storage pattern, see docs/2026-04-09-raw-storage-convert-on-read.md).
|
|
38
|
+
*
|
|
39
|
+
* WebXR local-floor frame: X=East, Y=Up, Z=South (toward viewer / backward).
|
|
40
|
+
*/
|
|
41
|
+
function extractOdomPosition(arPose) {
|
|
42
|
+
return [
|
|
43
|
+
arPose.position.x,
|
|
44
|
+
arPose.position.y,
|
|
45
|
+
arPose.position.z
|
|
46
|
+
];
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Extract odometry rotation tuple from ARPose.
|
|
50
|
+
*/
|
|
51
|
+
function extractOdomRotation(arPose) {
|
|
52
|
+
return [
|
|
53
|
+
arPose.orientation.x,
|
|
54
|
+
arPose.orientation.y,
|
|
55
|
+
arPose.orientation.z,
|
|
56
|
+
arPose.orientation.w
|
|
57
|
+
];
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Build a RawGpsPoint from GPS position data.
|
|
61
|
+
* Returns only raw sensor fields — no derived fields (coordinates, weight,
|
|
62
|
+
* zeroRef, deviceRotation). Derived fields are computed by the library
|
|
63
|
+
* reducer when the action is dispatched (raw-storage pattern).
|
|
64
|
+
*
|
|
65
|
+
* @param gpsPosition - GPS position from Geolocation API
|
|
66
|
+
* @param deviceOrientation - Optional device orientation from sensors
|
|
67
|
+
* @returns RawGpsPoint ready for the action payload
|
|
68
|
+
*/
|
|
69
|
+
function buildRawGpsPoint(gpsPosition, deviceOrientation) {
|
|
70
|
+
const compassAbsolute = deviceOrientation ? deviceOrientation.absolute : void 0;
|
|
71
|
+
return {
|
|
72
|
+
id: `gps-${++gpsEventCounter}`,
|
|
73
|
+
latitude: gpsPosition.lat,
|
|
74
|
+
longitude: gpsPosition.lon,
|
|
75
|
+
altitude: gpsPosition.altitude ?? void 0,
|
|
76
|
+
latLongAccuracy: gpsPosition.accuracy,
|
|
77
|
+
altitudeAccuracy: gpsPosition.altitudeAccuracy ?? void 0,
|
|
78
|
+
heading: gpsPosition.heading ?? void 0,
|
|
79
|
+
speed: gpsPosition.speed ?? void 0,
|
|
80
|
+
compassAbsolute,
|
|
81
|
+
timestamp: gpsPosition.timestamp
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Build a RecordGpsEventPayload from GPS position and AR pose.
|
|
86
|
+
* This is a pure function for testability.
|
|
87
|
+
*
|
|
88
|
+
* Stores only raw sensor data in the payload. The library reducer
|
|
89
|
+
* computes derived fields (coordinates, weight, deviceRotation)
|
|
90
|
+
* when the action is processed (raw-storage pattern).
|
|
91
|
+
*
|
|
92
|
+
* @param gpsPosition - GPS position from Geolocation API
|
|
93
|
+
* @param arPose - AR pose from WebXR
|
|
94
|
+
* @param deviceOrientation - Optional device orientation from sensors
|
|
95
|
+
* @returns RecordGpsEventPayload ready for dispatch
|
|
96
|
+
*/
|
|
97
|
+
function buildRecordGpsEventPayload(gpsPosition, arPose, deviceOrientation) {
|
|
98
|
+
const rawDeviceOrientation = deviceOrientation?.alpha != null && deviceOrientation?.beta != null && deviceOrientation?.gamma != null ? {
|
|
99
|
+
alpha: deviceOrientation.alpha,
|
|
100
|
+
beta: deviceOrientation.beta,
|
|
101
|
+
gamma: deviceOrientation.gamma,
|
|
102
|
+
absolute: deviceOrientation.absolute
|
|
103
|
+
} : void 0;
|
|
104
|
+
return {
|
|
105
|
+
odomPosition: extractOdomPosition(arPose),
|
|
106
|
+
odomRotation: extractOdomRotation(arPose),
|
|
107
|
+
rawGpsPoint: buildRawGpsPoint(gpsPosition, deviceOrientation),
|
|
108
|
+
rawDeviceOrientation
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Create a GPS position handler that dispatches combined GPS+AR events.
|
|
113
|
+
*
|
|
114
|
+
* This is the CRITICAL function that implements the correct architecture:
|
|
115
|
+
* - Called when GPS arrives
|
|
116
|
+
* - Reads current AR pose
|
|
117
|
+
* - Dispatches single combined action
|
|
118
|
+
*
|
|
119
|
+
* @param config - Configuration with store and getArPose function
|
|
120
|
+
* @returns Callback function for GPS watch
|
|
121
|
+
*/
|
|
122
|
+
function createGpsPositionHandler(config) {
|
|
123
|
+
const { store, getArPose } = config;
|
|
124
|
+
return (position) => {
|
|
125
|
+
if (!store.getState().recorder.isRecording) return;
|
|
126
|
+
const arPose = getArPose();
|
|
127
|
+
if (!arPose) {
|
|
128
|
+
log.warn("GPS arrived but no AR pose available");
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (!store.getState().gpsData) {
|
|
132
|
+
const zeroRef = {
|
|
133
|
+
lat: position.lat,
|
|
134
|
+
lon: position.lon
|
|
135
|
+
};
|
|
136
|
+
store.dispatch(setZeroPos(zeroRef));
|
|
137
|
+
log.info(`Set zero reference: ${zeroRef.lat}, ${zeroRef.lon}`);
|
|
138
|
+
}
|
|
139
|
+
if (!getZeroReference(store.getState())) {
|
|
140
|
+
log.error("Failed to get zero reference");
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const payload = buildRecordGpsEventPayload(position, arPose, lastDeviceOrientation);
|
|
144
|
+
store.dispatch(recordGpsEvent(payload));
|
|
145
|
+
const eventCount = getOdometryPositions(store.getState()).length;
|
|
146
|
+
log.info(`Recorded GPS event #${eventCount}`);
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
//#endregion
|
|
150
|
+
export { buildRawGpsPoint, buildRecordGpsEventPayload, createGpsPositionHandler, eulerToQuaternion, extractOdomPosition, extractOdomRotation, getLastDeviceOrientation, resetCoordinatorState, updateDeviceOrientation };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { _ as validateRecordingOptions, a as IMAGE_CONSTRAINTS, c as RecordingOptionsInput, d as loadRecordingOptions, f as resetRecordingOptions, g as validateImageOptions, h as validateDepthOptions, i as DepthCaptureOptions, l as STORAGE_KEY, m as validateArCrashIsolationOptions, n as DEFAULT_RECORDING_OPTIONS, o as ImageCaptureOptions, p as saveRecordingOptions, r as DEPTH_CONSTRAINTS, s as RecordingOptions, t as ArCrashIsolationOptions, u as cloneRecordingOptions } from "../recording-options-BYUFX8iU.js";
|
|
2
|
+
export { ArCrashIsolationOptions, DEFAULT_RECORDING_OPTIONS, DEPTH_CONSTRAINTS, DepthCaptureOptions, IMAGE_CONSTRAINTS, ImageCaptureOptions, RecordingOptions, RecordingOptionsInput, STORAGE_KEY, cloneRecordingOptions, loadRecordingOptions, resetRecordingOptions, saveRecordingOptions, validateArCrashIsolationOptions, validateDepthOptions, validateImageOptions, validateRecordingOptions };
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { createLogger } from "../utils/logger.js";
|
|
2
|
+
//#region ../src/state/recording-options.ts
|
|
3
|
+
/**
|
|
4
|
+
* Recording Options - User-configurable settings for data capture.
|
|
5
|
+
*
|
|
6
|
+
* Allows users to disable/configure high-frequency data streams
|
|
7
|
+
* (depth sampling, image capture) to improve performance on lower-end devices.
|
|
8
|
+
*
|
|
9
|
+
* Options persist in localStorage across sessions.
|
|
10
|
+
*/
|
|
11
|
+
const log = createLogger("RecordingOptions");
|
|
12
|
+
/**
|
|
13
|
+
* localStorage key for persisted options.
|
|
14
|
+
*
|
|
15
|
+
* **Multi-tab caveat:** All tabs/instances sharing the same origin will
|
|
16
|
+
* read and write this key. In multi-tab or embedded scenarios, concurrent
|
|
17
|
+
* saves can silently overwrite each other. Use a custom `storageKey`
|
|
18
|
+
* parameter in `loadRecordingOptions` / `saveRecordingOptions` to isolate
|
|
19
|
+
* instances when needed.
|
|
20
|
+
*/
|
|
21
|
+
const STORAGE_KEY = "gps-plus-slam-recorder-options";
|
|
22
|
+
/** Default recording options (all streams enabled) */
|
|
23
|
+
const DEFAULT_RECORDING_OPTIONS = {
|
|
24
|
+
depth: {
|
|
25
|
+
enabled: true,
|
|
26
|
+
intervalMs: 1e3,
|
|
27
|
+
gridSize: 3
|
|
28
|
+
},
|
|
29
|
+
images: {
|
|
30
|
+
enabled: true,
|
|
31
|
+
intervalMs: 2e3,
|
|
32
|
+
quality: .7,
|
|
33
|
+
resolutionDivisor: 1
|
|
34
|
+
},
|
|
35
|
+
arCrashIsolation: {
|
|
36
|
+
enableDomOverlay: true,
|
|
37
|
+
enableCameraAccess: true,
|
|
38
|
+
enableDepthSensingFeature: true,
|
|
39
|
+
enableCss3dRenderer: true,
|
|
40
|
+
enableCameraTextureAcquisition: true,
|
|
41
|
+
applyChromiumProjectionLayerWorkaround: true
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
/** Validation constraints for depth options */
|
|
45
|
+
const DEPTH_CONSTRAINTS = {
|
|
46
|
+
intervalMs: {
|
|
47
|
+
min: 500,
|
|
48
|
+
max: 5e3,
|
|
49
|
+
step: 100
|
|
50
|
+
},
|
|
51
|
+
gridSize: {
|
|
52
|
+
min: 2,
|
|
53
|
+
max: 10,
|
|
54
|
+
step: 1
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
/** Validation constraints for image options */
|
|
58
|
+
const IMAGE_CONSTRAINTS = {
|
|
59
|
+
intervalMs: {
|
|
60
|
+
min: 1e3,
|
|
61
|
+
max: 1e4,
|
|
62
|
+
step: 500
|
|
63
|
+
},
|
|
64
|
+
quality: {
|
|
65
|
+
min: .3,
|
|
66
|
+
max: 1,
|
|
67
|
+
step: .1
|
|
68
|
+
},
|
|
69
|
+
resolutionDivisor: {
|
|
70
|
+
min: 1,
|
|
71
|
+
max: 8,
|
|
72
|
+
step: 1
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Validate and normalize AR crash isolation flags.
|
|
77
|
+
* Missing or invalid values fall back to defaults.
|
|
78
|
+
*/
|
|
79
|
+
function validateArCrashIsolationOptions(options) {
|
|
80
|
+
const defaults = DEFAULT_RECORDING_OPTIONS.arCrashIsolation;
|
|
81
|
+
return {
|
|
82
|
+
enableDomOverlay: typeof options.enableDomOverlay === "boolean" ? options.enableDomOverlay : defaults.enableDomOverlay,
|
|
83
|
+
enableCameraAccess: typeof options.enableCameraAccess === "boolean" ? options.enableCameraAccess : defaults.enableCameraAccess,
|
|
84
|
+
enableDepthSensingFeature: typeof options.enableDepthSensingFeature === "boolean" ? options.enableDepthSensingFeature : defaults.enableDepthSensingFeature,
|
|
85
|
+
enableCss3dRenderer: typeof options.enableCss3dRenderer === "boolean" ? options.enableCss3dRenderer : defaults.enableCss3dRenderer,
|
|
86
|
+
enableCameraTextureAcquisition: typeof options.enableCameraTextureAcquisition === "boolean" ? options.enableCameraTextureAcquisition : defaults.enableCameraTextureAcquisition,
|
|
87
|
+
applyChromiumProjectionLayerWorkaround: typeof options.applyChromiumProjectionLayerWorkaround === "boolean" ? options.applyChromiumProjectionLayerWorkaround : defaults.applyChromiumProjectionLayerWorkaround
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Clamp a value to the specified constraints.
|
|
92
|
+
*/
|
|
93
|
+
function clamp(value, min, max) {
|
|
94
|
+
return Math.max(min, Math.min(max, value));
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Validate and normalize depth options.
|
|
98
|
+
* Invalid values are clamped to valid ranges.
|
|
99
|
+
*/
|
|
100
|
+
function validateDepthOptions(options) {
|
|
101
|
+
const defaults = DEFAULT_RECORDING_OPTIONS.depth;
|
|
102
|
+
return {
|
|
103
|
+
enabled: typeof options.enabled === "boolean" ? options.enabled : defaults.enabled,
|
|
104
|
+
intervalMs: clamp(typeof options.intervalMs === "number" ? options.intervalMs : defaults.intervalMs, DEPTH_CONSTRAINTS.intervalMs.min, DEPTH_CONSTRAINTS.intervalMs.max),
|
|
105
|
+
gridSize: clamp(typeof options.gridSize === "number" ? options.gridSize : defaults.gridSize, DEPTH_CONSTRAINTS.gridSize.min, DEPTH_CONSTRAINTS.gridSize.max)
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Validate and normalize image options.
|
|
110
|
+
* Invalid values are clamped to valid ranges.
|
|
111
|
+
*/
|
|
112
|
+
function validateImageOptions(options) {
|
|
113
|
+
const defaults = DEFAULT_RECORDING_OPTIONS.images;
|
|
114
|
+
return {
|
|
115
|
+
enabled: typeof options.enabled === "boolean" ? options.enabled : defaults.enabled,
|
|
116
|
+
intervalMs: clamp(typeof options.intervalMs === "number" ? options.intervalMs : defaults.intervalMs, IMAGE_CONSTRAINTS.intervalMs.min, IMAGE_CONSTRAINTS.intervalMs.max),
|
|
117
|
+
quality: clamp(typeof options.quality === "number" ? options.quality : defaults.quality, IMAGE_CONSTRAINTS.quality.min, IMAGE_CONSTRAINTS.quality.max),
|
|
118
|
+
resolutionDivisor: clamp(typeof options.resolutionDivisor === "number" ? options.resolutionDivisor : defaults.resolutionDivisor, IMAGE_CONSTRAINTS.resolutionDivisor.min, IMAGE_CONSTRAINTS.resolutionDivisor.max)
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Validate and normalize a full RecordingOptions object.
|
|
123
|
+
* Merges with defaults and clamps invalid values.
|
|
124
|
+
*/
|
|
125
|
+
function validateRecordingOptions(options) {
|
|
126
|
+
return {
|
|
127
|
+
depth: validateDepthOptions(options.depth ?? {}),
|
|
128
|
+
images: validateImageOptions(options.images ?? {}),
|
|
129
|
+
arCrashIsolation: validateArCrashIsolationOptions(options.arCrashIsolation ?? {})
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Load recording options from localStorage.
|
|
134
|
+
* Returns defaults if no saved options exist or parsing fails.
|
|
135
|
+
* Validates and merges with defaults to handle schema evolution.
|
|
136
|
+
* @param storageKey - Optional custom localStorage key (defaults to STORAGE_KEY).
|
|
137
|
+
*/
|
|
138
|
+
function loadRecordingOptions(storageKey = STORAGE_KEY) {
|
|
139
|
+
try {
|
|
140
|
+
const stored = localStorage.getItem(storageKey);
|
|
141
|
+
if (stored) {
|
|
142
|
+
const parsed = JSON.parse(stored);
|
|
143
|
+
log.debug("Loaded options from storage:", parsed);
|
|
144
|
+
return validateRecordingOptions(parsed);
|
|
145
|
+
}
|
|
146
|
+
} catch (err) {
|
|
147
|
+
log.warn("Failed to load recording options:", err);
|
|
148
|
+
}
|
|
149
|
+
log.debug("Using default recording options");
|
|
150
|
+
return cloneRecordingOptions(DEFAULT_RECORDING_OPTIONS);
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Save recording options to localStorage.
|
|
154
|
+
* Options are validated before saving.
|
|
155
|
+
* @param storageKey - Optional custom localStorage key (defaults to STORAGE_KEY).
|
|
156
|
+
*/
|
|
157
|
+
function saveRecordingOptions(options, storageKey = STORAGE_KEY) {
|
|
158
|
+
try {
|
|
159
|
+
const validated = validateRecordingOptions(options);
|
|
160
|
+
localStorage.setItem(storageKey, JSON.stringify(validated));
|
|
161
|
+
log.debug("Saved recording options:", validated);
|
|
162
|
+
} catch (err) {
|
|
163
|
+
log.warn("Failed to save recording options:", err);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Reset recording options to defaults.
|
|
168
|
+
* Clears localStorage and returns default options.
|
|
169
|
+
* @param storageKey - Optional custom localStorage key (defaults to STORAGE_KEY).
|
|
170
|
+
*/
|
|
171
|
+
function resetRecordingOptions(storageKey = STORAGE_KEY) {
|
|
172
|
+
try {
|
|
173
|
+
localStorage.removeItem(storageKey);
|
|
174
|
+
log.debug("Reset recording options to defaults");
|
|
175
|
+
} catch (err) {
|
|
176
|
+
log.warn("Failed to clear recording options from storage:", err);
|
|
177
|
+
}
|
|
178
|
+
return cloneRecordingOptions(DEFAULT_RECORDING_OPTIONS);
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Create a deep copy of recording options.
|
|
182
|
+
* Useful for creating mutable copies of the frozen defaults.
|
|
183
|
+
*/
|
|
184
|
+
function cloneRecordingOptions(options) {
|
|
185
|
+
return {
|
|
186
|
+
depth: { ...options.depth },
|
|
187
|
+
images: { ...options.images },
|
|
188
|
+
arCrashIsolation: { ...options.arCrashIsolation }
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
//#endregion
|
|
192
|
+
export { DEFAULT_RECORDING_OPTIONS, DEPTH_CONSTRAINTS, IMAGE_CONSTRAINTS, STORAGE_KEY, cloneRecordingOptions, loadRecordingOptions, resetRecordingOptions, saveRecordingOptions, validateArCrashIsolationOptions, validateDepthOptions, validateImageOptions, validateRecordingOptions };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { createRecorderStore } from "./store.js";
|
|
2
|
+
import { loadActionsFromZip } from "../storage/zip-reader.js";
|
|
3
|
+
import { NullStorageBackend } from "../storage/null-storage-backend.js";
|
|
4
|
+
//#region ../src/state/recording-replayer.ts
|
|
5
|
+
/**
|
|
6
|
+
* Recording Replayer
|
|
7
|
+
*
|
|
8
|
+
* Replays a recorded session from a zip file into a fresh store,
|
|
9
|
+
* producing the fully-computed CombinedRootState without any persistence
|
|
10
|
+
* side effects.
|
|
11
|
+
*
|
|
12
|
+
* This is the primary entry point for loading recordings for
|
|
13
|
+
* visualization, comparison, validation, or offline analysis.
|
|
14
|
+
*
|
|
15
|
+
* Uses NullStorageBackend to ensure no OPFS writes occur during replay.
|
|
16
|
+
*
|
|
17
|
+
* See also: Finding F5 in docs/2026-02-15-replay-integration-test-review.md
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Replay a recording session from zip data, returning the final state.
|
|
21
|
+
*
|
|
22
|
+
* Loads all actions from the zip, creates a store with no persistence,
|
|
23
|
+
* optionally migrates old-format actions, dispatches in order, and returns
|
|
24
|
+
* the resulting state.
|
|
25
|
+
*
|
|
26
|
+
* @param zipData - The zip file content as a Uint8Array
|
|
27
|
+
* @param options - Optional replay configuration (e.g., action migration)
|
|
28
|
+
* @returns The fully-replayed combined state (library + recorder)
|
|
29
|
+
* @throws If the zip cannot be parsed or contains invalid data
|
|
30
|
+
*/
|
|
31
|
+
async function replayRecording(zipData, options) {
|
|
32
|
+
const store = createRecorderStore({ storageBackend: new NullStorageBackend() });
|
|
33
|
+
let actions = (await loadActionsFromZip(zipData)).map((e) => e.action);
|
|
34
|
+
if (options?.migrateActions) actions = options.migrateActions(actions);
|
|
35
|
+
for (const action of actions) store.dispatch(action);
|
|
36
|
+
return store.getState();
|
|
37
|
+
}
|
|
38
|
+
//#endregion
|
|
39
|
+
export { replayRecording };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { a as resetRefPointsState, i as refPointsReducer, n as clearSessionRefPointUsage, o as selectCachedKnownRefPoints, r as incrementRefPointUsage, s as setImportedRefPoints, t as RefPointsState } from "../ref-points-slice-tC-Wd6XJ.js";
|
|
2
|
+
export { RefPointsState, clearSessionRefPointUsage, incrementRefPointUsage, refPointsReducer, resetRefPointsState, selectCachedKnownRefPoints, setImportedRefPoints };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { gpsToH3 } from "../ref-points/h3-ref-point.js";
|
|
2
|
+
import { createSelector, createSlice } from "@reduxjs/toolkit";
|
|
3
|
+
//#region ../src/state/ref-points-slice.ts
|
|
4
|
+
const initialState = {
|
|
5
|
+
importedRefPoints: [],
|
|
6
|
+
sessionRefPointUsage: {}
|
|
7
|
+
};
|
|
8
|
+
const refPointsSlice = createSlice({
|
|
9
|
+
name: "refPoints",
|
|
10
|
+
initialState,
|
|
11
|
+
reducers: {
|
|
12
|
+
setImportedRefPoints(state, action) {
|
|
13
|
+
state.importedRefPoints = action.payload;
|
|
14
|
+
},
|
|
15
|
+
incrementRefPointUsage(state, action) {
|
|
16
|
+
const id = action.payload;
|
|
17
|
+
state.sessionRefPointUsage[id] = (state.sessionRefPointUsage[id] ?? 0) + 1;
|
|
18
|
+
},
|
|
19
|
+
clearSessionRefPointUsage(state) {
|
|
20
|
+
state.sessionRefPointUsage = {};
|
|
21
|
+
},
|
|
22
|
+
resetRefPointsState() {
|
|
23
|
+
return initialState;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
const { setImportedRefPoints, incrementRefPointUsage, clearSessionRefPointUsage, resetRefPointsState } = refPointsSlice.actions;
|
|
28
|
+
const refPointsReducer = refPointsSlice.reducer;
|
|
29
|
+
/**
|
|
30
|
+
* Memoized selector that derives KnownRefPoint[] (with H3 indices) from
|
|
31
|
+
* importedRefPoints. Replaces the closure-based `recomputeKnownRefPoints()`
|
|
32
|
+
* in ref-point-handlers.ts.
|
|
33
|
+
*
|
|
34
|
+
* Uses createSelector (reselect) for standard RTK memoization — recomputes
|
|
35
|
+
* only when the importedRefPoints array reference changes.
|
|
36
|
+
*/
|
|
37
|
+
const selectCachedKnownRefPoints = createSelector((state) => state.importedRefPoints, (importedRefPoints) => importedRefPoints.map((rp) => ({
|
|
38
|
+
h3Index: gpsToH3(rp.lat, rp.lon),
|
|
39
|
+
displayName: rp.name || rp.id,
|
|
40
|
+
lat: rp.lat,
|
|
41
|
+
lon: rp.lon
|
|
42
|
+
})));
|
|
43
|
+
//#endregion
|
|
44
|
+
export { clearSessionRefPointUsage, incrementRefPointUsage, refPointsReducer, resetRefPointsState, selectCachedKnownRefPoints, setImportedRefPoints };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { a as ReplayAction, c as computeInterActionDelay, i as ProgressCallback, l as extractActionTimestamp, n as DEFAULT_MAX_DELAY_MS, o as ReplayEngine, r as ErrorCallback, s as ReplayState, t as CompleteCallback } from "../replay-engine-DuX316ae.js";
|
|
2
|
+
export { CompleteCallback, DEFAULT_MAX_DELAY_MS, ErrorCallback, ProgressCallback, ReplayAction, ReplayEngine, ReplayState, computeInterActionDelay, extractActionTimestamp };
|