@displayxr/inline3d 1.29.0 → 1.31.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/CHANGELOG.md CHANGED
@@ -5,6 +5,193 @@ entry points (`.`, `./three`) are frozen for 1.x, while the **scene subpaths** (
5
5
  `./splat`, `./model`) are a preview tier whose options may change in any release. Entries below say
6
6
  which tier they touch, because that is what tells you whether an upgrade can move your pixels.
7
7
 
8
+ ## 1.31.0 — 2026-10-05
9
+
10
+ The 1.29 `./call` option spellings stay accepted (with their warning) for one more release; they go in 1.32.
11
+
12
+ Touches the **preview tier** (`./viewer`, `./splat`, `./model`: the eye cameras they draw) and adds
13
+ one **core** option and export. Pixels move only around a tracking acquisition or loss.
14
+
15
+ ### Added — the tracking ease (`viewerEase`, default on)
16
+
17
+ - When a viewer is acquired (the views jump from the untracked fallback viewpoint to the tracked
18
+ eyes) or lost, the SDK's renderers now glide the eye cameras from where they were drawn to the
19
+ live views over 300 ms (smoothstep) instead of snapping in one frame. Triggered by a jump of more
20
+ than 0.15 eye separations in one frame within 600 ms of a `trackingState` edge (the state and the
21
+ jump need not land on the same frame; an edge with no jump changes nothing), or, where the browser
22
+ reports no state, by a one-frame jump of more than half an eye separation. The eye positions for that test are read off the projections,
23
+ so rig changes, scene scale and canvas size do not trigger it.
24
+ - Exact, not approximate: every DisplayXR frustum is affine in the eye position, so offsetting the
25
+ pose (in the view's own frame) and the projection (element-wise) by the same weight gives the
26
+ frustum the runtime would report for the eased eye. The rig map and the layer rig see consistent
27
+ views throughout.
28
+ - Left alone: ordinary head motion, a vendor-animated ramp, view-count changes (2D<->3D mode switch),
29
+ buffer resizes, replayed frames, mono.
30
+ - `createInline3D({ viewerEase })` sets the session default; `viewerEase` on `SceneViewer`,
31
+ `addSplat` and `addModel` overrides it. `false` (or `{ durationMs: 0 }`) restores the snap;
32
+ `{ durationMs, easing }` tunes it. Core exports `ViewerEase`, `resolveViewerEaseOption`,
33
+ `frameTrackingState`, `VIEWER_EASE_DEFAULT_MS` for pages that render their own `addScene`.
34
+ `handle.viewer.viewerEase.last` reports the last frame's weight and trigger.
35
+
36
+ ## 1.30.0 — 2026-10-05
37
+
38
+ Touches the **preview tier** only (`./call`, `./call/full`, the new `./camera` and `./splat/playcanvas`). RFC 0003
39
+ **phase C2**: the `./call` public surface is trimmed to what a page needs (one warning release
40
+ for the old names), options are regrouped, theming gets a real API, and the stereo camera
41
+ becomes its own subpath. The core entry points are untouched.
42
+
43
+ ### `./splat/playcanvas` — new preview subpath: splats without Spark/three on the page
44
+
45
+ - **`addSplat` from `@displayxr/inline3d/splat/playcanvas`** is `./splat`'s `addSplat` with the
46
+ engine fixed to `'playcanvas'`: same options, same synchronous handle. `./splat` imports `three`
47
+ and `@sparkjsdev/spark` statically (Spark is its default engine and its Spark path is
48
+ synchronous), so a page passing `engine: 'playcanvas'` there still downloaded both, measured at
49
+ ~1.7 MB gzipped on displayxr.org. This entry imports neither. `playcanvas` is still loaded on
50
+ demand, at the first `addSplat`. Passing any other `engine` throws at the call.
51
+ - Internal: the deferred PlayCanvas handle moved from `inline3d-splat.js` to
52
+ `inline3d-splat-deferred.js`, shared by both entries. `./splat` behaves exactly as before.
53
+ - `test/splat-pc-entry.test.mjs` walks the entry's static import graph and fails on any bare
54
+ package import, so the property can't silently regress.
55
+
56
+ ### `./camera` — new preview subpath (RFC 0003 §4)
57
+
58
+ - **`openCamera(opts)`** → a `StereoCamera`: device selection (`prefer: 'auto' | 'stereo' | 'mono'
59
+ | deviceId | MediaStream`, the `displayxrStereo` hint, the > 2.5:1 pair heuristic, the
60
+ busy-device skip), `calibration` / `rectify` (moved here from `./call`), `format` / `stereo`
61
+ (`{ rectified, baselineMm, horizontalFovDeg }`) / `stream` / `video`, `on('ended')` (revoked,
62
+ unplugged, taken — never fired by `close()`), `close()`. Rejects only when no camera opens, with
63
+ `code` `'camera-busy'` | `'permission-denied'` (new: a refused prompt / policy / insecure
64
+ context) | `'no-camera'`.
65
+ - **`addCameraView(wall, canvas, cam, { mirror, autoConverge, depth, aspect })`** — the self view:
66
+ woven on a 3D wall, flat elsewhere, mirrored **correctly** (each half mirrored AND the halves
67
+ swapped — pinned by a synthetic-pair test: a subject in front stays in front; the naive per-half
68
+ mirror is proven pseudoscopic). `autoConverge` runs the call's disparity tracker on the face.
69
+ - **`cam.capturePhoto({ type, quality, name })`** → `{ blob, width, height, layout, convergencePx,
70
+ stereo, suggestedName: '<base>_2x1.jpg', xmp }`: the RAW pair, with the stereo record written
71
+ INTO the JPEG as XMP (`dxr:` namespace — layout, columns×rows, convergence, baseline, FOV,
72
+ rectified, eye size, software). **`cam.record({ mimeType, mono, audio })`** → `stop()` →
73
+ `{ blob, mono?, suggestedName: '<base>_2x1.webm', monoSuggestedName?, convergencePx, tagged }`,
74
+ the record as `DXR_*` Matroska tags; `mono: true` adds a left-eye copy (Decision 13). Readers:
75
+ `readJpegStereoMeta`, `readWebmStereoMeta`, `readJpegXmp`, `parseStereoXmp`. Decision 9: the
76
+ metadata lives in the file, no sidecar. `suggestedName`, `isStereoCamera`, `CAMERA_SDK`.
77
+ - The call is built on it: `CallOptions.camera` accepts a `StereoCamera` (left open when the
78
+ call ends), `camera: 'auto'` opens one through `openCamera()`, and the call's self view IS an
79
+ `addCameraView` — one implementation of capture, mirroring and convergence. A camera that ends
80
+ under a call (`ended`) drops it to audio-only with "Retry camera".
81
+ - Also in the CDN bundle `dist/call.js` (~153 KB min now, was ~124). `camera.d.ts`,
82
+ `docs/camera.md`, `samples/camera/`.
83
+
84
+ ### `./call` — the public surface (RFC 0003 §2)
85
+
86
+ **Renamed / regrouped options** (the 1.29 spelling still works for this release with one
87
+ `console.warn` per key; removed in 1.31):
88
+
89
+ | 1.29 | 1.30 |
90
+ |---|---|
91
+ | `accent` | `theme.accent` |
92
+ | `tileAspect` | `theme.tileAspect` |
93
+ | `inviteBase`, `updateUrl` | `invite: { base, updateUrl }` |
94
+ | `browserUrl` | `landing: { browserUrl, allow2D }` |
95
+ | `maxLifted` | `liftOptions.max` |
96
+ | `calibration`, `rectify` | `openCamera({ calibration, rectify })` of `./camera`, passed as `camera` |
97
+ | `log` | `debug: true` + `diagnostics()` |
98
+ | `recoverSession`, `wallOptions` | removed — a call always recovers the document's session (`sharedInline3D()`) |
99
+ | `scrollIntoView` | removed — always on with `ui: true` |
100
+
101
+ **New options:** `theme` (`accent`, `ink`, `shell`, `danger`, `radius`, `font`, `tileAspect`,
102
+ `strings`) → CSS custom properties `--dxr-accent/-ink/-shell/-danger/-radius/-font` on the host
103
+ and the string table; `ui: 'tiles'` (badges + plates only); `layout: 'none'` (tiles carry
104
+ `data-dxr-peer`, `call.tile(id)` returns them, the grid box steps aside); `landing.allow2D`.
105
+ `<dxr-call>` attributes are unchanged (`accent`, `tile-aspect`, `invite-base`, `browser-url` map
106
+ to the new groups) plus `ui="tiles"`.
107
+
108
+ **Handle renames:** `cameraOff(on)` → `setCameraOff(on)` with **`cameraOff` now the boolean
109
+ getter**; `format` → `localFormat`; `join()` resolves `void`; **new** `tile(peerId)`,
110
+ `diagnostics()` (explicitly unstable: the wall, the transport, per-peer `hello` / `route` /
111
+ `convergencePx` / `autoConverge` / `lift` / `quality`, the lift pool). **Removed from the public
112
+ handle** (kept as non-enumerable getters that warn once, gone in 1.31): `wall`, `format`,
113
+ `sendHint`, `_debug`. `CallPeer` is trimmed to `id`, `format`, `display` (`'3D' | '2D→3D' |
114
+ '2D'`), `state`, `muted`, `cameraOff`, `speaking`; the rest is in `diagnostics()`.
115
+ `handle.state` no longer lists `'idle'`.
116
+
117
+ **Events:** `format` → **`display`** `{ id, display }` (the old event still fires for a listener
118
+ that subscribes to it, with a warning; gone in 1.31); `session` is internal (subscribing warns);
119
+ `left` carries `reason`. New types `CallErrorCode` (adds `'permission-denied'`), `CallWarningCode`,
120
+ `CallDisplay`, `CallTheme`, `CallStrings`, `LiftFunction`.
121
+
122
+ **Removed from the `./call` entry — 38 internal helpers**, each kept for this release as a
123
+ `@deprecated` wrapper that `console.warn`s once on first use (a primitive constant cannot warn)
124
+ and removed in 1.31; all stay importable by file path from `js/call/*.js` / `js/camera/*.js`:
125
+ `normalizeCallOptions`, `newRoomId`, `isValidRoomId`, `buildInviteLink`, `normalizeHello`,
126
+ `makeHello`, `routeFor`, `badgeFor`, `createLiveGate`, `convergenceShiftPx`, `lowPass`,
127
+ `clampShift`, `eyeCropRect`, `mirrorSwapOps`, `mirrorSwapPixels`, `maxBitrateKbps`,
128
+ `measureFocusDisparity`, `preferVideoCodecs`, `sortCodecCapabilities`, `VIDEO_CODEC_ORDER`,
129
+ `MeshTransport`, `clampMaxPeers`, `qrEncode`, `roomKey`, `SIGNAL_PROTOCOL`, `WIRE_VERSION`,
130
+ `CALL_SDK`, `PLATE_TEXT` (→ `theme.strings`), `CALL_ACCENTS` (→ `theme.accent` takes the names),
131
+ `normalizeMono3D`, `resolveLift`, `createLiftPool`, `createFrameWatch`, `liftConvergenceFor`,
132
+ `liftPriorityFor`, `setLiftPriority`, `defaultLiftSpecifier`, `LIFT_PRIORITY`. Types gone with
133
+ them: `CallHello`, `CallCalibration`, `CallRoute`, `Mono3DReason`, `CallMono3DInfo`,
134
+ `CallLiftHandle`, `CallLiftFunction` (→ `LiftFunction`), `LiftStreamPriority`, `CallLiftPool`.
135
+ **Stays public:** `mountCall`, `addCall`, `dxrSignaling`, `DXR_SIGNAL_DEFAULT`,
136
+ `parseInviteLink` (Decision 7), `DxrCallElement`, `defineCallElement`, `attrsToOpts`,
137
+ `CALL_EVENT_PREFIX` — 9 value exports (from 47).
138
+
139
+ Internal moves (no API): `js/call/capture.js` → `js/camera/capture.js`, `js/call/disparity.js` →
140
+ `js/camera/disparity.js`, the SBS geometry and convergence math → `js/camera/geometry.js` /
141
+ `js/camera/converge.js`, option normalisation → `js/call/options.js`; the old paths re-export.
142
+
143
+ ### Gates (RFC 0003 §7, C2)
144
+
145
+ - **API snapshot:** `tools/api-surface.mjs` pins the public surface of `call.d.ts`,
146
+ `call-full.d.ts` and `camera.d.ts` in `test/api-snapshot/*.api.txt`; `npm test` (and CI) fail
147
+ when the surface changes without `node tools/api-surface.mjs --update` in the same change. The
148
+ deprecated set is part of the record, so 1.31's removal is a visible diff.
149
+ - **Snippets:** `tools/check-snippets.mjs` compiles every fenced `js`/`ts`/`jsx` block of
150
+ `docs/call.md` + `docs/camera.md` and every call/camera sample against the public `.d.ts` only
151
+ (and fails on a deprecated import); run by `npm test` where `typescript` is installed and by
152
+ CI's typecheck job.
153
+ - **Tests:** camera unit tests (the mirror-and-swap proof, XMP/WebM round-trips, error codes,
154
+ view routes), the surface tests (deprecation wrappers, legacy options, handle renames, theme /
155
+ strings / parts / `ui: 'tiles'` / `layout: 'none'`), and a headless Chrome e2e
156
+ (`npm run test:e2e:camera`: the mirrored pair's pixels on a mock woven wall, a JPEG whose XMP
157
+ parses, a WebM whose tags parse). Panel runs still required: the mirrored self view on glass,
158
+ an SBS photo opened in `/player`.
159
+
160
+ ### `./call` + the hosted service (RFC 0003 §5, phase C3)
161
+
162
+ Also in this release: the hosted signalling service productised, with the **reference server**
163
+ (`signaling/`). Additive for pages; `dxr-signal/1` gains optional fields and new error codes only.
164
+
165
+ - **Publishable keys.** `mountCall(el, { key: 'pk_…' })` / `<dxr-call key="pk_…">` now mean
166
+ something: the hosted server validates the key against its origin list on connect, applies the
167
+ keyed tier's limits (10× anonymous), and attributes every session to the key id (billing
168
+ identity from day one; billing is off). Issued by hand: `tools/signal-keys.mjs`. No key = the
169
+ anonymous tier, as before, with tighter limits.
170
+ - **Limits** (RFC §5b): joins per address per minute, concurrent rooms per address / per key, room
171
+ lifetime (2 h / 8 h → `expired`), TURN mints per address per hour. A refused join is an `error`
172
+ with `rate-limited` (+ `error.retryMs`) / `quota` / `bad-key` / `origin-not-allowed` / `blocked`
173
+ — never a silent hang; reconnects honour `retryMs`.
174
+ - **TURN metering and the monthly cap** (RFC §5c, Decision 2): the service meters minted
175
+ credentials (and, when configured, Cloudflare's real per-key relay bytes) against the free
176
+ 1000 GB + a $100 hard cap. At 70 % anonymous credentials shorten, at 90 % anonymous sessions
177
+ get none (`warning` **`turn-shed`**), at 100 % nobody does (`error` **`turn-cap`**) — the call
178
+ still joins and direct P2P keeps working. `welcome.turn` / `SignalingSession.turn` carry the
179
+ decision (`SignalingTurnStatus`).
180
+ - **Domain move prepared, not cut over** (RFC §5g): the hosted server answers on every host of
181
+ `DXR_SIGNAL_ALIASES` (`dxr-signal.displayxr.workers.dev`, `signal.displayxr.org`) and
182
+ `dxrSignaling()` fails over between them on connect and reconnect. `DXR_SIGNAL_DEFAULT` is
183
+ unchanged; the alias list is internal (`js/call/signaling.js`, importable by path — the
184
+ `./call` entry keeps its 9 exports). `dxrSignaling(url, { aliases })` adds fallbacks for a
185
+ self-hosted URL. Options and the recommendation: `docs/signaling-domain.md`.
186
+ - **Docs:** `docs/privacy-call.md` (what the server sees, retention, the E2E caveat),
187
+ `signaling/README.md` (keys, limits, budget, self-host vars, a coturn recipe, staging),
188
+ `docs/call.md` troubleshooting rows for the new codes.
189
+ - **Server:** `signaling/gate.mjs` + `limits.mjs` + `keys.mjs` + `turn-budget.mjs`, shared by
190
+ the Worker (Durable Objects per subject + one budget, a KV namespace for keys, an hourly
191
+ analytics cron, `/admin/*` behind `ADMIN_TOKEN`) and the Node dev server (same admission in
192
+ memory). Staging twin `signaling/deploy/staging.toml`; the C3 gate
193
+ `test/e2e/signal-staging.e2e.mjs`.
194
+
8
195
  ## 1.29.0 — 2026-09-30
9
196
 
10
197
  Touches the **core** (additive: `sharedInline3D()`, `handle.rewoven()`) and the **preview tier**
package/README.md CHANGED
@@ -3,12 +3,12 @@
3
3
  Inline-3D **web samples** and a small JS SDK for the
4
4
  [DisplayXR Browser](https://github.com/DisplayXR/displayxr-browser) — the DisplayXR analog of
5
5
  [`immersive-web/webxr-samples`](https://github.com/immersive-web/webxr-samples). This is the canonical
6
- repo web developers clone to build glasses-free 3D pages, and the site the browser navigates to for the
6
+ repo web developers clone to build 3D pages for spatial displays, and the site the browser navigates to for the
7
7
  live demos.
8
8
 
9
9
  **▶ See it live:** <https://displayxr.github.io/displayxr-web/> — open in the
10
- [DisplayXR Browser](https://github.com/DisplayXR/displayxr-browser/releases) on DisplayXR hardware for
11
- glasses-free 3D; in any other browser the pages render as a normal 2D fallback, so they're safe to view
10
+ [DisplayXR Browser](https://github.com/DisplayXR/displayxr-browser/releases) on a spatial display to see
11
+ them in 3D; in any other browser the pages render as a normal 2D fallback, so they're safe to view
12
12
  anywhere.
13
13
 
14
14
  ## Install
@@ -24,6 +24,8 @@ npm install @displayxr/inline3d
24
24
  import { createInline3D } from '@displayxr/inline3d';
25
25
  import { EyeCamera, EdgeFeather } from '@displayxr/inline3d/three'; // optional three.js glue
26
26
  import { addSplat } from '@displayxr/inline3d/splat'; // experimental: 3DGS in a tile
27
+ // …or '@displayxr/inline3d/splat/playcanvas': the same addSplat on PlayCanvas, without the
28
+ // three + Spark download (use it whenever you pass engine: 'playcanvas')
27
29
  import { addModel } from '@displayxr/inline3d/model'; // experimental: glTF/GLB in a tile
28
30
  // (Draco / meshopt / KTX2 too —
29
31
  // you serve the decoder files)
@@ -105,7 +107,7 @@ that is intentionally **not** in 1.0): [`docs/sdk-stability.md`](docs/sdk-stabil
105
107
 
106
108
  ## Quick start
107
109
 
108
- One SDK call turns a `<canvas>` into a glasses-free-3D window. Everything degrades to plain 2D on a
110
+ One SDK call turns a `<canvas>` into a 3D window on a spatial display. Everything degrades to plain 2D on a
109
111
  non-DisplayXR browser, so a page is safe to ship anywhere.
110
112
 
111
113
  ```js
@@ -115,7 +117,7 @@ const wall = await createInline3D(); // opens an inline-3d session (detect
115
117
  if (!wall.supported) {
116
118
  // Not the DisplayXR Browser (or no 3D display) — your page's normal 2D content shows. Done.
117
119
  } else {
118
- // Woven, glasses-free 3D. Add content — one call per element:
120
+ // Woven 3D. Add content — one call per element:
119
121
  wall.addImage(canvas, 'photo-sbs.png'); // a still side-by-side 3D photo
120
122
  wall.addVideo(canvas, videoEl); // an SBS 3D video
121
123
  wall.addScene(canvas, (views, layer) => { /* render */ });// a live three.js / WebGL stereo scene
@@ -210,6 +212,8 @@ docs/
210
212
  the WebXR→inline-3d mapping table and the whole render loop
211
213
  woven-canvas-rules.md never show a raw side-by-side frame: the join, the rules,
212
214
  handle.firstWoven / rewoven(), reading the browser's `withheld` log line
215
+ camera.md The 3D camera primitive (/camera): openCamera, the mirrored self view,
216
+ capturePhoto / record with the stereo record inside the file.
213
217
  call.md 3D video calls: the <dxr-call> / mountCall() one-line embed, options,
214
218
  events, headless, theming, React, self-hosting, troubleshooting
215
219
  proposals/ browser-side asks the SDK is waiting on