@displayxr/inline3d 1.25.1 → 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 +33 -0
- package/index.d.ts +10 -0
- package/js/inline3d-splat-layer-rig.js +79 -7
- package/js/inline3d-splat-playcanvas.js +99 -6
- package/package.json +1 -1
- package/splat.d.ts +27 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,39 @@ 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
|
+
|
|
27
|
+
## 1.26.0 — 2026-09-25
|
|
28
|
+
|
|
29
|
+
Touches the **preview tier** (`./splat`, `engine: 'playcanvas'` only). Additive; nothing changes
|
|
30
|
+
for a page that does not call it.
|
|
31
|
+
|
|
32
|
+
- **`handle.setViewOffset({x, y})`** moves the 2D tier's mono EYE in its own plane, off-axis with
|
|
33
|
+
the focus plane pinned — a head-parallax analogue for a flat screen with no tracked eyes (a
|
|
34
|
+
phone's tilt). Normalised and clamped to the unit disc; |offset| = 1 swings the line of sight to
|
|
35
|
+
the focus by `orbitMaxDeg` (15°), the drag orbit's cone. It moves the eye, not the scene, so it
|
|
36
|
+
composes with the orbit instead of fighting its relax. A snap (ease it in the page); `null`
|
|
37
|
+
resets. Mono only: a no-op for the pixels in woven 3D, where the head tracker owns the eyes, and
|
|
38
|
+
ignored on `controls: 'page'`. **`handle.viewOffset`** reads it back. `pick()` and the effects'
|
|
39
|
+
eye frame follow the offset camera.
|
|
40
|
+
|
|
8
41
|
## 1.25.1 — 2026-09-25
|
|
9
42
|
|
|
10
43
|
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
|
);
|
|
@@ -452,6 +452,40 @@ export function pageViewAxis(M) {
|
|
|
452
452
|
return { origin: [M[12], M[13], M[14]], forward: [-M[8] / fl, -M[9] / fl, -M[10] / fl], scale: s };
|
|
453
453
|
}
|
|
454
454
|
|
|
455
|
+
/**
|
|
456
|
+
* handle.setViewOffset — the mono camera moved IN ITS OWN PLANE, off-axis, with the focus plane
|
|
457
|
+
* pinned. A head-parallax analogue for the 2D tier (a phone's tilt), where there are no tracked
|
|
458
|
+
* eyes: the eye slides sideways/up, the window through the focus plane stays where it was, so the
|
|
459
|
+
* focus stays put on screen while nearer content moves against the farther. Not a rotation — the
|
|
460
|
+
* orbit (setPose / the drag) turns the SCENE about the focus; this moves the EYE, and the two
|
|
461
|
+
* compose.
|
|
462
|
+
*
|
|
463
|
+
* e = (ox, oy) · c · tan(maxDeg) camera-plane displacement, display units
|
|
464
|
+
* pose' = pose · T(e.x, e.y, 0)
|
|
465
|
+
* P'[8] = P[8] − P[0]·e.x / c, P'[9] = P[9] − P[5]·e.y / c (the window shifted by −e·n/c)
|
|
466
|
+
*
|
|
467
|
+
* `c` is the focus's distance along the camera's view axis, so |offset| = 1 swings the line of
|
|
468
|
+
* sight to the focus by `maxDeg` — the same comfort cone as the drag orbit (orbitMaxDeg, 15°).
|
|
469
|
+
* Being an ANGLE it is scale-free: zoom scales the scene about the focus, and c does not move.
|
|
470
|
+
* Writes into outPose/outProj and returns true; false (outputs untouched) when the offset is zero
|
|
471
|
+
* or the focus is not in front of the camera — the caller then draws the unshifted camera.
|
|
472
|
+
*/
|
|
473
|
+
export function offsetMonoView(pose, proj, ox, oy, c, maxDeg, outPose, outProj) {
|
|
474
|
+
if ((!ox && !oy) || !(c > 1e-6) || !Number.isFinite(c)) return false;
|
|
475
|
+
const k = c * Math.tan(maxDeg * DEG);
|
|
476
|
+
const ex = ox * k;
|
|
477
|
+
const ey = oy * k;
|
|
478
|
+
outPose.set(pose);
|
|
479
|
+
// pose · T(e): translate along the camera's own x/y axes (columns 0 and 1).
|
|
480
|
+
outPose[12] = pose[12] + pose[0] * ex + pose[4] * ey;
|
|
481
|
+
outPose[13] = pose[13] + pose[1] * ex + pose[5] * ey;
|
|
482
|
+
outPose[14] = pose[14] + pose[2] * ex + pose[6] * ey;
|
|
483
|
+
outProj.set(proj);
|
|
484
|
+
outProj[8] = proj[8] - (proj[0] * ex) / c;
|
|
485
|
+
outProj[9] = proj[9] - (proj[5] * ey) / c;
|
|
486
|
+
return true;
|
|
487
|
+
}
|
|
488
|
+
|
|
455
489
|
/**
|
|
456
490
|
* The attach-pattern camera rig for controls:'page' — the auto-3D shim's buildRig, field for
|
|
457
491
|
* field: identity pose, the page's vertical FOV, convergence d, metersToVirtual = depth · d / 0.5
|
|
@@ -874,6 +908,10 @@ export class PlayCanvasSplatViewer {
|
|
|
874
908
|
this._orbitCentre = { x: 0, y: 0, z: 0 };
|
|
875
909
|
this._focusRecentres = true;
|
|
876
910
|
this._focusSettled = true;
|
|
911
|
+
// handle.setViewOffset: normalised camera-plane eye offset, mono only (offsetMonoView).
|
|
912
|
+
this._viewOffset = [0, 0];
|
|
913
|
+
this._offPose = new Float64Array(16);
|
|
914
|
+
this._offProj = new Float64Array(16);
|
|
877
915
|
this.onFocusChange = null;
|
|
878
916
|
this.onTick = null;
|
|
879
917
|
/** controls:'page' — `(views|null) => void`, run once per frame before anything else. */
|
|
@@ -1146,7 +1184,11 @@ export class PlayCanvasSplatViewer {
|
|
|
1146
1184
|
if (this._mode !== '3d') this.stopMono();
|
|
1147
1185
|
// The rig these views were located with: Blink chained the rig declared BEFORE this callback,
|
|
1148
1186
|
// and the tick below may declare a new one (a focus ease) for the NEXT locate.
|
|
1149
|
-
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
|
+
}
|
|
1150
1192
|
// Same moment, for the rig map: the rig these views were most likely located for (a tie-break).
|
|
1151
1193
|
this._rigAtPull = this.rigTrack ? this.rigTrack.latest : null;
|
|
1152
1194
|
// BEFORE the tick: a pose the page sets in here is the one this very frame renders.
|
|
@@ -1867,12 +1909,40 @@ export class PlayCanvasSplatViewer {
|
|
|
1867
1909
|
} else perspectiveFov(this.mono.fov, aspect, this.mono.near, this.mono.far, this.mono.proj);
|
|
1868
1910
|
}
|
|
1869
1911
|
|
|
1912
|
+
/**
|
|
1913
|
+
* handle.setViewOffset — a normalised eye offset in the mono camera's plane (see offsetMonoView),
|
|
1914
|
+
* clamped to the unit disc. Mono only: in woven 3D the head tracker owns the eyes and this is
|
|
1915
|
+
* stored but not drawn; on controls:'page' the page owns the camera and it is ignored. A snap,
|
|
1916
|
+
* like setPose, so the page eases it (and the frame loop draws it next frame).
|
|
1917
|
+
*/
|
|
1918
|
+
setViewOffset(x, y) {
|
|
1919
|
+
const r = Math.hypot(x, y);
|
|
1920
|
+
const k = r > 1 ? 1 / r : 1;
|
|
1921
|
+
this._viewOffset[0] = x * k;
|
|
1922
|
+
this._viewOffset[1] = y * k;
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
/** The mono camera as drawn: the offset one when setViewOffset is engaged, else mono itself. */
|
|
1926
|
+
_monoView() {
|
|
1927
|
+
const m = this.mono;
|
|
1928
|
+
const [ox, oy] = this._viewOffset;
|
|
1929
|
+
if (this.pageCamera || (!ox && !oy)) return { proj: m.proj, pose: m.pose };
|
|
1930
|
+
const o = this._orbitCentre;
|
|
1931
|
+
const p = m.pose;
|
|
1932
|
+
// The focus's distance along the view axis (−z): the pivot puts it at the orbit centre.
|
|
1933
|
+
const c = -((o.x - p[12]) * p[8] + (o.y - p[13]) * p[9] + (o.z + this._depthOffset - p[14]) * p[10]);
|
|
1934
|
+
return offsetMonoView(p, m.proj, ox, oy, c, this.orbitMaxDeg, this._offPose, this._offProj)
|
|
1935
|
+
? { proj: this._offProj, pose: this._offPose }
|
|
1936
|
+
: { proj: m.proj, pose: m.pose };
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1870
1939
|
_drawMono() {
|
|
1871
1940
|
const c = this.canvas;
|
|
1872
1941
|
this._monoEntry ||= [{ proj: this.mono.proj, pose: this.mono.pose, x: 0, y: 0, width: 0, height: 0 }];
|
|
1873
1942
|
const e = this._monoEntry[0];
|
|
1874
|
-
|
|
1875
|
-
e.
|
|
1943
|
+
const v = this._monoView();
|
|
1944
|
+
e.proj = v.proj;
|
|
1945
|
+
e.pose = v.pose;
|
|
1876
1946
|
e.width = c.width;
|
|
1877
1947
|
e.height = c.height;
|
|
1878
1948
|
this._drawEntries(this._monoEntry, null);
|
|
@@ -2135,7 +2205,7 @@ export class PlayCanvasSplatViewer {
|
|
|
2135
2205
|
*/
|
|
2136
2206
|
eyeFrame() {
|
|
2137
2207
|
const R = this.rigMatrix();
|
|
2138
|
-
const es = this._mode === '3d' && this._lastGood ? this._lastGood.entries : [
|
|
2208
|
+
const es = this._mode === '3d' && this._lastGood ? this._lastGood.entries : [this._monoView()];
|
|
2139
2209
|
const o = [0, 0, 0];
|
|
2140
2210
|
for (const e of es) {
|
|
2141
2211
|
const M = mat4Mul(R, e.pose);
|
|
@@ -2352,7 +2422,7 @@ export class PlayCanvasSplatViewer {
|
|
|
2352
2422
|
/** The camera currently on screen: [proj, pose] of the first eye in 3D, the mono camera else. */
|
|
2353
2423
|
currentView() {
|
|
2354
2424
|
if (this._mode === '3d' && this._lastGood) return this._lastGood.entries[0];
|
|
2355
|
-
return
|
|
2425
|
+
return this._monoView();
|
|
2356
2426
|
}
|
|
2357
2427
|
}
|
|
2358
2428
|
|
|
@@ -2984,6 +3054,12 @@ export function attachPlayCanvasSplat(out, wall, canvas, src, opts, pending = []
|
|
|
2984
3054
|
// The current per-eye buffer scale (the `renderScale` option, or the last setRenderScale). A
|
|
2985
3055
|
// real accessor: Object.assign below would copy a getter's value once.
|
|
2986
3056
|
Object.defineProperty(out, 'renderScale', { get: () => viewer.renderScale, enumerable: true, configurable: true });
|
|
3057
|
+
// The current eye offset (after the unit-disc clamp), a fresh object per read.
|
|
3058
|
+
Object.defineProperty(out, 'viewOffset', {
|
|
3059
|
+
get: () => ({ x: viewer._viewOffset[0], y: viewer._viewOffset[1] }),
|
|
3060
|
+
enumerable: true,
|
|
3061
|
+
configurable: true,
|
|
3062
|
+
});
|
|
2987
3063
|
Object.assign(out, {
|
|
2988
3064
|
backend: 'playcanvas',
|
|
2989
3065
|
engine: null, // { app, root, camera } once the engine has booted — see below
|
|
@@ -3083,6 +3159,23 @@ export function attachPlayCanvasSplat(out, wall, canvas, src, opts, pending = []
|
|
|
3083
3159
|
}
|
|
3084
3160
|
return out;
|
|
3085
3161
|
},
|
|
3162
|
+
/**
|
|
3163
|
+
* The 2D tier's eye offset: `{x, y}` normalised in the mono camera's plane (+x right, +y up),
|
|
3164
|
+
* clamped to the unit disc; |offset| = 1 swings the line of sight to the focus by orbitMaxDeg
|
|
3165
|
+
* (15°), off-axis so the focus plane stays put — a head-parallax analogue (a phone's tilt)
|
|
3166
|
+
* where there are no tracked eyes. Composes with the orbit. A snap: ease it in the page.
|
|
3167
|
+
* `null` = {0, 0}. Mono only — a no-op for the pixels in woven 3D (the head tracker owns the
|
|
3168
|
+
* eyes; the value is kept for a later fall back to 2D) and on controls:'page'.
|
|
3169
|
+
*/
|
|
3170
|
+
setViewOffset(o) {
|
|
3171
|
+
const x = o == null ? 0 : o.x ?? 0;
|
|
3172
|
+
const y = o == null ? 0 : o.y ?? 0;
|
|
3173
|
+
if (typeof x !== 'number' || typeof y !== 'number' || !Number.isFinite(x) || !Number.isFinite(y)) {
|
|
3174
|
+
throw new TypeError('@displayxr/inline3d/splat: setViewOffset takes {x, y} finite numbers (or null).');
|
|
3175
|
+
}
|
|
3176
|
+
viewer.setViewOffset(x, y);
|
|
3177
|
+
return out;
|
|
3178
|
+
},
|
|
3086
3179
|
/** Change the layer rig's tile-wide options live (no layer change): merge, `null` clears. */
|
|
3087
3180
|
setLayerRigOptions(o = {}) {
|
|
3088
3181
|
layerRigs().setOptions(validateLayerRigOptions(o, 'setLayerRigOptions'));
|
|
@@ -3097,7 +3190,7 @@ export function attachPlayCanvasSplat(out, wall, canvas, src, opts, pending = []
|
|
|
3097
3190
|
layerRigState() {
|
|
3098
3191
|
const lr = viewer.layerRigs;
|
|
3099
3192
|
if (lr) return lr.state();
|
|
3100
|
-
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 };
|
|
3101
3194
|
},
|
|
3102
3195
|
/**
|
|
3103
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). */
|
|
@@ -1002,6 +1013,21 @@ export interface SplatHandle {
|
|
|
1002
1013
|
setRenderScale(scale: number): SplatHandle;
|
|
1003
1014
|
/** `engine: 'playcanvas'` only. The current per-eye buffer scale. */
|
|
1004
1015
|
readonly renderScale: number;
|
|
1016
|
+
/**
|
|
1017
|
+
* `engine: 'playcanvas'` only. The 2D tier's EYE offset — a head-parallax analogue for a flat
|
|
1018
|
+
* screen with no tracked eyes (e.g. a phone's tilt). `{x, y}` is normalised in the mono camera's
|
|
1019
|
+
* plane (+x right, +y up) and clamped to the unit disc; |offset| = 1 moves the eye far enough to
|
|
1020
|
+
* swing its line of sight to the focus by `orbitMaxDeg` (15° by default), the drag orbit's comfort
|
|
1021
|
+
* cone. Off-axis: the focus plane stays put on screen while nearer content moves against the
|
|
1022
|
+
* farther. It moves the eye, not the scene, so it composes with the orbit (`setPose`, the drag)
|
|
1023
|
+
* rather than fighting it. A snap, like `setPose` — ease it in the page. `null` resets to
|
|
1024
|
+
* `{0, 0}`. Mono only: in woven 3D the head tracker owns the eyes, so the value is kept (it applies
|
|
1025
|
+
* again if the tile falls back to 2D) but draws nothing; ignored on `controls: 'page'`. Throws a
|
|
1026
|
+
* TypeError on a non-finite component.
|
|
1027
|
+
*/
|
|
1028
|
+
setViewOffset(offset: { x?: number; y?: number } | null): SplatHandle;
|
|
1029
|
+
/** `engine: 'playcanvas'` only. The current eye offset (after the unit-disc clamp). */
|
|
1030
|
+
readonly viewOffset: { x: number; y: number };
|
|
1005
1031
|
/** `engine: 'playcanvas'` only. Change the layer rig's tile-wide options live (merge; `null` clears). */
|
|
1006
1032
|
setLayerRigOptions(opts: SplatLayerRigOptions): SplatHandle;
|
|
1007
1033
|
/**
|