@displayxr/inline3d 1.15.0 → 1.16.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 +35 -0
- package/README.md +4 -1
- package/js/inline3d-model-playcanvas.js +1 -1
- package/js/inline3d-splat-playcanvas.js +350 -21
- package/js/inline3d-splat.js +25 -0
- package/package.json +1 -1
- package/splat.d.ts +70 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,41 @@ 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.16.0 — 2026-09-24
|
|
9
|
+
|
|
10
|
+
Touches the **preview tier** (`./splat`, `engine: 'playcanvas'` only). Additive. A page that never
|
|
11
|
+
calls `setRig` renders the same pixels, except for the `setSource` fix below.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **`handle.setRig(type, options?)`** (#36): a live, reversible rig switch on the PlayCanvas splat
|
|
16
|
+
handle. It applies on the next frame with no remount, reload or new session, and returns a
|
|
17
|
+
`Promise<handle>`.
|
|
18
|
+
- `setRig('display', { virtualDisplayHeight, fit, margin, frame, … })` frames the enabled meshes
|
|
19
|
+
under `handle.engine.root`, plus the splat when it is shown. The defaults are addModel's (0.24,
|
|
20
|
+
`contain`, 0.8, depthLimit 4, idleSpin 8), and it declares a display rig.
|
|
21
|
+
- While the splat is hidden, the page's meshes get addModel's tone mapping (`'neutral'`) and, if
|
|
22
|
+
the scene has no `envAtlas` of its own, addModel's neutral-studio IBL. Both are removed when the
|
|
23
|
+
rig switches away.
|
|
24
|
+
- `setRig('camera')` goes back to the asset's capture rig, re-resolved from the waterfall's own
|
|
25
|
+
inputs. `setRig('auto')` is the load-time result.
|
|
26
|
+
- The pose resets to the new rig's rest. `handle.rig.typeSource` reads `'setRig'`. The choice
|
|
27
|
+
sticks across `setSource` until `'auto'`, and `controls:'page'` throws.
|
|
28
|
+
- Gates, on a real GPU: a hidden splat plus `setRig('display')` plus DamagedHelmet or Fox renders
|
|
29
|
+
**MAE 0.0000** against `addModel(url, { engine: 'playcanvas' })`, in mono and fake stereo, with
|
|
30
|
+
`antialias: true`. With the tile's default MSAA off the residual is 0.11–0.44, silhouette edges
|
|
31
|
+
only. The round trip `setRig('display')` → `setRig('camera')` returns to the exact pre-switch
|
|
32
|
+
render (**MAE 0.0000**) and the same camera-rig declaration.
|
|
33
|
+
- See [`docs/playcanvas-adapter.md` § setRig](docs/playcanvas-adapter.md#setrig--switching-between-the-display-rig-and-the-camera-rig-36).
|
|
34
|
+
- **`addSplat(…, { engine: 'playcanvas', antialias: true })`** creates the tile's WebGL context with
|
|
35
|
+
MSAA. The default stays off. Use it when a page draws meshes under `handle.engine.root`.
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
|
|
39
|
+
- **`setSource` from a camera-rig asset to a display-rig asset** now declares the display rig to the
|
|
40
|
+
runtime. The camera rig used to stay declared. The new asset also orbits its frame centre again
|
|
41
|
+
(the camera rig's `recentre: false` was left behind).
|
|
42
|
+
|
|
8
43
|
## 1.15.0 — 2026-09-24
|
|
9
44
|
|
|
10
45
|
Touches the **preview tier** (`./splat`, `engine: 'playcanvas'` only). Additive; the existing transitions
|
package/README.md
CHANGED
|
@@ -45,7 +45,10 @@ API is not yet covered by the semver promise below.
|
|
|
45
45
|
renders the same splat window with the [PlayCanvas](https://playcanvas.com/) engine instead of
|
|
46
46
|
Spark — same handle, same rig/focus/camera-block behaviour, same `perf` presets (mapped onto the
|
|
47
47
|
engine's knobs) — and adds `setSource(src, { fadeMs })` (crossfading asset swaps), `feather`, a
|
|
48
|
-
tilt-and-relax orbit
|
|
48
|
+
tilt-and-relax orbit, `handle.engine` (the engine objects, for advanced pages) and
|
|
49
|
+
`handle.setRig('display' | 'camera' | 'auto')`: a live, reversible rig switch, so one persistent tile
|
|
50
|
+
can show a glTF under `handle.engine.root` exactly as `addModel` would and then go back to the photo's
|
|
51
|
+
capture rig ([§setRig](docs/playcanvas-adapter.md#setrig--switching-between-the-display-rig-and-the-camera-rig-36)). It needs the
|
|
49
52
|
optional peer `playcanvas` (`>=2.22.3 <3`) and reads `.sog`, `.ply` and a Streamed-SOG
|
|
50
53
|
`lod-meta.json`. **Spark stays the default**; a page that never passes `engine` never loads
|
|
51
54
|
`playcanvas`. What differs, and why: [`docs/playcanvas-adapter.md`](docs/playcanvas-adapter.md).
|
|
@@ -346,7 +346,7 @@ export function neutralStudioRGBE(width = 256, height = 128, env = NEUTRAL_STUDI
|
|
|
346
346
|
export const ENV_YAW_DEG = 0;
|
|
347
347
|
|
|
348
348
|
/** Build the default environment on this app and set it as the scene's envAtlas. */
|
|
349
|
-
function useNeutralStudio(pc, app, yawDeg) {
|
|
349
|
+
export function useNeutralStudio(pc, app, yawDeg) {
|
|
350
350
|
const W = 256;
|
|
351
351
|
const H = 128;
|
|
352
352
|
const tex = new pc.Texture(app.graphicsDevice, {
|
|
@@ -50,7 +50,7 @@ import {
|
|
|
50
50
|
budgetPerManager,
|
|
51
51
|
} from './inline3d-splat-perf.js';
|
|
52
52
|
import { boundsFromPositions, boundsFromPositionsAsync } from './inline3d-viewer.js';
|
|
53
|
-
import { cameraRigFromPose } from './inline3d-three.js';
|
|
53
|
+
import { cameraRigFromPose, displayRig } from './inline3d-three.js';
|
|
54
54
|
import {
|
|
55
55
|
SplatEffects,
|
|
56
56
|
EFFECTS,
|
|
@@ -912,6 +912,21 @@ export class PlayCanvasSplatViewer {
|
|
|
912
912
|
this._updateMonoProjection();
|
|
913
913
|
}
|
|
914
914
|
|
|
915
|
+
/**
|
|
916
|
+
* Switch the eye camera(s) to another tone mapping, by name (TONE_MAPPINGS). Live: the engine
|
|
917
|
+
* picks the new shader variant on the next draw. handle.setRig's display rig uses it to give a
|
|
918
|
+
* page's meshes addModel's 'neutral' while the splat is hidden.
|
|
919
|
+
*/
|
|
920
|
+
setToneMapping(name) {
|
|
921
|
+
const key = TONE_MAPPINGS[name] ? name : 'none';
|
|
922
|
+
this.toneMapping = key;
|
|
923
|
+
const pc = this.pc;
|
|
924
|
+
if (!pc) return;
|
|
925
|
+
const v = pc[TONE_MAPPINGS[key]] ?? pc.TONEMAP_NONE;
|
|
926
|
+
if (this.eye?.camera) this.eye.camera.toneMapping = v;
|
|
927
|
+
if (this._viewPath === 'cameras') for (const c of this._views) if (c?.camera) c.camera.toneMapping = v;
|
|
928
|
+
}
|
|
929
|
+
|
|
915
930
|
/** Forget the auto-fit (scale 1, no subject box) — the camera rig's framing. */
|
|
916
931
|
resetFit() {
|
|
917
932
|
this._fitScale = 1;
|
|
@@ -2151,6 +2166,100 @@ export function describeResource(res) {
|
|
|
2151
2166
|
* one-pass fade, with the coverage-linear alpha remap on both assets.
|
|
2152
2167
|
*/
|
|
2153
2168
|
|
|
2169
|
+
/**
|
|
2170
|
+
* handle.setRig's DISPLAY-rig defaults: addModel's, so a mesh framed through setRig('display')
|
|
2171
|
+
* sits exactly where `addModel(url, { engine: 'playcanvas' })` puts it (docs/playcanvas-adapter.md
|
|
2172
|
+
* §setRig). `toneMapping` applies to the page's meshes while the splat is hidden (the splat keeps
|
|
2173
|
+
* 'none' whenever it is shown); `environment: 'room'` installs addModel's neutral-studio IBL when
|
|
2174
|
+
* the page has no `scene.envAtlas` of its own, and removes it again when the rig changes back.
|
|
2175
|
+
*/
|
|
2176
|
+
export const SET_RIG_DISPLAY_DEFAULTS = Object.freeze({
|
|
2177
|
+
virtualDisplayHeight: 0.24,
|
|
2178
|
+
fit: 'contain',
|
|
2179
|
+
margin: 0.8,
|
|
2180
|
+
depthLimit: DEFAULT_DEPTH_LIMIT,
|
|
2181
|
+
fitSweep: true,
|
|
2182
|
+
idleSpin: 8,
|
|
2183
|
+
ipdFactor: 1,
|
|
2184
|
+
parallaxFactor: 1,
|
|
2185
|
+
perspectiveFactor: 1,
|
|
2186
|
+
toneMapping: 'neutral',
|
|
2187
|
+
environment: 'room',
|
|
2188
|
+
});
|
|
2189
|
+
|
|
2190
|
+
export const SET_RIG_TYPES = Object.freeze(['display', 'camera', 'auto']);
|
|
2191
|
+
const SET_RIG_FITS = ['contain', 'cover', 'height', 'none'];
|
|
2192
|
+
let warnedSetRigKeys = false;
|
|
2193
|
+
|
|
2194
|
+
const PAGE_SETRIG_ERROR =
|
|
2195
|
+
"@displayxr/inline3d/splat: setRig() is not available with controls:'page' — the page owns the " +
|
|
2196
|
+
'camera (its camera IS the rig). Drive it with handle.setCameraPose(matrixWorld, { verticalFovDeg, near, far }).';
|
|
2197
|
+
|
|
2198
|
+
/**
|
|
2199
|
+
* handle.setRig's arguments, validated and resolved (throws at the call, before anything runs).
|
|
2200
|
+
* @returns {{type:'display'|'camera'|'auto', o?:object}}
|
|
2201
|
+
*/
|
|
2202
|
+
export function validateSetRig(type, o = {}, pageMode = false) {
|
|
2203
|
+
if (pageMode) throw new Error(PAGE_SETRIG_ERROR);
|
|
2204
|
+
if (!SET_RIG_TYPES.includes(type)) {
|
|
2205
|
+
throw new Error(`@displayxr/inline3d/splat: setRig("${type}") — expected 'display', 'camera' or 'auto'.`);
|
|
2206
|
+
}
|
|
2207
|
+
if (o === null || typeof o !== 'object') throw new TypeError('@displayxr/inline3d/splat: setRig options must be an object.');
|
|
2208
|
+
if (type !== 'display') return { type };
|
|
2209
|
+
const D = SET_RIG_DISPLAY_DEFAULTS;
|
|
2210
|
+
const known = new Set([...Object.keys(D), 'frame']);
|
|
2211
|
+
const unknown = Object.keys(o).filter((k) => !known.has(k));
|
|
2212
|
+
if (unknown.length && !warnedSetRigKeys) {
|
|
2213
|
+
warnedSetRigKeys = true;
|
|
2214
|
+
console.warn(
|
|
2215
|
+
`[inline3d/splat] setRig('display') ignores ${unknown.join(', ')}` +
|
|
2216
|
+
(unknown.includes('transitionMs') ? ' — a rig switch is a clean cut (no eased transition in this version)' : '') +
|
|
2217
|
+
'.',
|
|
2218
|
+
);
|
|
2219
|
+
}
|
|
2220
|
+
const num = (k, ok) => {
|
|
2221
|
+
const v = o[k] === undefined ? D[k] : o[k];
|
|
2222
|
+
if (!Number.isFinite(v) || !ok(v)) throw new Error(`@displayxr/inline3d/splat: setRig('display') — bad ${k}: ${o[k]}.`);
|
|
2223
|
+
return v;
|
|
2224
|
+
};
|
|
2225
|
+
const fit = o.fit === undefined ? D.fit : o.fit;
|
|
2226
|
+
if (!SET_RIG_FITS.includes(fit)) throw new Error(`@displayxr/inline3d/splat: setRig('display') — fit "${fit}", expected ${SET_RIG_FITS.join(' | ')}.`);
|
|
2227
|
+
const toneMapping = o.toneMapping === undefined ? D.toneMapping : o.toneMapping;
|
|
2228
|
+
if (!TONE_MAPPINGS[toneMapping]) {
|
|
2229
|
+
throw new Error(`@displayxr/inline3d/splat: setRig('display') — toneMapping "${toneMapping}", expected ${Object.keys(TONE_MAPPINGS).join(' | ')}.`);
|
|
2230
|
+
}
|
|
2231
|
+
const environment = o.environment === undefined ? D.environment : o.environment;
|
|
2232
|
+
if (!['room', 'neutral', 'none'].includes(environment)) {
|
|
2233
|
+
throw new Error(`@displayxr/inline3d/splat: setRig('display') — environment "${environment}", expected 'room' | 'neutral' | 'none'.`);
|
|
2234
|
+
}
|
|
2235
|
+
let frame = null;
|
|
2236
|
+
if (o.frame != null) {
|
|
2237
|
+
const c = o.frame.center;
|
|
2238
|
+
const e = o.frame.extent;
|
|
2239
|
+
const v3 = (v) => (Array.isArray(v) || ArrayBuffer.isView(v) ? v.length >= 3 && [0, 1, 2].every((i) => Number.isFinite(v[i])) : v && [v.x, v.y, v.z].every(Number.isFinite));
|
|
2240
|
+
if (!v3(c) || !v3(e)) throw new Error("@displayxr/inline3d/splat: setRig('display') — frame must be { center: [x,y,z], extent: [x,y,z] }.");
|
|
2241
|
+
const a3 = (v) => (Array.isArray(v) || ArrayBuffer.isView(v) ? [v[0], v[1], v[2]] : [v.x, v.y, v.z]);
|
|
2242
|
+
frame = { center: a3(c), extent: a3(e) };
|
|
2243
|
+
}
|
|
2244
|
+
return {
|
|
2245
|
+
type,
|
|
2246
|
+
o: {
|
|
2247
|
+
vH: num('virtualDisplayHeight', (v) => v > 0),
|
|
2248
|
+
fit,
|
|
2249
|
+
margin: num('margin', (v) => v > 0),
|
|
2250
|
+
depthLimit: num('depthLimit', (v) => v > 0),
|
|
2251
|
+
fitSweep: o.fitSweep === undefined ? D.fitSweep : o.fitSweep !== false,
|
|
2252
|
+
idleSpin: num('idleSpin', () => true),
|
|
2253
|
+
ipdFactor: num('ipdFactor', (v) => v >= 0),
|
|
2254
|
+
parallaxFactor: num('parallaxFactor', (v) => v >= 0),
|
|
2255
|
+
perspectiveFactor: num('perspectiveFactor', (v) => v > 0),
|
|
2256
|
+
toneMapping,
|
|
2257
|
+
environment,
|
|
2258
|
+
frame,
|
|
2259
|
+
},
|
|
2260
|
+
};
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2154
2263
|
/** setSource's transitions and their defaults. */
|
|
2155
2264
|
export const SOURCE_TRANSITIONS = Object.freeze({
|
|
2156
2265
|
cut: {},
|
|
@@ -2248,6 +2357,7 @@ export function attachPlayCanvasSplat(out, wall, canvas, src, opts, pending = []
|
|
|
2248
2357
|
observe,
|
|
2249
2358
|
firstWovenHoldMs,
|
|
2250
2359
|
preserveDrawingBuffer = false,
|
|
2360
|
+
antialias = false,
|
|
2251
2361
|
} = opts;
|
|
2252
2362
|
// `sortIntervalMs` is accepted and has no effect here: the engine re-sorts when the camera
|
|
2253
2363
|
// ROTATES (one directional sort serves every view), not on a timer — docs/playcanvas-adapter.md.
|
|
@@ -2361,6 +2471,7 @@ export function attachPlayCanvasSplat(out, wall, canvas, src, opts, pending = []
|
|
|
2361
2471
|
},
|
|
2362
2472
|
setSource,
|
|
2363
2473
|
prepareSource,
|
|
2474
|
+
setRig,
|
|
2364
2475
|
/**
|
|
2365
2476
|
* Play a transition effect (inflate, deflate, sweep, dissolve, fade, pulse, custom) — see
|
|
2366
2477
|
* docs/splat-effects.md. Validated now; runs once the first asset is on screen. Resolves
|
|
@@ -2588,6 +2699,13 @@ export function attachPlayCanvasSplat(out, wall, canvas, src, opts, pending = []
|
|
|
2588
2699
|
|
|
2589
2700
|
// ── focus → view rig ──
|
|
2590
2701
|
let lastConvergence = Number.NaN;
|
|
2702
|
+
// The camera rig's descriptor object (rewritten in place per focus change) and the DISPLAY rig
|
|
2703
|
+
// declared through setViewRig, if any. `declaredDisplay` null + no camera rig pushed = the
|
|
2704
|
+
// `virtualDisplayHeight` shorthand addScene was built with, which is where every non-page tile
|
|
2705
|
+
// starts. A display rig after a camera rig has to be SENT (the shorthand cannot be restored),
|
|
2706
|
+
// as the explicit descriptor it is shorthand for (displayRig: identity pose, factors 1).
|
|
2707
|
+
let camRigObj = null;
|
|
2708
|
+
let declaredDisplay = pageMode ? 'page' : 'shorthand';
|
|
2591
2709
|
function pushViewRig(force) {
|
|
2592
2710
|
if (pageMode) return; // pageTick declares the rig every frame
|
|
2593
2711
|
if (!out.rig || out.rig.type !== 'camera') return;
|
|
@@ -2600,7 +2718,9 @@ export function attachPlayCanvasSplat(out, wall, canvas, src, opts, pending = []
|
|
|
2600
2718
|
lastConvergence = d;
|
|
2601
2719
|
out.rig.convergence = d;
|
|
2602
2720
|
const q = quatFromMatrix(pose);
|
|
2603
|
-
|
|
2721
|
+
camRigObj ||= {};
|
|
2722
|
+
declaredDisplay = null;
|
|
2723
|
+
out.viewRig = camRigObj = cameraRigFromPose(
|
|
2604
2724
|
{
|
|
2605
2725
|
position: { x: pose[12], y: pose[13], z: pose[14] },
|
|
2606
2726
|
orientation: { x: q[0], y: q[1], z: q[2], w: q[3] },
|
|
@@ -2610,7 +2730,7 @@ export function attachPlayCanvasSplat(out, wall, canvas, src, opts, pending = []
|
|
|
2610
2730
|
convergence: d > 0 ? d : 0,
|
|
2611
2731
|
ipdFactor: out.rig.ipdFactor,
|
|
2612
2732
|
parallaxFactor: out.rig.parallaxFactor,
|
|
2613
|
-
out:
|
|
2733
|
+
out: camRigObj,
|
|
2614
2734
|
},
|
|
2615
2735
|
);
|
|
2616
2736
|
handle?.setViewRig?.(out.viewRig);
|
|
@@ -2821,16 +2941,11 @@ export function attachPlayCanvasSplat(out, wall, canvas, src, opts, pending = []
|
|
|
2821
2941
|
? lift(frame)
|
|
2822
2942
|
: null;
|
|
2823
2943
|
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
const
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
cloud: sample,
|
|
2830
|
-
canvasAspect: box.height > 0 ? box.width / box.height : 4 / 3,
|
|
2831
|
-
});
|
|
2832
|
-
resolved.focusDefault = resolved.focus.slice();
|
|
2833
|
-
resolved.focusDefaultSource = resolved.focusSource;
|
|
2944
|
+
// What the waterfall was fed, kept with the asset: handle.setRig re-resolves from exactly this
|
|
2945
|
+
// (never from a stale copy of the result), so setRig('camera') / ('auto') land where a load did.
|
|
2946
|
+
const rigIn = { camera: loaded.camera, sample: loaded.pre?.rest || null, bounds };
|
|
2947
|
+
// A setRig choice is STICKY across setSource: the new asset is resolved on the chosen rig.
|
|
2948
|
+
const resolved = resolveFor(rigIn, rigOverride ? rigOverride.type : 'auto');
|
|
2834
2949
|
perfSpan('applyLoaded:rig', tB);
|
|
2835
2950
|
out.camera = loaded.camera;
|
|
2836
2951
|
out.rig = resolved;
|
|
@@ -2862,8 +2977,66 @@ export function attachPlayCanvasSplat(out, wall, canvas, src, opts, pending = []
|
|
|
2862
2977
|
}),
|
|
2863
2978
|
);
|
|
2864
2979
|
}
|
|
2865
|
-
} else
|
|
2980
|
+
} else {
|
|
2981
|
+
applyRig(resolved, bounds, false);
|
|
2982
|
+
}
|
|
2983
|
+
return { pickCentres, alpha8: cloud?.alpha8 || null, rigIn };
|
|
2984
|
+
}
|
|
2985
|
+
|
|
2986
|
+
/** resolveRig on an asset's kept inputs: 'auto' = the load-time waterfall, else forced. */
|
|
2987
|
+
function resolveFor(rigIn, type) {
|
|
2988
|
+
const box = canvas.getBoundingClientRect();
|
|
2989
|
+
const resolved = resolveRig({
|
|
2990
|
+
camera: rigIn.camera,
|
|
2991
|
+
opts: type === 'auto' ? rigOpts : { ...rigOpts, rig: type },
|
|
2992
|
+
cloud: rigIn.sample,
|
|
2993
|
+
canvasAspect: box.height > 0 ? box.width / box.height : 4 / 3,
|
|
2994
|
+
});
|
|
2995
|
+
if (type !== 'auto') resolved.typeSource = 'setRig';
|
|
2996
|
+
resolved.focusDefault = resolved.focus.slice();
|
|
2997
|
+
resolved.focusDefaultSource = resolved.focusSource;
|
|
2998
|
+
return resolved;
|
|
2999
|
+
}
|
|
3000
|
+
|
|
3001
|
+
/** The framing knobs the viewer's fit reads, as addSplat was booted with. */
|
|
3002
|
+
const bootFraming = { vH: virtualDisplayHeight, fit, margin, depthLimit, fitSweep };
|
|
3003
|
+
function setFraming(f) {
|
|
3004
|
+
viewer.vH = f.vH;
|
|
3005
|
+
viewer.fit = f.fit;
|
|
3006
|
+
viewer.margin = f.margin;
|
|
3007
|
+
viewer.depthLimit = f.depthLimit;
|
|
3008
|
+
viewer.fitSweep = f.fitSweep;
|
|
3009
|
+
}
|
|
3010
|
+
|
|
3011
|
+
/** Declare a DISPLAY rig to the runtime, unless that exact one is already declared. */
|
|
3012
|
+
function declareDisplay(f) {
|
|
3013
|
+
const key = `${f.vH}|${f.ipdFactor ?? 1}|${f.parallaxFactor ?? 1}|${f.perspectiveFactor ?? 1}`;
|
|
3014
|
+
if (declaredDisplay === key) return;
|
|
3015
|
+
// Back at the boot rig from the shorthand it was built with: nothing to say.
|
|
3016
|
+
if (declaredDisplay === 'shorthand' && key === `${bootFraming.vH}|1|1|1`) return;
|
|
3017
|
+
declaredDisplay = key;
|
|
3018
|
+
lastConvergence = Number.NaN;
|
|
3019
|
+
out.viewRig = displayRig({
|
|
3020
|
+
virtualDisplayHeight: f.vH,
|
|
3021
|
+
ipdFactor: f.ipdFactor ?? 1,
|
|
3022
|
+
parallaxFactor: f.parallaxFactor ?? 1,
|
|
3023
|
+
perspectiveFactor: f.perspectiveFactor ?? 1,
|
|
3024
|
+
});
|
|
3025
|
+
handle?.setViewRig?.(out.viewRig);
|
|
3026
|
+
}
|
|
3027
|
+
|
|
3028
|
+
/**
|
|
3029
|
+
* Put the viewer on a resolved rig: lens, framing, focus, and the rig declared to the runtime.
|
|
3030
|
+
* Shared by every load / setSource (reset=false: the pose is kept) and handle.setRig
|
|
3031
|
+
* (reset=true: the pose goes back to the rig's rest, yaw = pitch = 0, zoom 1, depth 0).
|
|
3032
|
+
*/
|
|
3033
|
+
function applyRig(resolved, bounds, reset) {
|
|
3034
|
+
const disp = rigOverride?.type === 'display' ? rigOverride.o : null;
|
|
3035
|
+
if (reset) viewer.resetPose();
|
|
3036
|
+
if (resolved.type === 'camera') {
|
|
3037
|
+
setFraming(bootFraming);
|
|
2866
3038
|
if (!('idleSpin' in opts)) viewer.idleSpin = 0;
|
|
3039
|
+
else if (reset) viewer.idleSpin = idleSpin;
|
|
2867
3040
|
// The camera rig never auto-fits (the capture IS the framing): a scale left by a previous
|
|
2868
3041
|
// display-rig asset (setSource) must not shrink this one.
|
|
2869
3042
|
viewer.resetFit();
|
|
@@ -2871,15 +3044,170 @@ export function attachPlayCanvasSplat(out, wall, canvas, src, opts, pending = []
|
|
|
2871
3044
|
viewer.setFocus(modelToContent(resolved.focus), { snap: true, recentre: false });
|
|
2872
3045
|
lastConvergence = Number.NaN;
|
|
2873
3046
|
pushViewRig(true);
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
3047
|
+
return;
|
|
3048
|
+
}
|
|
3049
|
+
if (viewer.mono.capture) viewer.useDisplayCamera();
|
|
3050
|
+
// The display rig orbits the frame centre (SceneViewer's recentre), whatever a camera rig
|
|
3051
|
+
// before it left in the viewer.
|
|
3052
|
+
viewer._focusRecentres = true;
|
|
3053
|
+
const target = disp ? displayTarget(disp) : null;
|
|
3054
|
+
if (target) {
|
|
3055
|
+
// setRig('display') framing the page's content: addModel's fit, on addModel's defaults.
|
|
3056
|
+
setFraming(disp);
|
|
3057
|
+
viewer.idleSpin = disp.idleSpin;
|
|
3058
|
+
viewer.fitTo(target.center, target.extent);
|
|
3059
|
+
resolved.focus = contentToModel(target.center);
|
|
3060
|
+
resolved.focusSource = 'frame';
|
|
3061
|
+
resolved.focusDefault = resolved.focus.slice();
|
|
3062
|
+
resolved.focusDefaultSource = 'frame';
|
|
3063
|
+
resolved.frame = target;
|
|
3064
|
+
declareDisplay(disp);
|
|
3065
|
+
return;
|
|
3066
|
+
}
|
|
3067
|
+
// The splat itself, as a display-rig addSplat frames it (the load-time path).
|
|
3068
|
+
setFraming(disp || bootFraming);
|
|
3069
|
+
if (reset) viewer.idleSpin = disp ? disp.idleSpin : idleSpin;
|
|
3070
|
+
if (bounds) viewer.fitTo(bounds.center, bounds.extent);
|
|
3071
|
+
else console.warn('[inline3d/splat] no usable bounds — subject is UNFRAMED (model scale)', src);
|
|
3072
|
+
if (resolved.focusSource === 'caller' || resolved.focusSource === 'block') {
|
|
3073
|
+
viewer.setFocus(modelToContent(resolved.focus), { snap: true, recentre: true });
|
|
3074
|
+
}
|
|
3075
|
+
if (disp) resolved.frame = bounds ? { center: bounds.center.slice(), extent: bounds.extent.slice(), source: 'splat' } : null;
|
|
3076
|
+
declareDisplay(disp || { vH: bootFraming.vH });
|
|
3077
|
+
}
|
|
3078
|
+
|
|
3079
|
+
// ── handle.setRig: a live, reversible rig switch (docs/playcanvas-adapter.md §setRig) ──
|
|
3080
|
+
/** null = 'auto' (the per-asset waterfall); else { type: 'camera' } | { type: 'display', o }. */
|
|
3081
|
+
let rigOverride = null;
|
|
3082
|
+
let rigSeq = 0;
|
|
3083
|
+
/** The neutral-studio IBL setRig('display') installed, and what it replaced — or null. */
|
|
3084
|
+
let rigEnv = null;
|
|
3085
|
+
const baseToneMapping = viewer.toneMapping;
|
|
3086
|
+
|
|
3087
|
+
/** Is the splat on screen: its entity (and every ancestor) enabled. */
|
|
3088
|
+
function splatShown() {
|
|
3089
|
+
let e = current?.entity || null;
|
|
3090
|
+
if (!e) return false;
|
|
3091
|
+
for (; e; e = e.parent) if (e.enabled === false) return false;
|
|
3092
|
+
return true;
|
|
3093
|
+
}
|
|
3094
|
+
|
|
3095
|
+
/**
|
|
3096
|
+
* World AABB of the page's meshes under handle.engine.root — enabled render components only,
|
|
3097
|
+
* union of their mesh instances' `aabb` (the engine's world box, skinning included). Content
|
|
3098
|
+
* space. The same measurement addModel's boundsOfEntity takes of its glTF.
|
|
3099
|
+
*/
|
|
3100
|
+
function rootMeshBounds() {
|
|
3101
|
+
const root = viewer.content;
|
|
3102
|
+
const renders = root?.findComponents ? root.findComponents('render') : [];
|
|
3103
|
+
let min = null;
|
|
3104
|
+
let max = null;
|
|
3105
|
+
for (const r of renders) {
|
|
3106
|
+
if (r.enabled === false || r.entity?.enabled === false) continue;
|
|
3107
|
+
for (const mi of r.meshInstances || []) {
|
|
3108
|
+
const b = mi.aabb;
|
|
3109
|
+
if (!b) continue;
|
|
3110
|
+
const c = b.center;
|
|
3111
|
+
const h = b.halfExtents;
|
|
3112
|
+
const lo = [c.x - h.x, c.y - h.y, c.z - h.z];
|
|
3113
|
+
const hi = [c.x + h.x, c.y + h.y, c.z + h.z];
|
|
3114
|
+
if (!lo.every(Number.isFinite) || !hi.every(Number.isFinite)) continue;
|
|
3115
|
+
if (!min) {
|
|
3116
|
+
min = lo;
|
|
3117
|
+
max = hi;
|
|
3118
|
+
} else {
|
|
3119
|
+
for (let i = 0; i < 3; i++) {
|
|
3120
|
+
if (lo[i] < min[i]) min[i] = lo[i];
|
|
3121
|
+
if (hi[i] > max[i]) max[i] = hi[i];
|
|
3122
|
+
}
|
|
3123
|
+
}
|
|
2880
3124
|
}
|
|
2881
3125
|
}
|
|
2882
|
-
return {
|
|
3126
|
+
return min ? { min, max } : null;
|
|
3127
|
+
}
|
|
3128
|
+
|
|
3129
|
+
/**
|
|
3130
|
+
* What setRig('display') frames: the caller's `frame`, else the meshes under root (plus the
|
|
3131
|
+
* splat's measured box if it is shown), else null — the splat, as a display-rig addSplat would.
|
|
3132
|
+
*/
|
|
3133
|
+
function displayTarget(disp) {
|
|
3134
|
+
if (disp.frame) {
|
|
3135
|
+
return { center: toArray3(disp.frame.center), extent: toArray3(disp.frame.extent).map((v) => Math.max(Math.abs(v), 1e-6)), source: 'caller' };
|
|
3136
|
+
}
|
|
3137
|
+
const m = rootMeshBounds();
|
|
3138
|
+
if (!m) return null;
|
|
3139
|
+
let { min, max } = m;
|
|
3140
|
+
let source = 'root';
|
|
3141
|
+
const sb = splatShown() ? out.frame : null;
|
|
3142
|
+
if (sb) {
|
|
3143
|
+
source = 'root+splat';
|
|
3144
|
+
min = min.map((v, i) => Math.min(v, sb.center[i] - sb.extent[i] / 2));
|
|
3145
|
+
max = max.map((v, i) => Math.max(v, sb.center[i] + sb.extent[i] / 2));
|
|
3146
|
+
}
|
|
3147
|
+
return {
|
|
3148
|
+
center: [(min[0] + max[0]) / 2, (min[1] + max[1]) / 2, (min[2] + max[2]) / 2],
|
|
3149
|
+
extent: [Math.max(max[0] - min[0], 1e-6), Math.max(max[1] - min[1], 1e-6), Math.max(max[2] - min[2], 1e-6)],
|
|
3150
|
+
source,
|
|
3151
|
+
};
|
|
3152
|
+
}
|
|
3153
|
+
|
|
3154
|
+
/** Per tick: addModel's tone mapping for the page's meshes while the splat is hidden. */
|
|
3155
|
+
viewer.onTick = pageMode
|
|
3156
|
+
? viewer.onTick
|
|
3157
|
+
: () => {
|
|
3158
|
+
const want = rigOverride?.type === 'display' && !splatShown() ? rigOverride.o.toneMapping : baseToneMapping;
|
|
3159
|
+
if (want !== viewer.toneMapping) viewer.setToneMapping(want);
|
|
3160
|
+
};
|
|
3161
|
+
|
|
3162
|
+
/** Install addModel's default IBL for the display rig, if the page has none of its own. */
|
|
3163
|
+
async function ensureRigEnvironment(disp, seq) {
|
|
3164
|
+
if (disp.environment === 'none' || viewer.sky || rigEnv) return;
|
|
3165
|
+
const app = viewer.app;
|
|
3166
|
+
const scene = app?.scene;
|
|
3167
|
+
if (!scene || scene.envAtlas) return; // the page lights its own meshes: leave it alone
|
|
3168
|
+
const m = await import('./inline3d-model-playcanvas.js');
|
|
3169
|
+
if (removed || seq !== rigSeq || rigOverride?.type !== 'display' || scene.envAtlas || rigEnv) return;
|
|
3170
|
+
const prev = { skyboxIntensity: scene.skyboxIntensity, exposure: scene.exposure, skyboxRotation: scene.skyboxRotation };
|
|
3171
|
+
const atlas = m.useNeutralStudio(pcModule, app, m.ENV_YAW_DEG);
|
|
3172
|
+
rigEnv = { atlas, prev };
|
|
3173
|
+
}
|
|
3174
|
+
|
|
3175
|
+
/** Undo ensureRigEnvironment (only what it installed, only if the page has not replaced it). */
|
|
3176
|
+
function dropRigEnvironment() {
|
|
3177
|
+
if (!rigEnv) return;
|
|
3178
|
+
const scene = viewer.app?.scene;
|
|
3179
|
+
const { atlas, prev } = rigEnv;
|
|
3180
|
+
rigEnv = null;
|
|
3181
|
+
if (!scene || scene.envAtlas !== atlas) return;
|
|
3182
|
+
scene.envAtlas = null;
|
|
3183
|
+
scene.skyboxIntensity = prev.skyboxIntensity;
|
|
3184
|
+
scene.exposure = prev.exposure;
|
|
3185
|
+
if (prev.skyboxRotation !== undefined) scene.skyboxRotation = prev.skyboxRotation;
|
|
3186
|
+
atlas.destroy?.();
|
|
3187
|
+
}
|
|
3188
|
+
|
|
3189
|
+
/**
|
|
3190
|
+
* handle.setRig(type, options) — switch the rig the window is seen through, live: no remount,
|
|
3191
|
+
* no reload, no new session. 'display' frames the page's meshes under handle.engine.root (plus
|
|
3192
|
+
* the splat when shown) on addModel's defaults; 'camera' is the asset's capture rig re-resolved
|
|
3193
|
+
* from the waterfall's own inputs; 'auto' is exactly what the load resolved. The pose resets to
|
|
3194
|
+
* the new rig's rest. Sticky across setSource until setRig('auto'). Resolves to the handle once
|
|
3195
|
+
* applied (the display rig's default environment included).
|
|
3196
|
+
*/
|
|
3197
|
+
function setRig(type, o = {}) {
|
|
3198
|
+
const req = validateSetRig(type, o, pageMode);
|
|
3199
|
+
const seq = ++rigSeq;
|
|
3200
|
+
return first.then(async () => {
|
|
3201
|
+
if (removed || seq !== rigSeq || !current) return out;
|
|
3202
|
+
rigOverride = req.type === 'auto' ? null : req;
|
|
3203
|
+
if (req.type !== 'display') dropRigEnvironment();
|
|
3204
|
+
const resolved = resolveFor(current.rigIn, req.type);
|
|
3205
|
+
out.rig = resolved;
|
|
3206
|
+
applyRig(resolved, current.rigIn.bounds, true);
|
|
3207
|
+
viewer.onTick?.(); // tone mapping now, not a frame late
|
|
3208
|
+
if (req.type === 'display') await ensureRigEnvironment(req.o, seq);
|
|
3209
|
+
return out;
|
|
3210
|
+
});
|
|
2883
3211
|
}
|
|
2884
3212
|
|
|
2885
3213
|
let pcModule = null;
|
|
@@ -2892,6 +3220,7 @@ export function attachPlayCanvasSplat(out, wall, canvas, src, opts, pending = []
|
|
|
2892
3220
|
preserveDrawingBuffer,
|
|
2893
3221
|
perf: perfResolved,
|
|
2894
3222
|
viewPath: opts.playcanvasViewPath,
|
|
3223
|
+
antialias: antialias === true,
|
|
2895
3224
|
});
|
|
2896
3225
|
if (!app || removed) return null;
|
|
2897
3226
|
pcModule = pc;
|
package/js/inline3d-splat.js
CHANGED
|
@@ -373,6 +373,13 @@ export function addSplat(wall, canvas, src, opts = {}) {
|
|
|
373
373
|
prepareSource() {
|
|
374
374
|
throw new Error("@displayxr/inline3d/splat: prepareSource() is implemented on the PlayCanvas backend only.");
|
|
375
375
|
},
|
|
376
|
+
/** Not on this backend: the live rig switch is a PlayCanvas-backend feature. */
|
|
377
|
+
setRig() {
|
|
378
|
+
throw new Error(
|
|
379
|
+
"@displayxr/inline3d/splat: setRig() is implemented on the PlayCanvas backend only " +
|
|
380
|
+
"(addSplat(…, { engine:'playcanvas' })).",
|
|
381
|
+
);
|
|
382
|
+
},
|
|
376
383
|
/** Not on this backend (splat effects are PlayCanvas-only in this version). */
|
|
377
384
|
playEffect() {
|
|
378
385
|
throw effectsNotOnSpark('playEffect()');
|
|
@@ -657,6 +664,16 @@ export function addSplat(wall, canvas, src, opts = {}) {
|
|
|
657
664
|
* Fields (`viewer`, `mesh`, `rig`, …) are null until then; `viewer` in particular appears one
|
|
658
665
|
* module-load later than on Spark.
|
|
659
666
|
*/
|
|
667
|
+
/** setRig's type/options shape, checked at the call before the adapter has loaded (it re-checks all of it). */
|
|
668
|
+
function validateSetRigArgs(type, o) {
|
|
669
|
+
if (!['display', 'camera', 'auto'].includes(type)) {
|
|
670
|
+
throw new Error(`@displayxr/inline3d/splat: setRig("${type}") — expected 'display', 'camera' or 'auto'.`);
|
|
671
|
+
}
|
|
672
|
+
if (o !== undefined && (o === null || typeof o !== 'object')) {
|
|
673
|
+
throw new TypeError('@displayxr/inline3d/splat: setRig options must be an object.');
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
660
677
|
function addSplatDeferred(wall, canvas, src, opts) {
|
|
661
678
|
const pending = [];
|
|
662
679
|
const queue = (name) => (...args) => {
|
|
@@ -711,6 +728,14 @@ function addSplatDeferred(wall, canvas, src, opts) {
|
|
|
711
728
|
// setSource replaces this stub on the same object by then).
|
|
712
729
|
setSource: (...args) => out.ready.then(() => out.setSource(...args)),
|
|
713
730
|
prepareSource: (...args) => out.ready.then(() => out.prepareSource(...args)),
|
|
731
|
+
// A rig switch before the first asset has landed: validated now (controls:'page' and a bad
|
|
732
|
+
// type throw at the call's own line), applied once it has.
|
|
733
|
+
setRig: page
|
|
734
|
+
? pageOnly('setRig')
|
|
735
|
+
: (type, o) => {
|
|
736
|
+
validateSetRigArgs(type, o);
|
|
737
|
+
return out.ready.then(() => out.setRig(type, o));
|
|
738
|
+
},
|
|
714
739
|
// Effects before the adapter has loaded: validated NOW (a bad call throws at its own line),
|
|
715
740
|
// then run once the first asset is on screen.
|
|
716
741
|
playEffect: (name, o) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@displayxr/inline3d",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.16.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
|
@@ -131,7 +131,8 @@ export interface SogCamera {
|
|
|
131
131
|
*/
|
|
132
132
|
export interface ResolvedRig {
|
|
133
133
|
type: 'camera' | 'display';
|
|
134
|
-
|
|
134
|
+
/** `'setRig'` after `handle.setRig('display' | 'camera')`; the waterfall's step otherwise. */
|
|
135
|
+
typeSource: 'caller' | 'block' | 'block-present' | 'default' | 'setRig';
|
|
135
136
|
rest: { position: number[]; rotation: number[] };
|
|
136
137
|
intrinsics: SogIntrinsics;
|
|
137
138
|
intrinsicsSource: 'block' | 'caller' | 'estimated' | 'fallback-28mm';
|
|
@@ -154,7 +155,14 @@ export interface ResolvedRig {
|
|
|
154
155
|
| 'median-disparity'
|
|
155
156
|
| 'default'
|
|
156
157
|
| 'picked'
|
|
157
|
-
| 'set'
|
|
158
|
+
| 'set'
|
|
159
|
+
| 'frame';
|
|
160
|
+
/**
|
|
161
|
+
* On a display rig set by `handle.setRig('display')`: what it framed, in `handle.engine.root`'s
|
|
162
|
+
* space, and where that came from (the page's meshes, meshes + the shown splat, the splat alone,
|
|
163
|
+
* or the caller's `frame`). Absent otherwise.
|
|
164
|
+
*/
|
|
165
|
+
frame?: { center: number[]; extent: number[]; source: 'root' | 'root+splat' | 'splat' | 'caller' } | null;
|
|
158
166
|
/** The block's own `focus.source` string (e.g. `'convergence'`, `'cloud-median'`), for diagnostics. */
|
|
159
167
|
blockFocusSource: string | null;
|
|
160
168
|
/** Fraction of the central crop's opacity-weighted mass the winning clump carried (`nearest-clump` only). */
|
|
@@ -187,6 +195,13 @@ export interface SplatOptions {
|
|
|
187
195
|
* the knob for the weave's zero-copy read race on large canvases.
|
|
188
196
|
*/
|
|
189
197
|
preserveDrawingBuffer?: boolean;
|
|
198
|
+
/**
|
|
199
|
+
* `engine: 'playcanvas'` only: create the tile's WebGL context with MSAA (default false — it buys
|
|
200
|
+
* nothing on alpha-blended splats). Turn it on when the page draws meshes under
|
|
201
|
+
* `handle.engine.root` and wants addModel's silhouettes (addModel's context has MSAA on);
|
|
202
|
+
* `setRig('display')` is then pixel-identical to an addModel tile. Fixed at context creation.
|
|
203
|
+
*/
|
|
204
|
+
antialias?: boolean;
|
|
190
205
|
/**
|
|
191
206
|
* `engine: 'playcanvas'` only: the `playcanvas` module namespace to use instead of
|
|
192
207
|
* `import('playcanvas')` — for a page that already bundles its own copy.
|
|
@@ -490,6 +505,46 @@ export interface SplatEffectState {
|
|
|
490
505
|
progress: number;
|
|
491
506
|
}
|
|
492
507
|
|
|
508
|
+
/**
|
|
509
|
+
* `handle.setRig('display', opts)`'s options. Every default is addModel's, so a mesh framed this
|
|
510
|
+
* way renders exactly as `addModel(url, { engine: 'playcanvas' })` renders it.
|
|
511
|
+
* docs/playcanvas-adapter.md §setRig.
|
|
512
|
+
*/
|
|
513
|
+
export interface SplatSetRigDisplayOptions {
|
|
514
|
+
/** Default 0.24. */
|
|
515
|
+
virtualDisplayHeight?: number;
|
|
516
|
+
/** Default `'contain'`. */
|
|
517
|
+
fit?: 'contain' | 'cover' | 'height' | 'none';
|
|
518
|
+
/** Default 0.8. */
|
|
519
|
+
margin?: number;
|
|
520
|
+
/** Default 4. */
|
|
521
|
+
depthLimit?: number;
|
|
522
|
+
/** Default true. */
|
|
523
|
+
fitSweep?: boolean;
|
|
524
|
+
/** Idle turntable, °/s after 2.5 s without input. Default 8 (addModel's); 0 for a still product. */
|
|
525
|
+
idleSpin?: number;
|
|
526
|
+
/** On the declared display rig. Default 1 each. */
|
|
527
|
+
ipdFactor?: number;
|
|
528
|
+
parallaxFactor?: number;
|
|
529
|
+
perspectiveFactor?: number;
|
|
530
|
+
/**
|
|
531
|
+
* The eye camera's tone mapping WHILE THE SPLAT IS HIDDEN (the splat's display-referred colours
|
|
532
|
+
* keep `'none'` whenever it is shown). Default `'neutral'` — addModel's (Khronos PBR Neutral).
|
|
533
|
+
*/
|
|
534
|
+
toneMapping?: 'none' | 'linear' | 'neutral' | 'aces' | 'aces2' | 'filmic' | 'hejl';
|
|
535
|
+
/**
|
|
536
|
+
* `'room'` (default; `'neutral'` is the same): install addModel's generated neutral-studio IBL
|
|
537
|
+
* when the scene has no `envAtlas` of its own, removed again when the rig switches away. A page
|
|
538
|
+
* that lights its own meshes is never touched. `'none'`: never install it.
|
|
539
|
+
*/
|
|
540
|
+
environment?: 'room' | 'neutral' | 'none';
|
|
541
|
+
/**
|
|
542
|
+
* Frame THIS box (in `handle.engine.root`'s space) instead of measuring. Default: the enabled
|
|
543
|
+
* meshes under root (+ the splat's box when it is shown); with no meshes, the splat itself.
|
|
544
|
+
*/
|
|
545
|
+
frame?: { center: number[] | { x: number; y: number; z: number }; extent: number[] | { x: number; y: number; z: number } };
|
|
546
|
+
}
|
|
547
|
+
|
|
493
548
|
/** `setSource`'s options. */
|
|
494
549
|
export interface SplatSourceOptions {
|
|
495
550
|
/** > 0 = a crossfade of this length (the 1.10 option; same as `transition: 'crossfade'`). */
|
|
@@ -727,6 +782,19 @@ export interface SplatHandle {
|
|
|
727
782
|
* SOG). `remove()` disposes any still unused.
|
|
728
783
|
*/
|
|
729
784
|
prepareSource(src: string | Blob | ArrayBuffer | Uint8Array, opts?: SplatPrepareOptions): Promise<SplatPreparedSource>;
|
|
785
|
+
/**
|
|
786
|
+
* PlayCanvas backend only — throws on Spark, and with `controls:'page'` (the page's camera IS
|
|
787
|
+
* the rig). Switch the rig live, on the next frame: no remount, no reload, no new session.
|
|
788
|
+
*
|
|
789
|
+
* `'display'` frames the page's meshes under `handle.engine.root` (+ the splat when shown) on
|
|
790
|
+
* addModel's defaults and declares a display rig; `'camera'` returns to the asset's capture rig,
|
|
791
|
+
* re-resolved from the waterfall's own inputs (lens, captureFit, focus rung); `'auto'` is what
|
|
792
|
+
* the load resolved. The pose resets to the new rig's rest; the declared view rig switches with
|
|
793
|
+
* it; the choice is sticky across `setSource` until `'auto'`. A clean cut (no eased transition).
|
|
794
|
+
* `handle.rig.typeSource` reads `'setRig'`. Resolves once applied. docs/playcanvas-adapter.md §setRig.
|
|
795
|
+
*/
|
|
796
|
+
setRig(type: 'display', opts?: SplatSetRigDisplayOptions): Promise<SplatHandle>;
|
|
797
|
+
setRig(type: 'camera' | 'auto'): Promise<SplatHandle>;
|
|
730
798
|
/**
|
|
731
799
|
* `engine: 'playcanvas'` only (throws on Spark). Play a transition/pulse/custom effect;
|
|
732
800
|
* validated at the call, run once the first asset is on screen. Resolves `{ finished }` —
|