@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
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// inline3d-buffer-limit.js — keep a woven canvas's backing store inside the device's GL limits.
|
|
2
|
+
//
|
|
3
|
+
// Internal to the SDK (core, ./viewer, ./splat on both engines, ./model on both engines). Not a
|
|
4
|
+
// public entry point; renderer-free and dependency-free, so the core can import it.
|
|
5
|
+
//
|
|
6
|
+
// WHY (measured, an Android 3D tablet on Adreno 740, DisplayXR Browser 154): a full-screen SBS
|
|
7
|
+
// canvas asked for 5120×1348 (2560 per eye) with renderScale 1. The browser reports
|
|
8
|
+
// MAX_TEXTURE_SIZE 4096 there (Chromium's `webgl_or_caps_max_texture_size_limit_4096` driver
|
|
9
|
+
// workaround) and SILENTLY clamps the WebGL drawing buffer to 4096×1348, while `canvas.width` still
|
|
10
|
+
// reads 5120. `XRDisplayLayer.getViewport()` splits `canvas.width` (the attribute), so each eye was
|
|
11
|
+
// drawn 2560 wide into a 4096-wide buffer: the eye boundary landed at 62.5% of the buffer while the
|
|
12
|
+
// weave splits it at 50%, and the panel showed a large double image. Nothing was logged. Windows
|
|
13
|
+
// and Linux report 16384, so the same page was fine there.
|
|
14
|
+
//
|
|
15
|
+
// Two layers of defence, both here:
|
|
16
|
+
// 1. clampEyeBuffer(): shrink the requested per-eye size, UNIFORMLY (the eye aspect is kept), so
|
|
17
|
+
// that the whole store (cols × eyeW by rows × eyeH) fits min(MAX_TEXTURE_SIZE,
|
|
18
|
+
// MAX_RENDERBUFFER_SIZE, MAX_VIEWPORT_DIMS). The browser then never has to clamp.
|
|
19
|
+
// 2. bufferScale(): if the drawing buffer STILL differs from canvas.width/height after a resize
|
|
20
|
+
// (a clamp we did not predict), the caller maps the layer's canvas-space viewports onto the
|
|
21
|
+
// real drawing buffer with these factors — never canvas.width / 2.
|
|
22
|
+
|
|
23
|
+
/** @typedef {{ maxW: number, maxH: number, nameW: string, nameH: string, valueW: number, valueH: number }} BufferLimits */
|
|
24
|
+
|
|
25
|
+
const cache = new WeakMap(); // gl context → BufferLimits
|
|
26
|
+
let probed; // undefined = not tried; null = no WebGL available; else BufferLimits
|
|
27
|
+
|
|
28
|
+
/** Smallest of the named candidates, with its name (the first one wins a tie). */
|
|
29
|
+
function smallest(cands) {
|
|
30
|
+
let best = null;
|
|
31
|
+
for (const [name, v] of cands) {
|
|
32
|
+
if (!(v > 0)) continue;
|
|
33
|
+
if (!best || v < best[1]) best = [name, v];
|
|
34
|
+
}
|
|
35
|
+
return best;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The largest backing store this GL context can hold and render into, queried once per context
|
|
40
|
+
* and cached. Returns null for anything that is not a GL context (or a context lost mid-query).
|
|
41
|
+
*
|
|
42
|
+
* @param {WebGLRenderingContext|WebGL2RenderingContext|null|undefined} gl
|
|
43
|
+
* @returns {BufferLimits|null}
|
|
44
|
+
*/
|
|
45
|
+
export function glBufferLimits(gl) {
|
|
46
|
+
if (!gl || typeof gl.getParameter !== 'function') return null;
|
|
47
|
+
const hit = cache.get(gl);
|
|
48
|
+
if (hit) return hit;
|
|
49
|
+
let tex, rb, vp;
|
|
50
|
+
try {
|
|
51
|
+
tex = gl.getParameter(gl.MAX_TEXTURE_SIZE ?? 0x0d33);
|
|
52
|
+
rb = gl.getParameter(gl.MAX_RENDERBUFFER_SIZE ?? 0x84e8);
|
|
53
|
+
vp = gl.getParameter(gl.MAX_VIEWPORT_DIMS ?? 0x0d3a);
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
const vpW = vp && vp.length >= 2 ? vp[0] : 0;
|
|
58
|
+
const vpH = vp && vp.length >= 2 ? vp[1] : 0;
|
|
59
|
+
const w = smallest([['MAX_TEXTURE_SIZE', tex], ['MAX_RENDERBUFFER_SIZE', rb], ['MAX_VIEWPORT_DIMS', vpW]]);
|
|
60
|
+
const h = smallest([['MAX_TEXTURE_SIZE', tex], ['MAX_RENDERBUFFER_SIZE', rb], ['MAX_VIEWPORT_DIMS', vpH]]);
|
|
61
|
+
if (!w || !h) return null; // a lost context answers 0/null — don't cache a non-answer
|
|
62
|
+
const lim = { maxW: w[1], maxH: h[1], nameW: w[0], nameH: h[0], valueW: w[1], valueH: h[1] };
|
|
63
|
+
cache.set(gl, lim);
|
|
64
|
+
return lim;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The limits of a throwaway WebGL context, for callers without one of their own yet: the core's
|
|
69
|
+
* image/video windows (2D canvases, which the weave still has to take as a GPU texture) and the
|
|
70
|
+
* PlayCanvas viewer before its engine has booted. Probed once per document, then the context is
|
|
71
|
+
* released. Null where there is no DOM or no WebGL (tests, workers, a GPU-less browser): no
|
|
72
|
+
* clamp is applied then.
|
|
73
|
+
*
|
|
74
|
+
* @returns {BufferLimits|null}
|
|
75
|
+
*/
|
|
76
|
+
export function probeBufferLimits() {
|
|
77
|
+
if (probed !== undefined) return probed;
|
|
78
|
+
probed = null;
|
|
79
|
+
try {
|
|
80
|
+
if (typeof document === 'undefined' || !document || typeof document.createElement !== 'function') return probed;
|
|
81
|
+
const c = document.createElement('canvas');
|
|
82
|
+
const gl = c.getContext('webgl2') || c.getContext('webgl');
|
|
83
|
+
if (!gl) return probed;
|
|
84
|
+
probed = glBufferLimits(gl);
|
|
85
|
+
gl.getExtension?.('WEBGL_lose_context')?.loseContext?.();
|
|
86
|
+
} catch {
|
|
87
|
+
probed = null;
|
|
88
|
+
}
|
|
89
|
+
return probed;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Tests only: forget the probed document limits. */
|
|
93
|
+
export function _resetProbedLimits() {
|
|
94
|
+
probed = undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Fit a requested per-eye size into the device's limits. The store is `cols × eyeW` by
|
|
99
|
+
* `rows × eyeH` (SBS: cols 2, rows 1; mono: cols 1). Both axes shrink by ONE factor, so the eye
|
|
100
|
+
* keeps its aspect; the result is floored so it can never round back over the limit.
|
|
101
|
+
*
|
|
102
|
+
* @param {number} eyeW requested per-eye width, buffer px
|
|
103
|
+
* @param {number} eyeH requested per-eye height, buffer px
|
|
104
|
+
* @param {BufferLimits|null} limits
|
|
105
|
+
* @param {{ cols?: number, rows?: number }} [layout]
|
|
106
|
+
* @returns {{ eyeW: number, eyeH: number, scale: number, clamped: boolean,
|
|
107
|
+
* reqW: number, reqH: number, bufW: number, bufH: number,
|
|
108
|
+
* limitName: string, limitValue: number }}
|
|
109
|
+
*/
|
|
110
|
+
export function clampEyeBuffer(eyeW, eyeH, limits, { cols = 2, rows = 1 } = {}) {
|
|
111
|
+
const reqW = eyeW * cols;
|
|
112
|
+
const reqH = eyeH * rows;
|
|
113
|
+
const base = { eyeW, eyeH, cols, scale: 1, clamped: false, reqW, reqH, bufW: reqW, bufH: reqH, limitName: '', limitValue: 0 };
|
|
114
|
+
if (!limits || !(eyeW > 0) || !(eyeH > 0)) return base;
|
|
115
|
+
const sw = limits.maxW / reqW;
|
|
116
|
+
const sh = limits.maxH / reqH;
|
|
117
|
+
const s = Math.min(sw, sh);
|
|
118
|
+
if (!(s < 1)) return base;
|
|
119
|
+
const byW = sw <= sh;
|
|
120
|
+
// Floor (never round up past the limit), and never above the per-axis cap either way.
|
|
121
|
+
const w = Math.max(1, Math.min(Math.floor(eyeW * s + 1e-9), Math.floor(limits.maxW / cols)));
|
|
122
|
+
const h = Math.max(1, Math.min(Math.floor(eyeH * s + 1e-9), Math.floor(limits.maxH / rows)));
|
|
123
|
+
return {
|
|
124
|
+
eyeW: w,
|
|
125
|
+
eyeH: h,
|
|
126
|
+
cols,
|
|
127
|
+
scale: s,
|
|
128
|
+
clamped: true,
|
|
129
|
+
reqW,
|
|
130
|
+
reqH,
|
|
131
|
+
bufW: w * cols,
|
|
132
|
+
bufH: h * rows,
|
|
133
|
+
limitName: byW ? limits.nameW : limits.nameH,
|
|
134
|
+
limitValue: byW ? limits.valueW : limits.valueH,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Round a scale for a human-readable warning: 0.8, 0.625, never 0.8000000001. */
|
|
139
|
+
export function roundScale(s) {
|
|
140
|
+
return Math.round(s * 1000) / 1000;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* The one warning text, so every path names the same things: the store it wanted, the limit, what
|
|
145
|
+
* it rendered at, and (where there is one) the renderScale request vs the effective value.
|
|
146
|
+
*/
|
|
147
|
+
export function clampWarning(tag, c, requestedScale) {
|
|
148
|
+
const rs =
|
|
149
|
+
typeof requestedScale === 'number'
|
|
150
|
+
? ` (renderScale ${roundScale(requestedScale)} → ${roundScale(requestedScale * c.scale)})`
|
|
151
|
+
: ` (scale ${roundScale(c.scale)})`;
|
|
152
|
+
return (
|
|
153
|
+
`${tag} ${c.cols === 1 ? 'buffer' : 'SBS buffer'} ${c.reqW}×${c.reqH} exceeds ${c.limitName} ${c.limitValue} on this device; ` +
|
|
154
|
+
`rendering at ${c.bufW}×${c.bufH}${rs}.`
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* How the layer's viewports (canvas-attribute space: XRDisplayLayer.getViewport() splits
|
|
160
|
+
* `canvas.width`) map onto the drawing buffer the GL context really has. {1, 1, false} whenever
|
|
161
|
+
* the two agree, which is every case the clamp above predicted.
|
|
162
|
+
*
|
|
163
|
+
* @param {{ width: number, height: number }} canvas
|
|
164
|
+
* @param {object|null} gl a WebGL context, or null (a 2D canvas: no separate drawing buffer)
|
|
165
|
+
*/
|
|
166
|
+
export function bufferScale(canvas, gl) {
|
|
167
|
+
const cw = canvas?.width || 0;
|
|
168
|
+
const ch = canvas?.height || 0;
|
|
169
|
+
const dw = gl && gl.drawingBufferWidth > 0 ? gl.drawingBufferWidth : cw;
|
|
170
|
+
const dh = gl && gl.drawingBufferHeight > 0 ? gl.drawingBufferHeight : ch;
|
|
171
|
+
const sx = cw > 0 ? dw / cw : 1;
|
|
172
|
+
const sy = ch > 0 ? dh / ch : 1;
|
|
173
|
+
return { sx, sy, w: dw, h: dh, mismatch: sx !== 1 || sy !== 1 };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Scale one {x, y, width, height} viewport by bufferScale()'s factors (identity returns it as is). */
|
|
177
|
+
export function scaleViewport(vp, sx, sy) {
|
|
178
|
+
if (sx === 1 && sy === 1) return vp;
|
|
179
|
+
return {
|
|
180
|
+
x: Math.round(vp.x * sx),
|
|
181
|
+
y: Math.round(vp.y * sy),
|
|
182
|
+
width: Math.max(1, Math.round(vp.width * sx)),
|
|
183
|
+
height: Math.max(1, Math.round(vp.height * sy)),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** The warning for a drawing buffer the clamp did not predict. `tail` says what happens next. */
|
|
188
|
+
export function mismatchWarning(
|
|
189
|
+
tag,
|
|
190
|
+
canvas,
|
|
191
|
+
b,
|
|
192
|
+
tail = 'laying the eyes out from the drawing buffer. Lower renderScale to stay inside the device limit.',
|
|
193
|
+
) {
|
|
194
|
+
return (
|
|
195
|
+
`${tag} the browser clamped this canvas's drawing buffer to ${b.w}×${b.h} (canvas.width/height ` +
|
|
196
|
+
`say ${canvas.width}×${canvas.height}); ${tail}`
|
|
197
|
+
);
|
|
198
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// inline3d-call-full.js — `@displayxr/inline3d/call/full`: the call entry with 2D→3D bundled.
|
|
2
|
+
//
|
|
3
|
+
// PREVIEW tier (docs/sdk-stability.md). RFC 0003 §1 "Lift under bundlers", mechanism 2.
|
|
4
|
+
//
|
|
5
|
+
// import { mountCall } from '@displayxr/inline3d/call/full';
|
|
6
|
+
// const call = await mountCall(el); // 2D participants are lifted to 3D on a 3D display
|
|
7
|
+
//
|
|
8
|
+
// The plain `./call` entry finds the lift module through a COMPUTED import, which a bundler cannot
|
|
9
|
+
// follow: a Vite/webpack/esbuild build of it resolves the specifier against the chunk URL, 404s,
|
|
10
|
+
// and — since 1.29 — says so (`warning { code: 'lift-not-bundled' }`) rather than staying flat in
|
|
11
|
+
// silence. This entry is the fix for the common case: it imports lift STATICALLY, so a bundler
|
|
12
|
+
// sees it, code-splits it, and ships it; `mono3D` defaults to that `lift`. Pages that do not want
|
|
13
|
+
// the depth-model plumbing keep importing `./call`. A page with its own `mono3D` (a function, or
|
|
14
|
+
// `'off'`) is left alone.
|
|
15
|
+
//
|
|
16
|
+
// Everything else — `addCall`, `dxrSignaling`, the types, the `<dxr-call>` registration — is the
|
|
17
|
+
// `./call` entry re-exported, and `<dxr-call>` is pointed at THIS `mountCall`, so importing this
|
|
18
|
+
// entry makes every element on the page lift too.
|
|
19
|
+
//
|
|
20
|
+
// Until `./lift` (js/lift/) lands on main, that path holds a PLACEHOLDER exporting no `lift`;
|
|
21
|
+
// this entry then behaves exactly like `./call` (`liftBundled === false`, and the same warning
|
|
22
|
+
// fires). The import is `* as`, so the day the real module replaces the placeholder nothing here
|
|
23
|
+
// changes.
|
|
24
|
+
|
|
25
|
+
import * as liftMod from './lift/index.js';
|
|
26
|
+
import { mountCall as mountCallBase, addCall as addCallBase, DxrCallElement } from './inline3d-call.js';
|
|
27
|
+
|
|
28
|
+
export * from './inline3d-call.js';
|
|
29
|
+
|
|
30
|
+
const lift = typeof liftMod.lift === 'function' ? liftMod.lift : null;
|
|
31
|
+
|
|
32
|
+
/** True when this copy of the SDK carries the lift module (false on a build before `./lift` landed). */
|
|
33
|
+
export const liftBundled = lift !== null;
|
|
34
|
+
|
|
35
|
+
const withLift = (opts) => (lift && (!opts || opts.mono3D === undefined) ? { ...(opts || {}), mono3D: lift } : opts || {});
|
|
36
|
+
|
|
37
|
+
/** `mountCall` of `./call`, with `mono3D` defaulted to the statically imported `lift`. */
|
|
38
|
+
export function mountCall(el, opts) {
|
|
39
|
+
return mountCallBase(el, withLift(opts));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** `addCall` of `./call`, with `mono3D` defaulted to the statically imported `lift`. */
|
|
43
|
+
export function addCall(wall, container, opts) {
|
|
44
|
+
return addCallBase(wall, container, withLift(opts));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// `<dxr-call>` on a page that imported this entry lifts too (the element's mount seam).
|
|
48
|
+
DxrCallElement.mount = mountCall;
|