@displayxr/inline3d 1.27.0 → 1.29.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 +224 -0
- package/README.md +6 -3
- package/call-full.d.ts +21 -0
- package/call.d.ts +454 -0
- package/dist/call.js +81 -0
- package/index.d.ts +34 -2
- package/js/call/capture.js +212 -0
- package/js/call/disparity.js +432 -0
- package/js/call/element.js +217 -0
- package/js/call/lift.js +323 -0
- package/js/call/qr.js +283 -0
- package/js/call/sdp.js +86 -0
- package/js/call/signaling.js +203 -0
- package/js/call/transport.js +589 -0
- package/js/call/ui.js +146 -0
- package/js/call/wire.js +434 -0
- package/js/inline3d-buffer-limit.js +198 -0
- package/js/inline3d-call-full.js +48 -0
- package/js/inline3d-call.js +1901 -0
- package/js/inline3d-model-playcanvas.js +6 -0
- package/js/inline3d-model.js +6 -0
- package/js/inline3d-player.js +649 -218
- package/js/inline3d-splat-playcanvas.js +134 -18
- package/js/inline3d-splat-video.js +328 -29
- package/js/inline3d-splat.js +5 -0
- package/js/inline3d-viewer.js +86 -10
- package/js/inline3d.js +257 -15
- package/js/lift/index.js +15 -0
- package/model.d.ts +10 -1
- package/package.json +29 -4
- package/player.d.ts +49 -4
- package/splat.d.ts +52 -3
- package/viewer.d.ts +12 -1
|
@@ -71,6 +71,14 @@ import { VideoPlane, validateSetVideo, PAGE_VIDEO_ERROR, eyeSplit, EYE_SPLIT_UNI
|
|
|
71
71
|
import { LayerRigCameras, validateLayerRig, validateLayerRigOptions, DEFAULT_VIEWER_DISTANCE_M } from './inline3d-splat-layer-rig.js';
|
|
72
72
|
import { RigTracker, remapViews, nodePose, sameRig } from './inline3d-splat-rig-map.js';
|
|
73
73
|
import { resolveDiag, DiagRecorder, startDiagLoop, registerDiag, DIAG_SWITCHES } from './inline3d-splat-diag.js';
|
|
74
|
+
import {
|
|
75
|
+
glBufferLimits,
|
|
76
|
+
probeBufferLimits,
|
|
77
|
+
clampEyeBuffer,
|
|
78
|
+
clampWarning,
|
|
79
|
+
bufferScale,
|
|
80
|
+
mismatchWarning,
|
|
81
|
+
} from './inline3d-buffer-limit.js';
|
|
74
82
|
import {
|
|
75
83
|
clamp,
|
|
76
84
|
finite,
|
|
@@ -890,6 +898,13 @@ export class PlayCanvasSplatViewer {
|
|
|
890
898
|
this.depthLimit = depthLimit;
|
|
891
899
|
this.fitSweep = fitSweep;
|
|
892
900
|
this.renderScale = renderScale;
|
|
901
|
+
// The device-limit clamp on the backing store (./inline3d-buffer-limit.js), as SceneViewer:
|
|
902
|
+
// the factor the last _resize applied on top of renderScale; warned once per viewer.
|
|
903
|
+
this._bufClamp = 1;
|
|
904
|
+
this._warnedBufClamp = false;
|
|
905
|
+
this._warnedBufMismatch = false;
|
|
906
|
+
/** Prefix for this viewer's warnings; ./model sets its own. */
|
|
907
|
+
this.logTag = opts.logTag || '[inline3d/splat]';
|
|
893
908
|
this.pitchLimit = pitchLimit;
|
|
894
909
|
this.idleSpin = this.pageCamera ? 0 : idleSpin;
|
|
895
910
|
this.flipY = opts.flipY !== false;
|
|
@@ -1320,6 +1335,9 @@ export class PlayCanvasSplatViewer {
|
|
|
1320
1335
|
// The SDK's frame drives the engine: no second rAF. `tick` is the engine's own loop body.
|
|
1321
1336
|
app.requestAnimationFrame = () => {};
|
|
1322
1337
|
this.app = app;
|
|
1338
|
+
// The store may have been sized before the engine existed (from the probed limits, or none):
|
|
1339
|
+
// re-check it against THIS context's. A no-op when nothing moves.
|
|
1340
|
+
this._scheduleResize();
|
|
1323
1341
|
|
|
1324
1342
|
// Footprint fix (§ patchGsplatFootprint) and the perf quad-extent cap, both as chunk
|
|
1325
1343
|
// overrides. The gsplat chunks are registered by GSplatComponentSystem during init, so
|
|
@@ -1897,6 +1915,9 @@ export class PlayCanvasSplatViewer {
|
|
|
1897
1915
|
const box = this.canvas.getBoundingClientRect();
|
|
1898
1916
|
const aspect = box.height > 0 ? box.width / box.height : 1;
|
|
1899
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 };
|
|
1900
1921
|
if (this.mono.capture) {
|
|
1901
1922
|
captureProjection(this.mono.capture, aspect, this.mono.near, this.mono.far, this.mono.proj, this.captureFit);
|
|
1902
1923
|
if (this.captureFit !== 'height') {
|
|
@@ -1943,8 +1964,9 @@ export class PlayCanvasSplatViewer {
|
|
|
1943
1964
|
const v = this._monoView();
|
|
1944
1965
|
e.proj = v.proj;
|
|
1945
1966
|
e.pose = v.pose;
|
|
1946
|
-
|
|
1947
|
-
e.
|
|
1967
|
+
const b = this._bufScale();
|
|
1968
|
+
e.width = b.w || c.width;
|
|
1969
|
+
e.height = b.h || c.height;
|
|
1948
1970
|
this._drawEntries(this._monoEntry, null);
|
|
1949
1971
|
}
|
|
1950
1972
|
|
|
@@ -1956,9 +1978,7 @@ export class PlayCanvasSplatViewer {
|
|
|
1956
1978
|
const app = this.app;
|
|
1957
1979
|
if (!app || !this.pc) return false;
|
|
1958
1980
|
const pc = this.pc;
|
|
1959
|
-
const
|
|
1960
|
-
const sx = cache && cache.bufW > 0 && el.width ? el.width / cache.bufW : 1;
|
|
1961
|
-
const sy = cache && cache.bufH > 0 && el.height ? el.height / cache.bufH : 1;
|
|
1981
|
+
const { b, sx, sy } = this._entryScale(cache);
|
|
1962
1982
|
if (this.pageCamera) {
|
|
1963
1983
|
// The page's near/far join the caller's nearClip/farClip as a floor and a cap: only the
|
|
1964
1984
|
// depth mapping moves, the runtime's frustum (fov, skew, principal point) stays untouched.
|
|
@@ -2033,8 +2053,8 @@ export class PlayCanvasSplatViewer {
|
|
|
2033
2053
|
this._budgetViews = entries.length;
|
|
2034
2054
|
this._applyBudget();
|
|
2035
2055
|
}
|
|
2036
|
-
const W =
|
|
2037
|
-
const H =
|
|
2056
|
+
const W = b.w || 1;
|
|
2057
|
+
const H = b.h || 1;
|
|
2038
2058
|
for (let i = 0; i < entries.length; i++) {
|
|
2039
2059
|
const e = entries[i];
|
|
2040
2060
|
const cam = cams[i];
|
|
@@ -2120,18 +2140,68 @@ export class PlayCanvasSplatViewer {
|
|
|
2120
2140
|
const box = this.canvas.getBoundingClientRect();
|
|
2121
2141
|
if (box.width < 1 || box.height < 1) return;
|
|
2122
2142
|
const dpr = Math.min((typeof window !== 'undefined' && window.devicePixelRatio) || 1, 2) * this.renderScale;
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2143
|
+
// Clamp to the device's GL limits BEFORE sizing (./inline3d-buffer-limit.js). Measured on an
|
|
2144
|
+
// Adreno 740 tablet: MAX_TEXTURE_SIZE 4096, a 5120-wide request silently became a 4096-wide
|
|
2145
|
+
// drawing buffer, getViewport() still split 5120, and the eye boundary sat at 62.5%.
|
|
2146
|
+
const c = clampEyeBuffer(
|
|
2147
|
+
Math.max(1, Math.round(box.width * dpr)),
|
|
2148
|
+
Math.max(1, Math.round(box.height * dpr)),
|
|
2149
|
+
glBufferLimits(this.app?.graphicsDevice?.gl) || probeBufferLimits(),
|
|
2150
|
+
{ cols: this._mode === 'mono' ? 1 : 2 },
|
|
2151
|
+
);
|
|
2152
|
+
this._noteClamp(c);
|
|
2153
|
+
const bufW = c.bufW;
|
|
2154
|
+
const h = c.bufH;
|
|
2126
2155
|
this._updateMonoProjection();
|
|
2127
2156
|
const el = this.canvas;
|
|
2128
|
-
if (el.width === bufW && el.height === h)
|
|
2157
|
+
if (el.width === bufW && el.height === h) {
|
|
2158
|
+
this._bufScale(); // unchanged; still name a browser-side clamp (once)
|
|
2159
|
+
return;
|
|
2160
|
+
}
|
|
2129
2161
|
el.width = bufW;
|
|
2130
2162
|
el.height = h;
|
|
2163
|
+
this._bufScale(); // warns once if the browser clamped anyway
|
|
2131
2164
|
if (this._mode === 'mono') this._drawMono();
|
|
2132
2165
|
else this._replayLastGood();
|
|
2133
2166
|
}
|
|
2134
2167
|
|
|
2168
|
+
/** The renderScale in force: the request times the device-limit clamp (SceneViewer's twin). */
|
|
2169
|
+
get effectiveRenderScale() {
|
|
2170
|
+
return this.renderScale * this._bufClamp;
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
_noteClamp(c) {
|
|
2174
|
+
this._bufClamp = c.scale;
|
|
2175
|
+
if (c.clamped && !this._warnedBufClamp) {
|
|
2176
|
+
this._warnedBufClamp = true;
|
|
2177
|
+
console.warn(clampWarning(this.logTag, c, this.renderScale));
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
/**
|
|
2182
|
+
* How a frame's entries map into the buffer. Entries are in canvas-attribute px (getViewport()
|
|
2183
|
+
* splits canvas.width), cached with that size; they are drawn into the REAL drawing buffer.
|
|
2184
|
+
* Identity unless a resize happened since the cache or the browser clamped the buffer behind
|
|
2185
|
+
* the SDK (the device-limit clamp in _resize normally prevents that).
|
|
2186
|
+
*/
|
|
2187
|
+
_entryScale(cache) {
|
|
2188
|
+
const b = this._bufScale();
|
|
2189
|
+
const sx = cache && cache.bufW > 0 && b.w ? b.w / cache.bufW : 1;
|
|
2190
|
+
const sy = cache && cache.bufH > 0 && b.h ? b.h / cache.bufH : 1;
|
|
2191
|
+
return { b, sx, sy };
|
|
2192
|
+
}
|
|
2193
|
+
|
|
2194
|
+
/** The real drawing buffer vs canvas.width/height; warns once per viewer on a mismatch. */
|
|
2195
|
+
_bufScale() {
|
|
2196
|
+
const el = this.canvas;
|
|
2197
|
+
const b = bufferScale(el, this.app?.graphicsDevice?.gl || null);
|
|
2198
|
+
if (b.mismatch && !this._warnedBufMismatch) {
|
|
2199
|
+
this._warnedBufMismatch = true;
|
|
2200
|
+
console.warn(mismatchWarning(this.logTag, el, b));
|
|
2201
|
+
}
|
|
2202
|
+
return b;
|
|
2203
|
+
}
|
|
2204
|
+
|
|
2135
2205
|
_tick() {
|
|
2136
2206
|
const t = now();
|
|
2137
2207
|
const dt = this._lastTick ? Math.min((t - this._lastTick) / 1000, MAX_DT_S) : 0;
|
|
@@ -3051,9 +3121,19 @@ export function attachPlayCanvasSplat(out, wall, canvas, src, opts, pending = []
|
|
|
3051
3121
|
let cancelPendingVideo = null;
|
|
3052
3122
|
let warnedAutoplayMuted = false;
|
|
3053
3123
|
|
|
3054
|
-
// The
|
|
3055
|
-
//
|
|
3056
|
-
|
|
3124
|
+
// The per-eye buffer scale IN FORCE: the request (the `renderScale` option, or the last
|
|
3125
|
+
// setRenderScale) times the device-limit clamp — e.g. 0.8 for a request of 1 on a device whose
|
|
3126
|
+
// MAX_TEXTURE_SIZE cannot hold the full SBS store (./inline3d-buffer-limit.js).
|
|
3127
|
+
// `renderScaleRequested` is the request as given. Real accessors: Object.assign below would
|
|
3128
|
+
// copy a getter's value once.
|
|
3129
|
+
Object.defineProperty(out, 'renderScale', { get: () => viewer.effectiveRenderScale, enumerable: true, configurable: true });
|
|
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 });
|
|
3057
3137
|
// The current eye offset (after the unit-disc clamp), a fresh object per read.
|
|
3058
3138
|
Object.defineProperty(out, 'viewOffset', {
|
|
3059
3139
|
get: () => ({ x: viewer._viewOffset[0], y: viewer._viewOffset[1] }),
|
|
@@ -3147,7 +3227,9 @@ export function attachPlayCanvasSplat(out, wall, canvas, src, opts, pending = []
|
|
|
3147
3227
|
/**
|
|
3148
3228
|
* Change the per-eye buffer scale live (the `renderScale` option): e.g. full resolution on a
|
|
3149
3229
|
* screen that hides the splat and draws only stage quads, 0.6 elsewhere. The backing store is
|
|
3150
|
-
* resized on the next animation frame (never mid-frame).
|
|
3230
|
+
* resized on the next animation frame (never mid-frame). The request is kept as given
|
|
3231
|
+
* (`renderScaleRequested`); the store is still clamped to the device's GL limits, and
|
|
3232
|
+
* `renderScale` reports what is in force. Returns the handle.
|
|
3151
3233
|
*/
|
|
3152
3234
|
setRenderScale(s) {
|
|
3153
3235
|
if (typeof s !== 'number' || !Number.isFinite(s) || s <= 0 || s > 4) {
|
|
@@ -4332,7 +4414,9 @@ export function attachPlayCanvasSplat(out, wall, canvas, src, opts, pending = []
|
|
|
4332
4414
|
* handle.setVideo(src, options) — play a stereo video on THIS handle: no new canvas, layer or
|
|
4333
4415
|
* session. The splat is hidden and a screen-locked plane on the display rig shows the video, each
|
|
4334
4416
|
* eye its own half (sbs: left/right, tb: top/bottom); flat (mono), the left half at full
|
|
4335
|
-
* 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() }`;
|
|
4336
4420
|
* the page drives transport through `video`. setVideo(null) exits and restores the splat, the rig
|
|
4337
4421
|
* and its declaration exactly as they were. Throws during an in-flight setSource.
|
|
4338
4422
|
*/
|
|
@@ -4359,7 +4443,7 @@ export function attachPlayCanvasSplat(out, wall, canvas, src, opts, pending = []
|
|
|
4359
4443
|
if (owned ? r.autoplay !== false : r.autoplay === true) autoplay(el);
|
|
4360
4444
|
// The guard state is taken NOW (setSource / setRig refuse from this call on), the pixels change
|
|
4361
4445
|
// on the first frame the video has.
|
|
4362
|
-
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() };
|
|
4363
4447
|
if (!prev || !prev.on) vid = state; // a pending one it supersedes is cancelled above
|
|
4364
4448
|
return first
|
|
4365
4449
|
.catch(() => null)
|
|
@@ -4371,6 +4455,9 @@ export function attachPlayCanvasSplat(out, wall, canvas, src, opts, pending = []
|
|
|
4371
4455
|
// Another video on screen: keep ITS snapshot (the pre-video state), swap the source.
|
|
4372
4456
|
state.saved = live.saved;
|
|
4373
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();
|
|
4374
4461
|
if (live.owned && live.el !== el) releaseOwned(live.el);
|
|
4375
4462
|
if (state.vH !== live.vH) {
|
|
4376
4463
|
viewer.vH = state.vH;
|
|
@@ -4381,13 +4468,42 @@ export function attachPlayCanvasSplat(out, wall, canvas, src, opts, pending = []
|
|
|
4381
4468
|
} else enterVideo(state);
|
|
4382
4469
|
vid = state;
|
|
4383
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;
|
|
4384
4473
|
viewer._videoPlane ||= new VideoPlane(viewer);
|
|
4385
|
-
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);
|
|
4386
4475
|
const plane = viewer._videoPlane;
|
|
4387
4476
|
return Object.freeze({
|
|
4388
4477
|
video: el,
|
|
4389
4478
|
format: state.format,
|
|
4390
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',
|
|
4391
4507
|
/** Exit (setVideo(null)) — a no-op once another setVideo replaced this one. */
|
|
4392
4508
|
remove: () => (vid === state ? setVideo(null) : Promise.resolve(null)),
|
|
4393
4509
|
/** Upload accounting: frames drawn with the plane up, and texture uploads (new frames). */
|