@displayxr/inline3d 1.9.0 → 1.9.1
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 +32 -0
- package/js/inline3d-splat-playcanvas.js +84 -4
- package/package.json +2 -1
- package/splat.d.ts +8 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,38 @@ 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.9.1 — 2026-09-23
|
|
9
|
+
|
|
10
|
+
Touches the **preview tier** (`./splat`, `engine: 'playcanvas'` only), plus the package manifest.
|
|
11
|
+
Nothing changes for Spark callers or for splat-only pages. The fixes affect only what a page can
|
|
12
|
+
do with `handle.engine`.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- **`handle.engine.root` is usable as documented.** The tile's `AppBase` now registers `Render`,
|
|
17
|
+
`Light` and `Anim` component systems and a `Container` resource handler, next to `Camera`,
|
|
18
|
+
`GSplat` and `Texture`/`GSplat`. That is exactly what a glTF needs, skinned and animated
|
|
19
|
+
included. Before, a page had to register them itself or `instantiateRenderEntity()` produced
|
|
20
|
+
nothing.
|
|
21
|
+
- A page that still registers them now gets the existing system back instead of an engine
|
|
22
|
+
"already registered" throw.
|
|
23
|
+
- Cost: +0.1 ms boot (`AppBase.init` 0.4 → 0.5 ms). +41.8 KB gzip only for a hand-tree-shaken
|
|
24
|
+
engine build; nothing for the SDK as shipped.
|
|
25
|
+
- Verified with a skinned, animated `.glb` over a splat: composited, depth-tested against nearer
|
|
26
|
+
splats, animation advancing.
|
|
27
|
+
- **`"./package.json"` is exported**, so `import pkg from '@displayxr/inline3d/package.json'` and
|
|
28
|
+
`require.resolve('@displayxr/inline3d/package.json')` work instead of
|
|
29
|
+
`ERR_PACKAGE_PATH_NOT_EXPORTED`.
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
|
|
33
|
+
- **`nearClip` / `farClip`** (PlayCanvas): a floor on the projection's near plane and a cap on its
|
|
34
|
+
far plane, for depth precision in a mixed mesh + splat scene. The adapter still owns the
|
|
35
|
+
projections, and unset leaves them untouched.
|
|
36
|
+
- Docs: `handle.engine` in [`docs/playcanvas-adapter.md`](docs/playcanvas-adapter.md). Covers what
|
|
37
|
+
is registered, the cost, and two engine gotchas (lights shine along local −Y; use `AnimTrack.name`
|
|
38
|
+
with `assignAnimation`).
|
|
39
|
+
|
|
8
40
|
## 1.9.0 — 2026-09-23
|
|
9
41
|
|
|
10
42
|
Touches the **preview tier** (`./splat`, `engine: 'playcanvas'` only). Additive: nothing changes for
|
|
@@ -84,6 +84,54 @@ import {
|
|
|
84
84
|
streamedBytesError,
|
|
85
85
|
} from './inline3d-splat-shared.js';
|
|
86
86
|
|
|
87
|
+
/**
|
|
88
|
+
* The component systems the tile's `AppBase` registers. Camera + GSplat draw the splat; Render,
|
|
89
|
+
* Light and Anim are what a page needs to put a glTF — skinned and animated included — and its
|
|
90
|
+
* lights under `handle.engine.root` (1.9.1; before that the page had to register them itself).
|
|
91
|
+
* Nothing else from the engine's full `Application` list (physics, UI, audio, particles, scripts
|
|
92
|
+
* and so on are the page's own business if it wants them).
|
|
93
|
+
*/
|
|
94
|
+
export const PLAYCANVAS_SYSTEMS = Object.freeze([
|
|
95
|
+
'CameraComponentSystem',
|
|
96
|
+
'GSplatComponentSystem',
|
|
97
|
+
'RenderComponentSystem',
|
|
98
|
+
'LightComponentSystem',
|
|
99
|
+
'AnimComponentSystem',
|
|
100
|
+
]);
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The resource handlers the tile's loader registers. Texture + GSplat load a splat (a bundled
|
|
104
|
+
* .sog is a zip of webp planes the loader registers as textures); Container loads a .glb/.gltf.
|
|
105
|
+
* The container's sub-assets (render, material, animation) arrive already loaded, so they need no
|
|
106
|
+
* handler of their own.
|
|
107
|
+
*/
|
|
108
|
+
export const PLAYCANVAS_HANDLERS = Object.freeze(['TextureHandler', 'GSplatHandler', 'ContainerHandler']);
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Registering a component system the app already has THROWS in the engine ("already
|
|
112
|
+
* registered"). Pages written before 1.9.1 add Render/Light/Anim themselves, so a second `add` of
|
|
113
|
+
* an id that exists is made a no-op that returns the registered system (the duplicate the page
|
|
114
|
+
* constructed is destroyed, so it leaves no listeners behind).
|
|
115
|
+
*/
|
|
116
|
+
function guardDuplicateSystems(app) {
|
|
117
|
+
const reg = app.systems;
|
|
118
|
+
if (!reg || typeof reg.add !== 'function' || reg._dxrGuarded) return;
|
|
119
|
+
const add = reg.add.bind(reg);
|
|
120
|
+
reg.add = (system) => {
|
|
121
|
+
const existing = system?.id ? reg[system.id] : null;
|
|
122
|
+
if (existing && existing !== system) {
|
|
123
|
+
try {
|
|
124
|
+
system.destroy?.();
|
|
125
|
+
} catch {
|
|
126
|
+
/* a half-built duplicate: nothing to release */
|
|
127
|
+
}
|
|
128
|
+
return existing;
|
|
129
|
+
}
|
|
130
|
+
return add(system);
|
|
131
|
+
};
|
|
132
|
+
reg._dxrGuarded = true;
|
|
133
|
+
}
|
|
134
|
+
|
|
87
135
|
/** The engine release this adapter was built and measured against (npm peer floor). */
|
|
88
136
|
export const PLAYCANVAS_TESTED = '2.22.3';
|
|
89
137
|
|
|
@@ -138,6 +186,24 @@ export function frustumFromProjection(P) {
|
|
|
138
186
|
};
|
|
139
187
|
}
|
|
140
188
|
|
|
189
|
+
/**
|
|
190
|
+
* Raise a perspective projection's near plane to at least `nearFloor` and lower its far plane to
|
|
191
|
+
* at most `farCap`, IN PLACE, leaving the frustum's shape (fov, skew, principal point) untouched —
|
|
192
|
+
* only the depth mapping (P[10], P[14]) is rewritten. Idempotent. A null bound is left alone.
|
|
193
|
+
*/
|
|
194
|
+
export function clampProjectionDepth(P, nearFloor, farCap) {
|
|
195
|
+
const f0 = frustumFromProjection(P);
|
|
196
|
+
let n = f0.nearClip;
|
|
197
|
+
let f = f0.farClip;
|
|
198
|
+
if (nearFloor !== null && nearFloor !== undefined && nearFloor > n) n = nearFloor;
|
|
199
|
+
if (farCap !== null && farCap !== undefined && farCap < f) f = farCap;
|
|
200
|
+
if (!(f > n)) f = n * 1.0001 + 1e-6;
|
|
201
|
+
if (n === f0.nearClip && f === f0.farClip) return P;
|
|
202
|
+
P[10] = -(f + n) / (f - n);
|
|
203
|
+
P[14] = (-2 * f * n) / (f - n);
|
|
204
|
+
return P;
|
|
205
|
+
}
|
|
206
|
+
|
|
141
207
|
/** Rigid pose (position + unit quaternion xyzw) as a column-major 4×4. */
|
|
142
208
|
export function poseMatrix(p, q, out = new Float64Array(16)) {
|
|
143
209
|
const [x, y, z, w] = q;
|
|
@@ -467,8 +533,15 @@ export class PlayCanvasSplatViewer {
|
|
|
467
533
|
orbitEase = {},
|
|
468
534
|
feather = 0,
|
|
469
535
|
captureFit = 'height',
|
|
536
|
+
nearClip,
|
|
537
|
+
farClip,
|
|
470
538
|
} = opts;
|
|
471
539
|
this.canvas = canvas;
|
|
540
|
+
// Depth range for a MIXED scene (meshes under handle.engine.root depth-test against each
|
|
541
|
+
// other; splats only test against them). The projections' own near/far stay the adapter's —
|
|
542
|
+
// these only raise the near (floor) and lower the far (cap). Unset: untouched.
|
|
543
|
+
this.nearClip = Number.isFinite(nearClip) && nearClip > 0 ? nearClip : null;
|
|
544
|
+
this.farClip = Number.isFinite(farClip) && farClip > 0 ? farClip : null;
|
|
472
545
|
// The tilt-and-relax orbit (./inline3d-splat-shared.js §ORBIT): drag tilts up to ±orbitMaxDeg
|
|
473
546
|
// from where the press started, easing with τ = orbitEase.drag; release relaxes back with
|
|
474
547
|
// τ = orbitEase.rest. `_orbitMode` is 'drag' | 'rest' | null (null = ordinary damping).
|
|
@@ -784,12 +857,14 @@ export class PlayCanvasSplatViewer {
|
|
|
784
857
|
const opts = new pc.AppOptions();
|
|
785
858
|
opts.graphicsDevice = device;
|
|
786
859
|
// No xr (AppBase constructs XrManager only when asked, and XrManager is what probes and
|
|
787
|
-
// can request immersive sessions), no mouse/keyboard/touch: the SDK owns input.
|
|
788
|
-
|
|
789
|
-
//
|
|
790
|
-
opts.
|
|
860
|
+
// can request immersive sessions), no mouse/keyboard/touch: the SDK owns input. Beyond the
|
|
861
|
+
// splat itself, exactly what a glTF-with-animation under `handle.engine.root` needs — see
|
|
862
|
+
// PLAYCANVAS_SYSTEMS / PLAYCANVAS_HANDLERS.
|
|
863
|
+
opts.componentSystems = PLAYCANVAS_SYSTEMS.map((n) => pc[n]).filter(Boolean);
|
|
864
|
+
opts.resourceHandlers = PLAYCANVAS_HANDLERS.map((n) => pc[n]).filter(Boolean);
|
|
791
865
|
const app = new pc.AppBase(this.canvas);
|
|
792
866
|
app.init(opts);
|
|
867
|
+
guardDuplicateSystems(app);
|
|
793
868
|
// The SDK sizes the buffer (double-width in 3D, 1:1 in mono), so the engine must never
|
|
794
869
|
// resize it. RESOLUTION_FIXED is AppBase's DEFAULT, and with it `updateCanvasSize()` is a
|
|
795
870
|
// no-op. Deliberately NOT calling setCanvasResolution/setCanvasFillMode: without explicit
|
|
@@ -1047,6 +1122,9 @@ export class PlayCanvasSplatViewer {
|
|
|
1047
1122
|
const el = this.canvas;
|
|
1048
1123
|
const sx = cache && cache.bufW > 0 && el.width ? el.width / cache.bufW : 1;
|
|
1049
1124
|
const sy = cache && cache.bufH > 0 && el.height ? el.height / cache.bufH : 1;
|
|
1125
|
+
if (this.nearClip !== null || this.farClip !== null) {
|
|
1126
|
+
for (const e of entries) clampProjectionDepth(e.proj, this.nearClip, this.farClip);
|
|
1127
|
+
}
|
|
1050
1128
|
const rect = (e) =>
|
|
1051
1129
|
sx !== 1 || sy !== 1
|
|
1052
1130
|
? [Math.round(e.x * sx), Math.round(e.y * sy), Math.max(1, Math.round(e.width * sx)), Math.max(1, Math.round(e.height * sy))]
|
|
@@ -1627,6 +1705,8 @@ export function attachPlayCanvasSplat(out, wall, canvas, src, opts, pending = []
|
|
|
1627
1705
|
orbitEase: opts.orbitEase,
|
|
1628
1706
|
feather,
|
|
1629
1707
|
captureFit,
|
|
1708
|
+
nearClip: opts.nearClip,
|
|
1709
|
+
farClip: opts.farClip,
|
|
1630
1710
|
});
|
|
1631
1711
|
|
|
1632
1712
|
let handle = null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@displayxr/inline3d",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.1",
|
|
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",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"types": "./model.d.ts",
|
|
28
28
|
"import": "./js/inline3d-model.js"
|
|
29
29
|
},
|
|
30
|
+
"./package.json": "./package.json",
|
|
30
31
|
"./undock": {
|
|
31
32
|
"types": "./index.d.ts",
|
|
32
33
|
"import": "./js/inline3d-undock.js"
|
package/splat.d.ts
CHANGED
|
@@ -259,6 +259,14 @@ export interface SplatOptions {
|
|
|
259
259
|
* `'height'`. Both backends; the 3D rig's vertical FOV follows the crop. Anything else throws.
|
|
260
260
|
*/
|
|
261
261
|
captureFit?: 'height' | 'cover';
|
|
262
|
+
/**
|
|
263
|
+
* PlayCanvas: a FLOOR on the projection's near plane, in world units (the adapter owns the
|
|
264
|
+
* projections; this only raises near, for depth precision when meshes share the scene under
|
|
265
|
+
* `handle.engine.root`). Anything nearer is clipped, splats included. Unset: untouched.
|
|
266
|
+
*/
|
|
267
|
+
nearClip?: number;
|
|
268
|
+
/** PlayCanvas: a CAP on the projection's far plane (only ever lowers it). Unset: untouched. */
|
|
269
|
+
farClip?: number;
|
|
262
270
|
/** Camera rig only: the distance in world metres that sits ON the glass. */
|
|
263
271
|
convergence?: number;
|
|
264
272
|
/**
|