@displayxr/inline3d 1.10.1 → 1.12.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 +90 -0
- package/README.md +27 -5
- package/js/inline3d-model-entry.js +242 -0
- package/js/inline3d-model-playcanvas.js +754 -0
- package/js/inline3d-playcanvas-engine.js +77 -0
- package/js/inline3d-splat-playcanvas.js +345 -22
- package/js/inline3d-splat-shared.js +121 -0
- package/js/inline3d-splat.js +56 -2
- package/model.d.ts +109 -30
- package/package.json +15 -3
- package/splat.d.ts +68 -0
|
@@ -316,3 +316,124 @@ export function playcanvasCannotRead(src, { fileType, fileName } = {}) {
|
|
|
316
316
|
`${what}. Pass engine:'spark' for .spz / .splat / .ksplat / .rad.`
|
|
317
317
|
);
|
|
318
318
|
}
|
|
319
|
+
|
|
320
|
+
// ── controls:'page' — the page drives the camera (docs/playcanvas-adapter.md §controls:'page') ──
|
|
321
|
+
|
|
322
|
+
/** Who owns the camera: the SDK's viewer (orbit, idle, fit, focus gestures) or the page. */
|
|
323
|
+
export const SPLAT_CONTROLS = Object.freeze(['viewer', 'page']);
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* `comfortDepth` default — the auto-3D shim's `depth`, so a game on `controls:'page'` gets the
|
|
327
|
+
* same stereo as the same game under the shim. It is the runtime's comfort number
|
|
328
|
+
* `ipd × metersToVirtual × convergenceDiopters × 0.5`, which the rig below makes equal to it by
|
|
329
|
+
* construction (`metersToVirtual = comfortDepth · d / 0.5`, `convergenceDiopters = 1 / d`).
|
|
330
|
+
*/
|
|
331
|
+
export const PAGE_COMFORT_DEPTH = 0.3;
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Options that mean nothing when the page owns the camera. Passing one is not an error (a page
|
|
335
|
+
* switching an existing call site over should not have to prune it first); it is named ONCE in a
|
|
336
|
+
* console.info line and ignored.
|
|
337
|
+
*/
|
|
338
|
+
export const PAGE_IGNORED_OPTIONS = Object.freeze([
|
|
339
|
+
'fit',
|
|
340
|
+
'virtualDisplayHeight',
|
|
341
|
+
'orbit',
|
|
342
|
+
'idleSpin',
|
|
343
|
+
'focusInput',
|
|
344
|
+
'margin',
|
|
345
|
+
'fitSweep',
|
|
346
|
+
'depthLimit',
|
|
347
|
+
'orbitMaxDeg',
|
|
348
|
+
'orbitEase',
|
|
349
|
+
'captureFit',
|
|
350
|
+
]);
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Validate the `controls` family at CALL time (both engines call this before anything loads).
|
|
354
|
+
*
|
|
355
|
+
* @returns {{page:boolean, comfortDepth:number, ignored:string[]}}
|
|
356
|
+
* @throws on an unknown `controls`, a `comfortDepth` outside (0, 1], or `rig:'display'` with
|
|
357
|
+
* `controls:'page'` (the page's camera IS the rig; a display rig has no camera).
|
|
358
|
+
*/
|
|
359
|
+
export function resolveControls(opts = {}) {
|
|
360
|
+
const controls = opts.controls ?? 'viewer';
|
|
361
|
+
if (!SPLAT_CONTROLS.includes(controls)) {
|
|
362
|
+
throw new Error(
|
|
363
|
+
`@displayxr/inline3d/splat: controls "${controls}" — expected ` +
|
|
364
|
+
`${SPLAT_CONTROLS.map((c) => `'${c}'`).join(' or ')} (default 'viewer').`,
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
const page = controls === 'page';
|
|
368
|
+
let comfortDepth = PAGE_COMFORT_DEPTH;
|
|
369
|
+
if (opts.comfortDepth !== undefined) {
|
|
370
|
+
const c = opts.comfortDepth;
|
|
371
|
+
if (typeof c !== 'number' || !Number.isFinite(c) || c <= 0 || c > 1) {
|
|
372
|
+
throw new Error(
|
|
373
|
+
`@displayxr/inline3d/splat: comfortDepth ${c} — expected a number in (0, 1] ` +
|
|
374
|
+
`(the runtime's comfort rule caps it at 1; the default is ${PAGE_COMFORT_DEPTH}).`,
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
comfortDepth = c;
|
|
378
|
+
}
|
|
379
|
+
if (page && opts.rig === 'display') {
|
|
380
|
+
throw new Error(
|
|
381
|
+
"@displayxr/inline3d/splat: controls:'page' with rig:'display' — the page's camera IS the " +
|
|
382
|
+
"rig, so it is always a camera rig. Drop `rig`, or use the default controls for a display rig.",
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
if (opts.onBeforeFrame !== undefined) {
|
|
386
|
+
if (typeof opts.onBeforeFrame !== 'function') {
|
|
387
|
+
throw new Error('@displayxr/inline3d/splat: onBeforeFrame must be a function (frame) => void.');
|
|
388
|
+
}
|
|
389
|
+
if (!page) {
|
|
390
|
+
throw new Error(
|
|
391
|
+
"@displayxr/inline3d/splat: onBeforeFrame needs controls:'page' — with the default controls " +
|
|
392
|
+
'the SDK owns the camera and there is no page pose to set before the draw.',
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
const ignored = page ? PAGE_IGNORED_OPTIONS.filter((k) => opts[k] !== undefined) : [];
|
|
397
|
+
return { page, comfortDepth, ignored };
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Validate one `setCameraPose(matrixWorld, o)` call and return a private copy (the page may
|
|
402
|
+
* reuse its arrays). Throws synchronously on a malformed call: a NaN in a camera matrix is a page
|
|
403
|
+
* bug that would otherwise render nothing, silently.
|
|
404
|
+
*
|
|
405
|
+
* @returns {{matrixWorld:Float64Array, verticalFovDeg:number, near:number, far:number,
|
|
406
|
+
* convergence:number|null}}
|
|
407
|
+
*/
|
|
408
|
+
export function normalizeCameraPose(matrixWorld, o = {}) {
|
|
409
|
+
const bad = (why) => new Error(`@displayxr/inline3d/splat: setCameraPose — ${why}`);
|
|
410
|
+
if (!matrixWorld || typeof matrixWorld.length !== 'number' || matrixWorld.length !== 16) {
|
|
411
|
+
throw bad('matrixWorld must be 16 numbers, column-major (a Float32Array(16) or an array).');
|
|
412
|
+
}
|
|
413
|
+
const m = new Float64Array(16);
|
|
414
|
+
for (let i = 0; i < 16; i++) {
|
|
415
|
+
const v = matrixWorld[i];
|
|
416
|
+
if (typeof v !== 'number' || !Number.isFinite(v)) throw bad(`matrixWorld[${i}] is ${v}, not a finite number.`);
|
|
417
|
+
m[i] = v;
|
|
418
|
+
}
|
|
419
|
+
const det =
|
|
420
|
+
m[0] * (m[5] * m[10] - m[9] * m[6]) - m[4] * (m[1] * m[10] - m[9] * m[2]) + m[8] * (m[1] * m[6] - m[5] * m[2]);
|
|
421
|
+
if (!(det > 1e-12)) {
|
|
422
|
+
throw bad(`matrixWorld is singular or mirrored (det ${det.toPrecision(3)}); a camera pose is a rotation, a translation and at most a uniform scale.`);
|
|
423
|
+
}
|
|
424
|
+
const fov = o.verticalFovDeg;
|
|
425
|
+
if (typeof fov !== 'number' || !(fov > 0 && fov < 180)) {
|
|
426
|
+
throw bad(`verticalFovDeg ${fov} — the FULL vertical angle in degrees, in (0, 180).`);
|
|
427
|
+
}
|
|
428
|
+
const near = o.near === undefined ? MONO_NEAR : o.near;
|
|
429
|
+
const far = o.far === undefined ? CAPTURE_FAR : o.far;
|
|
430
|
+
if (typeof near !== 'number' || !(near > 0) || !Number.isFinite(near)) throw bad(`near ${near} — expected a finite number > 0.`);
|
|
431
|
+
if (typeof far !== 'number' || !(far > near) || !Number.isFinite(far)) throw bad(`far ${far} — expected a finite number > near.`);
|
|
432
|
+
let convergence = null;
|
|
433
|
+
if (o.convergence !== undefined && o.convergence !== null) {
|
|
434
|
+
const c = o.convergence;
|
|
435
|
+
if (typeof c !== 'number' || !(c > 0) || !Number.isFinite(c)) throw bad(`convergence ${c} — metres along the view axis, > 0 (omit it for the adapter's own).`);
|
|
436
|
+
convergence = c;
|
|
437
|
+
}
|
|
438
|
+
return { matrixWorld: m, verticalFovDeg: fov, near, far, convergence };
|
|
439
|
+
}
|
package/js/inline3d-splat.js
CHANGED
|
@@ -34,6 +34,8 @@ import {
|
|
|
34
34
|
playcanvasCannotRead,
|
|
35
35
|
isStreamedUrl,
|
|
36
36
|
STREAMED_NEEDS_PLAYCANVAS,
|
|
37
|
+
resolveControls,
|
|
38
|
+
normalizeCameraPose,
|
|
37
39
|
} from './inline3d-splat-shared.js';
|
|
38
40
|
import {
|
|
39
41
|
resolveRig,
|
|
@@ -163,6 +165,9 @@ export function addSplat(wall, canvas, src, opts = {}) {
|
|
|
163
165
|
`${CAPTURE_FITS.map((f) => `'${f}'`).join(' or ')}.`,
|
|
164
166
|
);
|
|
165
167
|
}
|
|
168
|
+
// WHO OWNS THE CAMERA. Validated here, synchronously, for both engines (an unknown `controls`
|
|
169
|
+
// or a comfortDepth out of range is a page bug true of every call).
|
|
170
|
+
const { page: pageControls } = resolveControls(opts);
|
|
166
171
|
// WHICH ENGINE. Unset (or 'spark') is everything below, untouched. 'playcanvas' goes to
|
|
167
172
|
// ./inline3d-splat-playcanvas.js, imported DYNAMICALLY so a page that never asks for it never
|
|
168
173
|
// resolves `playcanvas` — see addSplatDeferred.
|
|
@@ -174,6 +179,16 @@ export function addSplat(wall, canvas, src, opts = {}) {
|
|
|
174
179
|
return addSplatDeferred(wall, canvas, src, opts);
|
|
175
180
|
}
|
|
176
181
|
|
|
182
|
+
// controls:'page' is a PlayCanvas-backend feature (docs/playcanvas-adapter.md §controls:'page'):
|
|
183
|
+
// on Spark it would need SceneViewer — which ./viewer and ./model share — to take an external
|
|
184
|
+
// camera in both its mono and eye paths. Refused by name rather than half-supported.
|
|
185
|
+
if (pageControls) {
|
|
186
|
+
throw new Error(
|
|
187
|
+
"@displayxr/inline3d/splat: controls:'page' is not supported on Spark (the default engine) — " +
|
|
188
|
+
"pass engine:'playcanvas'. Spark's viewer owns its camera; see docs/playcanvas-adapter.md.",
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
177
192
|
// A Streamed SOG on Spark is a page bug (Spark has no lod-meta.json reader): say so now, by
|
|
178
193
|
// name, rather than let Spark fail on an "unknown file type" that reads like a corrupt asset.
|
|
179
194
|
if (isStreamedUrl(src)) throw new Error(`@displayxr/inline3d/splat: ${STREAMED_NEEDS_PLAYCANVAS}`);
|
|
@@ -336,6 +351,13 @@ export function addSplat(wall, canvas, src, opts = {}) {
|
|
|
336
351
|
},
|
|
337
352
|
/** Called with the live focus, in the splat's own space, whenever it moves. */
|
|
338
353
|
onFocusChange: null,
|
|
354
|
+
/** Not on this backend: controls:'page' is a PlayCanvas-backend feature. */
|
|
355
|
+
setCameraPose() {
|
|
356
|
+
throw new Error(
|
|
357
|
+
"@displayxr/inline3d/splat: setCameraPose() needs addSplat(…, { engine:'playcanvas', controls:'page' }).",
|
|
358
|
+
);
|
|
359
|
+
},
|
|
360
|
+
getCameraPose: () => null,
|
|
339
361
|
/** Not on this backend: the crossfading asset swap is a PlayCanvas-backend feature. */
|
|
340
362
|
setSource() {
|
|
341
363
|
throw new Error(
|
|
@@ -622,6 +644,16 @@ function addSplatDeferred(wall, canvas, src, opts) {
|
|
|
622
644
|
pending.push([name, args]);
|
|
623
645
|
return name === 'setFocus' ? out : undefined;
|
|
624
646
|
};
|
|
647
|
+
const page = opts.controls === 'page';
|
|
648
|
+
const pageOnly = (name) => () => {
|
|
649
|
+
throw new Error(
|
|
650
|
+
`@displayxr/inline3d/splat: ${name}() is not available with controls:'page' — the page owns the ` +
|
|
651
|
+
'camera. Drive it with handle.setCameraPose(matrixWorld, { verticalFovDeg, near, far }).',
|
|
652
|
+
);
|
|
653
|
+
};
|
|
654
|
+
// Before the adapter lands, a page calling setCameraPose every frame keeps ONE pending pose
|
|
655
|
+
// (validated now, so a bad call throws at its own line): last call wins, as it will after.
|
|
656
|
+
let pendingPose = null;
|
|
625
657
|
const out = {
|
|
626
658
|
backend: 'playcanvas',
|
|
627
659
|
engine: null,
|
|
@@ -631,8 +663,30 @@ function addSplatDeferred(wall, canvas, src, opts) {
|
|
|
631
663
|
camera: null,
|
|
632
664
|
rig: null,
|
|
633
665
|
perf: null,
|
|
634
|
-
setPose: queue('setPose'),
|
|
635
|
-
resetPose: queue('resetPose'),
|
|
666
|
+
setPose: page ? pageOnly('setPose') : queue('setPose'),
|
|
667
|
+
resetPose: page ? pageOnly('resetPose') : queue('resetPose'),
|
|
668
|
+
setCameraPose(matrixWorld, o) {
|
|
669
|
+
if (!page) {
|
|
670
|
+
throw new Error(
|
|
671
|
+
"@displayxr/inline3d/splat: setCameraPose() needs addSplat(…, { controls:'page' }) — " +
|
|
672
|
+
'with the default controls the SDK owns the camera (use setPose).',
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
const pose = normalizeCameraPose(matrixWorld, o);
|
|
676
|
+
if (pendingPose) pendingPose[1] = [pose.matrixWorld, pose];
|
|
677
|
+
else pending.push((pendingPose = ['setCameraPose', [pose.matrixWorld, pose]]));
|
|
678
|
+
return out;
|
|
679
|
+
},
|
|
680
|
+
getCameraPose: () =>
|
|
681
|
+
pendingPose
|
|
682
|
+
? {
|
|
683
|
+
matrixWorld: Float32Array.from(pendingPose[1][1].matrixWorld),
|
|
684
|
+
verticalFovDeg: pendingPose[1][1].verticalFovDeg,
|
|
685
|
+
near: pendingPose[1][1].near,
|
|
686
|
+
far: pendingPose[1][1].far,
|
|
687
|
+
convergence: pendingPose[1][1].convergence,
|
|
688
|
+
}
|
|
689
|
+
: null,
|
|
636
690
|
setFocus: queue('setFocus'),
|
|
637
691
|
// A swap requested before the first asset has landed runs once it has (the adapter's own
|
|
638
692
|
// setSource replaces this stub on the same object by then).
|
package/model.d.ts
CHANGED
|
@@ -1,13 +1,25 @@
|
|
|
1
|
-
// Type definitions for @displayxr/inline3d/model.
|
|
1
|
+
// Type definitions for @displayxr/inline3d/model (and ./model/three).
|
|
2
2
|
// EXPERIMENTAL — not covered by the 1.x semver promise. See docs/sdk-stability.md.
|
|
3
3
|
|
|
4
4
|
import type { SceneViewer, SubjectBounds, OrbitPose } from './viewer.js';
|
|
5
5
|
import type { FirstWovenResult } from './index.js';
|
|
6
|
+
import type { PlayCanvasSplatViewer, SplatCameraPose, SplatFrameInfo } from './splat.js';
|
|
6
7
|
|
|
7
8
|
export interface ModelOptions {
|
|
9
|
+
/**
|
|
10
|
+
* Which engine renders the model (1.12). `'playcanvas'` is the DEFAULT (optional peer
|
|
11
|
+
* `playcanvas >=2.22.3 <3`); `'three'` is the 1.11 renderer, byte for byte (optional peer
|
|
12
|
+
* `three`). Both are imported dynamically, so a page loads only the engine it renders with.
|
|
13
|
+
*
|
|
14
|
+
* With no `engine`: a three.js object in `GLTFLoader` / `DRACOLoader` / `KTX2Loader` / `envMap`
|
|
15
|
+
* selects three; otherwise PlayCanvas, falling back to three (one console warning) when
|
|
16
|
+
* `playcanvas` cannot be imported, and rejecting `ready` naming both when neither can. An
|
|
17
|
+
* explicit `engine` never falls back. Anything else throws at call time.
|
|
18
|
+
*/
|
|
19
|
+
engine?: 'playcanvas' | 'three';
|
|
8
20
|
/** Metres of world the tile's height spans (default 0.24). */
|
|
9
21
|
virtualDisplayHeight?: number;
|
|
10
|
-
/** Precomputed subject bounds. Rarely needed for a mesh —
|
|
22
|
+
/** Precomputed subject bounds. Rarely needed for a mesh — its bounds are exact. */
|
|
11
23
|
frame?: SubjectBounds;
|
|
12
24
|
/** Degrees/second of turntable once idle (default 8). */
|
|
13
25
|
idleSpin?: number;
|
|
@@ -25,26 +37,42 @@ export interface ModelOptions {
|
|
|
25
37
|
/** Per-eye buffer scale; 0.5–0.7 is usually free (default 1). */
|
|
26
38
|
renderScale?: number;
|
|
27
39
|
feather?: number;
|
|
28
|
-
/** Built-in three-point lighting. Meshes arrive unlit; splats do not need this. */
|
|
29
40
|
/**
|
|
30
|
-
* How the mesh is lit.
|
|
31
|
-
*
|
|
32
|
-
*
|
|
41
|
+
* How the mesh is lit.
|
|
42
|
+
*
|
|
43
|
+
* PlayCanvas (default engine): `room` (default; `neutral` is an alias) is image-based lighting
|
|
44
|
+
* from an in-memory neutral photo studio fitted to the Khronos glTF Sample Viewer's "Studio
|
|
45
|
+
* Neutral", with Khronos PBR Neutral tone mapping and exposure 1 — the Sample Viewer's look.
|
|
46
|
+
* three: `room` bakes three's procedural RoomEnvironment, untonemapped (unchanged since 1.11).
|
|
47
|
+
*
|
|
48
|
+
* Both: `studio` is the three-point punctual rig (metal renders dark under it — nothing to
|
|
49
|
+
* reflect); `none` adds no light at all. Anything else throws on PlayCanvas.
|
|
33
50
|
*/
|
|
34
|
-
environment?: 'room' | 'studio' | 'none';
|
|
35
|
-
/**
|
|
36
|
-
|
|
37
|
-
|
|
51
|
+
environment?: 'room' | 'neutral' | 'studio' | 'none';
|
|
52
|
+
/**
|
|
53
|
+
* PlayCanvas only: yaw of the environment about +Y, degrees (default 0 = the Sample Viewer's
|
|
54
|
+
* default orientation).
|
|
55
|
+
*/
|
|
56
|
+
environmentRotation?: number;
|
|
57
|
+
/**
|
|
58
|
+
* Your own environment; overrides `environment`.
|
|
59
|
+
* three: a PMREM-processed texture. PlayCanvas: a URL of an equirect image (.hdr/.png/.jpg) or
|
|
60
|
+
* a `pc.Texture` (equirect or cubemap), prefiltered for you. A three texture on
|
|
61
|
+
* `engine: 'playcanvas'` throws; with no `engine` it selects three.
|
|
62
|
+
*/
|
|
63
|
+
envMap?: object | string;
|
|
64
|
+
/** three only: hand in the GLTFLoader class instead of resolving it from `three/addons/`. */
|
|
38
65
|
GLTFLoader?: unknown;
|
|
39
66
|
|
|
40
67
|
/**
|
|
41
|
-
* Where **your page** serves
|
|
68
|
+
* Where **your page** serves the Draco decoder and Basis (KTX2) transcoder — the SAME files on
|
|
69
|
+
* both engines: three's `libs/draco/` (`draco_wasm_wrapper.js` + `draco_decoder.wasm`) and
|
|
70
|
+
* `libs/basis/` (`basis_transcoder.js` + `.wasm`). The PlayCanvas engine's own Draco and Basis
|
|
71
|
+
* workers load them.
|
|
42
72
|
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
* offline build must not acquire a network dependency by loading a compressed file. Copy the
|
|
47
|
-
* files out of `three` and serve them yourself:
|
|
73
|
+
* The asset's `extensionsUsed` decides which decoders are wired, so an uncompressed model never
|
|
74
|
+
* touches any of this. The default is deliberately **not a CDN** — a page shipping an offline
|
|
75
|
+
* build must not acquire a network dependency by loading a compressed file:
|
|
48
76
|
*
|
|
49
77
|
* ```sh
|
|
50
78
|
* cp -r node_modules/three/examples/jsm/libs/draco/ public/draco/
|
|
@@ -52,25 +80,48 @@ export interface ModelOptions {
|
|
|
52
80
|
* ```
|
|
53
81
|
*
|
|
54
82
|
* A string is a parent directory holding `draco/` and `basis/`; an object overrides either key.
|
|
55
|
-
*
|
|
83
|
+
* On PlayCanvas the engine keeps ONE Draco and ONE Basis worker pool per page: the first tile
|
|
84
|
+
* that needs one sets its path, and a later tile asking for another path is warned.
|
|
56
85
|
*
|
|
57
86
|
* @default {draco:'/draco/', basis:'/basis/'}
|
|
58
87
|
*/
|
|
59
88
|
decoderPath?: string | { draco?: string; basis?: string };
|
|
60
|
-
/**
|
|
61
|
-
* DRACOLoader class **or** a ready instance, instead of resolving `three/addons/`. A class is
|
|
62
|
-
* constructed and pointed at `decoderPath`; an instance is used exactly as you configured it.
|
|
63
|
-
*/
|
|
89
|
+
/** three only: DRACOLoader class **or** a ready instance, instead of resolving `three/addons/`. */
|
|
64
90
|
DRACOLoader?: unknown;
|
|
91
|
+
/** three only: KTX2Loader class or instance. */
|
|
92
|
+
KTX2Loader?: unknown;
|
|
65
93
|
/**
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
94
|
+
* The `MeshoptDecoder` for `EXT_meshopt_compression`. Nothing to serve (pure JS). Default: three
|
|
95
|
+
* resolves `three/addons/libs/meshopt_decoder.module.js`; PlayCanvas resolves
|
|
96
|
+
* `meshoptimizer/decoder` (optional peer `meshoptimizer >=1`), only for an asset that declares it.
|
|
69
97
|
*/
|
|
70
|
-
KTX2Loader?: unknown;
|
|
71
|
-
/** The `MeshoptDecoder` namespace, instead of resolving `three/addons/`. Nothing to serve. */
|
|
72
98
|
meshoptDecoder?: unknown;
|
|
73
99
|
|
|
100
|
+
/** PlayCanvas only: the `playcanvas` module (or a namespace with the same members) to use. */
|
|
101
|
+
playcanvas?: object;
|
|
102
|
+
/** PlayCanvas only: MSAA on the tile's WebGL context (default true, as three's renderer). */
|
|
103
|
+
antialias?: boolean;
|
|
104
|
+
/** PlayCanvas only: the context's `preserveDrawingBuffer` (default false). */
|
|
105
|
+
preserveDrawingBuffer?: boolean;
|
|
106
|
+
/** PlayCanvas only: raise every projection's near plane to at least this (depth mapping only). */
|
|
107
|
+
nearClip?: number;
|
|
108
|
+
/** PlayCanvas only: lower every projection's far plane to at most this. */
|
|
109
|
+
farClip?: number;
|
|
110
|
+
/** PlayCanvas only: the drag's tilt cap in degrees (default 15, tilt-and-relax). */
|
|
111
|
+
orbitMaxDeg?: number;
|
|
112
|
+
/** PlayCanvas only: the orbit's easing time constants, seconds. */
|
|
113
|
+
orbitEase?: { drag?: number; rest?: number };
|
|
114
|
+
/**
|
|
115
|
+
* PlayCanvas only: who owns the camera — the SDK's viewer (default) or the page, through
|
|
116
|
+
* {@link ModelHandle.setCameraPose} every frame (the attach rig; converges on the model's bounds
|
|
117
|
+
* centre unless the pose gives a `convergence`).
|
|
118
|
+
*/
|
|
119
|
+
controls?: 'viewer' | 'page';
|
|
120
|
+
/** `controls: 'page'`: the comfort depth of the declared rig, in (0, 1] (default 0.3). */
|
|
121
|
+
comfortDepth?: number;
|
|
122
|
+
/** `controls: 'page'` only: runs inside the adapter's frame, before the draw. */
|
|
123
|
+
onBeforeFrame?: (frame: SplatFrameInfo) => void;
|
|
124
|
+
|
|
74
125
|
/** Element whose visibility gates the lazy create/close lifecycle. */
|
|
75
126
|
observe?: Element;
|
|
76
127
|
/** Forwarded to the core window: see `TileOptions.firstWovenHoldMs`. */
|
|
@@ -79,16 +130,43 @@ export interface ModelOptions {
|
|
|
79
130
|
|
|
80
131
|
/** What {@link addModel} returns — the same shape as addSplat's handle. */
|
|
81
132
|
export interface ModelHandle {
|
|
82
|
-
|
|
83
|
-
|
|
133
|
+
/** Which engine renders this tile. Can change from `'playcanvas'` to `'three'` on the fallback. */
|
|
134
|
+
readonly backend: 'playcanvas' | 'three';
|
|
135
|
+
/**
|
|
136
|
+
* three: the SceneViewer. PlayCanvas: the backend's viewer (the SceneViewer pose surface
|
|
137
|
+
* without three). Null until the backend module has loaded — one module fetch after addModel
|
|
138
|
+
* returns (1.11 returned it synchronously; `@displayxr/inline3d/model/three` still does).
|
|
139
|
+
*/
|
|
140
|
+
readonly viewer: SceneViewer | PlayCanvasSplatViewer | null;
|
|
141
|
+
/**
|
|
142
|
+
* The loaded glTF root — a three `Object3D`, or on PlayCanvas the render `pc.Entity`
|
|
143
|
+
* (`instantiateRenderEntity()`). Null until `ready` resolves.
|
|
144
|
+
*/
|
|
84
145
|
model: object | null;
|
|
146
|
+
/** PlayCanvas only: the container resource (animations, materials, textures). */
|
|
147
|
+
readonly container?: unknown;
|
|
148
|
+
/**
|
|
149
|
+
* PlayCanvas only (ADVANCED, not covered by the semver promise): `app` is the tile's AppBase,
|
|
150
|
+
* `root` the content root (the model's parent — add entities, lights or an `anim` component
|
|
151
|
+
* under it), `camera` the eye camera. Null on three and until the engine has booted.
|
|
152
|
+
*/
|
|
153
|
+
readonly engine: { readonly app: unknown; readonly root: unknown; readonly camera: unknown } | null;
|
|
85
154
|
/** Bounds used for framing; null until `ready` resolves. */
|
|
86
155
|
frame: SubjectBounds | null;
|
|
87
156
|
/** Resolves once the model has loaded and been framed; rejects if the load failed. */
|
|
88
157
|
readonly ready: Promise<ModelHandle>;
|
|
89
158
|
|
|
159
|
+
/** Throws on `controls: 'page'`. */
|
|
90
160
|
setPose(pose?: OrbitPose): void;
|
|
161
|
+
/** Throws on `controls: 'page'`. */
|
|
91
162
|
resetPose(): void;
|
|
163
|
+
/** `controls: 'page'` only (throws otherwise): the camera for the next frame, model space. */
|
|
164
|
+
setCameraPose(
|
|
165
|
+
matrixWorld: ArrayLike<number>,
|
|
166
|
+
opts: { verticalFovDeg: number; near?: number; far?: number; convergence?: number | null },
|
|
167
|
+
): ModelHandle;
|
|
168
|
+
/** The last pose the page set (a copy), or null. */
|
|
169
|
+
getCameraPose(): SplatCameraPose | null;
|
|
92
170
|
|
|
93
171
|
remove(): void;
|
|
94
172
|
exclude(el: Element): void;
|
|
@@ -102,10 +180,11 @@ export interface ModelHandle {
|
|
|
102
180
|
|
|
103
181
|
/**
|
|
104
182
|
* Load a glTF/GLB into an inline-3D window. Safe to call with an unsupported wall — it renders a
|
|
105
|
-
* flat, orbitable view instead, so pages need no branch.
|
|
183
|
+
* flat, orbitable view instead, so pages need no branch. Renders with PlayCanvas by default
|
|
184
|
+
* (1.12); pass `engine: 'three'` for the 1.11 renderer.
|
|
106
185
|
*
|
|
107
186
|
* Compressed assets (Draco, meshopt, KTX2/Basis) load too: the asset's declared extensions decide
|
|
108
|
-
* which decoders are
|
|
187
|
+
* which decoders are wired, and nothing is wired for an asset that declares none. Draco and
|
|
109
188
|
* KTX2 additionally need their runtime files served by your page — see {@link ModelOptions.decoderPath}.
|
|
110
189
|
* When a decoder is needed and unavailable, `ready` rejects with an Error naming the glTF
|
|
111
190
|
* extension, the option that fixes it and the path it looked in; the extension is also on the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@displayxr/inline3d",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.12.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",
|
|
@@ -24,6 +24,10 @@
|
|
|
24
24
|
"import": "./js/inline3d-splat.js"
|
|
25
25
|
},
|
|
26
26
|
"./model": {
|
|
27
|
+
"types": "./model.d.ts",
|
|
28
|
+
"import": "./js/inline3d-model-entry.js"
|
|
29
|
+
},
|
|
30
|
+
"./model/three": {
|
|
27
31
|
"types": "./model.d.ts",
|
|
28
32
|
"import": "./js/inline3d-model.js"
|
|
29
33
|
},
|
|
@@ -47,6 +51,9 @@
|
|
|
47
51
|
"js/inline3d-splat-playcanvas.js",
|
|
48
52
|
"js/inline3d-splat-shared.js",
|
|
49
53
|
"js/inline3d-model.js",
|
|
54
|
+
"js/inline3d-model-entry.js",
|
|
55
|
+
"js/inline3d-model-playcanvas.js",
|
|
56
|
+
"js/inline3d-playcanvas-engine.js",
|
|
50
57
|
"index.d.ts",
|
|
51
58
|
"three.d.ts",
|
|
52
59
|
"viewer.d.ts",
|
|
@@ -64,7 +71,8 @@
|
|
|
64
71
|
"stereo",
|
|
65
72
|
"3d-display",
|
|
66
73
|
"lightfield",
|
|
67
|
-
"three"
|
|
74
|
+
"three",
|
|
75
|
+
"playcanvas"
|
|
68
76
|
],
|
|
69
77
|
"homepage": "https://displayxr.github.io/displayxr-web/",
|
|
70
78
|
"repository": {
|
|
@@ -78,7 +86,8 @@
|
|
|
78
86
|
"peerDependencies": {
|
|
79
87
|
"three": ">=0.150.0",
|
|
80
88
|
"@sparkjsdev/spark": ">=2.0.0",
|
|
81
|
-
"playcanvas": ">=2.22.3 <3"
|
|
89
|
+
"playcanvas": ">=2.22.3 <3",
|
|
90
|
+
"meshoptimizer": ">=1.0.0"
|
|
82
91
|
},
|
|
83
92
|
"peerDependenciesMeta": {
|
|
84
93
|
"three": {
|
|
@@ -89,6 +98,9 @@
|
|
|
89
98
|
},
|
|
90
99
|
"playcanvas": {
|
|
91
100
|
"optional": true
|
|
101
|
+
},
|
|
102
|
+
"meshoptimizer": {
|
|
103
|
+
"optional": true
|
|
92
104
|
}
|
|
93
105
|
},
|
|
94
106
|
"publishConfig": {
|
package/splat.d.ts
CHANGED
|
@@ -304,6 +304,61 @@ export interface SplatOptions {
|
|
|
304
304
|
observe?: Element;
|
|
305
305
|
/** Forwarded to the core window: see `TileOptions.firstWovenHoldMs`. */
|
|
306
306
|
firstWovenHoldMs?: number;
|
|
307
|
+
/**
|
|
308
|
+
* Who owns the camera. `'viewer'` (the default): the SDK's orbit, idle spin, auto-fit and focus
|
|
309
|
+
* gestures. `'page'` (`engine: 'playcanvas'` only; throws on Spark): the page drives the camera
|
|
310
|
+
* every frame through {@link SplatHandle.setCameraPose} and the adapter keeps only the eye math —
|
|
311
|
+
* the attach-pattern camera rig, the runtime's projections, the mono fallback. `fit`,
|
|
312
|
+
* `virtualDisplayHeight`, `orbit`, `idleSpin`, `focusInput` (and the other framing/orbit knobs)
|
|
313
|
+
* are ignored, named once in a `console.info`; `rig: 'display'` throws.
|
|
314
|
+
*/
|
|
315
|
+
controls?: 'viewer' | 'page';
|
|
316
|
+
/**
|
|
317
|
+
* `controls: 'page'`: the comfort number `ipd × metersToVirtual × convergenceDiopters × 0.5` the
|
|
318
|
+
* rig is built to, in (0, 1]. Default 0.3, the auto-3D shim's. `metersToVirtual = comfortDepth ·
|
|
319
|
+
* d / 0.5`, so the depth budget is the same for a 10 cm subject and a 150 m castle.
|
|
320
|
+
*/
|
|
321
|
+
comfortDepth?: number;
|
|
322
|
+
/**
|
|
323
|
+
* `controls: 'page'` only: called once per adapter frame — the wall's session frame in 3D, the
|
|
324
|
+
* mono rAF in 2D — BEFORE anything renders. Call `handle.setCameraPose(…)` synchronously in
|
|
325
|
+
* here and THIS frame renders that pose (zero lag, the attach pattern); a pose set from the
|
|
326
|
+
* page's own rAF may be one frame late, since the two rAFs have no guaranteed order. A throw is
|
|
327
|
+
* caught and warned once; frames keep rendering.
|
|
328
|
+
*/
|
|
329
|
+
onBeforeFrame?: (frame: SplatFrameInfo) => void;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** What `onBeforeFrame` receives. */
|
|
333
|
+
export interface SplatFrameInfo {
|
|
334
|
+
/** `performance.now()` at the call. */
|
|
335
|
+
time: number;
|
|
336
|
+
/** The runtime's view list in 3D (valid only inside the call — copy what you keep); null in mono. */
|
|
337
|
+
views: readonly XRView[] | null;
|
|
338
|
+
/** Seconds since the previous call (0 on the first; capped at 0.1). */
|
|
339
|
+
dt: number;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** A page camera, as `setCameraPose` takes it and `getCameraPose` returns it. */
|
|
343
|
+
export interface SplatCameraPose {
|
|
344
|
+
/**
|
|
345
|
+
* Column-major world matrix of the page camera in the SPLAT's own space (the space of the camera
|
|
346
|
+
* block's `rest`), three.js convention: looks down −Z, +Y up. Rotation + translation + at most a
|
|
347
|
+
* UNIFORM scale (a page whose world scales the splat passes `inv(splatWorld) · camera.matrixWorld`;
|
|
348
|
+
* the scale is how page units reach the adapter). The adapter applies its own OpenCV → engine flip.
|
|
349
|
+
*/
|
|
350
|
+
matrixWorld: Float32Array;
|
|
351
|
+
/** Full vertical field of view, degrees. */
|
|
352
|
+
verticalFovDeg: number;
|
|
353
|
+
/** Page units. Default 0.001. A floor on every projection's near (depth mapping only). */
|
|
354
|
+
near: number;
|
|
355
|
+
/** Page units. Default 5000. A cap on every projection's far. */
|
|
356
|
+
far: number;
|
|
357
|
+
/**
|
|
358
|
+
* Zero-disparity distance along the view axis, page units; null = the adapter's own (the focus
|
|
359
|
+
* waterfall's, fixed until `setFocus` / `setSource`).
|
|
360
|
+
*/
|
|
361
|
+
convergence: number | null;
|
|
307
362
|
}
|
|
308
363
|
|
|
309
364
|
/** `handle.stats()` on `engine: 'playcanvas'`. */
|
|
@@ -384,8 +439,21 @@ export interface SplatHandle {
|
|
|
384
439
|
/** Resolves once the asset has loaded and been framed; rejects if the load failed. */
|
|
385
440
|
readonly ready: Promise<SplatHandle>;
|
|
386
441
|
|
|
442
|
+
/** Throws on `controls: 'page'` (the page owns the camera). */
|
|
387
443
|
setPose(pose?: OrbitPose): void;
|
|
444
|
+
/** Throws on `controls: 'page'`. */
|
|
388
445
|
resetPose(): void;
|
|
446
|
+
/**
|
|
447
|
+
* `controls: 'page'` only (throws otherwise): the camera for the next frame drawn — call it every
|
|
448
|
+
* frame you render, ideally inside `onBeforeFrame`. Last call wins; a page that stops calling
|
|
449
|
+
* keeps its last pose. Throws synchronously on a malformed matrix or lens.
|
|
450
|
+
*/
|
|
451
|
+
setCameraPose(
|
|
452
|
+
matrixWorld: ArrayLike<number>,
|
|
453
|
+
opts: { verticalFovDeg: number; near?: number; far?: number; convergence?: number },
|
|
454
|
+
): SplatHandle;
|
|
455
|
+
/** The last pose the page set (a copy), or null before the first `setCameraPose`. */
|
|
456
|
+
getCameraPose(): SplatCameraPose | null;
|
|
389
457
|
/**
|
|
390
458
|
* Point the window at something, in the SPLAT's own space (the space the `camera` block's
|
|
391
459
|
* `focus.point` is in). Null returns to whatever the waterfall resolved. Eased unless `snap`.
|