gps-plus-slam-app-framework 1.0.7 → 1.1.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/README.md +69 -15
- package/dist/{app-selectors-BeU0_ab8.d.ts → app-selectors-CR9OWodf.d.ts} +1 -1
- package/dist/ar/camera-blit-capture.d.ts +1 -1
- package/dist/ar/capability-checker.d.ts +2 -0
- package/dist/ar/capability-checker.js +25 -0
- package/dist/ar/capture-failure-tracker.d.ts +1 -1
- package/dist/ar/chromium-camera-access-workaround.d.ts +2 -2
- package/dist/ar/chromium-camera-access-workaround.js +197 -8
- package/dist/ar/depth-sampler.d.ts +2 -2
- package/dist/ar/enable-gps-ar.d.ts +2 -0
- package/dist/ar/enable-gps-ar.js +137 -0
- package/dist/ar/frame-loop.d.ts +1 -1
- package/dist/ar/frame-loop.js +24 -1
- package/dist/ar/image-capture.d.ts +1 -1
- package/dist/ar/index.d.ts +15 -11
- package/dist/ar/index.js +7 -3
- package/dist/ar/replay-scene.d.ts +1 -1
- package/dist/ar/scene-node-names.d.ts +1 -1
- package/dist/ar/webxr-session.d.ts +4 -4
- package/dist/ar/webxr-session.js +23 -38
- package/dist/ar/xr-error-handler.d.ts +1 -1
- package/dist/ar/xr-frame-loop.d.ts +2 -0
- package/dist/ar/xr-frame-loop.js +69 -0
- package/dist/ar-world-group-alignment-Dn4rQk_c.d.ts +43 -0
- package/dist/capability-checker-BTCmZRL4.d.ts +43 -0
- package/dist/chromium-camera-access-workaround-MifIwK9x.d.ts +137 -0
- package/dist/core/index.d.ts +1 -1
- package/dist/{create-slam-app-store-CNsZ3rcX.d.ts → create-slam-app-store-B76AGaI0.d.ts} +5 -5
- package/dist/{depth-sampler-xg8vna6c.d.ts → depth-sampler-CqrkRr2N.d.ts} +1 -1
- package/dist/enable-gps-ar-DNbO0zbg.d.ts +95 -0
- package/dist/{file-system-DTApEv_Q.d.ts → file-system-TIsDfamK.d.ts} +1 -1
- package/dist/frame-conversions-D2EYjeNa.d.ts +54 -0
- package/dist/{frame-loop-QJIxLin0.d.ts → frame-loop-BTeRpDm4.d.ts} +5 -0
- package/dist/{fused-path-CB4DwWOG.d.ts → fused-path-BdVHmDW9.d.ts} +1 -1
- package/dist/geo/h3-proximity.d.ts +1 -1
- package/dist/geo/index.d.ts +1 -1
- package/dist/{gps-anchor-CnWY61XK.d.ts → gps-anchor-CISLPQIb.d.ts} +12 -1
- package/dist/{gps-ar-pose-sampler-DsM1Kb2N.d.ts → gps-ar-pose-sampler-BypXZUqC.d.ts} +1 -1
- package/dist/{gps-event-coordinator-DBx9I1YX.d.ts → gps-event-coordinator-BKVZbS1M.d.ts} +3 -3
- package/dist/hit-test-reticle-BGXOxrUh.d.ts +45 -0
- package/dist/{image-capture-DZLxHDN9.d.ts → image-capture-BXZUmj7j.d.ts} +1 -1
- package/dist/index-22v8MXJX.d.ts +23 -0
- package/dist/index-BB2KEWbN2.d.ts +37 -0
- package/dist/index.d.ts +63 -57
- package/dist/index.js +14 -7
- package/dist/{leaflet-map-overlay-Cew7XAoO.d.ts → leaflet-map-overlay-jNt-kcUr.d.ts} +1 -1
- package/dist/{map-data-sih4g64v.d.ts → map-data-BGIBM5rv.d.ts} +1 -1
- package/dist/{map-overlay-draw-B3R1c9Xp.d.ts → map-overlay-draw-DcGs9UV3.d.ts} +1 -1
- package/dist/{null-storage-backend-BKOS7JpL.d.ts → null-storage-backend-BPQJSaMw.d.ts} +2 -2
- package/dist/{opfs-storage-backend-Cj_7PWoq.d.ts → opfs-storage-backend-u4QKvgvP.d.ts} +2 -2
- package/dist/{persistence-middleware-BkHIqvaA.d.ts → persistence-middleware-xB5sTllJ.d.ts} +1 -1
- package/dist/{recording-options-Dq6IsKAv.d.ts → recording-options-BSFpBSZH.d.ts} +10 -4
- package/dist/{recording-replayer-CmCIIZ02.d.ts → recording-replayer-CTL2cUE0.d.ts} +2 -2
- package/dist/{recording-slice-d6OtCrza.d.ts → recording-slice-Cb888P6d.d.ts} +2 -2
- package/dist/{replay-engine-BGBRQyXs.d.ts → replay-engine-B-5GIZtc.d.ts} +1 -1
- package/dist/{replay-scene-DpAGcAKY.d.ts → replay-scene-DkNfldPh.d.ts} +3 -3
- package/dist/sensors/gps-error-handler.d.ts +1 -1
- package/dist/sensors/gps.d.ts +1 -1
- package/dist/sensors/index.d.ts +3 -3
- package/dist/sensors/index.js +1 -1
- package/dist/sensors/permission-checker.d.ts +1 -1
- package/dist/session-disposers-M-oashRH.js +57 -0
- package/dist/state/app-selectors.d.ts +1 -1
- package/dist/state/combined-root-state.d.ts +1 -1
- package/dist/state/create-slam-app-store.d.ts +1 -1
- package/dist/state/gps-ar-pose-sampler.d.ts +3 -3
- package/dist/state/gps-event-coordinator.d.ts +1 -1
- package/dist/state/index.d.ts +17 -17
- package/dist/state/index.js +2 -2
- package/dist/state/persistence-middleware.d.ts +1 -1
- package/dist/state/persistence-middleware.js +9 -3
- package/dist/state/recording-options.d.ts +1 -1
- package/dist/state/recording-replayer.d.ts +2 -2
- package/dist/state/recording-slice.d.ts +1 -1
- package/dist/state/replay-engine.d.ts +1 -1
- package/dist/state/store-subscribers.d.ts +2 -2
- package/dist/state/subscribe-to-selector.d.ts +1 -1
- package/dist/state/tracking-quality.d.ts +1 -1
- package/dist/state/tracking-slice.d.ts +1 -1
- package/dist/state-BkIfpiyA.js +87 -0
- package/dist/storage/file-system-utils.d.ts +1 -1
- package/dist/storage/file-system.d.ts +1 -1
- package/dist/storage/index.d.ts +8 -8
- package/dist/storage/null-storage-backend.d.ts +1 -1
- package/dist/storage/opfs-storage-backend.d.ts +1 -1
- package/dist/storage/opfs-storage.d.ts +1 -1
- package/dist/storage/storage-backend.d.ts +1 -1
- package/dist/storage/zip-export.d.ts +1 -1
- package/dist/storage/zip-reader.d.ts +1 -1
- package/dist/{storage-backend-CT0Il_AB.d.ts → storage-backend-yDSKafAQ.d.ts} +1 -1
- package/dist/{store-subscribers-Dce_CoJF.d.ts → store-subscribers-swlbtg_z.d.ts} +2 -2
- package/dist/{subscribe-to-selector-DIHm3BRZ.d.ts → subscribe-to-selector--KQpgLXG.d.ts} +1 -1
- package/dist/test-utils/browser-mocks.d.ts +1 -1
- package/dist/test-utils/zip-round-trip-helpers.d.ts +1 -1
- package/dist/{tracking-slice-utCNND8t.d.ts → tracking-slice-CDREeoZ_.d.ts} +1 -1
- package/dist/types/ar-types.d.ts +1 -1
- package/dist/types/geo-types.d.ts +1 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/utils/concurrency.d.ts +1 -1
- package/dist/utils/failure-tracker.d.ts +1 -1
- package/dist/utils/format-file-size.d.ts +1 -1
- package/dist/utils/fused-path.d.ts +1 -1
- package/dist/utils/index.d.ts +6 -6
- package/dist/utils/list-formatter.d.ts +1 -1
- package/dist/utils/logger.d.ts +1 -1
- package/dist/visualization/accuracy-circles.d.ts +1 -1
- package/dist/visualization/alignment-lerper.d.ts +1 -1
- package/dist/visualization/ar-world-group-alignment.d.ts +2 -0
- package/dist/visualization/ar-world-group-alignment.js +51 -0
- package/dist/visualization/camera-follower.d.ts +1 -1
- package/dist/visualization/css3d-renderer-manager.d.ts +1 -1
- package/dist/visualization/frame-conversions.d.ts +2 -0
- package/dist/visualization/frame-conversions.js +88 -0
- package/dist/visualization/frustum-visibility.d.ts +1 -1
- package/dist/visualization/gps-anchor.d.ts +1 -1
- package/dist/visualization/gps-anchor.js +216 -1
- package/dist/visualization/gps-compass-cubes.d.ts +1 -1
- package/dist/visualization/gps-event-markers.d.ts +1 -1
- package/dist/visualization/hit-test-reticle.d.ts +2 -0
- package/dist/visualization/hit-test-reticle.js +81 -0
- package/dist/visualization/index.d.ts +19 -17
- package/dist/visualization/index.js +5 -2
- package/dist/visualization/leaflet-map-overlay.d.ts +1 -1
- package/dist/visualization/lerp-utils.d.ts +1 -1
- package/dist/visualization/map-data.d.ts +1 -1
- package/dist/visualization/map-overlay-draw.d.ts +1 -1
- package/dist/visualization/map-overlay.d.ts +1 -1
- package/dist/visualization/three-dispose.d.ts +1 -1
- package/dist/visualization/vis-colors.d.ts +1 -1
- package/dist/webxr-nue-basis-BbdqhqY6.js +54 -0
- package/dist/{webxr-session-DB5cIZnN.d.ts → webxr-session-BnArFCew.d.ts} +29 -8
- package/dist/xr-frame-loop-BKckC7xC.d.ts +70 -0
- package/package.json +1 -1
- package/dist/chromium-camera-access-workaround-NvChXsi8.d.ts +0 -61
- package/dist/gps-anchor-DjC_3MoT.js +0 -257
- package/dist/index-pOjUCKEP.d.ts +0 -24
- /package/dist/{accuracy-circles-CsxE_7x1.d.ts → accuracy-circles-DEKr0Hoh.d.ts} +0 -0
- /package/dist/{alignment-lerper-L1RbWHQ4.d.ts → alignment-lerper-CCZJf-Xb.d.ts} +0 -0
- /package/dist/{ar-types-Ck8LEq8k.d.ts → ar-types-isPsQptb.d.ts} +0 -0
- /package/dist/{camera-blit-capture-C1V9I1Z6.d.ts → camera-blit-capture-75WWa9Xb.d.ts} +0 -0
- /package/dist/{camera-follower-1k8mnw5J.d.ts → camera-follower-B-nS1sr6.d.ts} +0 -0
- /package/dist/{capture-failure-tracker-JKb1ST8E.d.ts → capture-failure-tracker-D4HYJNOj.d.ts} +0 -0
- /package/dist/{concurrency-CMw8pSaG.d.ts → concurrency-Bsmv53qw.d.ts} +0 -0
- /package/dist/{css3d-renderer-manager-CkjCb2HW.d.ts → css3d-renderer-manager-EDED3nvg.d.ts} +0 -0
- /package/dist/{failure-tracker-DnHXOIRa.d.ts → failure-tracker-D7ELffm0.d.ts} +0 -0
- /package/dist/{file-system-utils-CtKiiw27.d.ts → file-system-utils-BH6uIs2i.d.ts} +0 -0
- /package/dist/{format-file-size-BTS5H62-.d.ts → format-file-size-B7gJb3Md.d.ts} +0 -0
- /package/dist/{frustum-visibility-BHnFrSKQ.d.ts → frustum-visibility-DGnJqls0.d.ts} +0 -0
- /package/dist/{geo-types-CTuF23gG.d.ts → geo-types-clgzl8b5.d.ts} +0 -0
- /package/dist/{gps-DXoQb9Kf.d.ts → gps-B7AlMPz5.d.ts} +0 -0
- /package/dist/{gps-compass-cubes-CsLtFv4B.d.ts → gps-compass-cubes-Ys6Hf1nc.d.ts} +0 -0
- /package/dist/{gps-error-handler-Dg3ziRVY.d.ts → gps-error-handler-BLDhUlw5.d.ts} +0 -0
- /package/dist/{gps-event-markers-BGr-iLvi.d.ts → gps-event-markers-BlHq3jYV.d.ts} +0 -0
- /package/dist/{h3-proximity-BhfgNbdW.d.ts → h3-proximity-DJnghtir.d.ts} +0 -0
- /package/dist/{index-gzWJMVpo.d.ts → index--ldLph4V.d.ts} +0 -0
- /package/dist/{lerp-utils-B8MM8zCR.d.ts → lerp-utils-DNcSmKVX.d.ts} +0 -0
- /package/dist/{list-formatter-CNqi-E2j.d.ts → list-formatter-CsqcU4v5.d.ts} +0 -0
- /package/dist/{logger-C9vugiQb.d.ts → logger-BZ44Dhd3.d.ts} +0 -0
- /package/dist/{map-overlay-DAmreyA3.d.ts → map-overlay-yrFsUtGv.d.ts} +0 -0
- /package/dist/{opfs-storage-DOPkzgeT.d.ts → opfs-storage-LsLY6VZV.d.ts} +0 -0
- /package/dist/{permission-checker-DPJl_RU6.d.ts → permission-checker-CfV7INCa.d.ts} +0 -0
- /package/dist/{scene-node-names-6LBjPmwz.d.ts → scene-node-names-IHW7HR4S.d.ts} +0 -0
- /package/dist/{three-dispose-C2drkp6f.d.ts → three-dispose-WTBAfaWU.d.ts} +0 -0
- /package/dist/{vis-colors-CHQhjal3.d.ts → vis-colors-Djnyg_UH.d.ts} +0 -0
- /package/dist/{xr-error-handler-CDY5QxGb.d.ts → xr-error-handler-DcUFshNF.d.ts} +0 -0
- /package/dist/{zip-export-QO4iLIi7.d.ts → zip-export-C0_ErAqT.d.ts} +0 -0
- /package/dist/{zip-reader-D1105idz.d.ts → zip-reader-B2lzN8F5.d.ts} +0 -0
package/README.md
CHANGED
|
@@ -108,7 +108,7 @@ startGpsWatch(
|
|
|
108
108
|
);
|
|
109
109
|
```
|
|
110
110
|
|
|
111
|
-
See [`GpsPlusSlamJs_MinimalExample`](../GpsPlusSlamJs_MinimalExample/) for a complete, runnable smallest-possible consumer (Three.js scene + status panel, no AR, no recording).
|
|
111
|
+
See [`GpsPlusSlamJs_MinimalExample`](../GpsPlusSlamJs_MinimalExample/) for a complete, runnable smallest-possible consumer (Three.js scene + status panel, no AR, no recording). For the next rung up — a readable AR + GPS + persistence demo (a single GPS anchor that survives a page reload) — see [`GpsPlusSlamJs_AnchorStarter`](../GpsPlusSlamJs_AnchorStarter/). The example ladder is **trivial** (MinimalExample) → **starter** (AnchorStarter) → **full** (RecorderApp).
|
|
112
112
|
|
|
113
113
|
> **Imports.** Prefer subpath imports (`gps-plus-slam-app-framework/ar`, `…/state`, `…/sensors`, `…/storage`, `…/geo`, `…/visualization`, `…/utils`, `…/types`, `…/licensing`). The root barrel re-exports conflict-free names for convenience.
|
|
114
114
|
|
|
@@ -196,32 +196,85 @@ scene ← GPS-aligned (NUE) space, the scene root
|
|
|
196
196
|
(waypoints, POIs, navigation arrows, …)
|
|
197
197
|
```
|
|
198
198
|
|
|
199
|
-
When the alignment solver produces a new matrix, the framework writes it to `arWorldGroup.matrix
|
|
199
|
+
When the alignment solver produces a new matrix, the framework writes it to `arWorldGroup.matrix` (smoothly lerped — see `enableArWorldGroupAlignment` below). The camera moves with `arWorldGroup`; objects parented directly to `scene` do not.
|
|
200
|
+
|
|
201
|
+
> **Apply alignment to `arWorldGroup` — the framework default.** Call
|
|
202
|
+
> [`enableArWorldGroupAlignment({ store, arWorldGroup })`](src/visualization/ar-world-group-alignment.ts.md)
|
|
203
|
+
> once after AR starts. It subscribes to the store's alignment matrix, lerps it
|
|
204
|
+
> onto `arWorldGroup.matrix` each frame, and thereby **GPS-registers the view**:
|
|
205
|
+
> the camera and every object parented under `arWorldGroup` (including GPS
|
|
206
|
+
> anchors) shift together as alignment refines, so anchors stay stable in the AR
|
|
207
|
+
> overlay and only ever correct a small residual. The recorder wires its own
|
|
208
|
+
> lerper; the simpler apps use this helper. Forgetting it leaves the camera
|
|
209
|
+
> pure-VIO and forces each anchor to absorb the full alignment delta on every
|
|
210
|
+
> re-registration.
|
|
200
211
|
|
|
201
212
|
**Three options for placing your own `Object3D`:**
|
|
202
213
|
|
|
203
214
|
1. **Add it to `scene`** (with NUE-meter coordinates from `calcRelativeCoordsInMeters(zeroRef, …)`). The object's world pose stays at the correct latitude/longitude/altitude forever, but every time the alignment matrix is corrected the camera shifts inside `arWorldGroup`, so from the user's AR view the object visually "floats". Cheap and correct, but ugly during corrections — fine for small markers (e.g. ref-point spheres), not great for richer GPS-anchored content.
|
|
204
215
|
2. **Add it to `arWorldGroup`** with a fixed local transform. The object is frozen relative to AR-tracked content and stays visually fixed at the same 3D position for this session. This is a good fit for user-created local content such as a walked path, a temporary marker, a hit-test reticle, or an object the user placed by hand. The tradeoff is that its world / GPS pose drifts every time alignment is corrected, so this mode is not enough when the object must be replayed or shared by GPS coordinate.
|
|
205
|
-
3. **Use `createGpsAnchor` for objects that should stay visually stable at a GPS target.** The anchor owns a single `Object3D` inside `arWorldGroup
|
|
216
|
+
3. **Use `createGpsAnchor` for objects that should stay visually stable at a GPS target.** The anchor owns a single `Object3D` **inside `arWorldGroup`** (the factory throws if `object3D` is not a descendant of the `arWorldGroup` you pass — placing an anchor under the scene root defeats AR stability), bootstraps from median samples unless `skipBootstrap` is set, and re-derives the object's local pose from the current GPS target and alignment state each tick. Because the object rides the (lerped) `arWorldGroup` alignment, its motion relative to the camera between re-registrations is small. In the default `snap-when-offscreen` mode an accepted correction is committed **instantly** while the object is outside the camera frustum, making the (now small, residual) correction hard to notice; larger alignment jumps bypass that gate so the object does not stay in a stale location. Use `snap-every-tick` when correctness is more important than hiding visible position changes. Smoothing is **not** per-anchor: it lives once in the lerped `arWorldGroup.matrix` (`enableArWorldGroupAlignment`), so the whole AR world eases together. This is how the MinimalExample and AnchorStarter get the smooth alignment feel.
|
|
206
217
|
|
|
207
218
|
A pure-function `syncGpsAnchoredMeshes` reconciler (option 1, bulk markers) is shipped by the RecorderApp. Use `createGpsAnchor` when a single visible object, route cue, or POI needs the more careful bootstrap and correction policy.
|
|
208
219
|
|
|
220
|
+
> **Worked example.** The [`GpsPlusSlamJs_MinimalExample`](../GpsPlusSlamJs_MinimalExample/) ports the stock three.js `webxr_ar_hittest` example onto this convention and ends in a deliberate side-by-side **contrast demo**: a tap co-spawns an option-1 floater under `scene` and an option-3 `createGpsAnchor` marker under `arWorldGroup` at the same world pose, so the drift difference is visible. It is the canonical reference for options 1–3 and for the `registerXrFrameUpdate` + Enable-GPS-AR seams below.
|
|
221
|
+
|
|
222
|
+
## DOM-Overlay / HUD stacking convention
|
|
223
|
+
|
|
224
|
+
`initAR(container)` requests the WebXR **`dom-overlay`** feature with
|
|
225
|
+
`domOverlay.root = container` — the **same element you pass in**. During an
|
|
226
|
+
`immersive-ar` session the browser composites **only that element's subtree**
|
|
227
|
+
over the camera feed; everything else on the page is hidden until the session
|
|
228
|
+
ends.
|
|
229
|
+
|
|
230
|
+
> **The rule:** every HUD / overlay / button you want visible **in AR** must be
|
|
231
|
+
> a **DOM descendant of the element you pass to `initAR`**. A sibling overlay
|
|
232
|
+
> works in the 2D pre-AR layout but silently disappears the moment AR starts —
|
|
233
|
+
> a failure that only shows up on real AR hardware, never in a headless test.
|
|
234
|
+
|
|
235
|
+
This is **not** a `z-index` problem; `z-index`/`pointer-events` only govern the
|
|
236
|
+
2D (pre-AR) layout. Nesting is what determines in-AR visibility.
|
|
237
|
+
|
|
238
|
+
```html
|
|
239
|
+
<!-- ✅ Correct: HUD is inside the initAR container -->
|
|
240
|
+
<div id="app">
|
|
241
|
+
<!-- three.js canvas is injected here by initAR -->
|
|
242
|
+
<div id="hud">…</div>
|
|
243
|
+
</div>
|
|
244
|
+
|
|
245
|
+
<!-- ❌ Wrong: HUD is a sibling — it vanishes once AR starts -->
|
|
246
|
+
<div id="app"></div>
|
|
247
|
+
<div id="hud">…</div>
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
```js
|
|
251
|
+
await initAR(document.getElementById('app')!); // #app's subtree = the overlay root
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
A repo-meta guard (`tests/repo-config/hud-overlay-nesting.test.js`) asserts this
|
|
255
|
+
structurally for every app's `index.html`, so a new app that authors its overlay
|
|
256
|
+
as a sibling fails CI instead of failing silently in the field. Add new apps to
|
|
257
|
+
that guard's `APP_OVERLAY_CONTRACTS` list.
|
|
258
|
+
|
|
209
259
|
## Modules
|
|
210
260
|
|
|
211
261
|
### `ar/` — WebXR & 3D Scene
|
|
212
262
|
|
|
213
263
|
WebXR session lifecycle, Three.js renderer setup, image/depth capture, replay scene management.
|
|
214
264
|
|
|
215
|
-
| Export
|
|
216
|
-
|
|
|
217
|
-
| `initAR(container)`
|
|
218
|
-
| `endARSession()`
|
|
219
|
-
| `
|
|
220
|
-
| `
|
|
221
|
-
| `
|
|
222
|
-
| `
|
|
223
|
-
| `
|
|
224
|
-
| `
|
|
265
|
+
| Export | Description |
|
|
266
|
+
| ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
267
|
+
| `initAR(container, isolation?, features?)` | Start a WebXR AR session with Three.js rendering. `features.requestHitTest` opts the session into the WebXR `hit-test` feature |
|
|
268
|
+
| `endARSession()` | End the active XR session |
|
|
269
|
+
| `createEnableGpsArController()` | Headless "Enable GPS AR" orchestration (support check + permission bundling + sensor watches + `initAR`) with observable state; the app renders its own button over it |
|
|
270
|
+
| `registerXrFrameUpdate(cb)` | Per-frame access to the live `XRFrame` + reference space + session (valid only synchronously inside the callback). Enables app-side hit-test / other WebXR features |
|
|
271
|
+
| `isFullySupported(s)` / `capabilityMessage(s)` | WebXR + geolocation capability gating + a user-facing message |
|
|
272
|
+
| `startImageCapture()` / `stopImageCapture()` | Toggle camera frame capture |
|
|
273
|
+
| `ImageCaptureManager` | Configurable camera frame capture pipeline |
|
|
274
|
+
| `DepthSampler` | Depth buffer sampling with configurable grids |
|
|
275
|
+
| `CameraBlitCapture` | GPU blit-based camera capture |
|
|
276
|
+
| `initReplayScene(container)` | Create a 3D replay scene with orbit/FPS controls |
|
|
277
|
+
| `applyChromiumProjectionLayerWorkaround` | Chromium camera-access tab-crash workaround. Always deletes projection-layer hooks (forces `XRWebGLLayer`; required on every affected build incl. Chrome 150) and additionally persists `baseLayer` only on the affected Chrome window (148.0.7778.12 up to 149.0.7821) |
|
|
225
278
|
|
|
226
279
|
### `sensors/` — GPS & Permissions
|
|
227
280
|
|
|
@@ -354,10 +407,11 @@ src/
|
|
|
354
407
|
|
|
355
408
|
This framework is licensed under **Apache 2.0** — see [LICENSE](LICENSE).
|
|
356
409
|
|
|
357
|
-
> **Note:** This package depends on [gps-plus-slam-js](https://www.npmjs.com/package/gps-plus-slam-js), which is a **closed-source, proprietary** library distributed via npm under a separate license. A community license key is bundled with the framework
|
|
410
|
+
> **Note:** This package depends on [gps-plus-slam-js](https://www.npmjs.com/package/gps-plus-slam-js), which is a **closed-source, proprietary** library distributed via npm under a separate license. A free community license key is bundled with the framework so you can start building right away — no signup or API key required. See the core library's EULA for the full terms.
|
|
358
411
|
|
|
359
412
|
## See Also
|
|
360
413
|
|
|
361
414
|
- [gps-plus-slam-js](https://www.npmjs.com/package/gps-plus-slam-js) — Core alignment algorithms (closed-source)
|
|
362
|
-
- [`GpsPlusSlamJs_MinimalExample`](../GpsPlusSlamJs_MinimalExample/) — Smallest possible consumer of this framework
|
|
415
|
+
- [`GpsPlusSlamJs_MinimalExample`](../GpsPlusSlamJs_MinimalExample/) — Smallest possible consumer of this framework (trivial rung)
|
|
416
|
+
- [`GpsPlusSlamJs_AnchorStarter`](../GpsPlusSlamJs_AnchorStarter/) — Meaningful-minimal starter: a persistent GPS anchor across reload (starter rung)
|
|
363
417
|
- [`GpsPlusSlamJs_RecorderApp`](../GpsPlusSlamJs_RecorderApp/) — Full-featured recording app built on this framework
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { L as CombinedRootState, r as SlamAppRootState } from "./create-slam-app-store-
|
|
1
|
+
import { L as CombinedRootState, r as SlamAppRootState } from "./create-slam-app-store-B76AGaI0.js";
|
|
2
2
|
import * as _$gps_plus_slam_js0 from "gps-plus-slam-js";
|
|
3
3
|
import { ArImageCapture, GpsPoint, LatLong, Matrix4, Quaternion, Vector3 } from "gps-plus-slam-js";
|
|
4
4
|
import * as _$_reduxjs_toolkit0 from "@reduxjs/toolkit";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { i as computeCaptureSize, n as CameraBlitCaptureConfig, r as DEFAULT_BLIT_CONFIG, t as CameraBlitCapture } from "../camera-blit-capture-
|
|
1
|
+
import { i as computeCaptureSize, n as CameraBlitCaptureConfig, r as DEFAULT_BLIT_CONFIG, t as CameraBlitCapture } from "../camera-blit-capture-75WWa9Xb.js";
|
|
2
2
|
export { CameraBlitCapture, CameraBlitCaptureConfig, DEFAULT_BLIT_CONFIG, computeCaptureSize };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
//#region ../src/ar/capability-checker.ts
|
|
2
|
+
/** True only when both AR and GPS are available — the demo can run. */
|
|
3
|
+
function isFullySupported(support) {
|
|
4
|
+
return support.webxr && support.geolocation;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Build the capability-gated message shown when the demo cannot run. Names
|
|
8
|
+
* exactly which capabilities are missing so the user understands *why*, and
|
|
9
|
+
* always points them at the supported environment (an AR phone, outdoors).
|
|
10
|
+
* Returns `null` when everything is supported (no message needed).
|
|
11
|
+
*
|
|
12
|
+
* The trailing "to try …" clause is app-specific and supplied via
|
|
13
|
+
* `options.contextLabel`; without it the message ends neutrally at
|
|
14
|
+
* "…outdoors.".
|
|
15
|
+
*/
|
|
16
|
+
function capabilityMessage(support, options = {}) {
|
|
17
|
+
if (isFullySupported(support)) return null;
|
|
18
|
+
const missing = [];
|
|
19
|
+
if (!support.webxr) missing.push("WebXR augmented reality");
|
|
20
|
+
if (!support.geolocation) missing.push("GPS / geolocation");
|
|
21
|
+
const suffix = options.contextLabel ? `, to try ${options.contextLabel}` : "";
|
|
22
|
+
return `This demo needs ${missing.join(" and ")}, which this device or browser does not provide. Open it on an AR-capable phone (e.g. Chrome on Android with ARCore), outdoors${suffix}.`;
|
|
23
|
+
}
|
|
24
|
+
//#endregion
|
|
25
|
+
export { capabilityMessage, isFullySupported };
|
|
@@ -1,2 +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-
|
|
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-D4HYJNOj.js";
|
|
2
2
|
export { CAPTURE_FAILURE_WARNING, CaptureFailureTracker, CaptureFailureTrackerConfig, DEFAULT_CAPTURE_TRACKER_CONFIG, createCaptureFailureTracker };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export { ChromiumProjectionLayerWorkaroundResult, applyChromiumProjectionLayerWorkaround };
|
|
1
|
+
import { a as applyChromiumProjectionLayerWorkaround, i as ChromiumProjectionLayerWorkaroundResult, n as BASELAYER_WINDOW_MIN, o as needsBaseLayerPersistence, r as ChromeVersion, s as parseChromeVersion, t as BASELAYER_WINDOW_MAX } from "../chromium-camera-access-workaround-MifIwK9x.js";
|
|
2
|
+
export { BASELAYER_WINDOW_MAX, BASELAYER_WINDOW_MIN, ChromeVersion, ChromiumProjectionLayerWorkaroundResult, applyChromiumProjectionLayerWorkaround, needsBaseLayerPersistence, parseChromeVersion };
|
|
@@ -1,20 +1,207 @@
|
|
|
1
|
+
import { createLogger } from "../utils/logger.js";
|
|
1
2
|
//#region ../src/ar/chromium-camera-access-workaround.ts
|
|
2
3
|
/**
|
|
4
|
+
* Chromium WebXR camera-access tab-crash workaround.
|
|
5
|
+
*
|
|
6
|
+
* Background:
|
|
7
|
+
* Requesting `camera-access` as an optional WebXR feature on Android Chrome
|
|
8
|
+
* 147+ causes a fatal renderer-process crash (`CrRendererMain`) 1–2 seconds
|
|
9
|
+
* after entering AR. The crash reproduces in the upstream three.js
|
|
10
|
+
* `webxr_ar_hittest.html` example with `optionalFeatures: ['camera-access']`
|
|
11
|
+
* added — i.e. it is not specific to this app's session setup. See:
|
|
12
|
+
*
|
|
13
|
+
* - GpsPlusSlamJs_Docs/docs/2026-04-22-camera-access-reproducer-plan.md
|
|
14
|
+
* - GpsPlusSlamJs_Docs/docs/2026-06-04-camera-access-crash-regression-chrome-148.md
|
|
15
|
+
* - https://github.com/mrdoob/three.js/issues/33404
|
|
16
|
+
* - https://issues.chromium.org/issues/507508099 (root-cause, marked Fixed)
|
|
17
|
+
*
|
|
18
|
+
* There is NOT a single crash here — the Chromium tracker notes the crash on
|
|
19
|
+
* Chrome 147 is different from the crash on 148+, and the simple workaround
|
|
20
|
+
* below only helped on a subset of versions. The empirically-verified
|
|
21
|
+
* (on-device) timeline is:
|
|
22
|
+
*
|
|
23
|
+
* - 147 : deleting `createProjectionLayer` / `layers` forces three.js onto
|
|
24
|
+
* `XRWebGLLayer` and sidesteps the crash (deletes-only is sufficient).
|
|
25
|
+
* - 148.0.7778.12 .. 149.0.7821 : the delete-only trick is NOT sufficient on
|
|
26
|
+
* its own. An ADDITIONAL patch is needed: persist the `baseLayer`
|
|
27
|
+
* reference across `XRSession.prototype.updateRenderState` so three.js's
|
|
28
|
+
* later `depthNear`/`depthFar` update does not drop the active
|
|
29
|
+
* `glBaseLayer`. (Confirmed on-device on `148.0.7778.215`: BOTH the
|
|
30
|
+
* deletes and the baseLayer patch are required for the page not to
|
|
31
|
+
* crash. Earlier 148 builds (`< .7778.12`) are below the window per the
|
|
32
|
+
* tracker, which reported delete-only worked there.)
|
|
33
|
+
* - > 149.0.7821 (incl. Chrome 150) : the delete-only path is STILL required
|
|
34
|
+
* (confirmed on-device: Chrome 150 only stops crashing when the deletes
|
|
35
|
+
* are applied), but the extra baseLayer-persistence patch is NOT needed.
|
|
36
|
+
* The earlier assumption that Chromium fully fixed this on patched
|
|
37
|
+
* builds did not hold on real devices, so we no longer skip the
|
|
38
|
+
* workaround on "patched" Chrome.
|
|
39
|
+
*
|
|
40
|
+
* Resulting policy (see {@link applyChromiumProjectionLayerWorkaround}):
|
|
41
|
+
* - ALWAYS apply the deletes (every Chrome build, and unknown/non-Chromium
|
|
42
|
+
* environments — restoring the original always-on behavior).
|
|
43
|
+
* - Apply the baseLayer-persistence patch ONLY when a detected Chrome build
|
|
44
|
+
* falls inside the affected window
|
|
45
|
+
* [{@link BASELAYER_WINDOW_MIN} .. {@link BASELAYER_WINDOW_MAX}].
|
|
46
|
+
*
|
|
47
|
+
* Caveats:
|
|
48
|
+
* - This is a Chromium-specific hack and the upstream comment explicitly
|
|
49
|
+
* warns it "might break webxr on other devices" — keep it opt-in.
|
|
50
|
+
* - Must run BEFORE any WebXR session setup. Three.js reads these prototype
|
|
51
|
+
* members lazily when the first session starts, so calling this at app
|
|
52
|
+
* bootstrap (before `initAR()`) is sufficient.
|
|
53
|
+
* - Idempotent: safe to call repeatedly. Safe on environments where the
|
|
54
|
+
* prototypes do not exist (e.g. desktop browsers, jsdom).
|
|
55
|
+
*/
|
|
56
|
+
const log = createLogger("chromium-camera-access");
|
|
57
|
+
/** Marker set on our patched `updateRenderState` so we never double-wrap it. */
|
|
58
|
+
const BASE_LAYER_PATCH_MARKER = "__gpsBaseLayerPersistencePatch";
|
|
59
|
+
/**
|
|
60
|
+
* Inclusive lower bound of the Chrome window that additionally needs the
|
|
61
|
+
* `baseLayer`-persistence patch (on top of the deletes).
|
|
62
|
+
*
|
|
63
|
+
* Set to `148.0.7778.12`, the issue tracker's figure for when the delete-only
|
|
64
|
+
* trick stopped being sufficient. On-device confirmation: `148.0.7778.215`
|
|
65
|
+
* (well inside this window) crashes with deletes-only and needs BOTH patches.
|
|
66
|
+
* Earlier 148 builds (`< .7778.12`) and all of Chrome 147 stay below this
|
|
67
|
+
* bound (deletes-only) per the documented timeline — we have no on-device
|
|
68
|
+
* evidence that they need the baseLayer patch, so we do not apply it there.
|
|
69
|
+
*/
|
|
70
|
+
const BASELAYER_WINDOW_MIN = [
|
|
71
|
+
148,
|
|
72
|
+
0,
|
|
73
|
+
7778,
|
|
74
|
+
12
|
|
75
|
+
];
|
|
76
|
+
/**
|
|
77
|
+
* Inclusive upper bound of the Chrome window that additionally needs the
|
|
78
|
+
* `baseLayer`-persistence patch. The crash is fixed after `149.0.7819.0` and
|
|
79
|
+
* the camera image is correctly populated after `149.0.7821.0`; above this
|
|
80
|
+
* build the extra patch is no longer required (the deletes still are — see
|
|
81
|
+
* the module header and the on-device matrix for Chrome 150).
|
|
82
|
+
*
|
|
83
|
+
* @see https://github.com/mrdoob/three.js/issues/33404
|
|
84
|
+
*/
|
|
85
|
+
const BASELAYER_WINDOW_MAX = [
|
|
86
|
+
149,
|
|
87
|
+
0,
|
|
88
|
+
7821,
|
|
89
|
+
0
|
|
90
|
+
];
|
|
91
|
+
/**
|
|
92
|
+
* Parse a Chrome/Chromium version from a user-agent string.
|
|
93
|
+
*
|
|
94
|
+
* Matches the `Chrome/<major>.<minor>.<build>.<patch>` token (and the iOS
|
|
95
|
+
* `CriOS/...` variant). Returns null for non-Chromium user agents.
|
|
96
|
+
*/
|
|
97
|
+
function parseChromeVersion(userAgent) {
|
|
98
|
+
const match = /(?:Chrome|CriOS)\/(\d+)\.(\d+)\.(\d+)\.(\d+)/.exec(userAgent);
|
|
99
|
+
if (!match) return null;
|
|
100
|
+
return [
|
|
101
|
+
Number(match[1]),
|
|
102
|
+
Number(match[2]),
|
|
103
|
+
Number(match[3]),
|
|
104
|
+
Number(match[4])
|
|
105
|
+
];
|
|
106
|
+
}
|
|
107
|
+
/** Lexicographically compare two version tuples: returns true if `a` > `b`. */
|
|
108
|
+
function isVersionAfter(a, b) {
|
|
109
|
+
for (let i = 0; i < a.length; i++) {
|
|
110
|
+
const av = a[i] ?? 0;
|
|
111
|
+
const bv = b[i] ?? 0;
|
|
112
|
+
if (av !== bv) return av > bv;
|
|
113
|
+
}
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
/** True if `a` >= `b` (lexicographic). */
|
|
117
|
+
function isVersionAtLeast(a, b) {
|
|
118
|
+
return !isVersionAfter(b, a);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* True when the detected Chrome build falls inside the affected window
|
|
122
|
+
* [{@link BASELAYER_WINDOW_MIN} .. {@link BASELAYER_WINDOW_MAX}] (inclusive)
|
|
123
|
+
* and therefore additionally needs the `baseLayer`-persistence patch. Outside
|
|
124
|
+
* the window (including non-Chromium user agents) returns false — only the
|
|
125
|
+
* deletes are needed there.
|
|
126
|
+
*/
|
|
127
|
+
function needsBaseLayerPersistence(userAgent) {
|
|
128
|
+
const version = parseChromeVersion(userAgent);
|
|
129
|
+
if (version === null) return false;
|
|
130
|
+
return isVersionAtLeast(version, BASELAYER_WINDOW_MIN) && isVersionAtLeast(BASELAYER_WINDOW_MAX, version);
|
|
131
|
+
}
|
|
132
|
+
function getUserAgent() {
|
|
133
|
+
return globalThis.navigator?.userAgent ?? "";
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Wrap `XRSession.prototype.updateRenderState` so the `baseLayer` reference is
|
|
137
|
+
* persisted across calls. Three.js calls `updateRenderState({ depthNear,
|
|
138
|
+
* depthFar })` after the base layer was set; on affected Chrome builds that
|
|
139
|
+
* second call drops the active `glBaseLayer`, which leads to the crash on
|
|
140
|
+
* camera-texture access. Re-supplying the last-seen `baseLayer` keeps it
|
|
141
|
+
* alive.
|
|
142
|
+
*
|
|
143
|
+
* The last-seen `baseLayer` is tracked **per `XRSession` instance** via a
|
|
144
|
+
* `WeakMap` keyed on the call's `this`. A single shared variable would leak a
|
|
145
|
+
* previous session's `baseLayer` into a later session: an `XRWebGLLayer` is
|
|
146
|
+
* bound to the session it was created for, so re-supplying a stale layer to a
|
|
147
|
+
* different session throws `InvalidStateError`. Keying per session also lets
|
|
148
|
+
* the entries be garbage-collected with their sessions.
|
|
149
|
+
*
|
|
150
|
+
* Idempotent: a marker on the wrapper prevents double-wrapping.
|
|
151
|
+
*
|
|
152
|
+
* @returns true if the prototype was wrapped on this call.
|
|
153
|
+
*/
|
|
154
|
+
function patchUpdateRenderStateForBaseLayerPersistence() {
|
|
155
|
+
const session = globalThis.XRSession;
|
|
156
|
+
const original = session?.prototype.updateRenderState;
|
|
157
|
+
if (!session || typeof original !== "function") return false;
|
|
158
|
+
if (original[BASE_LAYER_PATCH_MARKER]) return false;
|
|
159
|
+
const lastBaseLayerBySession = /* @__PURE__ */ new WeakMap();
|
|
160
|
+
const patched = function(rawInit) {
|
|
161
|
+
const init = typeof rawInit === "object" && rawInit !== null ? rawInit : {};
|
|
162
|
+
if (typeof this !== "object" || this === null) return original.call(this, init);
|
|
163
|
+
if (init.baseLayer !== void 0) lastBaseLayerBySession.set(this, init.baseLayer);
|
|
164
|
+
const remembered = lastBaseLayerBySession.get(this);
|
|
165
|
+
return original.call(this, remembered !== void 0 ? {
|
|
166
|
+
...init,
|
|
167
|
+
baseLayer: remembered
|
|
168
|
+
} : init);
|
|
169
|
+
};
|
|
170
|
+
patched[BASE_LAYER_PATCH_MARKER] = true;
|
|
171
|
+
session.prototype.updateRenderState = patched;
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
3
175
|
* Apply the Chromium camera-access tab-crash workaround.
|
|
4
176
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
177
|
+
* Policy (derived from on-device testing — see the module header):
|
|
178
|
+
*
|
|
179
|
+
* - ALWAYS removes `XRWebGLBinding.prototype.createProjectionLayer`
|
|
180
|
+
* (three.js r184) and `XRRenderState.prototype.layers` (three.js r158) so
|
|
181
|
+
* three.js falls back to `XRWebGLLayer`. This is required on every affected
|
|
182
|
+
* Chrome build observed so far, including Chrome 150, and is also applied
|
|
183
|
+
* on unknown/non-Chromium environments (restoring the original always-on
|
|
184
|
+
* behavior).
|
|
185
|
+
* - ADDITIONALLY persists the `baseLayer` across
|
|
186
|
+
* `XRSession.prototype.updateRenderState`, but ONLY when a detected Chrome
|
|
187
|
+
* build falls inside the affected window
|
|
188
|
+
* [{@link BASELAYER_WINDOW_MIN} .. {@link BASELAYER_WINDOW_MAX}]. Outside
|
|
189
|
+
* that window the extra patch is unnecessary (and is skipped on unknown
|
|
190
|
+
* environments to avoid touching projection-layer devices like Quest).
|
|
9
191
|
*
|
|
10
192
|
* Call once during bootstrap, before any `requestSession()` call.
|
|
11
193
|
*
|
|
12
|
-
* @
|
|
194
|
+
* @param options.userAgent override the detected user agent (for testing).
|
|
195
|
+
* @returns which prototype members were actually changed on this call.
|
|
13
196
|
*/
|
|
14
|
-
function applyChromiumProjectionLayerWorkaround() {
|
|
197
|
+
function applyChromiumProjectionLayerWorkaround(options) {
|
|
198
|
+
const userAgent = options?.userAgent ?? getUserAgent();
|
|
199
|
+
const version = parseChromeVersion(userAgent);
|
|
15
200
|
const result = {
|
|
16
201
|
deletedCreateProjectionLayer: false,
|
|
17
|
-
deletedRenderStateLayers: false
|
|
202
|
+
deletedRenderStateLayers: false,
|
|
203
|
+
patchedUpdateRenderState: false,
|
|
204
|
+
detectedChromeVersion: version ? version.join(".") : null
|
|
18
205
|
};
|
|
19
206
|
const binding = globalThis.XRWebGLBinding;
|
|
20
207
|
if (binding && "createProjectionLayer" in binding.prototype) {
|
|
@@ -26,7 +213,9 @@ function applyChromiumProjectionLayerWorkaround() {
|
|
|
26
213
|
delete renderState.prototype.layers;
|
|
27
214
|
result.deletedRenderStateLayers = true;
|
|
28
215
|
}
|
|
216
|
+
if (needsBaseLayerPersistence(userAgent)) result.patchedUpdateRenderState = patchUpdateRenderStateForBaseLayerPersistence();
|
|
217
|
+
log.info("applied workaround", result);
|
|
29
218
|
return result;
|
|
30
219
|
}
|
|
31
220
|
//#endregion
|
|
32
|
-
export { applyChromiumProjectionLayerWorkaround };
|
|
221
|
+
export { BASELAYER_WINDOW_MAX, BASELAYER_WINDOW_MIN, applyChromiumProjectionLayerWorkaround, needsBaseLayerPersistence, parseChromeVersion };
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { i as DepthSample } from "../ar-types-
|
|
2
|
-
import { i as DepthSamplerConfig, n as DepthSampler, r as DepthSamplerCallbacks, t as DepthInfo } from "../depth-sampler-
|
|
1
|
+
import { i as DepthSample } from "../ar-types-isPsQptb.js";
|
|
2
|
+
import { i as DepthSamplerConfig, n as DepthSampler, r as DepthSamplerCallbacks, t as DepthInfo } from "../depth-sampler-CqrkRr2N.js";
|
|
3
3
|
export { DepthInfo, DepthSample, DepthSampler, DepthSamplerCallbacks, DepthSamplerConfig };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { a as EnableGpsArState, i as EnableGpsArResult, n as EnableGpsArController, o as EnableGpsArStatus, r as EnableGpsArDeps, s as createEnableGpsArController, t as EnableGpsArConfig } from "../enable-gps-ar-DNbO0zbg.js";
|
|
2
|
+
export { EnableGpsArConfig, EnableGpsArController, EnableGpsArDeps, EnableGpsArResult, EnableGpsArState, EnableGpsArStatus, createEnableGpsArController };
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { createLogger } from "../utils/logger.js";
|
|
2
|
+
import { requestGeolocationPermission, requestOrientationPermission, requestWebXRWithDepthPermission } from "../sensors/permission-checker.js";
|
|
3
|
+
import { startGpsWatch, startOrientationWatch, stopGpsWatch, stopOrientationWatch } from "../sensors/gps.js";
|
|
4
|
+
import { endARSession, initAR, isWebXRSupported } from "./webxr-session.js";
|
|
5
|
+
//#region ../src/ar/enable-gps-ar.ts
|
|
6
|
+
const log = createLogger("EnableGpsAr");
|
|
7
|
+
const defaultDeps = {
|
|
8
|
+
isWebXRSupported,
|
|
9
|
+
requestGeolocationPermission,
|
|
10
|
+
requestOrientationPermission,
|
|
11
|
+
requestWebXRWithDepthPermission,
|
|
12
|
+
startGpsWatch,
|
|
13
|
+
startOrientationWatch,
|
|
14
|
+
initAR,
|
|
15
|
+
stopGpsWatch,
|
|
16
|
+
stopOrientationWatch,
|
|
17
|
+
endARSession
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Create an "Enable GPS AR" controller. Pass partial `deps` in tests to inject
|
|
21
|
+
* fakes; production callers use the defaults.
|
|
22
|
+
*/
|
|
23
|
+
function createEnableGpsArController(deps = {}) {
|
|
24
|
+
const resolved = {
|
|
25
|
+
...defaultDeps,
|
|
26
|
+
...deps
|
|
27
|
+
};
|
|
28
|
+
let state = { status: "checking" };
|
|
29
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
30
|
+
let gpsWatchActive = false;
|
|
31
|
+
let orientationWatchActive = false;
|
|
32
|
+
function setState(next) {
|
|
33
|
+
state = next;
|
|
34
|
+
for (const listener of [...listeners]) try {
|
|
35
|
+
listener(state);
|
|
36
|
+
} catch (err) {
|
|
37
|
+
log.error("State listener threw; continuing dispatch:", err);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async function refreshSupport() {
|
|
41
|
+
if (state.status === "starting" || state.status === "running" || state.status === "stopping") return;
|
|
42
|
+
setState({ status: "checking" });
|
|
43
|
+
const supported = await probeSupport();
|
|
44
|
+
if (state.status !== "checking") return;
|
|
45
|
+
setState({ status: supported ? "ready" : "unsupported" });
|
|
46
|
+
}
|
|
47
|
+
async function probeSupport() {
|
|
48
|
+
try {
|
|
49
|
+
return await resolved.isWebXRSupported();
|
|
50
|
+
} catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Request the configured permissions. Returns an error message when a
|
|
56
|
+
* *blocking* permission (geolocation, or depth when opted in) is not granted,
|
|
57
|
+
* or `null` on success. Orientation is best-effort and never blocks.
|
|
58
|
+
*/
|
|
59
|
+
async function requestPermissions(config) {
|
|
60
|
+
const geolocation = await resolved.requestGeolocationPermission();
|
|
61
|
+
if (geolocation.granted !== true) return geolocation.error ?? "Location permission is required for GPS AR.";
|
|
62
|
+
try {
|
|
63
|
+
await resolved.requestOrientationPermission();
|
|
64
|
+
} catch {}
|
|
65
|
+
if (config.requestDepth) {
|
|
66
|
+
const depth = await resolved.requestWebXRWithDepthPermission();
|
|
67
|
+
if (depth.granted !== true) return depth.error ?? "Depth (3D map) permission was denied.";
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
async function enable(config) {
|
|
72
|
+
if (state.status === "starting" || state.status === "running" || state.status === "stopping") return {
|
|
73
|
+
ok: false,
|
|
74
|
+
error: "AR is already starting, running or stopping."
|
|
75
|
+
};
|
|
76
|
+
setState({ status: "starting" });
|
|
77
|
+
try {
|
|
78
|
+
const permissionError = await requestPermissions(config);
|
|
79
|
+
if (permissionError !== null) return fail(permissionError);
|
|
80
|
+
await resolved.initAR(config.container, config.isolationOptions, { requestHitTest: config.requestHitTest });
|
|
81
|
+
if (config.onGpsPosition) {
|
|
82
|
+
resolved.startGpsWatch(config.onGpsPosition);
|
|
83
|
+
gpsWatchActive = true;
|
|
84
|
+
}
|
|
85
|
+
if (config.onOrientation) {
|
|
86
|
+
resolved.startOrientationWatch(config.onOrientation);
|
|
87
|
+
orientationWatchActive = true;
|
|
88
|
+
}
|
|
89
|
+
setState({ status: "running" });
|
|
90
|
+
return { ok: true };
|
|
91
|
+
} catch (err) {
|
|
92
|
+
return fail(err instanceof Error ? err.message : String(err));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function fail(error) {
|
|
96
|
+
setState({
|
|
97
|
+
status: "error",
|
|
98
|
+
error
|
|
99
|
+
});
|
|
100
|
+
return {
|
|
101
|
+
ok: false,
|
|
102
|
+
error
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
async function disable() {
|
|
106
|
+
if (state.status !== "running") return;
|
|
107
|
+
setState({ status: "stopping" });
|
|
108
|
+
if (gpsWatchActive) {
|
|
109
|
+
resolved.stopGpsWatch();
|
|
110
|
+
gpsWatchActive = false;
|
|
111
|
+
}
|
|
112
|
+
if (orientationWatchActive) {
|
|
113
|
+
resolved.stopOrientationWatch();
|
|
114
|
+
orientationWatchActive = false;
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
await resolved.endARSession();
|
|
118
|
+
} catch (err) {
|
|
119
|
+
log.error("endARSession threw during disable; continuing teardown:", err);
|
|
120
|
+
}
|
|
121
|
+
setState({ status: "ready" });
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
getState: () => state,
|
|
125
|
+
subscribe: (listener) => {
|
|
126
|
+
listeners.add(listener);
|
|
127
|
+
return () => {
|
|
128
|
+
listeners.delete(listener);
|
|
129
|
+
};
|
|
130
|
+
},
|
|
131
|
+
refreshSupport,
|
|
132
|
+
enable,
|
|
133
|
+
disable
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
//#endregion
|
|
137
|
+
export { createEnableGpsArController };
|
package/dist/ar/frame-loop.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { i as runFrameUpdates, n as clearFrameUpdates, r as registerFrameUpdate, t as FrameUpdate } from "../frame-loop-
|
|
1
|
+
import { i as runFrameUpdates, n as clearFrameUpdates, r as registerFrameUpdate, t as FrameUpdate } from "../frame-loop-BTeRpDm4.js";
|
|
2
2
|
export { FrameUpdate, clearFrameUpdates, registerFrameUpdate, runFrameUpdates };
|
package/dist/ar/frame-loop.js
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
|
+
import { createLogger } from "../utils/logger.js";
|
|
1
2
|
//#region ../src/ar/frame-loop.ts
|
|
3
|
+
/**
|
|
4
|
+
* Per-frame callback registry. The WebXR session owns the single
|
|
5
|
+
* `renderer.setAnimationLoop(...)` hook for the page; components that need
|
|
6
|
+
* a per-frame tick register here and the session's `onXRFrame` invokes
|
|
7
|
+
* `runFrameUpdates(dt, elapsed)` once per frame.
|
|
8
|
+
*
|
|
9
|
+
* Plain functions, not a class: there is exactly one frame loop per session
|
|
10
|
+
* and singletons-of-singletons add no value. See
|
|
11
|
+
* `2026-05-13-ecs-migration-plan.md` for the design rationale and the
|
|
12
|
+
* rules new `FrameUpdate` bodies must follow (pure function of `dt` plus
|
|
13
|
+
* selectors; no direct Redux dispatch from inside a tick).
|
|
14
|
+
*/
|
|
15
|
+
const log = createLogger("FrameLoop");
|
|
2
16
|
const updates = /* @__PURE__ */ new Set();
|
|
3
17
|
/**
|
|
4
18
|
* Register a per-frame callback. Returns an unregister function.
|
|
@@ -23,10 +37,19 @@ function registerFrameUpdate(fn) {
|
|
|
23
37
|
* same frame are deferred to the next tick. Iterating the live `Set`
|
|
24
38
|
* would otherwise skip a not-yet-visited entry that an earlier handler
|
|
25
39
|
* unregistered — a hard-to-debug source of non-determinism.
|
|
40
|
+
*
|
|
41
|
+
* Each callback is invoked in its own `try/catch` so a throwing handler is
|
|
42
|
+
* isolated: it cannot abort the remaining callbacks nor propagate up through
|
|
43
|
+
* `onXRFrame` and kill the scene render for the whole frame. Failures are
|
|
44
|
+
* logged and the loop continues — mirrored by `runXrFrameUpdates`.
|
|
26
45
|
*/
|
|
27
46
|
function runFrameUpdates(dt, elapsed) {
|
|
28
47
|
const snapshot = Array.from(updates);
|
|
29
|
-
for (const fn of snapshot)
|
|
48
|
+
for (const fn of snapshot) try {
|
|
49
|
+
fn(dt, elapsed);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
log.error("A registered FrameUpdate threw; continuing the loop", error);
|
|
52
|
+
}
|
|
30
53
|
}
|
|
31
54
|
/**
|
|
32
55
|
* Clear all registrations. Called from `resetWebXRState()` so a fresh
|
|
@@ -1,2 +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-
|
|
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-BXZUmj7j.js";
|
|
2
2
|
export { CapturedImage, DEFAULT_CAPTURE_CONFIG, ImageCaptureCallbacks, ImageCaptureConfig, ImageCaptureManager, MIN_VALID_IMAGE_BYTES };
|