@displayxr/inline3d 1.11.0 → 1.12.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 +70 -0
- package/README.md +18 -5
- package/js/inline3d-model-entry.js +242 -0
- package/js/inline3d-model-playcanvas.js +754 -0
- package/js/inline3d-playcanvas-engine.js +82 -0
- package/js/inline3d-splat-playcanvas.js +247 -25
- package/js/inline3d-splat-shared.js +33 -0
- package/model.d.ts +109 -30
- package/package.json +15 -3
- package/splat.d.ts +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,76 @@ 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.12.1 — 2026-09-24
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **`setSource(…, { fadeMs })` is a real crossfade** (`./splat`, `engine: 'playcanvas'`). The fade
|
|
13
|
+
scaled every splat's alpha by t and drew both photos in one sorted pass, so whichever photo sat in
|
|
14
|
+
front won: an incoming photo in front covered 64 % of the picture at t = 0.2 (an 800 ms fade read
|
|
15
|
+
as a snap), one behind it only 18 % at t = 0.5 (a late pop). Now the last frame of the outgoing
|
|
16
|
+
asset is frozen into a texture (both eyes) and lerped with the live incoming one, per
|
|
17
|
+
premultiplied pixel, alpha included. Measured on three SHARP photo pairs, mono and a fake 2-view
|
|
18
|
+
stereo pair: the blend fraction is **t to ±0.001** at every tenth, identical in both eyes; the
|
|
19
|
+
frame at t = 0 equals the outgoing frame and the end state equals a `fadeMs: 0` swap (MAE 0.000,
|
|
20
|
+
colour and alpha). The outgoing photo holds still during the fade. If no frame is drawn within
|
|
21
|
+
250 ms (a hidden tab) the old one-pass fade runs, now with a coverage-linear alpha remap.
|
|
22
|
+
|
|
23
|
+
## 1.12.0 — 2026-09-24
|
|
24
|
+
|
|
25
|
+
Touches the **preview tier** (`./model`, and `./splat`'s `engine: 'playcanvas'` import path). A
|
|
26
|
+
page that calls `addModel` without `engine` now renders with PlayCanvas and **looks different**:
|
|
27
|
+
it is lit to match the Khronos glTF Sample Viewer instead of three's RoomEnvironment. Pass
|
|
28
|
+
`engine: 'three'` to keep 1.11 exactly. Epic [#36](https://github.com/DisplayXR/displayxr-web/issues/36);
|
|
29
|
+
design and measurements: [`docs/playcanvas-model-backend.md`](docs/playcanvas-model-backend.md).
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- **`addModel` defaults to PlayCanvas** (optional peer `playcanvas >=2.22.3 <3`). The handle is
|
|
34
|
+
the same: `ready`, `firstWoven`, `setPose`, `resetPose`, `exclude`, `unexclude`, `remove`,
|
|
35
|
+
`frame`, `model`, `viewer`. On PlayCanvas `model` is the render `pc.Entity`, and the handle also
|
|
36
|
+
carries `backend`, `engine` (`{ app, root, camera }`) and `container`.
|
|
37
|
+
- **Both engines are imported dynamically** by `./model` (now `js/inline3d-model-entry.js`): a
|
|
38
|
+
default page loads no three, an `engine: 'three'` page loads no PlayCanvas.
|
|
39
|
+
- **`handle.viewer` is null until the backend module has loaded**, on both engines (1.11 set it
|
|
40
|
+
synchronously). Read it after `ready`, or import **`@displayxr/inline3d/model/three`**: the 1.11
|
|
41
|
+
module, byte for byte, synchronous `viewer` included.
|
|
42
|
+
- **No `engine` and no `playcanvas` installed:** the tile renders on three with one console
|
|
43
|
+
warning. Neither installed: `ready` rejects naming both. With no `engine`, a three.js object in
|
|
44
|
+
`GLTFLoader` / `DRACOLoader` / `KTX2Loader` / `envMap` selects three (a 1.11 page that injects
|
|
45
|
+
loaders keeps working unchanged).
|
|
46
|
+
- **The PlayCanvas adapters import `playcanvas` by name** (`js/inline3d-playcanvas-engine.js`), not
|
|
47
|
+
as a namespace, so bundlers tree-shake the engine: 2,434,536 → 1,499,851 bytes minified,
|
|
48
|
+
**626 → 397 KB gzip** (esbuild 0.28.2), for `addModel` and for `addSplat(…, {engine:'playcanvas'})`.
|
|
49
|
+
|
|
50
|
+
### Added (PlayCanvas backend)
|
|
51
|
+
|
|
52
|
+
- **Lighting matched to the Khronos glTF Sample Viewer** (not to three.js): an in-memory neutral
|
|
53
|
+
studio fitted to the Sample Viewer's "Studio Neutral" (no asset, no licence), Khronos PBR Neutral
|
|
54
|
+
tone mapping, exposure 1, sRGB output, MSAA on. On-object MAE vs the Sample Viewer (/255):
|
|
55
|
+
DamagedHelmet 16.7 (three 36.2), Fox 6.4 (19.5), BrainStem 2.7 (26.7), MetalRoughSpheres 9.4 (26.4).
|
|
56
|
+
- `environment: 'neutral'` (alias of the default `'room'`), `environmentRotation` (degrees),
|
|
57
|
+
`envMap` as a URL (equirect .hdr/.png/.jpg) or a `pc.Texture`; `'studio'` and `'none'` as on three.
|
|
58
|
+
- Compressed glTF with the **same served files as three**: Draco and KTX2/Basis through the
|
|
59
|
+
engine's own workers, fed three's `libs/draco/` and `libs/basis/`; `decoderPath` unchanged; a
|
|
60
|
+
mis-served folder rejects `ready` naming the extension, the file and the option.
|
|
61
|
+
`EXT_meshopt_compression` via meshoptimizer's `MeshoptDecoder` (new optional peer
|
|
62
|
+
`meshoptimizer >=1`, loaded only for an asset that declares it; `meshoptDecoder` injects it).
|
|
63
|
+
- `controls: 'page'` + `setCameraPose` / `getCameraPose` / `onBeforeFrame` / `comfortDepth` on
|
|
64
|
+
models, as on splats (the rig converges on the model's bounds centre unless the pose says).
|
|
65
|
+
- `antialias`, `preserveDrawingBuffer`, `nearClip`, `farClip`, `orbitMaxDeg`, `orbitEase`, `playcanvas`.
|
|
66
|
+
|
|
67
|
+
### Throws at call time
|
|
68
|
+
|
|
69
|
+
- An unknown `engine` or `environment`; a PlayCanvas-only option with `engine: 'three'`; a three.js
|
|
70
|
+
loader or texture with `engine: 'playcanvas'`; a `meshoptDecoder` without `decodeGltfBuffer`;
|
|
71
|
+
`setCameraPose` without `controls: 'page'`; `setPose` / `resetPose` with it.
|
|
72
|
+
|
|
73
|
+
### Samples
|
|
74
|
+
|
|
75
|
+
- `samples/model/` (tiles A and C) and `samples/shopify/` render on the new default;
|
|
76
|
+
`?engine=three` switches back. Tile B stays on three (it composes a Spark splat into a three scene).
|
|
77
|
+
|
|
8
78
|
## 1.11.0 — 2026-09-24
|
|
9
79
|
|
|
10
80
|
Touches the **preview tier** (`./splat`, `engine: 'playcanvas'` only), additively. Nothing changes
|
package/README.md
CHANGED
|
@@ -34,8 +34,9 @@ No build step or bundler required — it's plain ES modules. You can also import
|
|
|
34
34
|
URL from a CDN (jsDelivr / unpkg) without npm. The samples in this repo import the SDK by relative
|
|
35
35
|
path (`./js/inline3d.js`) so they run straight off GitHub Pages; in your own app prefer the package.
|
|
36
36
|
|
|
37
|
-
`three
|
|
38
|
-
|
|
37
|
+
`three`, `@sparkjsdev/spark`, `playcanvas` and `meshoptimizer` are **optional peer dependencies**
|
|
38
|
+
— the core is dependency-free and only the scene subpaths need them (`/model` needs `playcanvas`,
|
|
39
|
+
or `three` with `engine: 'three'`). The two viewer subpaths are
|
|
39
40
|
**experimental**: they turn "one object in a tile, look around it, drag to spin" into a single
|
|
40
41
|
call (auto-framing on the zero-disparity plane, orbit, idle turntable, mono fallback), but their
|
|
41
42
|
API is not yet covered by the semver promise below.
|
|
@@ -48,9 +49,18 @@ tilt-and-relax orbit and `handle.engine` (the engine objects, for advanced pages
|
|
|
48
49
|
optional peer `playcanvas` (`>=2.22.3 <3`) and reads `.sog`, `.ply` and a Streamed-SOG
|
|
49
50
|
`lod-meta.json`. **Spark stays the default**; a page that never passes `engine` never loads
|
|
50
51
|
`playcanvas`. What differs, and why: [`docs/playcanvas-adapter.md`](docs/playcanvas-adapter.md).
|
|
51
|
-
Bundlers: the engine is a
|
|
52
|
+
Bundlers: the engine is reached through a named-import module (`js/inline3d-playcanvas-engine.js`,
|
|
53
|
+
so the unused ~40% of the engine tree-shakes away); its sort workers are Blob URLs (CSP
|
|
52
54
|
`worker-src blob:`), and esbuild needs `node:worker_threads` marked external.
|
|
53
55
|
|
|
56
|
+
**Models render with PlayCanvas by default (1.12).** `addModel(wall, canvas, 'chair.glb')` loads
|
|
57
|
+
the glTF with the PlayCanvas engine (optional peer `playcanvas`), lit to match the Khronos glTF
|
|
58
|
+
Sample Viewer: an in-memory neutral studio environment, Khronos PBR Neutral tone mapping,
|
|
59
|
+
exposure 1. `engine: 'three'` is the 1.11 three.js renderer, unchanged (also importable as
|
|
60
|
+
`@displayxr/inline3d/model/three`). Each engine is imported only when a tile asks for it. Draco
|
|
61
|
+
and KTX2 use the same served decoder folders on both engines. Design, the Sample-Viewer MAE table
|
|
62
|
+
and what throws: [`docs/playcanvas-model-backend.md`](docs/playcanvas-model-backend.md).
|
|
63
|
+
|
|
54
64
|
**Large scenes stream (preview).** On `engine: 'playcanvas'`, a Streamed SOG streams: pass the URL
|
|
55
65
|
of its `lod-meta.json` (or of its directory), not its bytes. The engine fetches only the chunks
|
|
56
66
|
the view needs and draws at most `perf.splatBudget` of them. That budget is per tile and shared
|
|
@@ -134,7 +144,7 @@ samples/
|
|
|
134
144
|
windows/ mixed 3D windows — still photos + a live video + a real-time three.js scene,
|
|
135
145
|
each woven with one SDK call, all on one session
|
|
136
146
|
splat/ a 3D Gaussian splat in a tile, auto-framed, with a 2D price plate over it
|
|
137
|
-
model/ a glTF mesh, a mesh+splat scene, and a Draco-COMPRESSED glTF
|
|
147
|
+
model/ a glTF mesh, a mesh+splat scene, and a Draco-COMPRESSED glTF (PlayCanvas; ?engine=three)
|
|
138
148
|
composition/ the 14-case 2D/3D overlap matrix — demo AND standing hardware regression
|
|
139
149
|
surface; red cases ship red (see samples/README.md)
|
|
140
150
|
vendor/draco/ three's Draco decoder, served for samples/model (compressed glTF needs it)
|
|
@@ -148,7 +158,10 @@ js/
|
|
|
148
158
|
(`perf` cuts overdraw; a `.sog`'s `camera` block picks the view rig)
|
|
149
159
|
inline3d-splat-playcanvas.js
|
|
150
160
|
preview: the `engine: 'playcanvas'` backend of addSplat(), loaded on demand
|
|
151
|
-
inline3d-model.js
|
|
161
|
+
inline3d-model-entry.js experimental: addModel() — picks the engine (PlayCanvas default)
|
|
162
|
+
inline3d-model-playcanvas.js the PlayCanvas model backend (Sample-Viewer lighting, engine decoders)
|
|
163
|
+
inline3d-model.js the three.js model backend (engine:'three'; ./model/three)
|
|
164
|
+
inline3d-playcanvas-engine.js named playcanvas imports for both PlayCanvas adapters (tree-shaking)
|
|
152
165
|
from what the asset declares (you serve the decoder files — see the guide)
|
|
153
166
|
docs/
|
|
154
167
|
authoring-inline-3d.md the authoring guide
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
// inline3d-model-entry.js — `@displayxr/inline3d/model`: a glTF/GLB model as an inline-3D window.
|
|
2
|
+
//
|
|
3
|
+
// EXPERIMENTAL. Not covered by the SDK's 1.x semver promise — see docs/sdk-stability.md.
|
|
4
|
+
//
|
|
5
|
+
// import { createInline3D } from '@displayxr/inline3d';
|
|
6
|
+
// import { addModel } from '@displayxr/inline3d/model';
|
|
7
|
+
//
|
|
8
|
+
// const wall = await createInline3D();
|
|
9
|
+
// const lamp = addModel(wall, canvas, 'lamp.glb', { virtualDisplayHeight: 0.3 });
|
|
10
|
+
// lamp.exclude(document.getElementById('buy'));
|
|
11
|
+
//
|
|
12
|
+
// WHICH ENGINE (1.12). PlayCanvas by default; `engine: 'three'` is the 1.11 renderer, unchanged
|
|
13
|
+
// (./inline3d-model.js, byte for byte — also importable directly as `@displayxr/inline3d/model/three`).
|
|
14
|
+
// BOTH are imported DYNAMICALLY from here, so a default page never resolves `three` and a
|
|
15
|
+
// three page never resolves `playcanvas`. The handle is returned synchronously on either engine;
|
|
16
|
+
// calls made before the backend has loaded (exclude() on the next line, above all) are queued
|
|
17
|
+
// and replayed, and `viewer` appears one module-load later — the same contract as
|
|
18
|
+
// `addSplat(…, { engine: 'playcanvas' })`.
|
|
19
|
+
//
|
|
20
|
+
// FALLBACK. With no `engine` given, a page that has three but not playcanvas keeps working: the
|
|
21
|
+
// engine import fails, one warning names what to install, and the tile renders on three. With
|
|
22
|
+
// neither, `ready` rejects naming both. An explicit `engine` never falls back.
|
|
23
|
+
|
|
24
|
+
/** The engines `addModel` renders with; the first is the default. */
|
|
25
|
+
export const MODEL_ENGINES = Object.freeze(['playcanvas', 'three']);
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Options whose VALUE is a three.js object. With no explicit `engine` they select three (the page
|
|
29
|
+
* plainly has it, and handing a three loader to PlayCanvas can only be a mistake); with
|
|
30
|
+
* `engine: 'playcanvas'` they throw.
|
|
31
|
+
*/
|
|
32
|
+
const THREE_ONLY = ['GLTFLoader', 'DRACOLoader', 'KTX2Loader'];
|
|
33
|
+
const isThreeTexture = (t) => !!t && typeof t === 'object' && (t.isTexture === true || t.isRenderTargetTexture === true);
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Which backend a call gets. Pure, synchronous.
|
|
37
|
+
*
|
|
38
|
+
* @returns {{engine:'playcanvas'|'three', explicit:boolean, reason:string}}
|
|
39
|
+
*/
|
|
40
|
+
export function resolveModelEngine(opts = {}) {
|
|
41
|
+
const e = opts.engine;
|
|
42
|
+
if (e !== undefined && e !== null) {
|
|
43
|
+
if (!MODEL_ENGINES.includes(e)) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
`@displayxr/inline3d/model: unknown engine "${e}" — expected ${MODEL_ENGINES.map((n) => `'${n}'`).join(' or ')} ` +
|
|
46
|
+
`(default 'playcanvas').`,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
return { engine: e, explicit: true, reason: 'option' };
|
|
50
|
+
}
|
|
51
|
+
const threeOpt = THREE_ONLY.find((k) => opts[k]) || (isThreeTexture(opts.envMap) ? 'envMap' : null);
|
|
52
|
+
if (threeOpt) return { engine: 'three', explicit: false, reason: `three.js object in \`${threeOpt}\`` };
|
|
53
|
+
return { engine: 'playcanvas', explicit: false, reason: 'default' };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Options that exist only on the PlayCanvas backend: an error on an explicit engine:'three'. */
|
|
57
|
+
const PLAYCANVAS_ONLY = ['playcanvas', 'environmentRotation', 'controls', 'comfortDepth', 'onBeforeFrame', 'antialias', 'preserveDrawingBuffer', 'nearClip', 'farClip', 'orbitMaxDeg', 'orbitEase'];
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Everything that can be checked before a module loads. Throws at CALL time — a page bug is true
|
|
61
|
+
* of every call and must not surface as an "asset failed" rejection.
|
|
62
|
+
*/
|
|
63
|
+
export function validateModelCall(canvas, src, opts, route) {
|
|
64
|
+
if (!canvas || typeof canvas.getBoundingClientRect !== 'function') {
|
|
65
|
+
throw new Error('@displayxr/inline3d/model: addModel(wall, canvas, src) — `canvas` must be an HTMLCanvasElement.');
|
|
66
|
+
}
|
|
67
|
+
if (typeof src !== 'string' || !src) {
|
|
68
|
+
throw new Error('@displayxr/inline3d/model: addModel(wall, canvas, src) — `src` must be the URL of a .glb or .gltf.');
|
|
69
|
+
}
|
|
70
|
+
if (route.engine === 'three' && route.explicit) {
|
|
71
|
+
const bad = PLAYCANVAS_ONLY.filter((k) => opts[k] !== undefined && !(k === 'controls' && opts[k] === 'viewer'));
|
|
72
|
+
if (bad.length) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
`@displayxr/inline3d/model: ${bad.map((k) => `\`${k}\``).join(', ')} ${bad.length === 1 ? 'is' : 'are'} PlayCanvas-backend ` +
|
|
75
|
+
"options and do nothing on engine:'three'. Drop them, or drop engine:'three'.",
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (route.engine === 'playcanvas' && route.explicit) {
|
|
80
|
+
const bad = THREE_ONLY.find((k) => opts[k]) || (isThreeTexture(opts.envMap) ? 'envMap' : null);
|
|
81
|
+
if (bad) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
`@displayxr/inline3d/model: \`${bad}\` is a three.js object and engine:'playcanvas' cannot use it. ` +
|
|
84
|
+
"Drop it (the engine decodes with its own readers; serve the files at `decoderPath`), or pass engine:'three'.",
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Load a glTF/GLB into an inline-3D window. See model.d.ts for every option.
|
|
92
|
+
*
|
|
93
|
+
* @returns {object} the handle, synchronously: `ready`, `firstWoven`, `setPose`, `resetPose`,
|
|
94
|
+
* `exclude`, `unexclude`, `remove` work at once; `viewer`, `model`, `frame`, `engine`
|
|
95
|
+
* fill in as the backend loads.
|
|
96
|
+
*/
|
|
97
|
+
export function addModel(wall, canvas, src, opts = {}) {
|
|
98
|
+
const route = resolveModelEngine(opts);
|
|
99
|
+
validateModelCall(canvas, src, opts, route);
|
|
100
|
+
if (route.engine === 'playcanvas') {
|
|
101
|
+
// The backend's own call-time checks (environment, envMap, controls, three loaders). Imported
|
|
102
|
+
// lazily below, so this is the cheap pure copy that lives there — see validatePlayCanvasModelOptions.
|
|
103
|
+
preValidatePlayCanvas(opts);
|
|
104
|
+
} else if (route.reason !== 'option' && route.reason !== 'default') {
|
|
105
|
+
console.info(`[inline3d/model] rendering ${src} with three.js: ${route.reason} (pass engine:'three' to say so explicitly).`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const pending = [];
|
|
109
|
+
const queue = (name) => (...args) => {
|
|
110
|
+
pending.push([name, args]);
|
|
111
|
+
};
|
|
112
|
+
const page = opts.controls === 'page';
|
|
113
|
+
let pendingPose = null;
|
|
114
|
+
const out = {
|
|
115
|
+
backend: route.engine,
|
|
116
|
+
engine: null,
|
|
117
|
+
viewer: null,
|
|
118
|
+
model: null,
|
|
119
|
+
frame: null,
|
|
120
|
+
setPose: queue('setPose'),
|
|
121
|
+
resetPose: queue('resetPose'),
|
|
122
|
+
setCameraPose(matrixWorld, o) {
|
|
123
|
+
if (!page) {
|
|
124
|
+
throw new Error("@displayxr/inline3d/model: setCameraPose() needs addModel(…, { controls:'page' }) — with the default controls the SDK owns the camera (use setPose).");
|
|
125
|
+
}
|
|
126
|
+
if (pendingPose) pendingPose[1] = [matrixWorld, o];
|
|
127
|
+
else pending.push((pendingPose = ['setCameraPose', [matrixWorld, o]]));
|
|
128
|
+
return out;
|
|
129
|
+
},
|
|
130
|
+
getCameraPose: () => null,
|
|
131
|
+
remove: queue('remove'),
|
|
132
|
+
exclude: queue('exclude'),
|
|
133
|
+
unexclude: queue('unexclude'),
|
|
134
|
+
};
|
|
135
|
+
out.firstWoven = new Promise((resolve) => {
|
|
136
|
+
out._resolveFirstWoven = resolve;
|
|
137
|
+
});
|
|
138
|
+
const settleFirstWoven = (reason) => {
|
|
139
|
+
if (typeof out._resolveFirstWoven === 'function') {
|
|
140
|
+
out._resolveFirstWoven(Object.freeze({ woven: false, confirmed: false, reason, ms: 0 }));
|
|
141
|
+
delete out._resolveFirstWoven;
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
out.ready = (async () => {
|
|
146
|
+
if (route.engine === 'three') return attachThree(out, wall, canvas, src, opts, pending);
|
|
147
|
+
const mod = await import('./inline3d-model-playcanvas.js');
|
|
148
|
+
let pc = opts.playcanvas || null;
|
|
149
|
+
if (!pc) {
|
|
150
|
+
try {
|
|
151
|
+
pc = await import('./inline3d-playcanvas-engine.js');
|
|
152
|
+
} catch (err) {
|
|
153
|
+
if (route.explicit) throw playcanvasMissing(err, false);
|
|
154
|
+
let three;
|
|
155
|
+
try {
|
|
156
|
+
three = await import('./inline3d-model.js');
|
|
157
|
+
} catch (err3) {
|
|
158
|
+
throw playcanvasMissing(err, err3);
|
|
159
|
+
}
|
|
160
|
+
console.warn(
|
|
161
|
+
`[inline3d/model] \`playcanvas\` could not be loaded (${err?.message || err}); rendering ${src} with three.js instead. ` +
|
|
162
|
+
"npm i playcanvas for the default renderer, or pass engine:'three' to silence this.",
|
|
163
|
+
);
|
|
164
|
+
out.backend = 'three';
|
|
165
|
+
return attachThree(out, wall, canvas, src, opts, pending, three);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return mod.attachPlayCanvasModel(out, wall, canvas, src, opts, pending, pc);
|
|
169
|
+
})().catch((err) => {
|
|
170
|
+
settleFirstWoven('layer-failed');
|
|
171
|
+
throw err;
|
|
172
|
+
});
|
|
173
|
+
return out;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function playcanvasMissing(err, err3) {
|
|
177
|
+
const e = new Error(
|
|
178
|
+
'@displayxr/inline3d/model: addModel renders with PlayCanvas by default and `playcanvas` could not be loaded' +
|
|
179
|
+
(err3 ? ' — and neither could `three` for the fallback' : '') +
|
|
180
|
+
". Install one: `npm i playcanvas` (default), or `npm i three` and pass engine:'three'. On a bare importmap, map " +
|
|
181
|
+
'"playcanvas" (or "three" + "three/addons/"). ' +
|
|
182
|
+
`Underlying error: ${err?.message || err}` +
|
|
183
|
+
(err3 ? ` / ${err3?.message || err3}` : ''),
|
|
184
|
+
);
|
|
185
|
+
e.cause = err;
|
|
186
|
+
return e;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Call-time checks for the PlayCanvas backend that need no module (mirrors the backend's own). */
|
|
190
|
+
function preValidatePlayCanvas(opts) {
|
|
191
|
+
const env = opts.environment;
|
|
192
|
+
if (env !== undefined && !['room', 'neutral', 'studio', 'none'].includes(env)) {
|
|
193
|
+
throw new Error(`@displayxr/inline3d/model: environment "${env}" — expected 'room', 'neutral', 'studio' or 'none'.`);
|
|
194
|
+
}
|
|
195
|
+
if (opts.envMap !== undefined && opts.envMap !== null && typeof opts.envMap !== 'string' && typeof opts.envMap !== 'object') {
|
|
196
|
+
throw new Error('@displayxr/inline3d/model: `envMap` must be a URL string or a pc.Texture.');
|
|
197
|
+
}
|
|
198
|
+
const c = opts.controls;
|
|
199
|
+
if (c !== undefined && c !== 'viewer' && c !== 'page') {
|
|
200
|
+
throw new Error(`@displayxr/inline3d/model: controls "${c}" — expected 'viewer' or 'page' (default 'viewer').`);
|
|
201
|
+
}
|
|
202
|
+
if (opts.onBeforeFrame !== undefined && (typeof opts.onBeforeFrame !== 'function' || c !== 'page')) {
|
|
203
|
+
throw new Error("@displayxr/inline3d/model: onBeforeFrame must be a function, and needs controls:'page'.");
|
|
204
|
+
}
|
|
205
|
+
if (opts.meshoptDecoder && typeof opts.meshoptDecoder.decodeGltfBuffer !== 'function') {
|
|
206
|
+
throw new Error("@displayxr/inline3d/model: `meshoptDecoder` must be meshoptimizer's MeshoptDecoder (it has decodeGltfBuffer()).");
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* engine:'three' — today's addModel (./inline3d-model.js), unchanged, behind this handle: its own
|
|
212
|
+
* handle's fields are forwarded (getters, since it fills them as it loads) and the queued calls
|
|
213
|
+
* are replayed into it.
|
|
214
|
+
*/
|
|
215
|
+
async function attachThree(out, wall, canvas, src, opts, pending, mod) {
|
|
216
|
+
const m = mod || (await import('./inline3d-model.js'));
|
|
217
|
+
const inner = m.addModel(wall, canvas, src, opts);
|
|
218
|
+
for (const k of ['viewer', 'model', 'frame']) {
|
|
219
|
+
Object.defineProperty(out, k, { get: () => inner[k], set: (v) => (inner[k] = v), enumerable: true, configurable: true });
|
|
220
|
+
}
|
|
221
|
+
out.backend = 'three';
|
|
222
|
+
out.engine = null;
|
|
223
|
+
out.setPose = inner.setPose;
|
|
224
|
+
out.resetPose = inner.resetPose;
|
|
225
|
+
out.remove = inner.remove;
|
|
226
|
+
out.exclude = inner.exclude;
|
|
227
|
+
out.unexclude = inner.unexclude;
|
|
228
|
+
out.inner = inner;
|
|
229
|
+
if (typeof out._resolveFirstWoven === 'function') {
|
|
230
|
+
out._resolveFirstWoven(inner.firstWoven);
|
|
231
|
+
delete out._resolveFirstWoven;
|
|
232
|
+
}
|
|
233
|
+
for (const [name, args] of pending) {
|
|
234
|
+
if (name === 'remove') {
|
|
235
|
+
inner.remove();
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
inner[name]?.(...args);
|
|
239
|
+
}
|
|
240
|
+
await inner.ready;
|
|
241
|
+
return out;
|
|
242
|
+
}
|