@displayxr/inline3d 1.45.1 → 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.
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,8 +1286,47 @@ 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
  },
1294
+ /**
1295
+ * Runtime ADR-046 Amendment 4: the depth under the pointer for THIS window, forwarded to its
1296
+ * live layer's `setCursorDepth` — `{ disparityPx, u, v }` (device px between the outermost
1297
+ * views, < 0 in front, RAW content disparity; u/v element-normalised, v down; no
1298
+ * `disparityPx` = nothing 3D there), or nothing / null to withdraw. Returns whether a live
1299
+ * layer took it. The depth cursors call this for you under `cursor: 'depth'`.
1300
+ */
1301
+ setCursorDepth: (depth) => {
1302
+ const layer = win.layer;
1303
+ if (!layer || typeof layer.setCursorDepth !== 'function') return false;
1304
+ try {
1305
+ if (depth) layer.setCursorDepth(depth);
1306
+ else layer.setCursorDepth();
1307
+ return true;
1308
+ } catch {
1309
+ return false;
1310
+ }
1311
+ },
1312
+ /**
1313
+ * "runtime" when the browser AND the runtime draw this window's cursor (the page then supplies
1314
+ * only the depth, setCursorDepth); "none" otherwise — including no live layer yet, so ask
1315
+ * again later. Never rejects.
1316
+ * @returns {Promise<'runtime'|'none'>}
1317
+ */
1318
+ getCursorDepthSupport: () => {
1319
+ const layer = win.layer;
1320
+ if (!layer || typeof layer.getCursorDepthSupport !== 'function') return Promise.resolve('none');
1321
+ try {
1322
+ return Promise.resolve(layer.getCursorDepthSupport()).then(
1323
+ (s) => (s === 'runtime' ? 'runtime' : 'none'),
1324
+ () => 'none',
1325
+ );
1326
+ } catch {
1327
+ return Promise.resolve('none');
1328
+ }
1329
+ },
1277
1330
  /**
1278
1331
  * The panel this window is weaving on: physical size in metres, pixel size, and the view
1279
1332
  * scale the runtime RECOMMENDS. Resolves null on a machine with no glasses-free display.
@@ -2331,6 +2384,10 @@ class Inline3D {
2331
2384
  // and rebuilds layers behind the page's back: a tile that scrolls away and back would
2332
2385
  // otherwise silently revert to the default display rig mid-scene.
2333
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,
2334
2391
  // The automatic 1-view collapse (the latch itself is manager-wide — the mode is the
2335
2392
  // display's, not this window's). `stereoSaved` records the factors in force when the panel
2336
2393
  // went flat, for diagnostics; the restore just re-pushes `viewRig`, which the flattening
@@ -2397,6 +2454,7 @@ class Inline3D {
2397
2454
  win.fwPromise = new Promise((resolve) => {
2398
2455
  win.fwResolve = resolve;
2399
2456
  });
2457
+ if (win.viewRig) warnViewRigOnce(win.rigWarned, win.viewRig); // as handle.setViewRig: read only
2400
2458
  this._windows.set(canvas, win);
2401
2459
  if (this._lazy && this._observer) {
2402
2460
  this._observer.observe(win.observeEl);
@@ -3394,7 +3452,19 @@ class Inline3D {
3394
3452
  * the browser's report; a confirmed `woven: false` is a capped 'withheld' level, which can still
3395
3453
  * recover, so it is NOT a terminal "will not weave" and leaves rewoven() usable.
3396
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
+
3397
3466
  _settleFirstWoven(win, woven, reason, confirmed = false) {
3467
+ this._noteWithheld(win, woven, reason, confirmed);
3398
3468
  // Every "will not weave" path lands here, so a pending rewoven() is released with it, and a
3399
3469
  // later one answers at once (firstWoven may have settled woven:true long before).
3400
3470
  if (!woven && !confirmed) {
@@ -3462,6 +3532,7 @@ class Inline3D {
3462
3532
  _settleRewoven(win, woven, reason, confirmed = false) {
3463
3533
  const rw = win.rw;
3464
3534
  if (!rw) return;
3535
+ this._noteWithheld(win, woven, reason, confirmed);
3465
3536
  win.rw = null;
3466
3537
  rw.resolve(Object.freeze({ woven, confirmed, reason, ms: Math.round(nowMs() - rw.calledAt) }));
3467
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.45.1",
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`).
@@ -143,6 +148,7 @@ export class EdgeFeather {
143
148
  ): void;
144
149
  }
145
150
 
151
+ import type { CursorRuntimeLink } from './cursor-depth.js';
146
152
  import type { CursorAnchorMode, CursorPlacement, CursorShape, CursorShapeHit, CursorShapeOption, CursorSoftenOption, CursorSoftenParams, CursorSoftening, CursorView } from './cursor-depth.js';
147
153
 
148
154
  /** One second of `debug` stats: each figure as `{ mean, p95, max, n }`. */
@@ -234,8 +240,17 @@ export class DepthCursor {
234
240
  readonly softening: CursorSoftening | null;
235
241
  /** The sprite drawn last: a name, 'none' or 'image'. */
236
242
  readonly shape: CursorShape | 'none' | 'image';
237
- /** Place the sprite for this frame — call with the views you are about to render, before rendering. */
238
- update(views: ArrayLike<XRView | CursorView>, nowMs?: number): CursorPlacement | { active: false };
243
+ /** The runtime-drawn cursor link (runtime ADR-046 Amendment 4). */
244
+ readonly runtime: CursorRuntimeLink;
245
+ /** The layer this canvas weaves through, as last passed to `update(views, { layer })`. */
246
+ layer: unknown;
247
+ /**
248
+ * Place the sprite for this frame — call with the views you are about to render, before rendering.
249
+ * Pass `{ layer }` (addScene's second callback argument): where it says "runtime" (ADR-046
250
+ * Amendment 4) the depth goes to the layer and the browser's own cursor is lifted by the runtime;
251
+ * no sprite is drawn. A number is the time in ms, as before.
252
+ */
253
+ update(views: ArrayLike<XRView | CursorView> | null, opts?: number | { nowMs?: number; layer?: unknown }): CursorPlacement | { active: false; runtime?: boolean };
239
254
  /** Set the pointer directly (canvas-normalised, v down), or null — for scripted input. */
240
255
  setPointer(u: number | null, v?: number): void;
241
256
  /** Remove listeners, restore the CSS cursor, free the sprite's GPU resources. */
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 },