@displayxr/inline3d 1.30.0 → 1.32.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 +112 -0
- package/README.md +5 -5
- package/call.d.ts +53 -4
- package/dist/call.js +4 -4
- package/index.d.ts +92 -0
- package/js/call/signaling.js +66 -9
- package/js/inline3d-call.js +31 -1
- package/js/inline3d-display-metrics.js +51 -0
- package/js/inline3d-model-playcanvas.js +1 -0
- package/js/inline3d-model.js +1 -0
- package/js/inline3d-splat-deferred.js +12 -0
- package/js/inline3d-splat-effects.js +92 -2
- package/js/inline3d-splat-playcanvas.js +284 -6
- package/js/inline3d-splat.js +15 -0
- package/js/inline3d-viewer-ease.js +360 -0
- package/js/inline3d-viewer.js +15 -4
- package/js/inline3d.js +34 -0
- package/model.d.ts +8 -1
- package/package.json +5 -2
- package/splat.d.ts +76 -1
- package/viewer.d.ts +9 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,83 @@ 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.32.0 — 2026-10-06
|
|
9
|
+
|
|
10
|
+
Touches the **core** additively (one new handle method, two new exports) and the **preview tier**
|
|
11
|
+
(`./splat`, PlayCanvas backend).
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Hosted signalling server (`signaling/`, not part of the npm package)
|
|
15
|
+
|
|
16
|
+
- **`welcome` is now always the first message a joined peer receives.** With a real TURN key the
|
|
17
|
+
server awaits a credential mint between reserving a peer's slot and welcoming it; another peer
|
|
18
|
+
joining or signalling in that window used to reach the first peer *before* its own welcome, and
|
|
19
|
+
that welcome's `peers` then omitted the newcomer. Messages addressed to a not-yet-welcomed peer
|
|
20
|
+
are now held and flushed behind its welcome, and the roster is read when the welcome is sent. The
|
|
21
|
+
SDK was already tolerant of the old order (its transport replays early `peer-joined`s); raw
|
|
22
|
+
`dxr-signal/1` clients were not. No protocol field changed.
|
|
23
|
+
|
|
24
|
+
### Core — `handle.displayMetrics()`
|
|
25
|
+
|
|
26
|
+
- **`handle.displayMetrics()`** → `{ canvasSizeM, metersPerCssPx, displaySizeM, nominalViewerM,
|
|
27
|
+
eyeSeparationM, source }`: the tile in physical units, derived from `getDisplayInfo()` (panel
|
|
28
|
+
pitch × `devicePixelRatio` × the canvas's CSS box). Never rejects. Anything the display can't
|
|
29
|
+
report comes from `DISPLAY_METRICS_DEFAULTS` and is flagged in `source`. **Eye separation is
|
|
30
|
+
always the 63 mm default**, because no browser or runtime surface reports one. Also exported as
|
|
31
|
+
the pure `displayMetricsFrom(info, rect, dpr)`.
|
|
32
|
+
|
|
33
|
+
### `./splat` (PlayCanvas backend)
|
|
34
|
+
|
|
35
|
+
- **`handle.setStereo({ ipdFactor, parallaxFactor })`**: live stereo strength. It re-declares the
|
|
36
|
+
rig in force on the next frame with no cut (pose, focus and framing kept). Keys merge, a `null`
|
|
37
|
+
key returns to the asset's own value, and `setStereo(null)` clears both. Sticky across
|
|
38
|
+
`setSource` / `setRig`. `handle.rig.stereoDefault` holds the asset's own values.
|
|
39
|
+
docs/playcanvas-adapter.md §setStereo.
|
|
40
|
+
- **`handle.setDepthEnvelope({ maxFrontM, edgeM, bandM, stage, rects, viewerM, canvasSizeM })`**:
|
|
41
|
+
the frame envelope. Content may come out of the glass only well inside the frame and away from 2D
|
|
42
|
+
elements. Gaussians past the cap slide back along their own rays from the declared camera rig,
|
|
43
|
+
so the picture, the sort order and the stereo rule all hold. It is tile-wide and keyed on the
|
|
44
|
+
live declared rig (convergence, lens, `ipdFactor × metersToVirtual`), so every asset is covered
|
|
45
|
+
from its first frame, through crossfades and through `setStereo`, with no per-asset install.
|
|
46
|
+
Camera rigs only. docs/splat-effects.md §Depth envelope.
|
|
47
|
+
- `setStereo({ ipdScale, parallaxScale })`: the same scalars RELATIVE to each asset's own
|
|
48
|
+
(`rig.stereoDefault`), re-applied to every asset `setSource` brings in. Per scalar, the absolute
|
|
49
|
+
and relative keys are exclusive; the last one given wins.
|
|
50
|
+
- **`splatHandle.displayMetrics()`**: the core tile's `displayMetrics()`, on the splat handle.
|
|
51
|
+
- The deferred PlayCanvas handle (what `addSplat` returns before the engine module loads) now has
|
|
52
|
+
`setStereo`, `setDepthEnvelope` and `displayMetrics`. A call right after `addSplat` was
|
|
53
|
+
`not a function`. A test now pins this.
|
|
54
|
+
- `STAGE_ORDER` gains `'envelope'` (between `custom` and `cull`), so a page's custom effect runs
|
|
55
|
+
before the cap.
|
|
56
|
+
|
|
57
|
+
## 1.31.0 — 2026-10-05
|
|
58
|
+
|
|
59
|
+
The 1.29 `./call` option spellings stay accepted (with their warning) for one more release; they go in 1.32.
|
|
60
|
+
|
|
61
|
+
Touches the **preview tier** (`./viewer`, `./splat`, `./model`: the eye cameras they draw) and adds
|
|
62
|
+
one **core** option and export. Pixels move only around a tracking acquisition or loss.
|
|
63
|
+
|
|
64
|
+
### Added — the tracking ease (`viewerEase`, default on)
|
|
65
|
+
|
|
66
|
+
- When a viewer is acquired (the views jump from the untracked fallback viewpoint to the tracked
|
|
67
|
+
eyes) or lost, the SDK's renderers now glide the eye cameras from where they were drawn to the
|
|
68
|
+
live views over 300 ms (smoothstep) instead of snapping in one frame. Triggered by a jump of more
|
|
69
|
+
than 0.15 eye separations in one frame within 600 ms of a `trackingState` edge (the state and the
|
|
70
|
+
jump need not land on the same frame; an edge with no jump changes nothing), or, where the browser
|
|
71
|
+
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,
|
|
72
|
+
so rig changes, scene scale and canvas size do not trigger it.
|
|
73
|
+
- Exact, not approximate: every DisplayXR frustum is affine in the eye position, so offsetting the
|
|
74
|
+
pose (in the view's own frame) and the projection (element-wise) by the same weight gives the
|
|
75
|
+
frustum the runtime would report for the eased eye. The rig map and the layer rig see consistent
|
|
76
|
+
views throughout.
|
|
77
|
+
- Left alone: ordinary head motion, a vendor-animated ramp, view-count changes (2D<->3D mode switch),
|
|
78
|
+
buffer resizes, replayed frames, mono.
|
|
79
|
+
- `createInline3D({ viewerEase })` sets the session default; `viewerEase` on `SceneViewer`,
|
|
80
|
+
`addSplat` and `addModel` overrides it. `false` (or `{ durationMs: 0 }`) restores the snap;
|
|
81
|
+
`{ durationMs, easing }` tunes it. Core exports `ViewerEase`, `resolveViewerEaseOption`,
|
|
82
|
+
`frameTrackingState`, `VIEWER_EASE_DEFAULT_MS` for pages that render their own `addScene`.
|
|
83
|
+
`handle.viewer.viewerEase.last` reports the last frame's weight and trigger.
|
|
84
|
+
|
|
8
85
|
## 1.30.0 — 2026-10-05
|
|
9
86
|
|
|
10
87
|
Touches the **preview tier** only (`./call`, `./call/full`, the new `./camera` and `./splat/playcanvas`). RFC 0003
|
|
@@ -129,6 +206,41 @@ Internal moves (no API): `js/call/capture.js` → `js/camera/capture.js`, `js/ca
|
|
|
129
206
|
parses, a WebM whose tags parse). Panel runs still required: the mirrored self view on glass,
|
|
130
207
|
an SBS photo opened in `/player`.
|
|
131
208
|
|
|
209
|
+
### `./call` + the hosted service (RFC 0003 §5, phase C3)
|
|
210
|
+
|
|
211
|
+
Also in this release: the hosted signalling service productised, with the **reference server**
|
|
212
|
+
(`signaling/`). Additive for pages; `dxr-signal/1` gains optional fields and new error codes only.
|
|
213
|
+
|
|
214
|
+
- **Publishable keys.** `mountCall(el, { key: 'pk_…' })` / `<dxr-call key="pk_…">` now mean
|
|
215
|
+
something: the hosted server validates the key against its origin list on connect, applies the
|
|
216
|
+
keyed tier's limits (10× anonymous), and attributes every session to the key id (billing
|
|
217
|
+
identity from day one; billing is off). Issued by hand: `tools/signal-keys.mjs`. No key = the
|
|
218
|
+
anonymous tier, as before, with tighter limits.
|
|
219
|
+
- **Limits** (RFC §5b): joins per address per minute, concurrent rooms per address / per key, room
|
|
220
|
+
lifetime (2 h / 8 h → `expired`), TURN mints per address per hour. A refused join is an `error`
|
|
221
|
+
with `rate-limited` (+ `error.retryMs`) / `quota` / `bad-key` / `origin-not-allowed` / `blocked`
|
|
222
|
+
— never a silent hang; reconnects honour `retryMs`.
|
|
223
|
+
- **TURN metering and the monthly cap** (RFC §5c, Decision 2): the service meters minted
|
|
224
|
+
credentials (and, when configured, Cloudflare's real per-key relay bytes) against the free
|
|
225
|
+
1000 GB + a $100 hard cap. At 70 % anonymous credentials shorten, at 90 % anonymous sessions
|
|
226
|
+
get none (`warning` **`turn-shed`**), at 100 % nobody does (`error` **`turn-cap`**) — the call
|
|
227
|
+
still joins and direct P2P keeps working. `welcome.turn` / `SignalingSession.turn` carry the
|
|
228
|
+
decision (`SignalingTurnStatus`).
|
|
229
|
+
- **Domain move prepared, not cut over** (RFC §5g): the hosted server answers on every host of
|
|
230
|
+
`DXR_SIGNAL_ALIASES` (`dxr-signal.displayxr.workers.dev`, `signal.displayxr.org`) and
|
|
231
|
+
`dxrSignaling()` fails over between them on connect and reconnect. `DXR_SIGNAL_DEFAULT` is
|
|
232
|
+
unchanged; the alias list is internal (`js/call/signaling.js`, importable by path — the
|
|
233
|
+
`./call` entry keeps its 9 exports). `dxrSignaling(url, { aliases })` adds fallbacks for a
|
|
234
|
+
self-hosted URL. Options and the recommendation: `docs/signaling-domain.md`.
|
|
235
|
+
- **Docs:** `docs/privacy-call.md` (what the server sees, retention, the E2E caveat),
|
|
236
|
+
`signaling/README.md` (keys, limits, budget, self-host vars, a coturn recipe, staging),
|
|
237
|
+
`docs/call.md` troubleshooting rows for the new codes.
|
|
238
|
+
- **Server:** `signaling/gate.mjs` + `limits.mjs` + `keys.mjs` + `turn-budget.mjs`, shared by
|
|
239
|
+
the Worker (Durable Objects per subject + one budget, a KV namespace for keys, an hourly
|
|
240
|
+
analytics cron, `/admin/*` behind `ADMIN_TOKEN`) and the Node dev server (same admission in
|
|
241
|
+
memory). Staging twin `signaling/deploy/staging.toml`; the C3 gate
|
|
242
|
+
`test/e2e/signal-staging.e2e.mjs`.
|
|
243
|
+
|
|
132
244
|
## 1.29.0 — 2026-09-30
|
|
133
245
|
|
|
134
246
|
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
|
|
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
|
|
11
|
-
|
|
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
|
|
@@ -107,7 +107,7 @@ that is intentionally **not** in 1.0): [`docs/sdk-stability.md`](docs/sdk-stabil
|
|
|
107
107
|
|
|
108
108
|
## Quick start
|
|
109
109
|
|
|
110
|
-
One SDK call turns a `<canvas>` into a
|
|
110
|
+
One SDK call turns a `<canvas>` into a 3D window on a spatial display. Everything degrades to plain 2D on a
|
|
111
111
|
non-DisplayXR browser, so a page is safe to ship anywhere.
|
|
112
112
|
|
|
113
113
|
```js
|
|
@@ -117,7 +117,7 @@ const wall = await createInline3D(); // opens an inline-3d session (detect
|
|
|
117
117
|
if (!wall.supported) {
|
|
118
118
|
// Not the DisplayXR Browser (or no 3D display) — your page's normal 2D content shows. Done.
|
|
119
119
|
} else {
|
|
120
|
-
// Woven
|
|
120
|
+
// Woven 3D. Add content — one call per element:
|
|
121
121
|
wall.addImage(canvas, 'photo-sbs.png'); // a still side-by-side 3D photo
|
|
122
122
|
wall.addVideo(canvas, videoEl); // an SBS 3D video
|
|
123
123
|
wall.addScene(canvas, (views, layer) => { /* render */ });// a live three.js / WebGL stereo scene
|
package/call.d.ts
CHANGED
|
@@ -60,19 +60,51 @@ export interface SignalingSession {
|
|
|
60
60
|
peers: string[];
|
|
61
61
|
/** Short-lived TURN credentials minted by the server, if it has any. */
|
|
62
62
|
iceServers?: RTCIceServer[];
|
|
63
|
+
/** Hosted service (RFC 0003 §5): the tier this session was admitted as. */
|
|
64
|
+
tier?: 'anon' | 'key';
|
|
65
|
+
/** Hosted service: the publishable key id this session is attributed to. */
|
|
66
|
+
key?: string;
|
|
67
|
+
/** Hosted service: why `iceServers` is (or is not) there — see {@link SignalingTurnStatus}. */
|
|
68
|
+
turn?: SignalingTurnStatus;
|
|
63
69
|
send(to: string, data: unknown): void;
|
|
64
70
|
leave(): void;
|
|
65
71
|
}
|
|
66
72
|
|
|
67
73
|
/** The hosted DisplayXR signalling server (`dxrSignaling()` with no URL). */
|
|
68
74
|
export const DXR_SIGNAL_DEFAULT: string;
|
|
75
|
+
/**
|
|
76
|
+
* The server's TURN decision for one session (`welcome.turn`, `dxr-signal/1`). `status` is the
|
|
77
|
+
* service-wide state of the monthly relay budget (`ok` / `degraded` / `off`); `reason` says why
|
|
78
|
+
* THIS session got no credentials: `unconfigured` (the server has no TURN — self-hosted), `budget`
|
|
79
|
+
* (anonymous session shed near the budget → `warning` `turn-shed`), `rate` (too many mints from
|
|
80
|
+
* this address → `turn-shed`), `cap` (the monthly cap → `error` `turn-cap`), `mint-failed`.
|
|
81
|
+
*/
|
|
82
|
+
export interface SignalingTurnStatus {
|
|
83
|
+
status: 'ok' | 'degraded' | 'off';
|
|
84
|
+
reason?: 'unconfigured' | 'budget' | 'cap' | 'rate' | 'mint-failed';
|
|
85
|
+
/** Credential lifetime, seconds, when minted. */
|
|
86
|
+
ttl?: number;
|
|
87
|
+
}
|
|
88
|
+
|
|
69
89
|
/**
|
|
70
90
|
* The `dxr-signal/1` JSON-over-WebSocket client (protocol: signaling/README.md). `url` is your
|
|
71
91
|
* server's base, e.g. `wss://signal.example.com` or `ws://localhost:8787` — the reference servers
|
|
72
92
|
* are `signaling/worker.mjs` (Cloudflare) and `node signaling/dev-server.mjs`. No URL = the hosted
|
|
73
93
|
* server. `key`: a publishable key for the hosted service (public by design; sent on connect).
|
|
94
|
+
* The hosted server answers on more than one host (RFC 0003 §5g) and the adapter fails over
|
|
95
|
+
* between them on connect and reconnect by itself; `aliases` adds fallbacks for a self-hosted URL.
|
|
74
96
|
*/
|
|
75
|
-
export function dxrSignaling(
|
|
97
|
+
export function dxrSignaling(
|
|
98
|
+
url?: string,
|
|
99
|
+
opts?: {
|
|
100
|
+
WebSocket?: any;
|
|
101
|
+
pingMs?: number;
|
|
102
|
+
/** A publishable key for the hosted service (public by design; sent on connect). */
|
|
103
|
+
key?: string;
|
|
104
|
+
/** Extra base URLs to fail over to when `url` is unreachable (the hosted default has its own). */
|
|
105
|
+
aliases?: string[];
|
|
106
|
+
}
|
|
107
|
+
): SignalingAdapter;
|
|
76
108
|
|
|
77
109
|
/** The eight named accents `theme.accent` takes (the same names `/player` uses). */
|
|
78
110
|
export type CallAccent = 'azure' | 'violet' | 'magenta' | 'sunset' | 'amber' | 'lime' | 'mint' | 'ice';
|
|
@@ -248,7 +280,13 @@ export interface CallPeer {
|
|
|
248
280
|
* `error` codes — fatal for a feature, never for the widget. `'camera-busy'` (held by another
|
|
249
281
|
* app: audio-only until `retryCamera()`), `'no-camera'`, `'permission-denied'`, `'unreachable'`
|
|
250
282
|
* (`error.peer` = the tile), `'room-full'`, `'session-ended'`, `'signaling-closed'`,
|
|
251
|
-
* `'
|
|
283
|
+
* `'signaling-unreachable'`, `'join-failed'`, `'camera-failed'`, `'rectify-failed'`,
|
|
284
|
+
* `'mount-failed'` (the element). From the hosted signalling service (RFC 0003 §5, C3):
|
|
285
|
+
* `'rate-limited'` / `'quota'` (a limit of the anonymous or keyed tier — the join is refused;
|
|
286
|
+
* `error.retryMs` says when a retry may work), `'bad-key'` / `'origin-not-allowed'` (the
|
|
287
|
+
* publishable key is unknown, revoked, or not allowed from this page's origin), `'blocked'`,
|
|
288
|
+
* `'expired'` (the room's lifetime is up), and `'turn-cap'` (the service's monthly relay budget
|
|
289
|
+
* is spent: the call still joins and direct connections work, but nobody gets TURN — not fatal).
|
|
252
290
|
*/
|
|
253
291
|
export type CallErrorCode =
|
|
254
292
|
| 'camera-busy'
|
|
@@ -262,11 +300,19 @@ export type CallErrorCode =
|
|
|
262
300
|
| 'camera-failed'
|
|
263
301
|
| 'rectify-failed'
|
|
264
302
|
| 'mount-failed'
|
|
265
|
-
|
|
303
|
+
| 'signaling-unreachable'
|
|
304
|
+
// Hosted signalling service (RFC 0003 §5, C3).
|
|
305
|
+
| 'rate-limited'
|
|
306
|
+
| 'quota'
|
|
307
|
+
| 'bad-key'
|
|
308
|
+
| 'origin-not-allowed'
|
|
309
|
+
| 'blocked'
|
|
310
|
+
| 'expired'
|
|
311
|
+
| 'turn-cap'
|
|
266
312
|
| (string & {});
|
|
267
313
|
|
|
268
314
|
/** `warning` codes — degraded, not broken. */
|
|
269
|
-
export type CallWarningCode = 'lift-not-bundled' | (string & {});
|
|
315
|
+
export type CallWarningCode = 'lift-not-bundled' | 'turn-shed' | (string & {});
|
|
270
316
|
|
|
271
317
|
export interface CallEvents {
|
|
272
318
|
joined: { room: string; id: string };
|
|
@@ -289,6 +335,9 @@ export interface CallEvents {
|
|
|
289
335
|
* module could not be imported from this build — they stay flat. Fix: `mono3D: lift` or
|
|
290
336
|
* `./call/full`. Emitted once per call with one `console.warn`. A display that honestly cannot
|
|
291
337
|
* lift is NOT this: that is `handle.mono3D.reason` (`'no-provider'` / `'no-webgpu'`).
|
|
338
|
+
* `'turn-shed'`: only THIS session got no TURN from the hosted service (an anonymous session
|
|
339
|
+
* while the monthly relay budget is nearly spent, or too many credential requests from this
|
|
340
|
+
* address this hour); the call runs STUN-only. A publishable key keeps TURN.
|
|
292
341
|
*/
|
|
293
342
|
warning: { code: CallWarningCode; message: string };
|
|
294
343
|
}
|