@displayxr/inline3d 1.26.0 → 1.28.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/js/inline3d.js CHANGED
@@ -35,6 +35,10 @@ export { undock, undockAvailable, undockUrl, tileScreenRect };
35
35
  // The eased 2D<->3D transition. A pure state machine (no DOM, no WebXR) ported from the native
36
36
  // `dxr::ModeSwitch`, so the browser eases the disparity around a mode switch the same way — and in
37
37
  // the same ORDER — as the native apps and the demos. See _requestRenderingModeEased.
38
+ // The device-limit clamp on a backing store (MAX_TEXTURE_SIZE & co.), shared with ./viewer,
39
+ // ./splat and ./model. Dependency-free.
40
+ import { probeBufferLimits, clampEyeBuffer, clampWarning, bufferScale, mismatchWarning } from './inline3d-buffer-limit.js';
41
+
38
42
  import {
39
43
  ModeSwitch,
40
44
  MODE_SWITCH_DEFAULT_DURATION_MS,
@@ -2340,17 +2344,108 @@ class Inline3D {
2340
2344
  win.autoExcluded.clear();
2341
2345
  }
2342
2346
 
2343
- /** The per-eye buffer size this window should have right now (explicit, or box × dpr). */
2344
- _eyeSize(win) {
2347
+ /**
2348
+ * The per-eye buffer size this window should have right now: explicit, or box × dpr — capped
2349
+ * at the SOURCE's own per-eye resolution for image and video windows.
2350
+ *
2351
+ * WHY THE CAP (measured, NP02J tablet, DisplayXR Browser 1.0.6): the player sample's 90vw
2352
+ * tile at dpr 2 asked for a 4608×1296 backing store for a 640×360-per-eye clip — 13× the
2353
+ * source's pixels, re-drawn every frame. The page fell to 25 fps and dropped 47 of 97 video
2354
+ * frames; the same clip in a 45vw tile (2304×648) ran at 57 fps with 0 dropped. Drawing a
2355
+ * source into a buffer larger than itself adds no detail: the compositor scales the layer to
2356
+ * the box either way, so upscaling here only spends GPU fill-rate. The cap keeps the BOX
2357
+ * aspect (the eyes stay correctly shaped) and only shrinks, never grows past box × dpr.
2358
+ * An explicit {width, height} is the page's call and is never capped by the source.
2359
+ *
2360
+ * THEN THE DEVICE LIMIT, on every window including an explicit size: the whole store (2 × eye
2361
+ * wide in SBS) must fit min(MAX_TEXTURE_SIZE, MAX_RENDERBUFFER_SIZE, MAX_VIEWPORT_DIMS), or
2362
+ * the weave cannot take it as one texture. An Android 3D tablet (Adreno 740) reports 4096 where
2363
+ * desktops report 16384. Both axes shrink by one factor (./inline3d-buffer-limit.js), warned
2364
+ * once per window. `win.bufScale` records the total shrink so buffer-px decoration
2365
+ * (cornerRadius, feather) keeps its on-screen size.
2366
+ */
2367
+ _eyeSize(win, sbs = true) {
2345
2368
  const dpr = Math.min(window.devicePixelRatio || 1, 2);
2346
- return {
2347
- w: win.reqW || Math.round((win.canvas.clientWidth || 256) * dpr),
2348
- h: win.reqH || Math.round((win.canvas.clientHeight || 256) * dpr),
2349
- };
2369
+ let w = win.reqW || Math.round((win.canvas.clientWidth || 256) * dpr);
2370
+ let h = win.reqH || Math.round((win.canvas.clientHeight || 256) * dpr);
2371
+ let scale = 1;
2372
+ const eye = !win.reqW && !win.reqH && this._sourceEyeSize(win);
2373
+ if (eye) {
2374
+ // The smallest buffer that still holds every source pixel along the axis that fills the box.
2375
+ const s = Math.max(eye.w / w, eye.h / h);
2376
+ if (s < 1) {
2377
+ scale = s;
2378
+ w = Math.max(1, Math.round(w * s));
2379
+ h = Math.max(1, Math.round(h * s));
2380
+ }
2381
+ }
2382
+ const c = clampEyeBuffer(w, h, probeBufferLimits(), { cols: sbs ? 2 : 1 });
2383
+ if (c.clamped) {
2384
+ scale *= c.scale;
2385
+ w = c.eyeW;
2386
+ h = c.eyeH;
2387
+ if (!win.warnedBufClamp) {
2388
+ win.warnedBufClamp = true;
2389
+ console.warn(clampWarning('[inline3d]', c));
2390
+ }
2391
+ }
2392
+ win.bufScale = scale;
2393
+ return { w, h };
2394
+ }
2395
+
2396
+ /** One eye of an image/video window's source (an SBS pair: left eye = left half), or null. */
2397
+ _sourceEyeSize(win) {
2398
+ if (win.kind !== 'video' && win.kind !== 'image') return null;
2399
+ const src = win.kind === 'video' ? win.video : win.img;
2400
+ if (!src) return null;
2401
+ const sw = src.videoWidth || src.naturalWidth || src.width;
2402
+ const sh = src.videoHeight || src.naturalHeight || src.height;
2403
+ if (!(sw > 1) || !(sh > 0)) return null;
2404
+ return { w: sw / 2, h: sh };
2405
+ }
2406
+
2407
+ /**
2408
+ * A scene canvas is the app's to size, so the core cannot clamp it — but it can SEE the browser
2409
+ * clamp it. Where the store exceeds the device's MAX_TEXTURE_SIZE (4096 on an Adreno 740 tablet)
2410
+ * the drawing buffer silently comes out smaller than canvas.width, while getViewport() keeps
2411
+ * splitting canvas.width: the eye boundary lands off-centre and the panel shows a double image.
2412
+ * Say so once. ./viewer, ./splat and ./model clamp before sizing and never trip this.
2413
+ *
2414
+ * Only called after the app has drawn a stereo frame, so the canvas already HAS its context:
2415
+ * getContext() of the same type returns it, of another type returns null — it never creates one.
2416
+ */
2417
+ _checkSceneBuffer(win) {
2418
+ const c = win.canvas;
2419
+ if (!c || typeof c.getContext !== 'function') return;
2420
+ if (win.sceneGl === undefined) {
2421
+ let gl = null;
2422
+ try {
2423
+ gl = c.getContext('webgl2') || c.getContext('webgl');
2424
+ } catch {
2425
+ gl = null;
2426
+ }
2427
+ win.sceneGl = gl && typeof gl.drawingBufferWidth === 'number' ? gl : null;
2428
+ }
2429
+ if (!win.sceneGl) return;
2430
+ const b = bufferScale(c, win.sceneGl);
2431
+ if (b.mismatch) {
2432
+ win.warnedBufMismatch = true;
2433
+ console.warn(
2434
+ mismatchWarning(
2435
+ '[inline3d]',
2436
+ c,
2437
+ b,
2438
+ 'getViewport() splits canvas.width, so the eyes are off-centre in the woven buffer. This ' +
2439
+ 'scene canvas is sized by the page: keep 2 × eye width and the height within ' +
2440
+ 'MAX_TEXTURE_SIZE / MAX_RENDERBUFFER_SIZE / MAX_VIEWPORT_DIMS, or map getViewport() onto ' +
2441
+ 'gl.drawingBufferWidth/Height.',
2442
+ ),
2443
+ );
2444
+ }
2350
2445
  }
2351
2446
 
2352
2447
  _sizeBuffer(win, sbs) {
2353
- const { w: boxW, h: boxH } = this._eyeSize(win);
2448
+ const { w: boxW, h: boxH } = this._eyeSize(win, sbs);
2354
2449
  win.eyeW = boxW;
2355
2450
  win.eyeH = boxH;
2356
2451
  win.canvas.width = sbs ? boxW * 2 : boxW; // SBS = two eye tiles wide
@@ -2473,10 +2568,25 @@ class Inline3D {
2473
2568
  const srcW = src.videoWidth || src.naturalWidth || src.width;
2474
2569
  const srcH = src.videoHeight || src.naturalHeight || src.height;
2475
2570
  if (!srcW || !srcH) return;
2571
+ // The buffer is capped at the source's per-eye resolution (_eyeSize), and a source's size is
2572
+ // only known once it has decoded — or changes on a new title. Re-derive on a change; the
2573
+ // resize clears the buffer, and the paint right below refills it.
2574
+ const srcKey = srcW + 'x' + srcH;
2575
+ if (win.srcKey !== srcKey) {
2576
+ win.srcKey = srcKey;
2577
+ if (win.ownsBuffer && !(win.reqW && win.reqH)) {
2578
+ const { w, h } = this._eyeSize(win, win.sbs);
2579
+ if (w !== win.eyeW || h !== win.eyeH) this._sizeBuffer(win, win.sbs);
2580
+ }
2581
+ }
2582
+ // cornerRadius / feather are buffer px at the UNCAPPED size; keep their on-screen size.
2583
+ const k = win.bufScale || 1;
2584
+ const radius = win.cornerRadius * k;
2585
+ const feather = win.feather * k;
2476
2586
  ctx.clearRect(0, 0, c.width, c.height);
2477
2587
  if (!win.sbs) {
2478
2588
  // Flat fallback: left eye only, stretched to the square buffer.
2479
- drawEye(ctx, src, 0, 0, srcW / 2, srcH, 0, 0, c.width, c.height, win.cornerRadius, win.feather);
2589
+ drawEye(ctx, src, 0, 0, srcW / 2, srcH, 0, 0, c.width, c.height, radius, feather);
2480
2590
  return;
2481
2591
  }
2482
2592
  const halfDst = c.width / 2;
@@ -2487,16 +2597,16 @@ class Inline3D {
2487
2597
  // as a squeezed pair). See _trackBakedStereo.
2488
2598
  const stereo = Math.min(this._stereoFactor, this._bakedStereo);
2489
2599
  if (stereo < 1) {
2490
- drawEye(ctx, src, 0, 0, srcW / 2, srcH, 0, 0, halfDst, c.height, win.cornerRadius, win.feather); // L
2491
- drawEyeBlend(ctx, src, srcW / 2, srcH, halfDst, c.height, stereo, win.cornerRadius, win.feather); // L+R
2600
+ drawEye(ctx, src, 0, 0, srcW / 2, srcH, 0, 0, halfDst, c.height, radius, feather); // L
2601
+ drawEyeBlend(ctx, src, srcW / 2, srcH, halfDst, c.height, stereo, radius, feather); // L+R
2492
2602
  return;
2493
2603
  }
2494
2604
  // A single stretched draw maps SBS source → SBS buffer (left→left, right→right); the
2495
2605
  // per-eye path is only needed to bake decoration (rounded corners / edge feather), which
2496
2606
  // MUST be applied to each eye separately — see drawEye/featherEye.
2497
- if (win.cornerRadius > 0 || win.feather > 0) {
2498
- drawEye(ctx, src, 0, 0, srcW / 2, srcH, 0, 0, halfDst, c.height, win.cornerRadius, win.feather); // L
2499
- drawEye(ctx, src, srcW / 2, 0, srcW / 2, srcH, halfDst, 0, halfDst, c.height, win.cornerRadius, win.feather); // R
2607
+ if (radius > 0 || feather > 0) {
2608
+ drawEye(ctx, src, 0, 0, srcW / 2, srcH, 0, 0, halfDst, c.height, radius, feather); // L
2609
+ drawEye(ctx, src, srcW / 2, 0, srcW / 2, srcH, halfDst, 0, halfDst, c.height, radius, feather); // R
2500
2610
  } else {
2501
2611
  ctx.drawImage(src, 0, 0, srcW, srcH, 0, 0, c.width, c.height);
2502
2612
  }
@@ -2656,7 +2766,10 @@ class Inline3D {
2656
2766
  win.onFrame(views, win.layer, f);
2657
2767
  // A stereo frame the page drew without throwing. A short view list is the load
2658
2768
  // fallback (a mono frame), which is not what a poster is waiting for.
2659
- if (views.length >= 2) win.fwStereo = true;
2769
+ if (views.length >= 2) {
2770
+ win.fwStereo = true;
2771
+ if (!win.warnedBufMismatch) this._checkSceneBuffer(win);
2772
+ }
2660
2773
  } catch (err) {
2661
2774
  if (!win.frameThrewWarned) {
2662
2775
  win.frameThrewWarned = true;
package/model.d.ts CHANGED
@@ -34,7 +34,11 @@ export interface ModelOptions {
34
34
  * so a long subject still fits once the turntable turns it (default true).
35
35
  */
36
36
  fitSweep?: boolean;
37
- /** Per-eye buffer scale; 0.5–0.7 is usually free (default 1). */
37
+ /**
38
+ * Per-eye buffer scale; 0.5–0.7 is usually free (default 1). Clamped to the device's GL limits: the whole store (2 × eye wide in 3D) is kept within
39
+ * min(MAX_TEXTURE_SIZE, MAX_RENDERBUFFER_SIZE, MAX_VIEWPORT_DIMS), both axes shrunk by one factor,
40
+ * with a one-time console warning. The handle's `renderScale` reports the value in force.
41
+ */
38
42
  renderScale?: number;
39
43
  feather?: number;
40
44
  /**
@@ -176,6 +180,11 @@ export interface ModelHandle {
176
180
  /** The last pose the page set (a copy), or null. */
177
181
  getCameraPose(): SplatCameraPose | null;
178
182
 
183
+ /** The per-eye buffer scale in force: the `renderScale` option times the device-limit clamp. */
184
+ readonly renderScale: number;
185
+ /** The `renderScale` option as given. */
186
+ readonly renderScaleRequested: number;
187
+
179
188
  remove(): void;
180
189
  exclude(el: Element): void;
181
190
  unexclude(el: Element): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@displayxr/inline3d",
3
- "version": "1.26.0",
3
+ "version": "1.28.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",
@@ -36,6 +36,10 @@
36
36
  "types": "./player.d.ts",
37
37
  "import": "./js/inline3d-player.js"
38
38
  },
39
+ "./call": {
40
+ "types": "./call.d.ts",
41
+ "import": "./js/inline3d-call.js"
42
+ },
39
43
  "./undock": {
40
44
  "types": "./index.d.ts",
41
45
  "import": "./js/inline3d-undock.js"
@@ -46,6 +50,7 @@
46
50
  "js/inline3d.js",
47
51
  "js/inline3d-undock.js",
48
52
  "js/inline3d-mode-switch.js",
53
+ "js/inline3d-buffer-limit.js",
49
54
  "js/inline3d-three.js",
50
55
  "js/inline3d-viewer.js",
51
56
  "js/inline3d-splat.js",
@@ -66,12 +71,15 @@
66
71
  "js/inline3d-pc-look.js",
67
72
  "js/inline3d-playcanvas-engine.js",
68
73
  "js/inline3d-player.js",
74
+ "js/inline3d-call.js",
75
+ "js/call/",
69
76
  "index.d.ts",
70
77
  "three.d.ts",
71
78
  "viewer.d.ts",
72
79
  "splat.d.ts",
73
80
  "model.d.ts",
74
81
  "player.d.ts",
82
+ "call.d.ts",
75
83
  "CHANGELOG.md",
76
84
  "README.md",
77
85
  "LICENSE",
package/player.d.ts CHANGED
@@ -267,7 +267,8 @@ export type PlayerAccent = 'azure' | 'violet' | 'magenta' | 'sunset' | 'amber' |
267
267
 
268
268
  /**
269
269
  * Pick the first source this browser can play from candidates listed best first; each is a URL, or
270
- * `{ src, type }` with a FULL `canPlayType` string (`'video/webm; codecs="vp9, opus"'`). The
271
- * DisplayXR Browser has no H.264/AAC, and a codec-less `'video/mp4'` still answers 'maybe'.
270
+ * `{ src, type }` with a FULL `canPlayType` string (`'video/webm; codecs="vp9, opus"'`). Only
271
+ * DisplayXR Browser 1.0.6+ plays H.264/AAC (via the OS decoders), and a codec-less `'video/mp4'`
272
+ * still answers 'maybe' — so list a VP9 WebM first when you have one.
272
273
  */
273
274
  export function pickSource(candidates: PlayerSource, canPlayType?: (type: string) => string): string | Blob | undefined;
package/splat.d.ts CHANGED
@@ -185,7 +185,11 @@ export interface ResolvedRig {
185
185
  * viewer's. docs/proposals/layer-display-rig.md.
186
186
  */
187
187
  export interface SplatLayerRigOptions {
188
- /** Nominal viewer distance in metres (default 0.6, the browser's own nominal). */
188
+ /**
189
+ * Nominal viewer distance n in metres. Default: the browser's
190
+ * `XRDisplayInfo.nominalViewerPosition.z` where it reports one, else 0.6. Passing it pins n
191
+ * (see {@link SplatLayerRigState.viewerDistanceSource}).
192
+ */
189
193
  viewerDistance?: number | null;
190
194
  /** An explicit gain k instead (1 = exactly the photo rig; larger = rounder). */
191
195
  gain?: number | null;
@@ -216,7 +220,14 @@ export interface SplatLayerRigState {
216
220
  rounded: boolean;
217
221
  /** Why not engaged (or what is wrong with some layers while engaged); null when all is well. */
218
222
  reason: string | null;
223
+ /** The n in use (metres). */
219
224
  viewerDistance: number;
225
+ /**
226
+ * Where `viewerDistance` came from: the page's option, the browser's reported nominal viewer
227
+ * (`XRDisplayInfo.nominalViewerPosition.z`, asked once per tile, asynchronously — early frames
228
+ * may read 'default' before it lands), or the 0.6 m fallback.
229
+ */
230
+ viewerDistanceSource: 'page' | 'browser' | 'default';
220
231
  /** The gain the last frame used (null off a camera rig). */
221
232
  gain: number | null;
222
233
  /** The plane that lands on the glass, as a distance from the photo's camera (world units). */
@@ -368,7 +379,11 @@ export interface SplatOptions {
368
379
  * so a long subject still fits once the turntable turns it (default true).
369
380
  */
370
381
  fitSweep?: boolean;
371
- /** Per-eye buffer scale; 0.5–0.7 is usually free (default 1). */
382
+ /**
383
+ * Per-eye buffer scale; 0.5–0.7 is usually free (default 1). Clamped to the device's GL limits: the whole store (2 × eye wide in 3D) is kept within
384
+ * min(MAX_TEXTURE_SIZE, MAX_RENDERBUFFER_SIZE, MAX_VIEWPORT_DIMS), both axes shrunk by one factor,
385
+ * with a one-time console warning. The handle's `renderScale` reports the value in force.
386
+ */
372
387
  renderScale?: number;
373
388
  feather?: number;
374
389
  /**
@@ -997,11 +1012,18 @@ export interface SplatHandle {
997
1012
  /**
998
1013
  * `engine: 'playcanvas'` only. Change the per-eye buffer scale live (the `renderScale` option),
999
1014
  * in (0, 4]; the backing store is resized on the next animation frame. Throws a RangeError
1000
- * otherwise.
1015
+ * otherwise. The request is kept as given (`renderScaleRequested`); the store is still clamped
1016
+ * to the device's GL limits, and `renderScale` reports what is in force.
1001
1017
  */
1002
1018
  setRenderScale(scale: number): SplatHandle;
1003
- /** `engine: 'playcanvas'` only. The current per-eye buffer scale. */
1019
+ /**
1020
+ * The per-eye buffer scale IN FORCE: the request times the device-limit clamp. E.g. 0.8 for a
1021
+ * request of 1 on a device whose MAX_TEXTURE_SIZE (4096) cannot hold a 5120-wide SBS store.
1022
+ * Both engines.
1023
+ */
1004
1024
  readonly renderScale: number;
1025
+ /** The per-eye buffer scale as requested (the option, or the last `setRenderScale`). Both engines. */
1026
+ readonly renderScaleRequested: number;
1005
1027
  /**
1006
1028
  * `engine: 'playcanvas'` only. The 2D tier's EYE offset — a head-parallax analogue for a flat
1007
1029
  * screen with no tracked eyes (e.g. a phone's tilt). `{x, y}` is normalised in the mono camera's
package/viewer.d.ts CHANGED
@@ -29,7 +29,11 @@ export interface SceneViewerOptions {
29
29
  orbit?: boolean;
30
30
  /** Degrees/second of turntable once idle. Ignored under prefers-reduced-motion. */
31
31
  idleSpin?: number;
32
- /** Per-eye buffer scale; 0.5–0.7 is usually free after the interlace (default 1). */
32
+ /**
33
+ * Per-eye buffer scale; 0.5–0.7 is usually free after the interlace (default 1). Clamped to the device's GL limits: the whole store (2 × eye wide in 3D) is kept within
34
+ * min(MAX_TEXTURE_SIZE, MAX_RENDERBUFFER_SIZE, MAX_VIEWPORT_DIMS), both axes shrunk by one factor,
35
+ * with a one-time console warning. `effectiveRenderScale` reports the value in force.
36
+ */
33
37
  renderScale?: number;
34
38
  /** Edge fade in buffer px (needs EdgeFeather from ./three). */
35
39
  feather?: number;
@@ -96,6 +100,13 @@ export declare class SceneViewer {
96
100
  readonly monoCamera: object;
97
101
  /** True while the side-by-side backing store is in use. */
98
102
  readonly is3D: boolean;
103
+ /** The requested per-eye buffer scale (the `renderScale` option). */
104
+ renderScale: number;
105
+ /**
106
+ * The per-eye buffer scale in force: `renderScale` times the device-limit clamp the last resize
107
+ * applied (1 where the store fits the device's MAX_TEXTURE_SIZE & co.).
108
+ */
109
+ readonly effectiveRenderScale: number;
99
110
 
100
111
  /** Centre the subject on the zero-disparity plane and scale it to the tile. */
101
112
  fitTo(