gps-plus-slam-app-framework 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (200) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +222 -0
  3. package/dist/alignment-lerper-D9BeDwZX.d.ts +29 -0
  4. package/dist/app-selectors-DyMzxCEC.d.ts +140 -0
  5. package/dist/ar/camera-blit-capture.d.ts +2 -0
  6. package/dist/ar/camera-blit-capture.js +253 -0
  7. package/dist/ar/capture-failure-tracker.d.ts +2 -0
  8. package/dist/ar/capture-failure-tracker.js +45 -0
  9. package/dist/ar/chromium-camera-access-workaround.d.ts +2 -0
  10. package/dist/ar/chromium-camera-access-workaround.js +32 -0
  11. package/dist/ar/depth-sampler.d.ts +3 -0
  12. package/dist/ar/depth-sampler.js +147 -0
  13. package/dist/ar/image-capture.d.ts +2 -0
  14. package/dist/ar/image-capture.js +152 -0
  15. package/dist/ar/index.d.ts +12 -0
  16. package/dist/ar/index.js +12 -0
  17. package/dist/ar/replay-scene.d.ts +2 -0
  18. package/dist/ar/replay-scene.js +325 -0
  19. package/dist/ar/scene-node-names.d.ts +2 -0
  20. package/dist/ar/scene-node-names.js +16 -0
  21. package/dist/ar/tracking-state.d.ts +2 -0
  22. package/dist/ar/tracking-state.js +164 -0
  23. package/dist/ar/webxr-session.d.ts +4 -0
  24. package/dist/ar/webxr-session.js +890 -0
  25. package/dist/ar/xr-camera-texture.d.ts +2 -0
  26. package/dist/ar/xr-camera-texture.js +39 -0
  27. package/dist/ar/xr-error-handler.d.ts +2 -0
  28. package/dist/ar/xr-error-handler.js +37 -0
  29. package/dist/ar-types-B-ORgk6Z.d.ts +82 -0
  30. package/dist/camera-blit-capture-DHuyrrxr.d.ts +108 -0
  31. package/dist/camera-follower-BMqPddxx.d.ts +29 -0
  32. package/dist/capture-failure-tracker-2Q6oI2uQ.d.ts +52 -0
  33. package/dist/chromium-camera-access-workaround-CU5zSKNr.d.ts +61 -0
  34. package/dist/community-license-key-ChZLCJyO.d.ts +21 -0
  35. package/dist/concurrency-p6gB30fT.d.ts +24 -0
  36. package/dist/css3d-renderer-manager-rgLMVAvd.d.ts +23 -0
  37. package/dist/depth-sampler-BNCtnEoP.d.ts +108 -0
  38. package/dist/failure-tracker-1sQ_Cgyw.d.ts +49 -0
  39. package/dist/file-system-BsQgYCU3.d.ts +128 -0
  40. package/dist/file-system-utils-5cWO8Xyv.d.ts +24 -0
  41. package/dist/format-file-size-onGq0k1Q.d.ts +24 -0
  42. package/dist/fused-path-DtqCSqJh.d.ts +25 -0
  43. package/dist/geo-types-Xx-LVcnw.d.ts +29 -0
  44. package/dist/gps-Ddzr2KlX.d.ts +54 -0
  45. package/dist/gps-compass-cubes-C-6JEnpR.d.ts +22 -0
  46. package/dist/gps-error-handler-VPuNOuf8.d.ts +40 -0
  47. package/dist/gps-event-markers-DpEy2qRd.d.ts +67 -0
  48. package/dist/h3-ref-point-CFhayNSZ.d.ts +59 -0
  49. package/dist/image-capture-2pKjYUS6.d.ts +130 -0
  50. package/dist/index.d.ts +56 -0
  51. package/dist/index.js +61 -0
  52. package/dist/leaflet-map-overlay-CvfQXTye.d.ts +114 -0
  53. package/dist/lerp-utils-87NWjUOD.d.ts +20 -0
  54. package/dist/licensing/community-license-key.d.ts +2 -0
  55. package/dist/licensing/community-license-key.js +21 -0
  56. package/dist/licensing/index.d.ts +2 -0
  57. package/dist/licensing/index.js +2 -0
  58. package/dist/list-formatter-DKQQVLcl.d.ts +14 -0
  59. package/dist/logger-B81iwxx0.d.ts +74 -0
  60. package/dist/map-overlay-DuiZuCTx.d.ts +153 -0
  61. package/dist/null-storage-backend-CsWvQwN8.d.ts +11 -0
  62. package/dist/opfs-storage-ChOzF_Wp.d.ts +181 -0
  63. package/dist/opfs-storage-backend-CVcmfJHN.d.ts +11 -0
  64. package/dist/permission-checker-jO2ziCWC.d.ts +146 -0
  65. package/dist/persistence-middleware-DrptY02i.d.ts +31 -0
  66. package/dist/recorder-slice-fcwAXrsh.d.ts +35 -0
  67. package/dist/recording-coordinator-DzflP07_.d.ts +81 -0
  68. package/dist/recording-options-BYUFX8iU.d.ts +166 -0
  69. package/dist/recording-replayer-oOzPITrd.d.ts +32 -0
  70. package/dist/ref-point-importer-B_wn5ABk.d.ts +62 -0
  71. package/dist/ref-point-loader-Pt1smgHS.d.ts +117 -0
  72. package/dist/ref-points/h3-ref-point.d.ts +2 -0
  73. package/dist/ref-points/h3-ref-point.js +74 -0
  74. package/dist/ref-points/index.d.ts +2 -0
  75. package/dist/ref-points/index.js +2 -0
  76. package/dist/ref-points-slice-tC-Wd6XJ.d.ts +48 -0
  77. package/dist/reference-points-CFWy8_wj.d.ts +68 -0
  78. package/dist/replay-engine-DuX316ae.d.ts +103 -0
  79. package/dist/replay-scene-DtUm6bsr.d.ts +109 -0
  80. package/dist/routing-slice-BlHTM8eh.d.ts +23 -0
  81. package/dist/scene-node-names-DPsaE2z3.d.ts +14 -0
  82. package/dist/sensors/gps-error-handler.d.ts +2 -0
  83. package/dist/sensors/gps-error-handler.js +53 -0
  84. package/dist/sensors/gps.d.ts +2 -0
  85. package/dist/sensors/gps.js +93 -0
  86. package/dist/sensors/index.d.ts +4 -0
  87. package/dist/sensors/index.js +4 -0
  88. package/dist/sensors/permission-checker.d.ts +2 -0
  89. package/dist/sensors/permission-checker.js +397 -0
  90. package/dist/state/app-selectors.d.ts +2 -0
  91. package/dist/state/app-selectors.js +35 -0
  92. package/dist/state/index.d.ts +17 -0
  93. package/dist/state/index.js +13 -0
  94. package/dist/state/persistence-middleware.d.ts +2 -0
  95. package/dist/state/persistence-middleware.js +71 -0
  96. package/dist/state/recorder-slice.d.ts +2 -0
  97. package/dist/state/recorder-slice.js +47 -0
  98. package/dist/state/recording-coordinator.d.ts +2 -0
  99. package/dist/state/recording-coordinator.js +150 -0
  100. package/dist/state/recording-options.d.ts +2 -0
  101. package/dist/state/recording-options.js +192 -0
  102. package/dist/state/recording-replayer.d.ts +2 -0
  103. package/dist/state/recording-replayer.js +39 -0
  104. package/dist/state/ref-points-slice.d.ts +2 -0
  105. package/dist/state/ref-points-slice.js +44 -0
  106. package/dist/state/replay-engine.d.ts +2 -0
  107. package/dist/state/replay-engine.js +230 -0
  108. package/dist/state/routing-slice.d.ts +2 -0
  109. package/dist/state/routing-slice.js +13 -0
  110. package/dist/state/store-subscribers.d.ts +3 -0
  111. package/dist/state/store-subscribers.js +97 -0
  112. package/dist/state/store.d.ts +9 -0
  113. package/dist/state/store.js +71 -0
  114. package/dist/state/subscribe-to-selector.d.ts +2 -0
  115. package/dist/state/subscribe-to-selector.js +26 -0
  116. package/dist/storage/file-system-utils.d.ts +2 -0
  117. package/dist/storage/file-system-utils.js +31 -0
  118. package/dist/storage/file-system.d.ts +2 -0
  119. package/dist/storage/file-system.js +229 -0
  120. package/dist/storage/index.d.ts +11 -0
  121. package/dist/storage/index.js +10 -0
  122. package/dist/storage/null-storage-backend.d.ts +2 -0
  123. package/dist/storage/null-storage-backend.js +8 -0
  124. package/dist/storage/opfs-storage-backend.d.ts +2 -0
  125. package/dist/storage/opfs-storage-backend.js +15 -0
  126. package/dist/storage/opfs-storage.d.ts +2 -0
  127. package/dist/storage/opfs-storage.js +242 -0
  128. package/dist/storage/ref-point-importer.d.ts +2 -0
  129. package/dist/storage/ref-point-importer.js +163 -0
  130. package/dist/storage/ref-point-loader.d.ts +2 -0
  131. package/dist/storage/ref-point-loader.js +248 -0
  132. package/dist/storage/ref-point-recovery.d.ts +28 -0
  133. package/dist/storage/ref-point-recovery.js +159 -0
  134. package/dist/storage/storage-backend.d.ts +2 -0
  135. package/dist/storage/storage-backend.js +1 -0
  136. package/dist/storage/zip-export.d.ts +2 -0
  137. package/dist/storage/zip-export.js +214 -0
  138. package/dist/storage/zip-reader.d.ts +2 -0
  139. package/dist/storage/zip-reader.js +190 -0
  140. package/dist/storage-backend-BcEItnFr.d.ts +20 -0
  141. package/dist/store-C7PKOj8S.d.ts +96 -0
  142. package/dist/store-subscribers-CuoIzB5T.d.ts +77 -0
  143. package/dist/subscribe-to-selector-UTZ0KkAc.d.ts +26 -0
  144. package/dist/test-utils/browser-mocks.d.ts +294 -0
  145. package/dist/test-utils/browser-mocks.js +568 -0
  146. package/dist/test-utils/zip-round-trip-helpers.d.ts +60 -0
  147. package/dist/test-utils/zip-round-trip-helpers.js +207 -0
  148. package/dist/three-dispose-CCBHjE_a.d.ts +30 -0
  149. package/dist/tracking-state-B9QfuZCg.d.ts +101 -0
  150. package/dist/types/ar-types.d.ts +2 -0
  151. package/dist/types/ar-types.js +1 -0
  152. package/dist/types/geo-types.d.ts +2 -0
  153. package/dist/types/geo-types.js +1 -0
  154. package/dist/types/index.d.ts +3 -0
  155. package/dist/types/index.js +1 -0
  156. package/dist/utils/concurrency.d.ts +2 -0
  157. package/dist/utils/concurrency.js +40 -0
  158. package/dist/utils/failure-tracker.d.ts +2 -0
  159. package/dist/utils/failure-tracker.js +48 -0
  160. package/dist/utils/format-file-size.d.ts +2 -0
  161. package/dist/utils/format-file-size.js +41 -0
  162. package/dist/utils/fused-path.d.ts +2 -0
  163. package/dist/utils/fused-path.js +44 -0
  164. package/dist/utils/index.d.ts +7 -0
  165. package/dist/utils/index.js +7 -0
  166. package/dist/utils/list-formatter.d.ts +2 -0
  167. package/dist/utils/list-formatter.js +17 -0
  168. package/dist/utils/logger.d.ts +2 -0
  169. package/dist/utils/logger.js +226 -0
  170. package/dist/vis-colors-Qi2vlRGt.d.ts +63 -0
  171. package/dist/visualization/alignment-lerper.d.ts +2 -0
  172. package/dist/visualization/alignment-lerper.js +68 -0
  173. package/dist/visualization/camera-follower.d.ts +2 -0
  174. package/dist/visualization/camera-follower.js +51 -0
  175. package/dist/visualization/css3d-renderer-manager.d.ts +2 -0
  176. package/dist/visualization/css3d-renderer-manager.js +41 -0
  177. package/dist/visualization/gps-compass-cubes.d.ts +2 -0
  178. package/dist/visualization/gps-compass-cubes.js +129 -0
  179. package/dist/visualization/gps-event-markers.d.ts +2 -0
  180. package/dist/visualization/gps-event-markers.js +159 -0
  181. package/dist/visualization/index.d.ts +12 -0
  182. package/dist/visualization/index.js +12 -0
  183. package/dist/visualization/leaflet-map-overlay.d.ts +2 -0
  184. package/dist/visualization/leaflet-map-overlay.js +313 -0
  185. package/dist/visualization/lerp-utils.d.ts +2 -0
  186. package/dist/visualization/lerp-utils.js +22 -0
  187. package/dist/visualization/map-overlay.d.ts +2 -0
  188. package/dist/visualization/map-overlay.js +241 -0
  189. package/dist/visualization/reference-points.d.ts +2 -0
  190. package/dist/visualization/reference-points.js +160 -0
  191. package/dist/visualization/three-dispose.d.ts +2 -0
  192. package/dist/visualization/three-dispose.js +55 -0
  193. package/dist/visualization/vis-colors.d.ts +2 -0
  194. package/dist/visualization/vis-colors.js +74 -0
  195. package/dist/webxr-session-COlSrXdL.d.ts +316 -0
  196. package/dist/xr-camera-texture-BbukNE79.d.ts +53 -0
  197. package/dist/xr-error-handler-DTHfmvrl.d.ts +24 -0
  198. package/dist/zip-export-Rtpi75JJ.d.ts +87 -0
  199. package/dist/zip-reader-D7pkpX8K.d.ts +97 -0
  200. package/package.json +104 -0
