@displayxr/inline3d 1.26.0 → 1.27.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 +19 -0
- package/index.d.ts +10 -0
- package/js/inline3d-splat-layer-rig.js +79 -7
- package/js/inline3d-splat-playcanvas.js +6 -2
- package/package.json +1 -1
- package/splat.d.ts +12 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,25 @@ 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.27.0 — 2026-09-26
|
|
9
|
+
|
|
10
|
+
Touches the **preview tier** (`./splat`, `engine: 'playcanvas'` only) and adds one optional field
|
|
11
|
+
to the core `XRDisplayInfo` type. A page on a browser that does not report the new field sees no
|
|
12
|
+
change.
|
|
13
|
+
|
|
14
|
+
- **`setLayerRig`'s default `viewerDistance` now comes from the browser.** Where
|
|
15
|
+
`XRDisplayInfo.nominalViewerPosition` is reported (DisplayXR Browser patch 0221, Windows and
|
|
16
|
+
Android), the layer rig's n is its **z** — the runtime's nominal viewer distance for the panel,
|
|
17
|
+
the value its camera rig is calibrated to — instead of the fixed 0.6 m. The tile asks the frame's
|
|
18
|
+
`XRDisplayLayer.getDisplayInfo()` once, asynchronously; frames drawn before it answers use 0.6 m.
|
|
19
|
+
A page's `viewerDistance` still wins, and clearing it (`null`) falls back to the browser's value,
|
|
20
|
+
then 0.6 m. **Pixels move** on a panel whose nominal distance is not 0.6 m: display-rig objects'
|
|
21
|
+
pop-out scales by 0.6/n (that was the bug); the contact plane and the 2D picture do not move.
|
|
22
|
+
- **`layerRigState().viewerDistanceSource`** — `'page'` | `'browser'` | `'default'` — says where
|
|
23
|
+
`viewerDistance` came from, and the WARN line prints it: `viewerDistance=0.45m(browser)`.
|
|
24
|
+
- **Types:** `XRDisplayInfo.nominalViewerPosition?: DOMPointReadOnly` (metres, display space, +z
|
|
25
|
+
out of the glass; absent when unreported), `SplatLayerRigState.viewerDistanceSource`.
|
|
26
|
+
|
|
8
27
|
## 1.26.0 — 2026-09-25
|
|
9
28
|
|
|
10
29
|
Touches the **preview tier** (`./splat`, `engine: 'playcanvas'` only). Additive; nothing changes
|
package/index.d.ts
CHANGED
|
@@ -103,6 +103,16 @@ export interface XRDisplayInfo {
|
|
|
103
103
|
displayPixelHeight: number;
|
|
104
104
|
recommendedViewScaleX: number;
|
|
105
105
|
recommendedViewScaleY: number;
|
|
106
|
+
/**
|
|
107
|
+
* The runtime's NOMINAL viewer position, metres, in display space: origin at the panel
|
|
108
|
+
* centre, +x right, +y up, +z out of the glass toward the viewer (w = 1). A constant of the
|
|
109
|
+
* panel — where the runtime seats the viewer when nobody is tracked, and the distance its
|
|
110
|
+
* camera rig is calibrated to (`z` is that rig's n) — NOT the live eye position.
|
|
111
|
+
*
|
|
112
|
+
* Absent on browsers that predate it (DisplayXR Browser patch 0221) and when the runtime
|
|
113
|
+
* reports nothing usable, so read it as `info.nominalViewerPosition?.z ?? fallback`.
|
|
114
|
+
*/
|
|
115
|
+
nominalViewerPosition?: DOMPointReadOnly;
|
|
106
116
|
}
|
|
107
117
|
|
|
108
118
|
/**
|
|
@@ -16,8 +16,10 @@
|
|
|
16
16
|
// view_round_i = view_photo_i · M_i, proj_round_i = proj_photo_i
|
|
17
17
|
//
|
|
18
18
|
// k = D / (m·n): D the declared convergence (world units), m the declared metersToVirtual, n the
|
|
19
|
-
// nominal viewer distance in metres
|
|
20
|
-
//
|
|
19
|
+
// nominal viewer distance in metres. n comes from the page's `viewerDistance` if it passed one,
|
|
20
|
+
// else from the browser (XRDisplayInfo.nominalViewerPosition.z, DisplayXR Browser 0221+), else
|
|
21
|
+
// 0.6 m (design note §5, (c)1). Everything here is PLAIN ARITHMETIC on arrays, unit-testable
|
|
22
|
+
// without a GPU.
|
|
21
23
|
|
|
22
24
|
import { invertAffine } from './inline3d-splat-rig-map.js';
|
|
23
25
|
|
|
@@ -27,6 +29,32 @@ export const LAYER_RIGS = Object.freeze(['display', 'camera']);
|
|
|
27
29
|
/** Nominal viewer distance (m) when neither the page nor the platform says: the browser's own. */
|
|
28
30
|
export const DEFAULT_VIEWER_DISTANCE_M = 0.6;
|
|
29
31
|
|
|
32
|
+
/** How many times a tile asks the layer for its display info before giving up (rejections only). */
|
|
33
|
+
const MAX_DISPLAY_INFO_ASKS = 3;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The runtime's nominal viewer DISTANCE n (m) from an XRDisplayInfo, or null when the browser does
|
|
37
|
+
* not report one. It is the position's **z** — the distance out of the glass along the display
|
|
38
|
+
* normal — not its length: the runtime's camera rig reads only `nominal_viewer->z`
|
|
39
|
+
* (displayxr-common dxr_view_math.c, `eye_local.z = m2v·(eye.z − nominal_z)`), and z is what
|
|
40
|
+
* D/n undoes. A browser without the field, a null info, or a z that is not finite and > 0 → null.
|
|
41
|
+
*/
|
|
42
|
+
export function nominalViewerDistanceFromInfo(info) {
|
|
43
|
+
const z = info && info.nominalViewerPosition ? info.nominalViewerPosition.z : undefined;
|
|
44
|
+
return typeof z === 'number' && Number.isFinite(z) && z > 0 ? z : null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The n the layer rig uses, and where it came from: the page's `viewerDistance` wins ('page'),
|
|
49
|
+
* then the browser's nominal viewer ('browser'), then DEFAULT_VIEWER_DISTANCE_M ('default').
|
|
50
|
+
* @returns {{ value: number, source: 'page'|'browser'|'default' }}
|
|
51
|
+
*/
|
|
52
|
+
export function resolveViewerDistance(pageViewerDistance, browserViewerDistance) {
|
|
53
|
+
if (pageViewerDistance !== null && pageViewerDistance !== undefined) return { value: pageViewerDistance, source: 'page' };
|
|
54
|
+
if (browserViewerDistance !== null && browserViewerDistance !== undefined) return { value: browserViewerDistance, source: 'browser' };
|
|
55
|
+
return { value: DEFAULT_VIEWER_DISTANCE_M, source: 'default' };
|
|
56
|
+
}
|
|
57
|
+
|
|
30
58
|
/** A round eye may not reach the window plane: h(E') is floored at this fraction of D. */
|
|
31
59
|
const MIN_EYE_HEIGHT_FRAC = 1e-3;
|
|
32
60
|
|
|
@@ -45,7 +73,8 @@ export const LAYER_RIG_OPTION_KEYS = Object.freeze(['viewerDistance', 'gain', 'p
|
|
|
45
73
|
/**
|
|
46
74
|
* setLayerRig / setLayerRigOptions options, validated (throws at the call). Only the keys given
|
|
47
75
|
* are returned: they MERGE into the tile's options. `null` clears a key (back to its default).
|
|
48
|
-
* viewerDistance m, > 0 the nominal viewer distance n (default
|
|
76
|
+
* viewerDistance m, > 0 the nominal viewer distance n (default: the browser's
|
|
77
|
+
* XRDisplayInfo.nominalViewerPosition.z, else 0.6)
|
|
49
78
|
* gain > 0 an explicit rounding gain k instead of D/(m·n)
|
|
50
79
|
* planeOffset m, finite moves the stage TOWARD the viewer by this much on the panel (display
|
|
51
80
|
* space): the plane that lands on the glass moves back by offset·D/n
|
|
@@ -330,6 +359,11 @@ export class LayerRigCameras {
|
|
|
330
359
|
this.layers = new Map();
|
|
331
360
|
/** The tile's options: merged across setLayerRig / setLayerRigOptions calls. */
|
|
332
361
|
this.opts = { viewerDistance: null, gain: null, planeOffset: null, planeDistance: null };
|
|
362
|
+
/** n as the browser reports it (XRDisplayInfo.nominalViewerPosition.z), once known; else null. */
|
|
363
|
+
this.browserViewerDistance = null;
|
|
364
|
+
this._infoAsks = 0;
|
|
365
|
+
this._infoPending = false;
|
|
366
|
+
this._infoSettled = false;
|
|
333
367
|
this.disabled = false; // the kill switch (diag 'nolayerrig')
|
|
334
368
|
this.cams = { display: [], post: [] };
|
|
335
369
|
this.moved = { display: [], post: [] };
|
|
@@ -369,6 +403,41 @@ export class LayerRigCameras {
|
|
|
369
403
|
for (const [k, v] of Object.entries(o)) this.opts[k] = v;
|
|
370
404
|
}
|
|
371
405
|
|
|
406
|
+
/** The n this tile uses right now, and its source ('page' | 'browser' | 'default'). */
|
|
407
|
+
viewerDistance() {
|
|
408
|
+
return resolveViewerDistance(this.opts.viewerDistance, this.browserViewerDistance);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Called with the frame's XRDisplayLayer: ask it ONCE for the display info and keep the
|
|
413
|
+
* browser's nominal viewer distance. Asynchronous — frames drawn before it resolves use the
|
|
414
|
+
* page's value or 0.6 m, and the state's `viewerDistanceSource` says which. A browser without
|
|
415
|
+
* getDisplayInfo (or without the field) leaves the default in place; a REJECTED ask (e.g. no live
|
|
416
|
+
* layer yet) is retried on later frames, up to MAX_DISPLAY_INFO_ASKS times.
|
|
417
|
+
*/
|
|
418
|
+
noteLayer(layer) {
|
|
419
|
+
if (this._infoSettled || this._infoPending || this._infoAsks >= MAX_DISPLAY_INFO_ASKS) return;
|
|
420
|
+
if (!layer || typeof layer.getDisplayInfo !== 'function') return;
|
|
421
|
+
this._infoAsks++;
|
|
422
|
+
this._infoPending = true;
|
|
423
|
+
let p;
|
|
424
|
+
try {
|
|
425
|
+
p = Promise.resolve(layer.getDisplayInfo());
|
|
426
|
+
} catch (e) {
|
|
427
|
+
p = Promise.reject(e);
|
|
428
|
+
}
|
|
429
|
+
return p.then(
|
|
430
|
+
(info) => {
|
|
431
|
+
this._infoPending = false;
|
|
432
|
+
this._infoSettled = true;
|
|
433
|
+
this.browserViewerDistance = nominalViewerDistanceFromInfo(info);
|
|
434
|
+
},
|
|
435
|
+
() => {
|
|
436
|
+
this._infoPending = false;
|
|
437
|
+
}
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
|
|
372
441
|
/** The eye camera entities of the current view path. */
|
|
373
442
|
_eyeCams() {
|
|
374
443
|
const v = this.viewer;
|
|
@@ -494,8 +563,9 @@ export class LayerRigCameras {
|
|
|
494
563
|
L.residual = residual;
|
|
495
564
|
const hasRun = this.sync();
|
|
496
565
|
const frame = cameraRigFrame(rig);
|
|
497
|
-
const
|
|
498
|
-
const
|
|
566
|
+
const opts = { ...this.opts, viewerDistance: this.viewerDistance().value };
|
|
567
|
+
const k = frame ? layerRigGain(frame, opts) : null;
|
|
568
|
+
const plane = frame ? layerRigPlane(frame, opts) : null;
|
|
499
569
|
L.gain = k;
|
|
500
570
|
L.planeM = plane;
|
|
501
571
|
L.photoConvergenceM = frame ? frame.D : null;
|
|
@@ -606,6 +676,7 @@ export class LayerRigCameras {
|
|
|
606
676
|
/** The public state (handle.layerRigState). */
|
|
607
677
|
state() {
|
|
608
678
|
const L = this.last;
|
|
679
|
+
const n = this.viewerDistance();
|
|
609
680
|
return {
|
|
610
681
|
display: [...this.requests.keys()],
|
|
611
682
|
disabled: this.disabled,
|
|
@@ -613,7 +684,8 @@ export class LayerRigCameras {
|
|
|
613
684
|
engaged: !!L.engaged,
|
|
614
685
|
rounded: !!L.engaged,
|
|
615
686
|
reason: this.disabled ? 'kill switch (?dxrdiag=nolayerrig)' : L.reason,
|
|
616
|
-
viewerDistance:
|
|
687
|
+
viewerDistance: n.value,
|
|
688
|
+
viewerDistanceSource: n.source,
|
|
617
689
|
gain: L.gain,
|
|
618
690
|
planeM: L.planeM,
|
|
619
691
|
photoConvergenceM: L.photoConvergenceM ?? null,
|
|
@@ -634,7 +706,7 @@ export function layerRigLine(st) {
|
|
|
634
706
|
const n = (x, d = 3) => (x === null || x === undefined ? '-' : Number(x).toFixed(d));
|
|
635
707
|
return (
|
|
636
708
|
`path=${st.path ?? '-'} engaged=${st.engaged} layers=[${st.display.join(', ')}] ` +
|
|
637
|
-
`viewerDistance=${n(st.viewerDistance, 2)}m gain=${n(st.gain)} planeM=${n(st.planeM)} ` +
|
|
709
|
+
`viewerDistance=${n(st.viewerDistance, 2)}m(${st.viewerDistanceSource ?? '-'}) gain=${n(st.gain)} planeM=${n(st.planeM)} ` +
|
|
638
710
|
`photoConvergenceM=${n(st.photoConvergenceM)} planeOffset=${n(st.planeOffset)}m ` +
|
|
639
711
|
`located=${st.located === null || st.located === undefined ? '-' : st.located} reason=${st.reason ?? '-'}`
|
|
640
712
|
);
|
|
@@ -1184,7 +1184,11 @@ export class PlayCanvasSplatViewer {
|
|
|
1184
1184
|
if (this._mode !== '3d') this.stopMono();
|
|
1185
1185
|
// The rig these views were located with: Blink chained the rig declared BEFORE this callback,
|
|
1186
1186
|
// and the tick below may declare a new one (a focus ease) for the NEXT locate.
|
|
1187
|
-
if (this.layerRigs?.active)
|
|
1187
|
+
if (this.layerRigs?.active) {
|
|
1188
|
+
this._rigSnap = snapshotRig(this.layerRigSource?.(), this._rigSnap);
|
|
1189
|
+
// n for the layer rig: the browser's nominal viewer, asked once (async; no-op after).
|
|
1190
|
+
this.layerRigs.noteLayer(layer);
|
|
1191
|
+
}
|
|
1188
1192
|
// Same moment, for the rig map: the rig these views were most likely located for (a tie-break).
|
|
1189
1193
|
this._rigAtPull = this.rigTrack ? this.rigTrack.latest : null;
|
|
1190
1194
|
// BEFORE the tick: a pose the page sets in here is the one this very frame renders.
|
|
@@ -3186,7 +3190,7 @@ export function attachPlayCanvasSplat(out, wall, canvas, src, opts, pending = []
|
|
|
3186
3190
|
layerRigState() {
|
|
3187
3191
|
const lr = viewer.layerRigs;
|
|
3188
3192
|
if (lr) return lr.state();
|
|
3189
|
-
return { display: [], disabled: false, path: null, engaged: false, rounded: false, reason: 'no layer on the display rig', viewerDistance: DEFAULT_VIEWER_DISTANCE_M, gain: null, planeM: null, photoConvergenceM: null, planeOffset: 0, located: null };
|
|
3193
|
+
return { display: [], disabled: false, path: null, engaged: false, rounded: false, reason: 'no layer on the display rig', viewerDistance: DEFAULT_VIEWER_DISTANCE_M, viewerDistanceSource: 'default', gain: null, planeM: null, photoConvergenceM: null, planeOffset: 0, located: null };
|
|
3190
3194
|
},
|
|
3191
3195
|
/**
|
|
3192
3196
|
* An unlit material showing the left half of `texture` to left-eye views and the right half to
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@displayxr/inline3d",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.27.0",
|
|
4
4
|
"description": "Turn any HTML <canvas> into a glasses-free-3D window on a DisplayXR display, inside an ordinary web page. Dependency-free; progressive enhancement (falls back to plain 2D everywhere else).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "./index.d.ts",
|
package/splat.d.ts
CHANGED
|
@@ -185,7 +185,11 @@ export interface ResolvedRig {
|
|
|
185
185
|
* viewer's. docs/proposals/layer-display-rig.md.
|
|
186
186
|
*/
|
|
187
187
|
export interface SplatLayerRigOptions {
|
|
188
|
-
/**
|
|
188
|
+
/**
|
|
189
|
+
* Nominal viewer distance n in metres. Default: the browser's
|
|
190
|
+
* `XRDisplayInfo.nominalViewerPosition.z` where it reports one, else 0.6. Passing it pins n
|
|
191
|
+
* (see {@link SplatLayerRigState.viewerDistanceSource}).
|
|
192
|
+
*/
|
|
189
193
|
viewerDistance?: number | null;
|
|
190
194
|
/** An explicit gain k instead (1 = exactly the photo rig; larger = rounder). */
|
|
191
195
|
gain?: number | null;
|
|
@@ -216,7 +220,14 @@ export interface SplatLayerRigState {
|
|
|
216
220
|
rounded: boolean;
|
|
217
221
|
/** Why not engaged (or what is wrong with some layers while engaged); null when all is well. */
|
|
218
222
|
reason: string | null;
|
|
223
|
+
/** The n in use (metres). */
|
|
219
224
|
viewerDistance: number;
|
|
225
|
+
/**
|
|
226
|
+
* Where `viewerDistance` came from: the page's option, the browser's reported nominal viewer
|
|
227
|
+
* (`XRDisplayInfo.nominalViewerPosition.z`, asked once per tile, asynchronously — early frames
|
|
228
|
+
* may read 'default' before it lands), or the 0.6 m fallback.
|
|
229
|
+
*/
|
|
230
|
+
viewerDistanceSource: 'page' | 'browser' | 'default';
|
|
220
231
|
/** The gain the last frame used (null off a camera rig). */
|
|
221
232
|
gain: number | null;
|
|
222
233
|
/** The plane that lands on the glass, as a distance from the photo's camera (world units). */
|