@displayxr/inline3d 1.25.0 → 1.26.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 +25 -0
- package/js/inline3d-splat-playcanvas.js +111 -4
- package/package.json +1 -1
- package/splat.d.ts +23 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,31 @@ 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.26.0 — 2026-09-25
|
|
9
|
+
|
|
10
|
+
Touches the **preview tier** (`./splat`, `engine: 'playcanvas'` only). Additive; nothing changes
|
|
11
|
+
for a page that does not call it.
|
|
12
|
+
|
|
13
|
+
- **`handle.setViewOffset({x, y})`** moves the 2D tier's mono EYE in its own plane, off-axis with
|
|
14
|
+
the focus plane pinned — a head-parallax analogue for a flat screen with no tracked eyes (a
|
|
15
|
+
phone's tilt). Normalised and clamped to the unit disc; |offset| = 1 swings the line of sight to
|
|
16
|
+
the focus by `orbitMaxDeg` (15°), the drag orbit's cone. It moves the eye, not the scene, so it
|
|
17
|
+
composes with the orbit instead of fighting its relax. A snap (ease it in the page); `null`
|
|
18
|
+
resets. Mono only: a no-op for the pixels in woven 3D, where the head tracker owns the eyes, and
|
|
19
|
+
ignored on `controls: 'page'`. **`handle.viewOffset`** reads it back. `pick()` and the effects'
|
|
20
|
+
eye frame follow the offset camera.
|
|
21
|
+
|
|
22
|
+
## 1.25.1 — 2026-09-25
|
|
23
|
+
|
|
24
|
+
Touches the **preview tier** (`./splat`, `engine: 'playcanvas'` only). Additive; nothing changes
|
|
25
|
+
for a page that does not call it.
|
|
26
|
+
|
|
27
|
+
- **`handle.setRenderScale(s)`** changes the per-eye buffer scale (the `renderScale` option) live,
|
|
28
|
+
in (0, 4], and resizes the backing store on the next animation frame; **`handle.renderScale`**
|
|
29
|
+
reads it back. For an app whose screens differ: full resolution where the splat is hidden and
|
|
30
|
+
only stage quads draw, a lower scale where a large splat is on screen. Replaces poking the
|
|
31
|
+
viewer's internal field.
|
|
32
|
+
|
|
8
33
|
## 1.25.0 — 2026-09-25
|
|
9
34
|
|
|
10
35
|
Touches the **preview tier** (a new subpath, `./player`) and the **core** (`.`, frozen tier): new
|
|
@@ -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. */
|
|
@@ -1867,12 +1905,40 @@ export class PlayCanvasSplatViewer {
|
|
|
1867
1905
|
} else perspectiveFov(this.mono.fov, aspect, this.mono.near, this.mono.far, this.mono.proj);
|
|
1868
1906
|
}
|
|
1869
1907
|
|
|
1908
|
+
/**
|
|
1909
|
+
* handle.setViewOffset — a normalised eye offset in the mono camera's plane (see offsetMonoView),
|
|
1910
|
+
* clamped to the unit disc. Mono only: in woven 3D the head tracker owns the eyes and this is
|
|
1911
|
+
* stored but not drawn; on controls:'page' the page owns the camera and it is ignored. A snap,
|
|
1912
|
+
* like setPose, so the page eases it (and the frame loop draws it next frame).
|
|
1913
|
+
*/
|
|
1914
|
+
setViewOffset(x, y) {
|
|
1915
|
+
const r = Math.hypot(x, y);
|
|
1916
|
+
const k = r > 1 ? 1 / r : 1;
|
|
1917
|
+
this._viewOffset[0] = x * k;
|
|
1918
|
+
this._viewOffset[1] = y * k;
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
/** The mono camera as drawn: the offset one when setViewOffset is engaged, else mono itself. */
|
|
1922
|
+
_monoView() {
|
|
1923
|
+
const m = this.mono;
|
|
1924
|
+
const [ox, oy] = this._viewOffset;
|
|
1925
|
+
if (this.pageCamera || (!ox && !oy)) return { proj: m.proj, pose: m.pose };
|
|
1926
|
+
const o = this._orbitCentre;
|
|
1927
|
+
const p = m.pose;
|
|
1928
|
+
// The focus's distance along the view axis (−z): the pivot puts it at the orbit centre.
|
|
1929
|
+
const c = -((o.x - p[12]) * p[8] + (o.y - p[13]) * p[9] + (o.z + this._depthOffset - p[14]) * p[10]);
|
|
1930
|
+
return offsetMonoView(p, m.proj, ox, oy, c, this.orbitMaxDeg, this._offPose, this._offProj)
|
|
1931
|
+
? { proj: this._offProj, pose: this._offPose }
|
|
1932
|
+
: { proj: m.proj, pose: m.pose };
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1870
1935
|
_drawMono() {
|
|
1871
1936
|
const c = this.canvas;
|
|
1872
1937
|
this._monoEntry ||= [{ proj: this.mono.proj, pose: this.mono.pose, x: 0, y: 0, width: 0, height: 0 }];
|
|
1873
1938
|
const e = this._monoEntry[0];
|
|
1874
|
-
|
|
1875
|
-
e.
|
|
1939
|
+
const v = this._monoView();
|
|
1940
|
+
e.proj = v.proj;
|
|
1941
|
+
e.pose = v.pose;
|
|
1876
1942
|
e.width = c.width;
|
|
1877
1943
|
e.height = c.height;
|
|
1878
1944
|
this._drawEntries(this._monoEntry, null);
|
|
@@ -2135,7 +2201,7 @@ export class PlayCanvasSplatViewer {
|
|
|
2135
2201
|
*/
|
|
2136
2202
|
eyeFrame() {
|
|
2137
2203
|
const R = this.rigMatrix();
|
|
2138
|
-
const es = this._mode === '3d' && this._lastGood ? this._lastGood.entries : [
|
|
2204
|
+
const es = this._mode === '3d' && this._lastGood ? this._lastGood.entries : [this._monoView()];
|
|
2139
2205
|
const o = [0, 0, 0];
|
|
2140
2206
|
for (const e of es) {
|
|
2141
2207
|
const M = mat4Mul(R, e.pose);
|
|
@@ -2352,7 +2418,7 @@ export class PlayCanvasSplatViewer {
|
|
|
2352
2418
|
/** The camera currently on screen: [proj, pose] of the first eye in 3D, the mono camera else. */
|
|
2353
2419
|
currentView() {
|
|
2354
2420
|
if (this._mode === '3d' && this._lastGood) return this._lastGood.entries[0];
|
|
2355
|
-
return
|
|
2421
|
+
return this._monoView();
|
|
2356
2422
|
}
|
|
2357
2423
|
}
|
|
2358
2424
|
|
|
@@ -2981,6 +3047,15 @@ export function attachPlayCanvasSplat(out, wall, canvas, src, opts, pending = []
|
|
|
2981
3047
|
let cancelPendingVideo = null;
|
|
2982
3048
|
let warnedAutoplayMuted = false;
|
|
2983
3049
|
|
|
3050
|
+
// The current per-eye buffer scale (the `renderScale` option, or the last setRenderScale). A
|
|
3051
|
+
// real accessor: Object.assign below would copy a getter's value once.
|
|
3052
|
+
Object.defineProperty(out, 'renderScale', { get: () => viewer.renderScale, enumerable: true, configurable: true });
|
|
3053
|
+
// The current eye offset (after the unit-disc clamp), a fresh object per read.
|
|
3054
|
+
Object.defineProperty(out, 'viewOffset', {
|
|
3055
|
+
get: () => ({ x: viewer._viewOffset[0], y: viewer._viewOffset[1] }),
|
|
3056
|
+
enumerable: true,
|
|
3057
|
+
configurable: true,
|
|
3058
|
+
});
|
|
2984
3059
|
Object.assign(out, {
|
|
2985
3060
|
backend: 'playcanvas',
|
|
2986
3061
|
engine: null, // { app, root, camera } once the engine has booted — see below
|
|
@@ -3065,6 +3140,38 @@ export function attachPlayCanvasSplat(out, wall, canvas, src, opts, pending = []
|
|
|
3065
3140
|
layerRigs().set(layer, r.rig, r.opts);
|
|
3066
3141
|
return out;
|
|
3067
3142
|
},
|
|
3143
|
+
/**
|
|
3144
|
+
* Change the per-eye buffer scale live (the `renderScale` option): e.g. full resolution on a
|
|
3145
|
+
* screen that hides the splat and draws only stage quads, 0.6 elsewhere. The backing store is
|
|
3146
|
+
* resized on the next animation frame (never mid-frame). Returns the handle.
|
|
3147
|
+
*/
|
|
3148
|
+
setRenderScale(s) {
|
|
3149
|
+
if (typeof s !== 'number' || !Number.isFinite(s) || s <= 0 || s > 4) {
|
|
3150
|
+
throw new RangeError('@displayxr/inline3d/splat: setRenderScale takes a number in (0, 4].');
|
|
3151
|
+
}
|
|
3152
|
+
if (viewer.renderScale !== s) {
|
|
3153
|
+
viewer.renderScale = s;
|
|
3154
|
+
viewer._scheduleResize();
|
|
3155
|
+
}
|
|
3156
|
+
return out;
|
|
3157
|
+
},
|
|
3158
|
+
/**
|
|
3159
|
+
* The 2D tier's eye offset: `{x, y}` normalised in the mono camera's plane (+x right, +y up),
|
|
3160
|
+
* clamped to the unit disc; |offset| = 1 swings the line of sight to the focus by orbitMaxDeg
|
|
3161
|
+
* (15°), off-axis so the focus plane stays put — a head-parallax analogue (a phone's tilt)
|
|
3162
|
+
* where there are no tracked eyes. Composes with the orbit. A snap: ease it in the page.
|
|
3163
|
+
* `null` = {0, 0}. Mono only — a no-op for the pixels in woven 3D (the head tracker owns the
|
|
3164
|
+
* eyes; the value is kept for a later fall back to 2D) and on controls:'page'.
|
|
3165
|
+
*/
|
|
3166
|
+
setViewOffset(o) {
|
|
3167
|
+
const x = o == null ? 0 : o.x ?? 0;
|
|
3168
|
+
const y = o == null ? 0 : o.y ?? 0;
|
|
3169
|
+
if (typeof x !== 'number' || typeof y !== 'number' || !Number.isFinite(x) || !Number.isFinite(y)) {
|
|
3170
|
+
throw new TypeError('@displayxr/inline3d/splat: setViewOffset takes {x, y} finite numbers (or null).');
|
|
3171
|
+
}
|
|
3172
|
+
viewer.setViewOffset(x, y);
|
|
3173
|
+
return out;
|
|
3174
|
+
},
|
|
3068
3175
|
/** Change the layer rig's tile-wide options live (no layer change): merge, `null` clears. */
|
|
3069
3176
|
setLayerRigOptions(o = {}) {
|
|
3070
3177
|
layerRigs().setOptions(validateLayerRigOptions(o, 'setLayerRigOptions'));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@displayxr/inline3d",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.26.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
|
@@ -994,6 +994,29 @@ export interface SplatHandle {
|
|
|
994
994
|
* `?dxrdiag=nolayerrig`. Callable before `ready`. docs/proposals/layer-display-rig.md.
|
|
995
995
|
*/
|
|
996
996
|
setLayerRig(layer: string | number | { id: number }, rig: 'display' | 'camera', opts?: SplatLayerRigOptions): SplatHandle;
|
|
997
|
+
/**
|
|
998
|
+
* `engine: 'playcanvas'` only. Change the per-eye buffer scale live (the `renderScale` option),
|
|
999
|
+
* in (0, 4]; the backing store is resized on the next animation frame. Throws a RangeError
|
|
1000
|
+
* otherwise.
|
|
1001
|
+
*/
|
|
1002
|
+
setRenderScale(scale: number): SplatHandle;
|
|
1003
|
+
/** `engine: 'playcanvas'` only. The current per-eye buffer scale. */
|
|
1004
|
+
readonly renderScale: number;
|
|
1005
|
+
/**
|
|
1006
|
+
* `engine: 'playcanvas'` only. The 2D tier's EYE offset — a head-parallax analogue for a flat
|
|
1007
|
+
* screen with no tracked eyes (e.g. a phone's tilt). `{x, y}` is normalised in the mono camera's
|
|
1008
|
+
* plane (+x right, +y up) and clamped to the unit disc; |offset| = 1 moves the eye far enough to
|
|
1009
|
+
* swing its line of sight to the focus by `orbitMaxDeg` (15° by default), the drag orbit's comfort
|
|
1010
|
+
* cone. Off-axis: the focus plane stays put on screen while nearer content moves against the
|
|
1011
|
+
* farther. It moves the eye, not the scene, so it composes with the orbit (`setPose`, the drag)
|
|
1012
|
+
* rather than fighting it. A snap, like `setPose` — ease it in the page. `null` resets to
|
|
1013
|
+
* `{0, 0}`. Mono only: in woven 3D the head tracker owns the eyes, so the value is kept (it applies
|
|
1014
|
+
* again if the tile falls back to 2D) but draws nothing; ignored on `controls: 'page'`. Throws a
|
|
1015
|
+
* TypeError on a non-finite component.
|
|
1016
|
+
*/
|
|
1017
|
+
setViewOffset(offset: { x?: number; y?: number } | null): SplatHandle;
|
|
1018
|
+
/** `engine: 'playcanvas'` only. The current eye offset (after the unit-disc clamp). */
|
|
1019
|
+
readonly viewOffset: { x: number; y: number };
|
|
997
1020
|
/** `engine: 'playcanvas'` only. Change the layer rig's tile-wide options live (merge; `null` clears). */
|
|
998
1021
|
setLayerRigOptions(opts: SplatLayerRigOptions): SplatHandle;
|
|
999
1022
|
/**
|