@displayxr/inline3d 1.25.1 → 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 +14 -0
- package/js/inline3d-splat-playcanvas.js +93 -4
- package/package.json +1 -1
- package/splat.d.ts +15 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,20 @@ 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
|
+
|
|
8
22
|
## 1.25.1 — 2026-09-25
|
|
9
23
|
|
|
10
24
|
Touches the **preview tier** (`./splat`, `engine: 'playcanvas'` only). Additive; nothing changes
|
|
@@ -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
|
|
|
@@ -2984,6 +3050,12 @@ export function attachPlayCanvasSplat(out, wall, canvas, src, opts, pending = []
|
|
|
2984
3050
|
// The current per-eye buffer scale (the `renderScale` option, or the last setRenderScale). A
|
|
2985
3051
|
// real accessor: Object.assign below would copy a getter's value once.
|
|
2986
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
|
+
});
|
|
2987
3059
|
Object.assign(out, {
|
|
2988
3060
|
backend: 'playcanvas',
|
|
2989
3061
|
engine: null, // { app, root, camera } once the engine has booted — see below
|
|
@@ -3083,6 +3155,23 @@ export function attachPlayCanvasSplat(out, wall, canvas, src, opts, pending = []
|
|
|
3083
3155
|
}
|
|
3084
3156
|
return out;
|
|
3085
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
|
+
},
|
|
3086
3175
|
/** Change the layer rig's tile-wide options live (no layer change): merge, `null` clears. */
|
|
3087
3176
|
setLayerRigOptions(o = {}) {
|
|
3088
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
|
@@ -1002,6 +1002,21 @@ export interface SplatHandle {
|
|
|
1002
1002
|
setRenderScale(scale: number): SplatHandle;
|
|
1003
1003
|
/** `engine: 'playcanvas'` only. The current per-eye buffer scale. */
|
|
1004
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 };
|
|
1005
1020
|
/** `engine: 'playcanvas'` only. Change the layer rig's tile-wide options live (merge; `null` clears). */
|
|
1006
1021
|
setLayerRigOptions(opts: SplatLayerRigOptions): SplatHandle;
|
|
1007
1022
|
/**
|