@displayxr/inline3d 1.46.0 → 1.47.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.
@@ -73,6 +73,7 @@ import {
73
73
  mismatchWarning,
74
74
  } from './inline3d-buffer-limit.js';
75
75
  import { viewerEaseFor, frameTrackingState } from './inline3d-viewer-ease.js';
76
+ import { parseClearColor } from './inline3d-splat-shared.js';
76
77
 
77
78
  const clamp = (v, lo, hi) => (v < lo ? lo : v > hi ? hi : v);
78
79
  // NaN/Infinity into a transform silently blanks the tile — three propagates it into the
@@ -269,6 +270,10 @@ export class SceneViewer {
269
270
  * @param {number} [opts.feather=0] edge fade in buffer px (needs ./three's EdgeFeather).
270
271
  * @param {number[]} [opts.pitchLimit=[-60,60]] degrees; stops the viewer rolling under the
271
272
  * subject, which reads as broken rather than as a feature.
273
+ * @param {number[]|string} [opts.clearColor] OPT-IN eye clear, `[r,g,b,a]` in 0..1 or a CSS
274
+ * colour. Unset: the transparent clear (0,0,0,0), exactly as before. An opaque clear
275
+ * avoids stale / black pixels on the weave (web#198); match the page colour behind the
276
+ * canvas.
272
277
  */
273
278
  constructor(THREE, canvas, opts = {}) {
274
279
  const {
@@ -307,11 +312,19 @@ export class SceneViewer {
307
312
  this.logTag = opts.logTag || '[inline3d/viewer]';
308
313
  this.pitchLimit = pitchLimit;
309
314
  this.idleSpin = idleSpin;
315
+ // Opt-in (null = off, and then nothing below differs from before it existed).
316
+ this.clearColor = parseClearColor(opts.clearColor, this.logTag);
310
317
 
311
318
  // alpha + a zero-alpha clear so the tile can dissolve into the page rather than ending at
312
319
  // a hard rectangle. An opaque scene.background would defeat both this and the feather.
313
320
  this.renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
314
321
  this.renderer.setClearColor(0x000000, 0);
322
+ // clearColor (opt-in) replaces that clear. three reads a hex as sRGB, as a CSS colour is.
323
+ if (this.clearColor) {
324
+ const [r, g, b, a] = this.clearColor;
325
+ const hex = (Math.round(r * 255) << 16) | (Math.round(g * 255) << 8) | Math.round(b * 255);
326
+ this.renderer.setClearColor(hex, a);
327
+ }
315
328
  // MUST be 1. layer.getViewport() reports BACKING-STORE px, but three.js multiplies whatever
316
329
  // you pass setViewport()/setScissor() by the renderer's pixelRatio — so any other value
317
330
  // silently scales every eye viewport (at dpr 2 the left eye covers the whole canvas). It
package/js/inline3d.js CHANGED
@@ -36,6 +36,20 @@ import { setSessionViewerEase } from './inline3d-viewer-ease.js';
36
36
  export { ViewerEase, resolveViewerEaseOption, frameTrackingState, VIEWER_EASE_DEFAULT_MS } from './inline3d-viewer-ease.js';
37
37
  export { DISPLAY_METRICS_DEFAULTS, displayMetricsFrom } from './inline3d-display-metrics.js';
38
38
  import { displayMetricsFrom } from './inline3d-display-metrics.js';
39
+ // The depth budget: pure measurements (how deep is this scene, in % of the tile width) and the
40
+ // one-time rig warnings. Never a behaviour change — see ./inline3d-depth-budget.js.
41
+ export {
42
+ DEPTH_EYE_SEPARATION_M,
43
+ DEPTH_DEFAULT_VIEWER_M,
44
+ disparityPct,
45
+ cameraRigDisparityPct,
46
+ depthBudgetFromBounds,
47
+ comfortZone,
48
+ comfortDepthFactor,
49
+ cameraRigComfortFactor,
50
+ withheldReasonHint,
51
+ } from './inline3d-depth-budget.js';
52
+ import { warnViewRigOnce, withheldReasonHint } from './inline3d-depth-budget.js';
39
53
 
40
54
  // The eased 2D<->3D transition. A pure state machine (no DOM, no WebXR) ported from the native
41
55
  // `dxr::ModeSwitch`, so the browser eases the disparity around a mode switch the same way — and in
@@ -1272,6 +1286,9 @@ class Inline3D {
1272
1286
  },
1273
1287
  setViewRig: (rig) => {
1274
1288
  win.viewRig = rig || null;
1289
+ // One-time console warnings for a rig that looks like a mistake (convergence at infinity,
1290
+ // factors > 1, a FOV in degrees). Read only: the rig goes out exactly as given.
1291
+ if (rig) warnViewRigOnce(win.rigWarned, rig);
1275
1292
  return this._pushViewRig(win);
1276
1293
  },
1277
1294
  /**
@@ -2367,6 +2384,10 @@ class Inline3D {
2367
2384
  // and rebuilds layers behind the page's back: a tile that scrolls away and back would
2368
2385
  // otherwise silently revert to the default display rig mid-scene.
2369
2386
  viewRig: opts.viewRig || null,
2387
+ // Which one-time rig warnings this window has given (setViewRig / opts.viewRig), and
2388
+ // whether its 'withheld' reason has been logged. Diagnostics only.
2389
+ rigWarned: new Set(),
2390
+ warnedWithheld: false,
2370
2391
  // The automatic 1-view collapse (the latch itself is manager-wide — the mode is the
2371
2392
  // display's, not this window's). `stereoSaved` records the factors in force when the panel
2372
2393
  // went flat, for diagnostics; the restore just re-pushes `viewRig`, which the flattening
@@ -2433,6 +2454,7 @@ class Inline3D {
2433
2454
  win.fwPromise = new Promise((resolve) => {
2434
2455
  win.fwResolve = resolve;
2435
2456
  });
2457
+ if (win.viewRig) warnViewRigOnce(win.rigWarned, win.viewRig); // as handle.setViewRig: read only
2436
2458
  this._windows.set(canvas, win);
2437
2459
  if (this._lazy && this._observer) {
2438
2460
  this._observer.observe(win.observeEl);
@@ -3430,7 +3452,19 @@ class Inline3D {
3430
3452
  * the browser's report; a confirmed `woven: false` is a capped 'withheld' level, which can still
3431
3453
  * recover, so it is NOT a terminal "will not weave" and leaves rewoven() usable.
3432
3454
  */
3455
+ /**
3456
+ * The browser REPORTED this layer withheld long enough to settle a firstWoven / rewoven on it:
3457
+ * say why, once per window (the reason is otherwise only in the settled result). A console line,
3458
+ * nothing else — the window keeps doing exactly what it did.
3459
+ */
3460
+ _noteWithheld(win, woven, reason, confirmed) {
3461
+ if (woven || !confirmed || win.warnedWithheld || !reason || reason === 'pending' || reason === 'hold-capped') return;
3462
+ win.warnedWithheld = true;
3463
+ console.warn(`[inline3d] the browser withheld this canvas from the weave (${reason}): ${withheldReasonHint(reason)}.`, win.canvas);
3464
+ }
3465
+
3433
3466
  _settleFirstWoven(win, woven, reason, confirmed = false) {
3467
+ this._noteWithheld(win, woven, reason, confirmed);
3434
3468
  // Every "will not weave" path lands here, so a pending rewoven() is released with it, and a
3435
3469
  // later one answers at once (firstWoven may have settled woven:true long before).
3436
3470
  if (!woven && !confirmed) {
@@ -3498,6 +3532,7 @@ class Inline3D {
3498
3532
  _settleRewoven(win, woven, reason, confirmed = false) {
3499
3533
  const rw = win.rw;
3500
3534
  if (!rw) return;
3535
+ this._noteWithheld(win, woven, reason, confirmed);
3501
3536
  win.rw = null;
3502
3537
  rw.resolve(Object.freeze({ woven, confirmed, reason, ms: Math.round(nowMs() - rw.calledAt) }));
3503
3538
  }
package/llms.txt ADDED
@@ -0,0 +1,21 @@
1
+ # @displayxr/inline3d — DisplayXR inline-3D web SDK
2
+
3
+ > Glasses-free 3D for web pages on a DisplayXR spatial display: you render one image per eye into a
4
+ > canvas, the DisplayXR Browser weaves them for the panel; an ordinary browser shows the page in 2D.
5
+
6
+ Read first: docs/making-3d-look-right.md — nine depth and comfort rules from real panels
7
+ (keep the viewers' object placement and frame the first view, lay flat arrangements back into
8
+ depth, keep moving things' forward swing small, fixed convergence and no near geometry in worlds,
9
+ low contrast behind the subject, text as HTML near the glass, no coplanar surfaces, no sub-pixel
10
+ detail, a cover that always comes off). The SDK's defaults do the rest.
11
+
12
+ ## Samples (they show the API, not a look to copy)
13
+ - samples/model/, samples/splat/, samples/hello-cube/, samples/camera-rig/, samples/display-modes/
14
+
15
+ ## Reference
16
+ - docs/authoring-inline-3d.md — every API, both view rigs, comfort maths
17
+ - docs/woven-canvas-rules.md — what the browser does to a woven canvas; CSS, remount and cover rules
18
+ - docs/porting-three-js-apps.md — moving an existing three.js app over
19
+ - docs/authoring-motion-and-effects.md, docs/splat-effects.md — motion, reveals, depth envelopes
20
+ - index.d.ts, three.d.ts, viewer.d.ts, model.d.ts, splat.d.ts — the public API with JSDoc
21
+ - CHANGELOG.md — what changed per release
package/model.d.ts CHANGED
@@ -30,6 +30,33 @@ export interface ModelOptions {
30
30
  margin?: number;
31
31
  /** Backstop on total depth, in display heights (default 4.0). Rarely binds. */
32
32
  depthLimit?: number;
33
+ /**
34
+ * OPT-IN eye clear: `[r, g, b, a]` in 0..1 (alpha defaults to 1) or a CSS colour string (`#hex`,
35
+ * `rgb()`/`rgba()`, `transparent`, or any colour the browser parses). Unset: the transparent
36
+ * clear, exactly as before. An OPAQUE clear is recommended: where the weave reads an alpha-0
37
+ * pixel it can show stale or black pixels (web#198). Make the page colour behind the canvas
38
+ * match, so the 2D fallback and any feathered edge look the same. Unreadable → one warning,
39
+ * the default clear stays.
40
+ */
41
+ clearColor?: [number, number, number] | [number, number, number, number] | string;
42
+ /**
43
+ * OPT-IN comfort depth (display rig only). `'auto'` (the Shibata zone of comfort) or
44
+ * `{ zone: 'shibata' | 'percival' | <diopters> }`. The SDK measures the framed subject's depth
45
+ * (tile heights in front of / behind the glass), the canvas's physical height
46
+ * (`displayMetrics()`) and the display's NOMINAL viewer distance (`getDisplayInfo()`, 0.6 m when
47
+ * it reports none — never the live head distance), and multiplies the display rig's `ipdFactor`
48
+ * and `parallaxFactor` (together) by `comfortDepthFactor()` of those, on top of
49
+ * `setStereo`'s values. It ONLY EVER LOWERS DEPTH (the factor is ≤ 1), because the same depth in
50
+ * tile heights carries more vergence load on a small or close device (a tablet at 0.4 m) than on
51
+ * a laptop at 0.6 m: where the subject already fits, nothing changes.
52
+ *
53
+ * Measured when the subject is first framed, when the canvas resizes and when the display
54
+ * reports a change — NOT on zoom or orbit (those are framing choices the developer owns; the box
55
+ * is the framed one at zoom 1), and not per frame. Read the factor in force from `depthFactor`.
56
+ * Ignored, with one console warning, on a camera rig or under `controls: 'page'`. Anything else
57
+ * throws at the call. Unset: nothing is measured or declared — the tile is exactly as before.
58
+ */
59
+ depth?: 'auto' | { zone?: 'shibata' | 'percival' | number };
33
60
  /**
34
61
  * Fit the horizontal against the box's DIAGONAL (width and depth) rather than width alone,
35
62
  * so a long subject still fits once the turntable turns it (default true).
@@ -203,6 +230,32 @@ export interface ModelHandle {
203
230
  /** Resolves once the model has loaded and been framed; rejects if the load failed. */
204
231
  readonly ready: Promise<ModelHandle>;
205
232
 
233
+ /**
234
+ * Live stereo strength — a depth slider, mirroring `SplatHandle.setStereo`. On the display rig
235
+ * (the default) the values are RELATIVE: 1 = what the display shows by construction, below 1 a
236
+ * comfort dial, 0 flat (above 1 exaggerates; sent as given, with a one-time warning). A key given
237
+ * replaces, a key set to `null` returns to 1, `setStereo(null)` clears both. `ipdScale` /
238
+ * `parallaxScale` are accepted too (a model's own values are 1, so they agree). Sticky; applied
239
+ * from the next frame with no cut (pose and framing stay). Re-declares the display rig
240
+ * explicitly (`handle.viewRig`); nothing is declared before the first call. Under
241
+ * `controls: 'page'` (PlayCanvas) the page's camera rig takes them, ABSOLUTE.
242
+ */
243
+ setStereo(opts: { ipdFactor?: number | null; parallaxFactor?: number | null; ipdScale?: number | null; parallaxScale?: number | null } | null): ModelHandle;
244
+ /** The rig's stereo in force (read back; `setStereo` writes it). Null until the backend has loaded. */
245
+ readonly rig: {
246
+ readonly type: 'display' | 'camera';
247
+ readonly ipdFactor: number;
248
+ readonly parallaxFactor: number;
249
+ readonly stereoDefault: { readonly ipdFactor: number; readonly parallaxFactor: number };
250
+ } | null;
251
+ /**
252
+ * The `depth` option's comfort factor in force, in (0, 1]: what the display rig's ipdFactor and
253
+ * parallaxFactor are multiplied by (under `setStereo`'s values). 1 without the option, before
254
+ * the first measurement, and on a rig it does not apply to.
255
+ */
256
+ readonly depthFactor: number;
257
+ /** The view-rig descriptor last declared by `setStereo` (or `controls: 'page'`), if any. */
258
+ readonly viewRig?: object;
206
259
  /** Throws on `controls: 'page'`. */
207
260
  setPose(pose?: OrbitPose): void;
208
261
  /** Throws on `controls: 'page'`. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@displayxr/inline3d",
3
- "version": "1.46.0",
3
+ "version": "1.47.0",
4
4
  "description": "Turn any HTML <canvas> into a 3D window on a spatial display, inside an ordinary web page, for the DisplayXR Browser. Dependency-free; progressive enhancement (falls back to plain 2D everywhere else).",
5
5
  "type": "module",
6
6
  "types": "./index.d.ts",
@@ -19,6 +19,10 @@
19
19
  "types": "./cursor-depth.d.ts",
20
20
  "import": "./js/inline3d-cursor-depth.js"
21
21
  },
22
+ "./depth": {
23
+ "types": "./depth.d.ts",
24
+ "import": "./js/inline3d-depth-budget.js"
25
+ },
22
26
  "./viewer": {
23
27
  "types": "./viewer.d.ts",
24
28
  "import": "./js/inline3d-viewer.js"
@@ -67,9 +71,13 @@
67
71
  "./dist/*.js"
68
72
  ],
69
73
  "files": [
74
+ "llms.txt",
75
+ "docs/making-3d-look-right.md",
76
+ "docs/woven-canvas-rules.md",
70
77
  "js/inline3d.js",
71
78
  "js/inline3d-undock.js",
72
79
  "js/inline3d-display-metrics.js",
80
+ "js/inline3d-depth-budget.js",
73
81
  "js/inline3d-mode-switch.js",
74
82
  "js/inline3d-buffer-limit.js",
75
83
  "js/inline3d-viewer-ease.js",
@@ -111,6 +119,7 @@
111
119
  "dist/",
112
120
  "index.d.ts",
113
121
  "three.d.ts",
122
+ "depth.d.ts",
114
123
  "cursor-depth.d.ts",
115
124
  "viewer.d.ts",
116
125
  "splat.d.ts",
package/splat.d.ts CHANGED
@@ -409,6 +409,33 @@ export interface SplatOptions {
409
409
  margin?: number;
410
410
  /** Backstop on total depth, in display heights (default 4.0). Rarely binds. */
411
411
  depthLimit?: number;
412
+ /**
413
+ * OPT-IN eye clear: `[r, g, b, a]` in 0..1 (alpha defaults to 1) or a CSS colour string (`#hex`,
414
+ * `rgb()`/`rgba()`, `transparent`, or any colour the browser parses). Unset: the transparent
415
+ * clear, exactly as before. An OPAQUE clear is recommended: where the weave reads an alpha-0
416
+ * pixel it can show stale or black pixels (web#198). Make the page colour behind the canvas
417
+ * match, so the 2D fallback and any feathered edge look the same. Unreadable → one warning,
418
+ * the default clear stays.
419
+ */
420
+ clearColor?: [number, number, number] | [number, number, number, number] | string;
421
+ /**
422
+ * OPT-IN comfort depth (display rig only). `'auto'` (the Shibata zone of comfort) or
423
+ * `{ zone: 'shibata' | 'percival' | <diopters> }`. The SDK measures the framed subject's depth
424
+ * (tile heights in front of / behind the glass), the canvas's physical height
425
+ * (`displayMetrics()`) and the display's NOMINAL viewer distance (`getDisplayInfo()`, 0.6 m when
426
+ * it reports none — never the live head distance), and multiplies the display rig's `ipdFactor`
427
+ * and `parallaxFactor` (together) by `comfortDepthFactor()` of those, on top of
428
+ * `setStereo`'s values (Spark has no setStereo: the factor is the rig's whole ipdFactor / parallaxFactor). It ONLY EVER LOWERS DEPTH (the factor is ≤ 1), because the same depth in
429
+ * tile heights carries more vergence load on a small or close device (a tablet at 0.4 m) than on
430
+ * a laptop at 0.6 m: where the subject already fits, nothing changes.
431
+ *
432
+ * Measured when the subject is first framed, when the canvas resizes and when the display
433
+ * reports a change — NOT on zoom or orbit (those are framing choices the developer owns; the box
434
+ * is the framed one at zoom 1), and not per frame. Read the factor in force from `depthFactor`.
435
+ * Ignored, with one console warning, on a camera rig (an asset with a capture camera, `rig: 'camera'`) or under `controls: 'page'`. Anything else
436
+ * throws at the call. Unset: nothing is measured or declared — the tile is exactly as before.
437
+ */
438
+ depth?: 'auto' | { zone?: 'shibata' | 'percival' | number };
412
439
  /**
413
440
  * Fit the horizontal against the box's DIAGONAL (width and depth) rather than width alone,
414
441
  * so a long subject still fits once the turntable turns it (default true).
@@ -1109,6 +1136,12 @@ export interface SplatHandle {
1109
1136
  */
1110
1137
  warmEntity(entity: unknown, opts?: { timeoutMs?: number }): Promise<number>;
1111
1138
  setStereo(opts: SplatStereoOptions | null): SplatHandle;
1139
+ /**
1140
+ * The `depth` option's comfort factor in force, in (0, 1]: what the display rig's ipdFactor and
1141
+ * parallaxFactor are multiplied by (under `setStereo`'s values). 1 without the option, before
1142
+ * the first measurement, and on a rig it does not apply to.
1143
+ */
1144
+ readonly depthFactor: number;
1112
1145
  /** This tile in physical units — the core `TileHandle.displayMetrics()`, on the splat handle. Never rejects. */
1113
1146
  displayMetrics(): Promise<DisplayMetrics>;
1114
1147
  /**
package/three.d.ts CHANGED
@@ -3,6 +3,7 @@
3
3
  // bundles three.js (it is an optional peer dependency).
4
4
 
5
5
  import type { XRViewRigInit } from './index.js';
6
+ export { disparityPct, cameraRigDisparityPct, depthBudgetFromBounds, comfortZone, comfortDepthFactor, cameraRigComfortFactor } from './depth.js';
6
7
 
7
8
  /**
8
9
  * A reusable three.js camera driven directly by an XRView's matrices. Construct once with your
@@ -88,8 +89,12 @@ export interface DisplayRigOptions {
88
89
  * (so a camera parented under a dolly still reports world space) unless `attach` is set, which
89
90
  * emits identity instead.
90
91
  *
91
- * Comfort rule, from the runtime: `ipdFactor × metersToVirtual × convergenceDiopters × N <= 1`
92
- * (N ≈ 0.5 m nominal viewing distance). Nothing here enforces it; the runtime clamps.
92
+ * Comfort rule: `ipdFactor × metersToVirtual × convergenceDiopters × N <= 1` (N = the nominal
93
+ * viewer distance the display reports, 0.6 m on a desktop panel). Nothing enforces it: the
94
+ * runtime applies an app-declared camera rig's gain as given (XR_DXR_view_rig §3) and clamps only
95
+ * values outside a field's sanity range. `convergence` 0 / unset (infinity: the whole scene in
96
+ * front of the glass) logs one console warning per page; the descriptor is built as asked.
97
+ * {@link cameraRigDisparityPct} measures the disparity a rig gives.
93
98
  *
94
99
  * @param THREE your imported three.js module namespace.
95
100
  * @param camera a `THREE.PerspectiveCamera` (`.fov` degrees, `.matrixWorld`).
package/viewer.d.ts CHANGED
@@ -48,6 +48,15 @@ export interface SceneViewerOptions {
48
48
  * `createInline3D({ viewerEase })`, else on. `false` restores the snap.
49
49
  */
50
50
  viewerEase?: ViewerEaseOption;
51
+ /**
52
+ * OPT-IN eye clear: `[r, g, b, a]` in 0..1 (alpha defaults to 1) or a CSS colour string (`#hex`,
53
+ * `rgb()`/`rgba()`, `transparent`, or any colour the browser parses). Unset: the transparent
54
+ * clear, exactly as before. An OPAQUE clear is recommended: where the weave reads an alpha-0
55
+ * pixel it can show stale or black pixels (web#198). Make the page colour behind the canvas
56
+ * match, so the 2D fallback and any feathered edge look the same. Unreadable → one warning,
57
+ * the default clear stays.
58
+ */
59
+ clearColor?: [number, number, number] | [number, number, number, number] | string;
51
60
  }
52
61
 
53
62
  export interface OrbitPose {
@@ -117,6 +126,8 @@ export declare class SceneViewer {
117
126
  */
118
127
  readonly effectiveRenderScale: number;
119
128
 
129
+ /** The `clearColor` option, parsed (`[r,g,b,a]`), or null = the transparent clear. */
130
+ readonly clearColor: number[] | null;
120
131
  /** Centre the subject on the zero-disparity plane and scale it to the tile. */
121
132
  fitTo(
122
133
  center: number[] | { x: number; y: number; z: number },