@@ -0,0 +1,253 @@
1
+ import { createLogger } from "../utils/logger.js";
2
+ import { disposeObject3D } from "../visualization/three-dispose.js";
3
+ import * as THREE from "three";
4
+ //#region ../src/ar/camera-blit-capture.ts
5
+ /**
6
+ * Camera Blit Capture Module
7
+ *
8
+ * Implements the "blit" technique to read WebXR opaque camera textures.
9
+ *
10
+ * Problem: In WebXR AR mode, the camera texture provided by
11
+ * renderer.xr.getCameraTexture() is an "opaque texture" backed by a
12
+ * protected GPU buffer (OES_external_image). Standard canvas.toBlob()
13
+ * or gl.readPixels() return all-black data.
14
+ *
15
+ * Solution: Render the opaque texture onto a fullscreen quad targeting
16
+ * an intermediate WebGLRenderTarget (standard RGBA). This GPU draw call
17
+ * forces conversion from the opaque format to a readable texture.
18
+ * Then readRenderTargetPixels() successfully extracts the pixel data.
19
+ *
20
+ * See: docs/2026-02-06-bug-camera-frames-black.md
21
+ */
22
+ const log = createLogger("CameraBlitCapture");
23
+ /**
24
+ * Default blit capture config.
25
+ * 512×512 is a good balance between quality and readPixels performance on mobile.
26
+ */
27
+ const DEFAULT_BLIT_CONFIG = {
28
+ width: 512,
29
+ height: 512
30
+ };
31
+ /**
32
+ * Compute the capture dimensions from the native camera resolution
33
+ * and a user-configurable resolution divisor.
34
+ *
35
+ * @param cameraWidth - Native camera width in pixels (from XRCamera)
36
+ * @param cameraHeight - Native camera height in pixels (from XRCamera)
37
+ * @param divisor - Resolution divisor: 1 = full, 2 = half, 4 = quarter, etc.
38
+ * Values ≤ 0 are treated as 1. Fractional values < 1 are treated as 1.
39
+ * @returns Integer pixel dimensions, clamped to at least 1×1.
40
+ * Falls back to DEFAULT_BLIT_CONFIG when inputs are invalid (≤ 0).
41
+ */
42
+ function computeCaptureSize(cameraWidth, cameraHeight, divisor) {
43
+ if (cameraWidth <= 0 || cameraHeight <= 0) return {
44
+ width: DEFAULT_BLIT_CONFIG.width,
45
+ height: DEFAULT_BLIT_CONFIG.height
46
+ };
47
+ const safeDivisor = divisor >= 1 ? divisor : 1;
48
+ return {
49
+ width: Math.max(1, Math.floor(cameraWidth / safeDivisor)),
50
+ height: Math.max(1, Math.floor(cameraHeight / safeDivisor))
51
+ };
52
+ }
53
+ /**
54
+ * Number of sampled pixels to check in isBlackFrame.
55
+ * We sample a subset rather than checking every pixel for speed.
56
+ */
57
+ const BLACK_CHECK_SAMPLE_COUNT = 100;
58
+ /**
59
+ * Captures readable pixel data from WebXR opaque camera textures
60
+ * using the intermediate render target "blit" technique.
61
+ *
62
+ * Usage:
63
+ * 1. Create once when AR session starts
64
+ * 2. Call captureToBlob() when a frame needs to be captured
65
+ * 3. Call dispose() when AR session ends
66
+ */
67
+ var CameraBlitCapture = class {
68
+ renderTarget;
69
+ blitScene;
70
+ blitCamera;
71
+ blitMaterial;
72
+ quad;
73
+ pixelBuffer;
74
+ width;
75
+ height;
76
+ disposed = false;
77
+ constructor(config = DEFAULT_BLIT_CONFIG) {
78
+ this.width = config.width;
79
+ this.height = config.height;
80
+ this.renderTarget = new THREE.WebGLRenderTarget(this.width, this.height, {
81
+ format: THREE.RGBAFormat,
82
+ type: THREE.UnsignedByteType
83
+ });
84
+ this.pixelBuffer = new Uint8Array(this.width * this.height * 4);
85
+ this.blitScene = new THREE.Scene();
86
+ this.blitCamera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
87
+ this.blitMaterial = new THREE.ShaderMaterial({
88
+ uniforms: { tDiffuse: { value: null } },
89
+ vertexShader: `
90
+ varying vec2 vUv;
91
+ void main() {
92
+ vUv = uv;
93
+ gl_Position = vec4(position, 1.0);
94
+ }
95
+ `,
96
+ fragmentShader: `
97
+ uniform sampler2D tDiffuse;
98
+ varying vec2 vUv;
99
+ void main() {
100
+ gl_FragColor = texture2D(tDiffuse, vUv);
101
+ }
102
+ `,
103
+ depthTest: false,
104
+ depthWrite: false
105
+ });
106
+ this.quad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), this.blitMaterial);
107
+ this.blitScene.add(this.quad);
108
+ }
109
+ /**
110
+ * Capture the WebXR camera texture to a JPEG Blob via the blit technique.
111
+ *
112
+ * IMPORTANT: Must be called within the XR animation frame callback,
113
+ * while the camera texture is valid.
114
+ *
115
+ * @param renderer - The Three.js WebGL renderer
116
+ * @param cameraTexture - The opaque camera texture from renderer.xr.getCameraTexture()
117
+ * @param quality - JPEG quality 0.0-1.0
118
+ * @returns JPEG Blob, or null if capture fails
119
+ */
120
+ async captureToBlob(renderer, cameraTexture, quality) {
121
+ if (this.disposed) {
122
+ log.warn("captureToBlob called after dispose");
123
+ return null;
124
+ }
125
+ try {
126
+ this.blitMaterial.uniforms.tDiffuse.value = cameraTexture;
127
+ const currentRenderTarget = renderer.getRenderTarget();
128
+ const currentXrEnabled = renderer.xr.enabled;
129
+ renderer.xr.enabled = false;
130
+ renderer.setRenderTarget(this.renderTarget);
131
+ renderer.render(this.blitScene, this.blitCamera);
132
+ renderer.readRenderTargetPixels(this.renderTarget, 0, 0, this.width, this.height, this.pixelBuffer);
133
+ renderer.setRenderTarget(currentRenderTarget);
134
+ renderer.xr.enabled = currentXrEnabled;
135
+ this.blitMaterial.uniforms.tDiffuse.value = null;
136
+ return await this.pixelsToJpegBlob(quality);
137
+ } catch (error) {
138
+ log.error("Blit capture failed:", error);
139
+ return null;
140
+ }
141
+ }
142
+ /**
143
+ * Check if a pixel buffer is entirely black (all zeros).
144
+ * Uses sampling for performance (checks BLACK_CHECK_SAMPLE_COUNT evenly-spaced pixels).
145
+ *
146
+ * This is useful to detect if the blit technique failed to convert
147
+ * the opaque texture (different from a legitimately dark scene).
148
+ *
149
+ * @param pixels - RGBA pixel buffer
150
+ * @returns true if all sampled pixels are zero
151
+ */
152
+ static isBlackFrame(pixels) {
153
+ if (pixels.length === 0) return true;
154
+ const step = Math.max(1, Math.floor(pixels.length / (BLACK_CHECK_SAMPLE_COUNT * 4)));
155
+ for (let i = 0; i < pixels.length; i += step * 4) {
156
+ const r = pixels[i];
157
+ const g = pixels[i + 1];
158
+ const b = pixels[i + 2];
159
+ if (r > 0 || g > 0 || b > 0) return false;
160
+ }
161
+ return true;
162
+ }
163
+ /**
164
+ * Convert the internal pixel buffer to a JPEG Blob using OffscreenCanvas
165
+ * (or fallback to regular Canvas).
166
+ */
167
+ async pixelsToJpegBlob(quality) {
168
+ if (typeof OffscreenCanvas !== "undefined") return this.pixelsToJpegViaOffscreenCanvas(quality);
169
+ return this.pixelsToJpegViaCanvas(quality);
170
+ }
171
+ getFlippedImageData() {
172
+ const clampedBuffer = new Uint8ClampedArray(this.pixelBuffer);
173
+ const imageData = new ImageData(clampedBuffer, this.width, this.height);
174
+ this.flipImageDataVertically(imageData);
175
+ return imageData;
176
+ }
177
+ async pixelsToJpegViaOffscreenCanvas(quality) {
178
+ const offscreen = new OffscreenCanvas(this.width, this.height);
179
+ const ctx = offscreen.getContext("2d");
180
+ if (!ctx) return null;
181
+ const imageData = this.getFlippedImageData();
182
+ ctx.putImageData(imageData, 0, 0);
183
+ return offscreen.convertToBlob({
184
+ type: "image/jpeg",
185
+ quality
186
+ });
187
+ }
188
+ pixelsToJpegViaCanvas(quality) {
189
+ return new Promise((resolve) => {
190
+ const canvas = document.createElement("canvas");
191
+ canvas.width = this.width;
192
+ canvas.height = this.height;
193
+ const ctx = canvas.getContext("2d");
194
+ if (!ctx) {
195
+ resolve(null);
196
+ return;
197
+ }
198
+ const imageData = this.getFlippedImageData();
199
+ ctx.putImageData(imageData, 0, 0);
200
+ canvas.toBlob((blob) => resolve(blob), "image/jpeg", quality);
201
+ });
202
+ }
203
+ /**
204
+ * Flip ImageData vertically in-place.
205
+ * WebGL readPixels returns bottom-row-first; canvas expects top-row-first.
206
+ */
207
+ flipImageDataVertically(imageData) {
208
+ const { data, width, height } = imageData;
209
+ const rowSize = width * 4;
210
+ const tempRow = new Uint8ClampedArray(rowSize);
211
+ for (let y = 0; y < Math.floor(height / 2); y++) {
212
+ const topOffset = y * rowSize;
213
+ const bottomOffset = (height - 1 - y) * rowSize;
214
+ tempRow.set(data.subarray(topOffset, topOffset + rowSize));
215
+ data.copyWithin(topOffset, bottomOffset, bottomOffset + rowSize);
216
+ data.set(tempRow, bottomOffset);
217
+ }
218
+ }
219
+ /**
220
+ * Resize the render target and pixel buffer to match new dimensions.
221
+ * Call this when the camera resolution is first known or changes.
222
+ *
223
+ * No-op when dimensions already match (avoids per-frame reallocation).
224
+ * Returns false if disposed, dimensions unchanged, or dimensions invalid.
225
+ *
226
+ * @param newWidth - Desired capture width in pixels
227
+ * @param newHeight - Desired capture height in pixels
228
+ * @returns true if resources were actually resized
229
+ */
230
+ resizeIfNeeded(newWidth, newHeight) {
231
+ if (this.disposed) return false;
232
+ if (newWidth <= 0 || newHeight <= 0) return false;
233
+ if (newWidth === this.width && newHeight === this.height) return false;
234
+ this.width = newWidth;
235
+ this.height = newHeight;
236
+ this.renderTarget.setSize(this.width, this.height);
237
+ this.pixelBuffer = new Uint8Array(this.width * this.height * 4);
238
+ log.info(`Render target resized to ${this.width}×${this.height}`);
239
+ return true;
240
+ }
241
+ /**
242
+ * Dispose GPU resources. Call when AR session ends.
243
+ */
244
+ dispose() {
245
+ if (this.disposed) return;
246
+ this.disposed = true;
247
+ this.renderTarget.dispose();
248
+ disposeObject3D(this.quad);
249
+ log.info("CameraBlitCapture disposed");
250
+ }
251
+ };
252
+ //#endregion
253
+ export { CameraBlitCapture, DEFAULT_BLIT_CONFIG, computeCaptureSize };
@@ -0,0 +1,2 @@
1
+ import { a as createCaptureFailureTracker, i as DEFAULT_CAPTURE_TRACKER_CONFIG, n as CaptureFailureTracker, r as CaptureFailureTrackerConfig, t as CAPTURE_FAILURE_WARNING } from "../capture-failure-tracker-2Q6oI2uQ.js";
2
+ export { CAPTURE_FAILURE_WARNING, CaptureFailureTracker, CaptureFailureTrackerConfig, DEFAULT_CAPTURE_TRACKER_CONFIG, createCaptureFailureTracker };
@@ -0,0 +1,45 @@
1
+ import { createFailureTracker } from "../utils/failure-tracker.js";
2
+ //#region ../src/ar/capture-failure-tracker.ts
3
+ /**
4
+ * Capture Failure Tracker
5
+ *
6
+ * Tracks consecutive image capture failures and notifies users when
7
+ * multiple frames fail to capture (e.g., low memory on mobile devices).
8
+ *
9
+ * Field Test Readiness Issue #11: Silent image capture failures
10
+ *
11
+ * Thin wrapper around the generic failure tracker factory.
12
+ */
13
+ /**
14
+ * Default configuration values.
15
+ */
16
+ const DEFAULT_CAPTURE_TRACKER_CONFIG = { failureThreshold: 5 };
17
+ /**
18
+ * User-facing warning message when threshold is exceeded.
19
+ */
20
+ const CAPTURE_FAILURE_WARNING = "Multiple image captures failed. Device may be low on memory.";
21
+ /**
22
+ * Create a new capture failure tracker.
23
+ *
24
+ * @param config - Configuration with warning callback and optional threshold
25
+ * @returns CaptureFailureTracker instance
26
+ */
27
+ function createCaptureFailureTracker(config) {
28
+ const tracker = createFailureTracker({
29
+ label: "CaptureFailure",
30
+ warningMessage: CAPTURE_FAILURE_WARNING,
31
+ defaultThreshold: DEFAULT_CAPTURE_TRACKER_CONFIG.failureThreshold,
32
+ onWarning: config.onWarning,
33
+ failureThreshold: config.failureThreshold,
34
+ logLevel: "warn"
35
+ });
36
+ return {
37
+ recordSuccess: () => tracker.recordSuccess(),
38
+ recordFailure: () => tracker.recordFailure(),
39
+ getFailureCount: () => tracker.getFailureCount(),
40
+ hasWarned: () => tracker.hasWarned(),
41
+ reset: () => tracker.reset()
42
+ };
43
+ }
44
+ //#endregion
45
+ export { CAPTURE_FAILURE_WARNING, DEFAULT_CAPTURE_TRACKER_CONFIG, createCaptureFailureTracker };
@@ -0,0 +1,2 @@
1
+ import { n as applyChromiumProjectionLayerWorkaround, t as ChromiumProjectionLayerWorkaroundResult } from "../chromium-camera-access-workaround-CU5zSKNr.js";
2
+ export { ChromiumProjectionLayerWorkaroundResult, applyChromiumProjectionLayerWorkaround };
@@ -0,0 +1,32 @@
1
+ //#region ../src/ar/chromium-camera-access-workaround.ts
2
+ /**
3
+ * Apply the Chromium camera-access tab-crash workaround.
4
+ *
5
+ * Removes `XRWebGLBinding.prototype.createProjectionLayer` (three.js r184)
6
+ * and `XRRenderState.prototype.layers` (three.js r158) so three.js falls
7
+ * back to `XRWebGLLayer`, which avoids the renderer-process crash that
8
+ * happens when both projection layers and `camera-access` are enabled.
9
+ *
10
+ * Call once during bootstrap, before any `requestSession()` call.
11
+ *
12
+ * @returns which prototype members were actually deleted on this call.
13
+ */
14
+ function applyChromiumProjectionLayerWorkaround() {
15
+ const result = {
16
+ deletedCreateProjectionLayer: false,
17
+ deletedRenderStateLayers: false
18
+ };
19
+ const binding = globalThis.XRWebGLBinding;
20
+ if (binding && "createProjectionLayer" in binding.prototype) {
21
+ delete binding.prototype.createProjectionLayer;
22
+ result.deletedCreateProjectionLayer = true;
23
+ }
24
+ const renderState = globalThis.XRRenderState;
25
+ if (renderState && "layers" in renderState.prototype) {
26
+ delete renderState.prototype.layers;
27
+ result.deletedRenderStateLayers = true;
28
+ }
29
+ return result;
30
+ }
31
+ //#endregion
32
+ export { applyChromiumProjectionLayerWorkaround };
@@ -0,0 +1,3 @@
1
+ import { i as DepthSample } from "../ar-types-B-ORgk6Z.js";
2
+ import { i as DepthSamplerConfig, n as DepthSampler, r as DepthSamplerCallbacks, t as DepthInfo } from "../depth-sampler-BNCtnEoP.js";
3
+ export { DepthInfo, DepthSample, DepthSampler, DepthSamplerCallbacks, DepthSamplerConfig };
@@ -0,0 +1,147 @@
1
+ import { extractOdomPosition } from "../state/recording-coordinator.js";
2
+ //#region ../src/ar/depth-sampler.ts
3
+ const DEFAULT_CONFIG = {
4
+ intervalMs: 1e3,
5
+ gridSize: 3,
6
+ unavailabilityThresholdMs: 5e3
7
+ };
8
+ /**
9
+ * Samples sparse depth points from WebXR depth sensing.
10
+ *
11
+ * Usage:
12
+ * ```ts
13
+ * const sampler = new DepthSampler({
14
+ * onSampleCaptured: (sample) => saveSample(sample),
15
+ * getCurrentPose: () => arSession.getCurrentPose(),
16
+ * });
17
+ * sampler.start();
18
+ * // In frame loop:
19
+ * sampler.onFrame(timestamp, depthInfo);
20
+ * ```
21
+ */
22
+ var DepthSampler = class {
23
+ callbacks;
24
+ config;
25
+ running = false;
26
+ sampleCount = 0;
27
+ lastSampleTime = -Infinity;
28
+ /** Timestamp when sampling started (for unavailability detection) */
29
+ startTime = -Infinity;
30
+ /** Whether we've ever received valid depth data */
31
+ depthReceived = false;
32
+ /** Whether we've already fired the unavailable callback */
33
+ unavailableCallbackFired = false;
34
+ constructor(callbacks, config) {
35
+ this.callbacks = callbacks;
36
+ this.config = {
37
+ ...DEFAULT_CONFIG,
38
+ ...config
39
+ };
40
+ }
41
+ /**
42
+ * Start depth sampling.
43
+ */
44
+ start() {
45
+ this.running = true;
46
+ this.sampleCount = 0;
47
+ this.lastSampleTime = -Infinity;
48
+ this.startTime = performance.now();
49
+ this.depthReceived = false;
50
+ this.unavailableCallbackFired = false;
51
+ }
52
+ /**
53
+ * Stop depth sampling.
54
+ */
55
+ stop() {
56
+ this.running = false;
57
+ }
58
+ /**
59
+ * Check if sampler is currently running.
60
+ */
61
+ isRunning() {
62
+ return this.running;
63
+ }
64
+ /**
65
+ * Get the number of samples captured since start.
66
+ */
67
+ getSampleCount() {
68
+ return this.sampleCount;
69
+ }
70
+ /**
71
+ * Get the current configuration.
72
+ */
73
+ getConfig() {
74
+ return { ...this.config };
75
+ }
76
+ /**
77
+ * Called each frame with depth information.
78
+ *
79
+ * @param timestamp - Current frame timestamp in milliseconds
80
+ * @param depthInfo - WebXR depth information, or null if unavailable
81
+ */
82
+ onFrame(timestamp, depthInfo) {
83
+ if (!this.running) return;
84
+ if (!depthInfo) {
85
+ this.checkDepthUnavailability();
86
+ return;
87
+ }
88
+ this.depthReceived = true;
89
+ if (timestamp - this.lastSampleTime < this.config.intervalMs) return;
90
+ const pose = this.callbacks.getCurrentPose();
91
+ if (!pose) return;
92
+ const points = this.sampleGrid(depthInfo);
93
+ const sample = {
94
+ timestamp: performance.timeOrigin + timestamp,
95
+ cameraPos: extractOdomPosition(pose),
96
+ cameraRot: [
97
+ pose.orientation.x,
98
+ pose.orientation.y,
99
+ pose.orientation.z,
100
+ pose.orientation.w
101
+ ],
102
+ points
103
+ };
104
+ this.lastSampleTime = timestamp;
105
+ this.sampleCount++;
106
+ this.callbacks.onSampleCaptured(sample);
107
+ }
108
+ /**
109
+ * Sample a grid of depth points from the depth buffer.
110
+ */
111
+ sampleGrid(depthInfo) {
112
+ const points = [];
113
+ const gridSize = this.config.gridSize;
114
+ for (let row = 0; row < gridSize; row++) for (let col = 0; col < gridSize; col++) {
115
+ const screenX = (col + 1) / (gridSize + 1);
116
+ const screenY = (row + 1) / (gridSize + 1);
117
+ const depthM = depthInfo.getDepthInMeters(screenX, screenY);
118
+ points.push({
119
+ screenX,
120
+ screenY,
121
+ depthM
122
+ });
123
+ }
124
+ return points;
125
+ }
126
+ /**
127
+ * Check if depth has been unavailable for longer than the threshold.
128
+ * If so, fire the onDepthUnavailable callback (once).
129
+ */
130
+ checkDepthUnavailability() {
131
+ if (this.depthReceived || this.unavailableCallbackFired) return;
132
+ if (!this.callbacks.onDepthUnavailable) return;
133
+ if (performance.now() - this.startTime >= this.config.unavailabilityThresholdMs) {
134
+ this.unavailableCallbackFired = true;
135
+ this.callbacks.onDepthUnavailable();
136
+ }
137
+ }
138
+ /**
139
+ * Check if depth data has ever been received.
140
+ * Useful for testing and status display.
141
+ */
142
+ hasReceivedDepth() {
143
+ return this.depthReceived;
144
+ }
145
+ };
146
+ //#endregion
147
+ export { DepthSampler };
@@ -0,0 +1,2 @@
1
+ import { a as ImageCaptureManager, i as ImageCaptureConfig, n as DEFAULT_CAPTURE_CONFIG, o as MIN_VALID_IMAGE_BYTES, r as ImageCaptureCallbacks, t as CapturedImage } from "../image-capture-2pKjYUS6.js";
2
+ export { CapturedImage, DEFAULT_CAPTURE_CONFIG, ImageCaptureCallbacks, ImageCaptureConfig, ImageCaptureManager, MIN_VALID_IMAGE_BYTES };
@@ -0,0 +1,152 @@
1
+ import { createLogger } from "../utils/logger.js";
2
+ //#region ../src/ar/image-capture.ts
3
+ /**
4
+ * Image Capture Module
5
+ *
6
+ * Captures periodic JPEG screenshots from the WebGL canvas during AR recording.
7
+ * Uses async toBlob() for better performance (non-blocking).
8
+ *
9
+ * Includes validation to detect suspiciously small images (likely black/empty)
10
+ * which can occur on mobile devices when the WebGL context hasn't composited yet.
11
+ */
12
+ const log = createLogger("ImageCapture");
13
+ /**
14
+ * Minimum expected blob size in bytes for a valid JPEG image.
15
+ * A black/empty 1920x1080 JPEG still compresses to ~2-3KB due to headers.
16
+ * A real camera frame at 0.7 quality should be 50KB+.
17
+ * We use a conservative threshold to catch obviously broken captures.
18
+ */
19
+ const MIN_VALID_IMAGE_BYTES = 5e3;
20
+ /**
21
+ * Default capture configuration
22
+ */
23
+ const DEFAULT_CAPTURE_CONFIG = {
24
+ intervalMs: 2e3,
25
+ quality: .7,
26
+ captureTimeoutMs: 5e3
27
+ };
28
+ /**
29
+ * Manages periodic image capture from a canvas.
30
+ *
31
+ * Usage:
32
+ * 1. Create with canvas and callbacks
33
+ * 2. Call start() when recording begins
34
+ * 3. Call onFrame(time) each XR frame
35
+ * 4. Call stop() when recording ends
36
+ */
37
+ var ImageCaptureManager = class {
38
+ canvas;
39
+ callbacks;
40
+ config;
41
+ capturing = false;
42
+ lastCaptureTime = 0;
43
+ frameCount = 0;
44
+ captureInProgress = false;
45
+ captureTimeoutId = null;
46
+ constructor(canvas, callbacks, config = DEFAULT_CAPTURE_CONFIG) {
47
+ this.canvas = canvas;
48
+ this.callbacks = callbacks;
49
+ this.config = config;
50
+ }
51
+ /**
52
+ * Start capturing images.
53
+ * Resets frame counter and last capture time.
54
+ */
55
+ start() {
56
+ this.capturing = true;
57
+ this.lastCaptureTime = 0;
58
+ this.frameCount = 0;
59
+ }
60
+ /**
61
+ * Stop capturing images.
62
+ * Clears any pending safety timeout and resets in-flight capture state.
63
+ */
64
+ stop() {
65
+ this.capturing = false;
66
+ this.clearCaptureTimeout();
67
+ this.captureInProgress = false;
68
+ }
69
+ /**
70
+ * Check if capture manager is active.
71
+ */
72
+ isCapturing() {
73
+ return this.capturing;
74
+ }
75
+ /**
76
+ * Get the current frame count.
77
+ */
78
+ getFrameCount() {
79
+ return this.frameCount;
80
+ }
81
+ /**
82
+ * Called each XR frame to check if a capture is needed.
83
+ *
84
+ * @param time - Frame timestamp in milliseconds (from requestAnimationFrame)
85
+ */
86
+ onFrame(time) {
87
+ if (!this.capturing) return;
88
+ if (this.captureInProgress) return;
89
+ const elapsed = time - this.lastCaptureTime;
90
+ if (this.lastCaptureTime > 0 && elapsed < this.config.intervalMs) return;
91
+ const pose = this.callbacks.getCurrentPose();
92
+ if (!pose) return;
93
+ this.captureInProgress = true;
94
+ this.lastCaptureTime = time;
95
+ const timestamp = Date.now();
96
+ const screenRotation = this.callbacks.getScreenRotation();
97
+ const frameIndex = ++this.frameCount;
98
+ this.captureTimeoutId = setTimeout(() => {
99
+ if (this.captureInProgress) {
100
+ log.error(`Capture timeout after ${this.config.captureTimeoutMs}ms — force-resetting captureInProgress`);
101
+ this.captureInProgress = false;
102
+ this.captureTimeoutId = null;
103
+ }
104
+ }, this.config.captureTimeoutMs);
105
+ if (this.callbacks.captureFrame) this.callbacks.captureFrame(this.config.quality).then((blob) => {
106
+ this.handleCapturedBlob(blob, timestamp, frameIndex, pose, screenRotation);
107
+ }).catch(() => {
108
+ this.clearCaptureTimeout();
109
+ this.captureInProgress = false;
110
+ this.callbacks.onCaptureFailed?.();
111
+ });
112
+ else this.canvas.toBlob((blob) => {
113
+ this.handleCapturedBlob(blob, timestamp, frameIndex, pose, screenRotation);
114
+ }, "image/jpeg", this.config.quality);
115
+ }
116
+ /**
117
+ * Clear the safety timeout for captureInProgress.
118
+ * Called when capture completes (success or failure) before the timeout fires.
119
+ */
120
+ clearCaptureTimeout() {
121
+ if (this.captureTimeoutId !== null) {
122
+ clearTimeout(this.captureTimeoutId);
123
+ this.captureTimeoutId = null;
124
+ }
125
+ }
126
+ /**
127
+ * Common handler for captured blobs (from either canvas.toBlob or captureFrame).
128
+ * Handles null blobs, suspicious image detection, and dispatching onCaptured.
129
+ */
130
+ handleCapturedBlob(blob, timestamp, frameIndex, pose, screenRotation) {
131
+ this.clearCaptureTimeout();
132
+ this.captureInProgress = false;
133
+ if (!blob) {
134
+ this.callbacks.onCaptureFailed?.();
135
+ return;
136
+ }
137
+ if (blob.size < 5e3) {
138
+ log.error(`Suspicious image at frame ${frameIndex}: blob size ${blob.size} bytes is below minimum ${MIN_VALID_IMAGE_BYTES} bytes. Image may be black/empty.`);
139
+ this.callbacks.onSuspiciousImage?.(blob.size, frameIndex);
140
+ }
141
+ this.callbacks.onCaptured({
142
+ blob,
143
+ timestamp,
144
+ frameIndex,
145
+ position: pose.position,
146
+ rotation: pose.orientation,
147
+ screenRotation
148
+ });
149
+ }
150
+ };
151
+ //#endregion
152
+ export { DEFAULT_CAPTURE_CONFIG, ImageCaptureManager, MIN_VALID_IMAGE_BYTES };
@@ -0,0 +1,12 @@
1
+ import { i as computeCaptureSize, n as CameraBlitCaptureConfig, r as DEFAULT_BLIT_CONFIG, t as CameraBlitCapture } from "../camera-blit-capture-DHuyrrxr.js";
2
+ import { a as createCaptureFailureTracker, i as DEFAULT_CAPTURE_TRACKER_CONFIG, n as CaptureFailureTracker, r as CaptureFailureTrackerConfig, t as CAPTURE_FAILURE_WARNING } from "../capture-failure-tracker-2Q6oI2uQ.js";
3
+ import { n as applyChromiumProjectionLayerWorkaround, t as ChromiumProjectionLayerWorkaroundResult } from "../chromium-camera-access-workaround-CU5zSKNr.js";
4
+ import { i as DepthSamplerConfig, n as DepthSampler, r as DepthSamplerCallbacks, t as DepthInfo } from "../depth-sampler-BNCtnEoP.js";
5
+ import { a as ImageCaptureManager, i as ImageCaptureConfig, n as DEFAULT_CAPTURE_CONFIG, o as MIN_VALID_IMAGE_BYTES, r as ImageCaptureCallbacks, t as CapturedImage } from "../image-capture-2pKjYUS6.js";
6
+ import { a as getCameraFollower, c as getReplayState, d as updateOrbitTarget, i as getAlignmentLerper, l as initReplayScene, n as ReplaySceneState, o as getCameraMode, r as disposeReplayScene, t as CameraMode, u as toggleCameraMode } from "../replay-scene-DtUm6bsr.js";
7
+ import { t as SCENE_NODE } from "../scene-node-names-DPsaE2z3.js";
8
+ import { a as TrackingStateManager, i as TrackingStateCallbacks, r as TrackingState, t as DeviceOrientation } from "../tracking-state-B9QfuZCg.js";
9
+ import { i as acquireCameraTexture, n as RendererLike, r as XRCameraLike, t as CameraTextureResult } from "../xr-camera-texture-BbukNE79.js";
10
+ import { A as setTrackingLostCallback, D as setImageCaptureCallback, E as setFrameCallback, F as stopDepthCapture, I as stopImageCapture, N as startDepthCapture, P as startImageCapture, T as setDepthCaptureCallback, d as getDepthSampleCount, f as getImageCaptureFrameCount, g as initAR, i as endARSession, k as setTrackingCallbacks, p as getLiveCss3dManager } from "../webxr-session-COlSrXdL.js";
11
+ import { n as XR_ERROR_MESSAGE_UNKNOWN, r as getXrErrorMessage, t as XR_ERROR_MESSAGES } from "../xr-error-handler-DTHfmvrl.js";
12
+ export { CAPTURE_FAILURE_WARNING, CameraBlitCapture, type CameraBlitCaptureConfig, type CameraMode, type CameraTextureResult, type CaptureFailureTracker, type CaptureFailureTrackerConfig, type CapturedImage, type ChromiumProjectionLayerWorkaroundResult, DEFAULT_BLIT_CONFIG, DEFAULT_CAPTURE_CONFIG, DEFAULT_CAPTURE_TRACKER_CONFIG, type DepthInfo, DepthSampler, type DepthSamplerCallbacks, type DepthSamplerConfig, type DeviceOrientation, type ImageCaptureCallbacks, type ImageCaptureConfig, ImageCaptureManager, MIN_VALID_IMAGE_BYTES, type RendererLike, type ReplaySceneState, SCENE_NODE, TrackingState, type TrackingStateCallbacks, TrackingStateManager, type XRCameraLike, XR_ERROR_MESSAGES, XR_ERROR_MESSAGE_UNKNOWN, acquireCameraTexture, applyChromiumProjectionLayerWorkaround, computeCaptureSize, createCaptureFailureTracker, disposeReplayScene, endARSession, getAlignmentLerper, getCameraFollower, getCameraMode, getDepthSampleCount, getImageCaptureFrameCount, getLiveCss3dManager, getReplayState, getXrErrorMessage, initAR, initReplayScene, setDepthCaptureCallback, setFrameCallback, setImageCaptureCallback, setTrackingCallbacks, setTrackingLostCallback, startDepthCapture, startImageCapture, stopDepthCapture, stopImageCapture, toggleCameraMode, updateOrbitTarget };
@@ -0,0 +1,12 @@
1
+ import { CameraBlitCapture, DEFAULT_BLIT_CONFIG, computeCaptureSize } from "./camera-blit-capture.js";
2
+ import { applyChromiumProjectionLayerWorkaround } from "./chromium-camera-access-workaround.js";
3
+ import { CAPTURE_FAILURE_WARNING, DEFAULT_CAPTURE_TRACKER_CONFIG, createCaptureFailureTracker } from "./capture-failure-tracker.js";
4
+ import { DepthSampler } from "./depth-sampler.js";
5
+ import { DEFAULT_CAPTURE_CONFIG, ImageCaptureManager, MIN_VALID_IMAGE_BYTES } from "./image-capture.js";
6
+ import { TrackingState, TrackingStateManager } from "./tracking-state.js";
7
+ import { acquireCameraTexture } from "./xr-camera-texture.js";
8
+ import { SCENE_NODE } from "./scene-node-names.js";
9
+ import { endARSession, getDepthSampleCount, getImageCaptureFrameCount, getLiveCss3dManager, initAR, setDepthCaptureCallback, setFrameCallback, setImageCaptureCallback, setTrackingCallbacks, setTrackingLostCallback, startDepthCapture, startImageCapture, stopDepthCapture, stopImageCapture } from "./webxr-session.js";
10
+ import { disposeReplayScene, getAlignmentLerper, getCameraFollower, getCameraMode, getReplayState, initReplayScene, toggleCameraMode, updateOrbitTarget } from "./replay-scene.js";
11
+ import { XR_ERROR_MESSAGES, XR_ERROR_MESSAGE_UNKNOWN, getXrErrorMessage } from "./xr-error-handler.js";
12
+ export { CAPTURE_FAILURE_WARNING, CameraBlitCapture, DEFAULT_BLIT_CONFIG, DEFAULT_CAPTURE_CONFIG, DEFAULT_CAPTURE_TRACKER_CONFIG, DepthSampler, ImageCaptureManager, MIN_VALID_IMAGE_BYTES, SCENE_NODE, TrackingState, TrackingStateManager, XR_ERROR_MESSAGES, XR_ERROR_MESSAGE_UNKNOWN, acquireCameraTexture, applyChromiumProjectionLayerWorkaround, computeCaptureSize, createCaptureFailureTracker, disposeReplayScene, endARSession, getAlignmentLerper, getCameraFollower, getCameraMode, getDepthSampleCount, getImageCaptureFrameCount, getLiveCss3dManager, getReplayState, getXrErrorMessage, initAR, initReplayScene, setDepthCaptureCallback, setFrameCallback, setImageCaptureCallback, setTrackingCallbacks, setTrackingLostCallback, startDepthCapture, startImageCapture, stopDepthCapture, stopImageCapture, toggleCameraMode, updateOrbitTarget };
@@ -0,0 +1,2 @@
1
+ import { a as getCameraFollower, c as getReplayState, d as updateOrbitTarget, i as getAlignmentLerper, l as initReplayScene, n as ReplaySceneState, o as getCameraMode, r as disposeReplayScene, s as getCss3dManager, t as CameraMode, u as toggleCameraMode } from "../replay-scene-DtUm6bsr.js";
2
+ export { CameraMode, ReplaySceneState, disposeReplayScene, getAlignmentLerper, getCameraFollower, getCameraMode, getCss3dManager, getReplayState, initReplayScene, toggleCameraMode, updateOrbitTarget };