@displayxr/inline3d 1.46.0 → 1.47.1

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 CHANGED
@@ -5,6 +5,94 @@ entry points (`.`, `./three`) are frozen for 1.x, while the **scene subpaths** (
5
5
  `./splat`, `./model`) are a preview tier whose options may change in any release. Entries below say
6
6
  which tier they touch, because that is what tells you whether an upgrade can move your pixels.
7
7
 
8
+ ## 1.47.1 — 2026-10-29
9
+
10
+ ### Fixed — a splat on a camera rig briefly declared convergence at infinity (`./splat`, PlayCanvas)
11
+
12
+ Touches the preview tier (`./splat` on PlayCanvas) only.
13
+
14
+ - When a camera-rig splat's focus sat at or behind its camera — in practice the first push, before
15
+ the first focus has landed — the adapter declared `convergence: 0` (infinity), which asks for
16
+ the whole scene in front of the glass, for one push. It now keeps the rig as declared until the
17
+ focus is in front of the camera. Seen on a panel as one such rig in the first 0.3–2 s of each
18
+ page of an app built on 1.47.0 (and silently in earlier versions); 1.47.0's one-time
19
+ convergence-0 warning is what surfaced it, and it no longer fires from the SDK's own rigs.
20
+
21
+ ## 1.47.0 — 2026-10-29
22
+
23
+ The look-right release: a short guide for people and coding agents (`docs/making-3d-look-right.md`,
24
+ `llms.txt`), depth measurements and comfort-zone helpers, and opt-in `clearColor`, `setStereo` on
25
+ `./model` and `depth: 'auto'`. Every addition is opt-in: without the new options a page renders
26
+ exactly as on 1.46.0; the only default change is a few one-time console warnings.
27
+
28
+ ### Comfort depth: zone-of-comfort helpers and an opt-in `depth` option
29
+
30
+ Touches the core (`.`: new pure exports), `./three` and `./depth` (the same pure exports), and the
31
+ preview tier (`./model`, `./splat`: one new opt-in option, one read-only handle field). Without
32
+ `depth`, every page renders exactly as before: no rig is declared that was not, nothing is measured.
33
+
34
+ - **`comfortZone({ viewerM = 0.6, zone = 'shibata' })`** → `{ zone, viewerM, frontD, behindD,
35
+ frontMm, behindMm }`: the vergence–accommodation comfort zone at a viewing distance, in diopters
36
+ and on-screen mm. `'shibata'` (Shibata et al. 2011: ≈ 0.57 D in front / 0.63 D behind at 0.6 m;
37
+ behind unbounded once the far limit passes infinity), `'percival'` (±1/3 D), or a number.
38
+ - **`comfortDepthFactor({ frontH, backH, tileHeightM, viewerM, zone })`** → the largest `k ≤ 1` that
39
+ puts a display-rig subject (depth in tile heights) inside the zone, from the exact geometry at
40
+ real proportions. Only ever lowers depth; 1 on missing / invalid input.
41
+ - **`cameraRigComfortFactor({ zNear, zFar, convergence | convergenceDiopters, verticalFov, aspect,
42
+ tileHeightM, viewerM, zone, ipdFactor, metersToVirtual })`** → the multiplier on a camera rig's
43
+ eye separation that puts its nearest / farthest points at the zone's edge. A measurement, **no
44
+ cap**: a camera rig's ipdFactor 1 is only physical in metre-scale worlds, so this may be well
45
+ above 1. Infinity with nothing off the plane, NaN on bad input. No warning is added for camera
46
+ factors above 1.
47
+ - **`depth: 'auto' | { zone }`** on `addModel` (both engines) and `addSplat` (Spark and
48
+ PlayCanvas), display rig only: the framed subject's depth (zoom 1, no orbit), the canvas's
49
+ physical height (`displayMetrics()`) and the display's NOMINAL viewer distance (0.6 m when it
50
+ reports none) give `k = comfortDepthFactor(…)`, applied to the display rig's `ipdFactor` and
51
+ `parallaxFactor` together, multiplied with `setStereo`'s values (Spark, which has no setStereo:
52
+ the whole factor). Re-measured on first framing, a canvas resize, the first weave and a display
53
+ event; never on zoom, orbit or per frame; re-declared only when k changes. Ignored with one
54
+ warning on a camera rig / `controls: 'page'`; an invalid value throws at the call.
55
+ `handle.depthFactor` reads the factor in force (1 without the option).
56
+
57
+ ### Making 3D look right: depth measurements, opt-in helpers and one-time warnings
58
+
59
+ Touches the core (`.`: new pure exports, console warnings only), `./three` (re-exports, one
60
+ warning), the new `./depth` subpath, and the preview tier (`./viewer`, `./model`, `./splat`), all
61
+ opt-in. **Nothing clamps, refuses or overrides what a page asks for**: without the new options
62
+ every page renders exactly as on 1.46.0; the only default change is the console warnings below.
63
+
64
+ - **Depth measurements** (`./depth`, also on the root; the pure helpers on `./three`):
65
+ `disparityPct({ depthH, aspect, viewerM, heightM, ipdFactor })` (display rig: `e·z/(V∓z)` over
66
+ the tile width, the small-depth limit without `heightM`),
67
+ `cameraRigDisparityPct({ z, convergence, ipdFactor, metersToVirtual, verticalFov, aspect })`
68
+ (`100·b·(1/D − 1/z)/(2·tan(fov/2)·aspect)`, + behind, − in front), and `depthBudgetFromBounds`
69
+ (a `getSubjectBounds()` box → tile heights and % of the tile width). Pure; for a raw `addScene`
70
+ page or an agent checking its numbers. No budget tiers are built in: depth ranges measured on a
71
+ panel are in `docs/authoring-inline-3d.md`, as reference points.
72
+ - **`clearColor`** on `SceneViewer` / `addModel` / `addSplat` (three and PlayCanvas):
73
+ `[r, g, b, a]` in 0..1 or a CSS colour. Unset = the transparent clear, as before. An opaque clear
74
+ avoids the stale / black pixels the weave can show where it reads alpha 0 (web#198); match the
75
+ page colour behind the canvas.
76
+ - **`ModelHandle.setStereo({ ipdFactor, parallaxFactor } | null)`** (both engines), mirroring
77
+ `SplatHandle.setStereo`: a depth slider. RELATIVE on the display rig (1 = by construction), sticky,
78
+ `null` restores; `ipdScale` / `parallaxScale` accepted. Re-declares the display rig explicitly
79
+ from the first call (nothing is declared before it). `handle.rig` reads the values in force;
80
+ under `controls: 'page'` the page's camera rig takes them, absolute.
81
+ - **One-time console warnings** (`[inline3d]`, once per handle, the rig still sent as given): a
82
+ camera rig with `convergenceDiopters` 0 / unset ("convergence at infinity puts the whole scene in
83
+ front of the glass …"; also once per page from `cameraRigFromCamera` / `cameraRigFromPose` at
84
+ convergence 0, not repeated by `setViewRig` for that descriptor), a display rig with
85
+ `ipdFactor` or `parallaxFactor` above 1, a camera rig whose `verticalFov` is above π (degrees).
86
+ And a layer the browser keeps `'withheld'` until `firstWoven` / `rewoven()` settles on it now
87
+ logs its `withheldReason` once, with a hint (`cross-pass:*` / `no-quad`: a `filter`,
88
+ `opacity < 1`, `mask`, `clip-path`, `mix-blend-mode` or `backdrop-filter` on the canvas or an
89
+ ancestor). A transient withheld (a move, a resize) says nothing. `withheldReasonHint` is exported.
90
+ - **Docs corrected**: the camera-rig comfort note said N ≈ 0.5 m and "the runtime clamps". The
91
+ nominal viewer is 0.6 m on a desktop panel, and the runtime does NOT comfort-clamp an
92
+ app-declared rig (XR_DXR_view_rig §3); it clamps only values outside a field's sanity range.
93
+ - Internal: `validateSetStereo` moved to `js/inline3d-splat-shared.js` (still exported from the
94
+ PlayCanvas splat module); new `js/inline3d-depth-budget.js` is in the published file set.
95
+
8
96
  ## 1.46.0 — 2026-10-14
9
97
 
10
98
  ### Depth cursor: when the runtime draws the web cursor, hand it the depth (runtime ADR-046 Amendment 4)
package/README.md CHANGED
@@ -11,6 +11,9 @@ live demos.
11
11
  them in 3D; in any other browser the pages render as a normal 2D fallback, so they're safe to view
12
12
  anywhere.
13
13
 
14
+ > **New here, or an AI coding agent? Read [Depth and comfort on a 3D display](docs/making-3d-look-right.md)
15
+ > first:** nine rules from real panels; the SDK's defaults do the rest.
16
+
14
17
  ## Install
15
18
 
16
19
  The SDK is published as **[`@displayxr/inline3d`](https://www.npmjs.com/package/@displayxr/inline3d)**
@@ -107,6 +107,6 @@ package.
107
107
  | Asset | Used by | Source | Credit and licence |
108
108
  |---|---|---|---|
109
109
  | Damaged Helmet (`DamagedHelmet.glb`) | `samples/model/` (tiles A and B), loaded at run time, not vendored | [KhronosGroup/glTF-Sample-Assets](https://github.com/KhronosGroup/glTF-Sample-Assets/tree/c6a6bd13ab2b3c685c7903d03561b8a9392f38b8/Models/DamagedHelmet) at commit `c6a6bd13ab2b3c685c7903d03561b8a9392f38b8`, via jsDelivr, unmodified | © 2018 ctxwing (rebuild and conversion to glTF 2.0), [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/legalcode). Earlier version of the model ("Battle Damaged Sci-fi Helmet") © 2016 theblueturtle_, [CC BY-NC 4.0](https://creativecommons.org/licenses/by-nc/4.0/legalcode). As stated in the asset's `README.md` and `LICENSE.md`. |
110
- | Duck, Draco variant (`samples/model/assets/glTF-Draco/`) | `samples/model/` (tile C) | Khronos glTF-Sample-Assets | © Sony, [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/). |
110
+ | Duck, Draco variant (`samples/model/assets/glTF-Draco/`); glTF-Binary variant (`samples/starter-model/assets/Duck.glb`, glTF-Sample-Assets commit `c6a6bd13ab2b3c685c7903d03561b8a9392f38b8`, unmodified) | `samples/model/` (tile C), `samples/starter-model/` | Khronos glTF-Sample-Assets | © Sony, [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/). |
111
111
  | Butterfly splat (`samples/splat/assets/butterfly.sog`) | `samples/splat/`, `samples/model/` (tile B) | [Spark](https://sparkjs.dev/) `assets/splats/butterfly.spz`, converted to SOG with PlayCanvas splat-transform (177,132 gaussians, 3 SH bands kept) | Butterfly splat © World Labs (from Spark, sparkjs.dev), used with permission; converted to SOG. |
112
112
  | Draco decoder (`vendor/draco/`) | `samples/model/` | three.js `examples/jsm/libs/draco/` | © The Draco Authors, Apache-2.0. |
package/depth.d.ts ADDED
@@ -0,0 +1,184 @@
1
+ // Type definitions for @displayxr/inline3d/depth — depth measurements (also re-exported by the
2
+ // root entry and, minus the warning helpers, by ./three).
3
+ //
4
+ // Measurements and pure helpers only: nothing here changes a pixel, clamps a rig or refuses a
5
+ // value (the opt-in `depth` option of ./model and ./splat is what applies comfortDepthFactor). The units are RELATIVE — disparity as % of the TILE's width, or depth in TILE HEIGHTS on
6
+ // a display rig — so they hold on any panel. Depth ranges measured on a panel are in
7
+ // docs/authoring-inline-3d.md, as reference points.
8
+
9
+ /** The eye separation every estimate assumes, metres (0.063). No browser surface reports a real one. */
10
+ export const DEPTH_EYE_SEPARATION_M: number;
11
+ /** The viewer distance used when the display reports no nominal viewer, metres (0.6, a desktop panel). */
12
+ export const DEPTH_DEFAULT_VIEWER_M: number;
13
+
14
+ /** What a depth report measured against. */
15
+ export interface DepthReportBasis {
16
+ rig: 'display';
17
+ /** The tile's width / height. */
18
+ aspect: number;
19
+ /** Nominal viewer distance used, metres. */
20
+ viewerM: number;
21
+ /** The tile's physical height, metres, or null (then the small-depth formula, which ignores it). */
22
+ heightM: number | null;
23
+ /** The display rig's ipdFactor in force (disparity scales with it). */
24
+ ipdFactor: number;
25
+ eyeSeparationM: number;
26
+ }
27
+
28
+ /**
29
+ * How deep a display-rig subject is. `frontH` / `backH` are MAGNITUDES in tile heights (0 when
30
+ * nothing is on that side); `frontPct` / `backPct` the matching disparity magnitudes, % of the
31
+ * tile width.
32
+ */
33
+ export interface DepthReport {
34
+ frontH: number;
35
+ backH: number;
36
+ frontPct: number;
37
+ backPct: number;
38
+ basis: DepthReportBasis;
39
+ }
40
+
41
+ /**
42
+ * Disparity, % of the tile width, of a point `depthH` tile heights out of the glass on a DISPLAY
43
+ * rig: physical depth `z = depthH · heightM`, disparity `e · z / (V − z)` (e = 0.063 m × ipdFactor,
44
+ * V = `viewerM`) over the tile width `aspect · heightM`. Without `heightM` the small-depth limit
45
+ * `100 · e · depthH / (aspect · V)`. SIGNED: `depthH > 0` (in front) gives a NEGATIVE disparity,
46
+ * behind a positive one — as {@link cameraRigDisparityPct}. −Infinity at or past the viewer.
47
+ */
48
+ export function disparityPct(o: {
49
+ depthH: number;
50
+ aspect: number;
51
+ /** Default 0.6. */
52
+ viewerM?: number;
53
+ heightM?: number;
54
+ /** Default 1. */
55
+ ipdFactor?: number;
56
+ /** Default 0.063. */
57
+ eyeSeparationM?: number;
58
+ }): number;
59
+
60
+ /**
61
+ * Disparity, % of the tile width, of a point `z` world units down a CAMERA rig's view axis:
62
+ * `100 · b · (1/D − 1/z) / (2 · tan(verticalFov/2) · aspect)`, `b = 0.063 · ipdFactor ·
63
+ * metersToVirtual`. `+` = behind the glass, `−` = in front. `convergence` 0 / unset (and no
64
+ * `convergenceDiopters`) is infinity: everything is in front. NaN on bad input.
65
+ */
66
+ export function cameraRigDisparityPct(o: {
67
+ z: number;
68
+ /** Zero-disparity distance, world units (0 = infinity). */
69
+ convergence?: number;
70
+ /** Or the descriptor's 1 / distance; wins over `convergence`. */
71
+ convergenceDiopters?: number;
72
+ ipdFactor?: number;
73
+ metersToVirtual?: number;
74
+ /** FULL vertical angle, RADIANS. */
75
+ verticalFov: number;
76
+ aspect: number;
77
+ }): number;
78
+
79
+ /**
80
+ * A {@link DepthReport} from a display-rig subject box: `front` / `back` as
81
+ * `SceneViewer.getSubjectBounds()` reports them (display space, +z toward the viewer, glass at 0),
82
+ * in the units where one tile height is `virtualDisplayHeight`.
83
+ */
84
+ export function depthBudgetFromBounds(o: {
85
+ front: number;
86
+ back: number;
87
+ virtualDisplayHeight: number;
88
+ aspect: number;
89
+ viewerM?: number;
90
+ heightM?: number;
91
+ ipdFactor?: number;
92
+ }): DepthReport;
93
+
94
+ /**
95
+ * A comfort zone (vergence–accommodation): `'shibata'` (Shibata et al. 2011, "The zone of
96
+ * comfort", J. Vision — the default), `'percival'` (±1/3 D), or a number: a symmetric |ΔD| bound,
97
+ * diopters (> 0).
98
+ */
99
+ export type ComfortZoneName = 'shibata' | 'percival' | number;
100
+
101
+ /**
102
+ * A comfort zone at one viewing distance: the largest comfortable vergence change in front of the
103
+ * glass (`frontD`) and behind it (`behindD`), diopters, and the matching on-screen disparities in
104
+ * mm (`e · V · ΔD`). `behindD` is Infinity when the zone's far limit is past optical infinity.
105
+ */
106
+ export interface ComfortZoneReport {
107
+ zone: ComfortZoneName;
108
+ viewerM: number;
109
+ frontD: number;
110
+ behindD: number;
111
+ frontMm: number;
112
+ behindMm: number;
113
+ }
114
+
115
+ /**
116
+ * The comfort zone for a viewer at `viewerM` (default 0.6 m). Shibata: near vergence limit
117
+ * `Dv/1.035 + 0.626`, far `Dv/1.129 − 0.442` (Dv = 1/V), so 0.57 D in front / 0.63 D behind at
118
+ * 0.6 m; Percival: ±1/3 D. NaN fields on bad input (a distance ≤ 0, an unknown zone).
119
+ */
120
+ export function comfortZone(o?: { viewerM?: number; zone?: ComfortZoneName }): ComfortZoneReport;
121
+
122
+ /**
123
+ * How much a DISPLAY rig's depth would have to shrink for a subject to sit inside a comfort zone:
124
+ * the largest `k ≤ 1` such that its disparities × k (ipdFactor, parallaxFactor moved with it)
125
+ * stay within {@link comfortZone}. ONLY EVER LOWERS DEPTH: 1 = it already fits, never above 1.
126
+ *
127
+ * Exact geometry at real proportions: the nearest point at `V − frontH · tileHeightM` from the
128
+ * viewer, the farthest at `V + backH · tileHeightM`, each side's `|1/V − 1/z|` against the zone.
129
+ * Use the NOMINAL viewer distance (what the display is built for), not a live head distance. The
130
+ * same depth in tile heights asks more of a small, close screen — a tablet at 0.4 m carries more
131
+ * vergence per tile height than a laptop at 0.6 m — which is why the answer depends on the panel.
132
+ *
133
+ * Returns 1 (no change) for missing or invalid input, and when the front reaches the viewer.
134
+ */
135
+ export function comfortDepthFactor(o: {
136
+ /** MAGNITUDE, tile heights in front of the glass. */
137
+ frontH: number;
138
+ /** MAGNITUDE, tile heights behind it. */
139
+ backH: number;
140
+ /** The tile's (canvas's) physical height, metres. */
141
+ tileHeightM: number;
142
+ /** NOMINAL viewer distance, metres. Default 0.6. */
143
+ viewerM?: number;
144
+ /** Default 'shibata'. */
145
+ zone?: ComfortZoneName;
146
+ }): number;
147
+
148
+ /**
149
+ * The CAMERA-rig counterpart: the multiplier on the eye separation (ipdFactor) that puts the
150
+ * scene's nearest (`zNear`) and farthest (`zFar`, may be Infinity) points at the edge of the
151
+ * comfort zone. Measurement only; the page decides whether to apply it. Unlike the display-rig
152
+ * factor it can EXCEED 1, because a camera rig's 1 (63 mm × metersToVirtual in scene units) is
153
+ * only physical in metre-scale worlds — in large scene units it renders nearly flat.
154
+ *
155
+ * Disparity `f = b · (1/D − 1/z) / (2 · tan(verticalFov/2) · aspect)` of the tile width
156
+ * (b = 0.063 · ipdFactor · metersToVirtual), on the glass `p = f · aspect · tileHeightM`, in
157
+ * diopters `p / (0.063 · viewerM)`; the result is the smallest zone side / |ΔD| over the sides
158
+ * with content. Convergence 0 / unset = infinity (everything in front). Infinity when nothing is
159
+ * off the plane, NaN on bad input.
160
+ */
161
+ export function cameraRigComfortFactor(o: {
162
+ zNear: number;
163
+ zFar: number;
164
+ /** Zero-disparity distance, world units (0 = infinity). */
165
+ convergence?: number;
166
+ /** Or 1 / distance; wins over `convergence`. */
167
+ convergenceDiopters?: number;
168
+ /** FULL vertical angle, RADIANS. */
169
+ verticalFov: number;
170
+ aspect: number;
171
+ tileHeightM: number;
172
+ /** Default 0.6. */
173
+ viewerM?: number;
174
+ /** Default 'shibata'. */
175
+ zone?: ComfortZoneName;
176
+ /** Default 1. */
177
+ ipdFactor?: number;
178
+ /** Default 1. */
179
+ metersToVirtual?: number;
180
+ }): number;
181
+
182
+ /** The hint `[inline3d]` logs for a `withheldReason` token (docs/woven-canvas-rules.md §3). */
183
+ export function withheldReasonHint(reason: string): string;
184
+