@displayxr/inline3d 1.28.0 → 1.30.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 +233 -0
- package/README.md +10 -3
- package/call-full.d.ts +21 -0
- package/call.d.ts +328 -217
- package/camera.d.ts +257 -0
- package/dist/call.js +86 -0
- package/index.d.ts +27 -1
- package/js/call/capture.js +4 -167
- package/js/call/disparity.js +4 -0
- package/js/call/element.js +221 -0
- package/js/call/lift.js +9 -0
- package/js/call/options.js +132 -0
- package/js/call/signaling.js +7 -202
- package/js/call/ui.js +106 -9
- package/js/call/wire.js +8 -147
- package/js/camera/capture.js +220 -0
- package/js/camera/converge.js +82 -0
- package/js/camera/disparity.js +432 -0
- package/js/camera/geometry.js +80 -0
- package/js/camera/metadata.js +398 -0
- package/js/inline3d-call-full.js +48 -0
- package/js/inline3d-call.js +609 -318
- package/js/inline3d-camera.js +678 -0
- package/js/inline3d-player.js +646 -216
- package/js/inline3d-splat-deferred.js +153 -0
- package/js/inline3d-splat-pc-entry.js +45 -0
- package/js/inline3d-splat-playcanvas.js +46 -3
- package/js/inline3d-splat-video.js +328 -29
- package/js/inline3d-splat.js +2 -145
- package/js/inline3d.js +130 -1
- package/js/lift/index.js +15 -0
- package/package.json +39 -4
- package/player.d.ts +46 -2
- package/splat-playcanvas.d.ts +4 -0
- package/splat.d.ts +38 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// inline3d-splat-deferred.js — the engine:'playcanvas' handle, shared by both splat entries.
|
|
2
|
+
//
|
|
3
|
+
// Internal. ./splat (Spark by default) and ./splat/playcanvas (PlayCanvas only) both build their
|
|
4
|
+
// PlayCanvas handle here. This module must never import `three` or `@sparkjsdev/spark`, directly
|
|
5
|
+
// or through anything it imports: that is what lets ./splat/playcanvas keep both off the page
|
|
6
|
+
// (test/splat-pc-entry.test.mjs walks the import graph to hold it to that).
|
|
7
|
+
|
|
8
|
+
import { normalizeCameraPose } from './inline3d-splat-shared.js';
|
|
9
|
+
import { validateEffectCall } from './inline3d-splat-effects.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The `engine: 'playcanvas'` handle, returned SYNCHRONOUSLY like the Spark one.
|
|
13
|
+
*
|
|
14
|
+
* The adapter module is loaded on demand, so for a moment the handle exists before its
|
|
15
|
+
* implementation does. Rather than make every caller await something new, the handle starts as
|
|
16
|
+
* stubs that QUEUE: `exclude()` (which a product page calls on the very next line), `setPose`,
|
|
17
|
+
* `setFocus`, `remove` — and the adapter replays the queue into the real implementation, on the
|
|
18
|
+
* SAME object, the moment it arrives. `ready` resolves to this object, as it does on Spark.
|
|
19
|
+
* Fields (`viewer`, `mesh`, `rig`, …) are null until then; `viewer` in particular appears one
|
|
20
|
+
* module-load later than on Spark.
|
|
21
|
+
*/
|
|
22
|
+
/** setRig's type/options shape, checked at the call before the adapter has loaded (it re-checks all of it). */
|
|
23
|
+
function validateSetRigArgs(type, o) {
|
|
24
|
+
if (!['display', 'camera', 'auto'].includes(type)) {
|
|
25
|
+
throw new Error(`@displayxr/inline3d/splat: setRig("${type}") — expected 'display', 'camera' or 'auto'.`);
|
|
26
|
+
}
|
|
27
|
+
if (o !== undefined && (o === null || typeof o !== 'object')) {
|
|
28
|
+
throw new TypeError('@displayxr/inline3d/splat: setRig options must be an object.');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function addSplatDeferred(wall, canvas, src, opts) {
|
|
33
|
+
const pending = [];
|
|
34
|
+
const queue = (name) => (...args) => {
|
|
35
|
+
pending.push([name, args]);
|
|
36
|
+
return name === 'setFocus' ? out : undefined;
|
|
37
|
+
};
|
|
38
|
+
const page = opts.controls === 'page';
|
|
39
|
+
const pageOnly = (name) => () => {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`@displayxr/inline3d/splat: ${name}() is not available with controls:'page' — the page owns the ` +
|
|
42
|
+
'camera. Drive it with handle.setCameraPose(matrixWorld, { verticalFovDeg, near, far }).',
|
|
43
|
+
);
|
|
44
|
+
};
|
|
45
|
+
// Before the adapter lands, a page calling setCameraPose every frame keeps ONE pending pose
|
|
46
|
+
// (validated now, so a bad call throws at its own line): last call wins, as it will after.
|
|
47
|
+
let pendingPose = null;
|
|
48
|
+
const out = {
|
|
49
|
+
backend: 'playcanvas',
|
|
50
|
+
engine: null,
|
|
51
|
+
viewer: null,
|
|
52
|
+
mesh: null,
|
|
53
|
+
frame: null,
|
|
54
|
+
camera: null,
|
|
55
|
+
rig: null,
|
|
56
|
+
perf: null,
|
|
57
|
+
setPose: page ? pageOnly('setPose') : queue('setPose'),
|
|
58
|
+
resetPose: page ? pageOnly('resetPose') : queue('resetPose'),
|
|
59
|
+
setCameraPose(matrixWorld, o) {
|
|
60
|
+
if (!page) {
|
|
61
|
+
throw new Error(
|
|
62
|
+
"@displayxr/inline3d/splat: setCameraPose() needs addSplat(…, { controls:'page' }) — " +
|
|
63
|
+
'with the default controls the SDK owns the camera (use setPose).',
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
const pose = normalizeCameraPose(matrixWorld, o);
|
|
67
|
+
if (pendingPose) pendingPose[1] = [pose.matrixWorld, pose];
|
|
68
|
+
else pending.push((pendingPose = ['setCameraPose', [pose.matrixWorld, pose]]));
|
|
69
|
+
return out;
|
|
70
|
+
},
|
|
71
|
+
getCameraPose: () =>
|
|
72
|
+
pendingPose
|
|
73
|
+
? {
|
|
74
|
+
matrixWorld: Float32Array.from(pendingPose[1][1].matrixWorld),
|
|
75
|
+
verticalFovDeg: pendingPose[1][1].verticalFovDeg,
|
|
76
|
+
near: pendingPose[1][1].near,
|
|
77
|
+
far: pendingPose[1][1].far,
|
|
78
|
+
convergence: pendingPose[1][1].convergence,
|
|
79
|
+
}
|
|
80
|
+
: null,
|
|
81
|
+
setFocus: queue('setFocus'),
|
|
82
|
+
// A swap requested before the first asset has landed runs once it has (the adapter's own
|
|
83
|
+
// setSource replaces this stub on the same object by then).
|
|
84
|
+
setSource: (...args) => out.ready.then(() => out.setSource(...args)),
|
|
85
|
+
prepareSource: (...args) => out.ready.then(() => out.prepareSource(...args)),
|
|
86
|
+
// A rig switch before the first asset has landed: validated now (controls:'page' and a bad
|
|
87
|
+
// type throw at the call's own line), applied once it has.
|
|
88
|
+
setRig: page
|
|
89
|
+
? pageOnly('setRig')
|
|
90
|
+
: (type, o) => {
|
|
91
|
+
validateSetRigArgs(type, o);
|
|
92
|
+
return out.ready.then(() => out.setRig(type, o));
|
|
93
|
+
},
|
|
94
|
+
// A video before the first asset has landed: controls:'page' throws at the call's own line,
|
|
95
|
+
// the rest is validated by the adapter's setVideo, which runs once the asset is on screen.
|
|
96
|
+
setVideo: page ? pageOnly('setVideo') : (src, o) => out.ready.then(() => out.setVideo(src, o)),
|
|
97
|
+
// Effects before the adapter has loaded: validated NOW (a bad call throws at its own line),
|
|
98
|
+
// then run once the first asset is on screen.
|
|
99
|
+
playEffect: (name, o) => {
|
|
100
|
+
validateEffectCall(name, o, 'play');
|
|
101
|
+
return out.ready.then(() => out.playEffect(name, o));
|
|
102
|
+
},
|
|
103
|
+
setEffect: (name, params) => {
|
|
104
|
+
validateEffectCall(name, params, 'set');
|
|
105
|
+
pending.push(['setEffect', [name, params]]);
|
|
106
|
+
return out;
|
|
107
|
+
},
|
|
108
|
+
stopEffect: queue('stopEffect'),
|
|
109
|
+
// A layer-rig request before the adapter has loaded: queued, replayed in order once it has
|
|
110
|
+
// (it needs no asset, only the engine's layers — which the adapter resolves lazily).
|
|
111
|
+
setLayerRig: (...args) => {
|
|
112
|
+
pending.push(['setLayerRig', args]);
|
|
113
|
+
return out;
|
|
114
|
+
},
|
|
115
|
+
setLayerRigOptions: (...args) => {
|
|
116
|
+
pending.push(['setLayerRigOptions', args]);
|
|
117
|
+
return out;
|
|
118
|
+
},
|
|
119
|
+
layerRigState: () => ({ display: [], disabled: false, path: null, engaged: false, rounded: false, reason: 'the PlayCanvas adapter has not loaded yet', viewerDistance: 0.6, gain: null, planeM: null, photoConvergenceM: null, planeOffset: 0, located: null }),
|
|
120
|
+
makeSbsMaterial: () => {
|
|
121
|
+
throw new Error('@displayxr/inline3d/splat: makeSbsMaterial() needs the engine — call it after `await handle.ready`.');
|
|
122
|
+
},
|
|
123
|
+
effects: () => [],
|
|
124
|
+
getFocus: () => null,
|
|
125
|
+
// A plain data slot the adapter reads at call time, so a callback assigned on the very next
|
|
126
|
+
// line after addSplat — before the module has loaded — is the one that fires.
|
|
127
|
+
onFocusChange: null,
|
|
128
|
+
pick: () => null,
|
|
129
|
+
// Splat accounting (resident / budget / first frame); null until the adapter has loaded.
|
|
130
|
+
stats: () => null,
|
|
131
|
+
remove: queue('remove'),
|
|
132
|
+
exclude: queue('exclude'),
|
|
133
|
+
unexclude: queue('unexclude'),
|
|
134
|
+
};
|
|
135
|
+
// `firstWoven` exists from the first line, like every other field a page reads right away; the
|
|
136
|
+
// adapter settles it with the core handle's own once the module has loaded.
|
|
137
|
+
out.firstWoven = new Promise((resolve) => {
|
|
138
|
+
out._resolveFirstWoven = resolve;
|
|
139
|
+
});
|
|
140
|
+
// The ONE owner of `ready`: the adapter returns its load promise and never touches this field.
|
|
141
|
+
out.ready = import('./inline3d-splat-playcanvas.js')
|
|
142
|
+
.then((m) => m.attachPlayCanvasSplat(out, wall, canvas, src, opts, pending))
|
|
143
|
+
.catch((err) => {
|
|
144
|
+
// The adapter warns about its own load failures; this is for the module not arriving.
|
|
145
|
+
if (!out.viewer) console.warn('[inline3d/splat] engine:playcanvas failed to start', err);
|
|
146
|
+
if (out._resolveFirstWoven) {
|
|
147
|
+
out._resolveFirstWoven(Object.freeze({ woven: false, confirmed: false, reason: 'layer-failed', ms: 0 }));
|
|
148
|
+
delete out._resolveFirstWoven;
|
|
149
|
+
}
|
|
150
|
+
throw err;
|
|
151
|
+
});
|
|
152
|
+
return out;
|
|
153
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// inline3d-splat-pc-entry.js — `@displayxr/inline3d/splat/playcanvas`: addSplat on PlayCanvas only.
|
|
2
|
+
//
|
|
3
|
+
// EXPERIMENTAL, like ./splat. Same addSplat, same handle, same options, with the engine fixed to
|
|
4
|
+
// 'playcanvas'. The difference is what the page downloads: ./splat statically imports `three`
|
|
5
|
+
// and `@sparkjsdev/spark` because Spark is its default engine and its Spark path is synchronous,
|
|
6
|
+
// so a page passing engine:'playcanvas' there still pays for both (~1.7 MB gzipped). This entry
|
|
7
|
+
// imports neither, directly or transitively. `playcanvas` itself is still loaded on demand, when
|
|
8
|
+
// the first splat is added.
|
|
9
|
+
//
|
|
10
|
+
// import { createInline3D } from '@displayxr/inline3d';
|
|
11
|
+
// import { addSplat } from '@displayxr/inline3d/splat/playcanvas';
|
|
12
|
+
//
|
|
13
|
+
// const wall = await createInline3D();
|
|
14
|
+
// const scene = addSplat(wall, canvas, 'lake.sog', { reveal: 'sweep' });
|
|
15
|
+
//
|
|
16
|
+
// Not here: measureSplatBounds (it measures a Spark mesh — import it from ./splat).
|
|
17
|
+
|
|
18
|
+
import { resolveRevealOption } from './inline3d-splat-effects.js';
|
|
19
|
+
import { CAPTURE_FITS, playcanvasCannotRead, resolveControls } from './inline3d-splat-shared.js';
|
|
20
|
+
import { addSplatDeferred } from './inline3d-splat-deferred.js';
|
|
21
|
+
|
|
22
|
+
export { applySplatPerf, SPLAT_PERF_PRESETS } from './inline3d-splat-perf.js';
|
|
23
|
+
export { readSogCamera, readSogMeta } from './inline3d-sog.js';
|
|
24
|
+
export { resolveRig } from './inline3d-splat-rig.js';
|
|
25
|
+
|
|
26
|
+
/** addSplat with `engine: 'playcanvas'`; the same validation, at the call, as ./splat's. */
|
|
27
|
+
export function addSplat(wall, canvas, src, opts = {}) {
|
|
28
|
+
if (opts.engine !== undefined && opts.engine !== 'playcanvas') {
|
|
29
|
+
throw new Error(
|
|
30
|
+
`@displayxr/inline3d/splat/playcanvas: engine "${opts.engine}" — this entry is PlayCanvas ` +
|
|
31
|
+
"only. Import addSplat from '@displayxr/inline3d/splat' for Spark.",
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
if (opts.captureFit !== undefined && !CAPTURE_FITS.includes(opts.captureFit)) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
`@displayxr/inline3d/splat: captureFit "${opts.captureFit}" — expected ` +
|
|
37
|
+
`${CAPTURE_FITS.map((f) => `'${f}'`).join(' or ')}.`,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
resolveControls(opts);
|
|
41
|
+
resolveRevealOption(opts.reveal);
|
|
42
|
+
const why = playcanvasCannotRead(src, opts);
|
|
43
|
+
if (why) throw new Error(`@displayxr/inline3d/splat: ${why}`);
|
|
44
|
+
return addSplatDeferred(wall, canvas, src, { ...opts, engine: 'playcanvas' });
|
|
45
|
+
}
|
|
@@ -1915,6 +1915,9 @@ export class PlayCanvasSplatViewer {
|
|
|
1915
1915
|
const box = this.canvas.getBoundingClientRect();
|
|
1916
1916
|
const aspect = box.height > 0 ? box.width / box.height : 1;
|
|
1917
1917
|
this.boxAspect = aspect; // setVideo's plane reads it per frame (no layout read in the draw)
|
|
1918
|
+
// and this, for its on-screen rect: a NEW object only when the size changed (the plane's rect
|
|
1919
|
+
// check compares identity, so an unchanged frame allocates nothing).
|
|
1920
|
+
if (!this.boxCss || this.boxCss.w !== (box.width || 0) || this.boxCss.h !== (box.height || 0)) this.boxCss = { w: box.width || 0, h: box.height || 0 };
|
|
1918
1921
|
if (this.mono.capture) {
|
|
1919
1922
|
captureProjection(this.mono.capture, aspect, this.mono.near, this.mono.far, this.mono.proj, this.captureFit);
|
|
1920
1923
|
if (this.captureFit !== 'height') {
|
|
@@ -3125,6 +3128,12 @@ export function attachPlayCanvasSplat(out, wall, canvas, src, opts, pending = []
|
|
|
3125
3128
|
// copy a getter's value once.
|
|
3126
3129
|
Object.defineProperty(out, 'renderScale', { get: () => viewer.effectiveRenderScale, enumerable: true, configurable: true });
|
|
3127
3130
|
Object.defineProperty(out, 'renderScaleRequested', { get: () => viewer.renderScale, enumerable: true, configurable: true });
|
|
3131
|
+
// setVideo's current element: the <video> the plane is SHOWING (not one still waiting for its first
|
|
3132
|
+
// frame), else null. Read-only. A player attached to this handle (./player attachPlayer) reads it
|
|
3133
|
+
// to notice another setVideo taking the slot, which no event reports.
|
|
3134
|
+
Object.defineProperty(out, 'videoElement', { get: () => (vid && vid.on ? vid.el : null), enumerable: true, configurable: true });
|
|
3135
|
+
// The tile's canvas, for page chrome that sits beside it (attachPlayer's controls are its siblings).
|
|
3136
|
+
Object.defineProperty(out, 'canvas', { value: canvas, enumerable: true, configurable: true });
|
|
3128
3137
|
// The current eye offset (after the unit-disc clamp), a fresh object per read.
|
|
3129
3138
|
Object.defineProperty(out, 'viewOffset', {
|
|
3130
3139
|
get: () => ({ x: viewer._viewOffset[0], y: viewer._viewOffset[1] }),
|
|
@@ -4405,7 +4414,9 @@ export function attachPlayCanvasSplat(out, wall, canvas, src, opts, pending = []
|
|
|
4405
4414
|
* handle.setVideo(src, options) — play a stereo video on THIS handle: no new canvas, layer or
|
|
4406
4415
|
* session. The splat is hidden and a screen-locked plane on the display rig shows the video, each
|
|
4407
4416
|
* eye its own half (sbs: left/right, tb: top/bottom); flat (mono), the left half at full
|
|
4408
|
-
* resolution. Applies on the first frame the video has
|
|
4417
|
+
* resolution. Applies on the first frame the video has; `transition: 'crossfade'` (with
|
|
4418
|
+
* `durationMs`, `easing`) dissolves from a video already on the plane, on the GPU, and is a cut
|
|
4419
|
+
* from the splat. Resolves to `{ video, transition, remove(), stats() }`;
|
|
4409
4420
|
* the page drives transport through `video`. setVideo(null) exits and restores the splat, the rig
|
|
4410
4421
|
* and its declaration exactly as they were. Throws during an in-flight setSource.
|
|
4411
4422
|
*/
|
|
@@ -4432,7 +4443,7 @@ export function attachPlayCanvasSplat(out, wall, canvas, src, opts, pending = []
|
|
|
4432
4443
|
if (owned ? r.autoplay !== false : r.autoplay === true) autoplay(el);
|
|
4433
4444
|
// The guard state is taken NOW (setSource / setRig refuse from this call on), the pixels change
|
|
4434
4445
|
// on the first frame the video has.
|
|
4435
|
-
const state = { seq, el, owned, format: r.format, fit: r.fit, vH: r.vH ?? bootFraming.vH, on: false, saved: null, pending: true };
|
|
4446
|
+
const state = { seq, el, owned, format: r.format, fit: r.fit, band: r.band, vH: r.vH ?? bootFraming.vH, on: false, saved: null, pending: true, rectOffs: new Set() };
|
|
4436
4447
|
if (!prev || !prev.on) vid = state; // a pending one it supersedes is cancelled above
|
|
4437
4448
|
return first
|
|
4438
4449
|
.catch(() => null)
|
|
@@ -4444,6 +4455,9 @@ export function attachPlayCanvasSplat(out, wall, canvas, src, opts, pending = []
|
|
|
4444
4455
|
// Another video on screen: keep ITS snapshot (the pre-video state), swap the source.
|
|
4445
4456
|
state.saved = live.saved;
|
|
4446
4457
|
state.on = true;
|
|
4458
|
+
// S2: the plane outlives this swap; the replaced video's rect subscriptions go with it.
|
|
4459
|
+
for (const off of live.rectOffs) off();
|
|
4460
|
+
live.rectOffs.clear();
|
|
4447
4461
|
if (live.owned && live.el !== el) releaseOwned(live.el);
|
|
4448
4462
|
if (state.vH !== live.vH) {
|
|
4449
4463
|
viewer.vH = state.vH;
|
|
@@ -4454,13 +4468,42 @@ export function attachPlayCanvasSplat(out, wall, canvas, src, opts, pending = []
|
|
|
4454
4468
|
} else enterVideo(state);
|
|
4455
4469
|
vid = state;
|
|
4456
4470
|
state.pending = false;
|
|
4471
|
+
// A crossfade only dissolves between two videos: from the splat (entry) it is a cut.
|
|
4472
|
+
const fade = live && r.transition.type === 'crossfade' && r.transition.durationMs > 0 ? r.transition : null;
|
|
4457
4473
|
viewer._videoPlane ||= new VideoPlane(viewer);
|
|
4458
|
-
viewer._videoPlane.setSource(el, { format: state.format, fit: state.fit, vH: state.vH });
|
|
4474
|
+
const faded = viewer._videoPlane.setSource(el, { format: state.format, fit: state.fit, vH: state.vH, band: state.band }, fade);
|
|
4459
4475
|
const plane = viewer._videoPlane;
|
|
4460
4476
|
return Object.freeze({
|
|
4461
4477
|
video: el,
|
|
4462
4478
|
format: state.format,
|
|
4463
4479
|
fit: state.fit,
|
|
4480
|
+
/** The letterbox band's aspect, or null. */
|
|
4481
|
+
band: state.band,
|
|
4482
|
+
/**
|
|
4483
|
+
* Where the picture is on screen, in CSS px from the canvas's top-left, clipped to the
|
|
4484
|
+
* canvas: `{ x, y, width, height }`. The bars are the canvas minus this. Null once
|
|
4485
|
+
* another setVideo replaced this one, or before the first drawn frame.
|
|
4486
|
+
*/
|
|
4487
|
+
get rect() {
|
|
4488
|
+
return vid === state && viewer._videoPlane === plane ? plane.rect : null;
|
|
4489
|
+
},
|
|
4490
|
+
/** Called with the new rect whenever it changes (resize, band, a new size). Returns an unsubscribe. */
|
|
4491
|
+
onRectChange(cb) {
|
|
4492
|
+
if (typeof cb !== 'function') throw new TypeError('@displayxr/inline3d/splat: onRectChange(cb) — expected a function.');
|
|
4493
|
+
if (vid !== state) return () => {};
|
|
4494
|
+
// S2: never outlives this video — every subscription is dropped at the swap that
|
|
4495
|
+
// replaces it (and with the plane on setVideo(null)).
|
|
4496
|
+
// Each subscription is its own entry on the plane, so calling this twice is harmless.
|
|
4497
|
+
const off = plane.onRect(cb);
|
|
4498
|
+
const unsubscribe = () => {
|
|
4499
|
+
off();
|
|
4500
|
+
state.rectOffs.delete(unsubscribe);
|
|
4501
|
+
};
|
|
4502
|
+
state.rectOffs.add(unsubscribe);
|
|
4503
|
+
return unsubscribe;
|
|
4504
|
+
},
|
|
4505
|
+
/** What this swap did: 'crossfade' only when a fade actually started, else 'cut'. */
|
|
4506
|
+
transition: faded ? 'crossfade' : 'cut',
|
|
4464
4507
|
/** Exit (setVideo(null)) — a no-op once another setVideo replaced this one. */
|
|
4465
4508
|
remove: () => (vid === state ? setVideo(null) : Promise.resolve(null)),
|
|
4466
4509
|
/** Upload accounting: frames drawn with the plane up, and texture uploads (new frames). */
|