@displayxr/inline3d 1.34.0 → 1.36.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 CHANGED
@@ -5,6 +5,63 @@ 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.36.0 — 2026-10-07
9
+
10
+ Additive everywhere it touches: the **core** `./three` (new helpers, minor per
11
+ `docs/sdk-stability.md`), the **preview-tier** `./model` and `./splat` (one new option,
12
+ `cursor: 'depth'`), and a new dependency-free subpath `./cursor-depth`. Nothing existing changes;
13
+ nothing runs unless a page asks for it (`cursor: 'depth'`, or constructs a `DepthCursor`).
14
+
15
+ ### Depth-aware cursor — `DepthCursor` (`./three`) + `./cursor-depth`
16
+
17
+ - **The cursor rises to the content it hovers.** The CSS cursor is drawn on the glass (zero
18
+ disparity), so over content that pops out it sits *behind* what it covers: a depth violation.
19
+ `new DepthCursor(THREE, { canvas, hitTest: raycastHitTest(THREE, [model]) })` tracks the pointer,
20
+ hit-tests the cursor footprint against your content, and places a crosshair just in front of the
21
+ nearest hit (add `cursor.object` to the scene; call `cursor.update(views)` each frame before
22
+ rendering). It hides the CSS cursor exactly while the crosshair replaces it, and gives it back in
23
+ 2D and off the canvas.
24
+ - **Same maths as native apps.** `./cursor-depth` is a line-for-line port of the runtime's
25
+ `u_cursor_depth` (displayxr-runtime ADR-046, `XR_DXR_cursor_depth`): the same geometry
26
+ (solved from the views alone, so display and camera rigs, scale and head tracking just work),
27
+ the same defaults (margin, comfort clamp, fast rise / slow sink), and the same test numbers.
28
+ - **Head motion leaves it put.** The crosshair stays under the mouse on the glass while you look
29
+ around; only its depth follows the content under that line of sight.
30
+ - **`./model` and `./splat`: `cursor: 'depth'`** (preview tier, additive). This is one option on
31
+ both engines:
32
+ - **PlayCanvas:** hit-tests with the engine's own depth picker. One low-res pass covers meshes
33
+ and splats; it runs every other frame, only while the pointer is over the tile.
34
+ - **three / Spark:** raycasts the subject. On Spark that is about 8 ms a ray, so it's amortised
35
+ to one ray a frame; PlayCanvas is the cheaper engine for splats with the cursor.
36
+ - **No option, no cost:** nothing is built, and on PlayCanvas the cursor module isn't even
37
+ downloaded.
38
+ - **Samples:** `samples/model/?cursor=depth`, `samples/splat/?cursor=depth`.
39
+ - `DepthCursor` option `raysPerFrame`, for an expensive hit test.
40
+ - **e2e:** `npm run test:e2e:cursor` drives the real viewers (both engines, model and splat) with
41
+ two off-axis views and checks the crosshair sits at *content − margin* over the subject and on
42
+ the glass off it.
43
+
44
+ ## 1.35.0 — 2026-10-06
45
+
46
+ Touches the **preview tier** (`./camera`, `./call`) additively. The core is unchanged (one new
47
+ unit test pins its existing behaviour).
48
+
49
+ ### `./call` + `./camera` — the self view never shows a squeezed pair (web#131)
50
+
51
+ - **The self view follows its tile's `firstWoven`.** Seen once in the DisplayXR Browser: a
52
+ call's self view showed the whole packed L|R pair in each eye under a `You · 3D` badge, the
53
+ look of a canvas the browser is not weaving. `addCameraView` now watches `handle.firstWoven`
54
+ the way a remote tile does: on `woven: false` (`'layer-failed'`, `'session-ended'`) the view
55
+ drops to `'flat-left'` (one eye, mirrored), re-registers a refused layer up to 4 times with
56
+ backoff, and re-weaves on `_reroute(true, wall)` when a wall comes back.
57
+ - **The self badge is honest**: `3D` only while the view is on the woven route.
58
+ - **`CameraView` additions**: `fallbackReason`, `weaveState()` and the `onRouteChange` option
59
+ (types: `CameraViewWeaveState` in `camera.d.ts`).
60
+ - **`call.diagnostics().self`** = `{ route, woven, reason, firstWoven, layerRetries }`, so the
61
+ next occurrence can be diagnosed from devtools.
62
+ - Hardening, not a root-cause fix: `firstWoven` is the SDK's hold timer, so a layer the browser
63
+ silently never joins still reads as woven.
64
+
8
65
  ## 1.34.0 — 2026-10-06
9
66
 
10
67
  ### `./model` — the PlayCanvas drag is a turntable again
package/camera.d.ts CHANGED
@@ -193,6 +193,24 @@ export interface CameraViewOptions {
193
193
  depth?: number;
194
194
  /** The tile's aspect (w/h) = the woven buffer's per-eye aspect. Default 16/9. */
195
195
  aspect?: number;
196
+ /**
197
+ * Called when the view's route changes — including when the wall says the tile will not weave
198
+ * and the view drops to its flat left eye by itself — and when its `firstWoven` settles. Keep a
199
+ * "3D" badge on `view.woven`, not on the wall.
200
+ */
201
+ onRouteChange?: (route: CameraView['route'], state: CameraViewWeaveState) => void;
202
+ }
203
+
204
+ /** What a {@link CameraView} actually shows (`view.weaveState()`), for diagnostics. */
205
+ export interface CameraViewWeaveState {
206
+ route: 'woven-sbs' | 'flat-left' | 'flat' | null;
207
+ woven: boolean;
208
+ /** Why a pair on a 3D wall is flat: the tile's `firstWoven` reason (`'layer-failed'`, `'session-ended'`), else null. */
209
+ reason: string | null;
210
+ /** The current registration's settled `firstWoven`, `'pending'`, or null off the woven route. */
211
+ firstWoven: { woven: boolean; confirmed: boolean; reason: string | null; ms: number } | 'pending' | null;
212
+ /** Layer re-registrations attempted after a `'layer-failed'` (bounded; reset by a woven result). */
213
+ layerRetries: number;
196
214
  }
197
215
 
198
216
  /** A self view from {@link addCameraView}. */
@@ -200,7 +218,12 @@ export interface CameraView {
200
218
  readonly canvas: HTMLCanvasElement;
201
219
  /** `'woven-sbs'`: 3D on the panel; `'flat-left'`: a pair shown as its left eye; `'flat'`: mono. */
202
220
  readonly route: 'woven-sbs' | 'flat-left' | 'flat' | null;
221
+ /** On the woven route. A tile the wall will not weave drops the view flat, so this goes false with it. */
203
222
  readonly woven: boolean;
223
+ /** Why a pair on a 3D wall is shown flat (`'layer-failed'`, `'session-ended'`), else null. */
224
+ readonly fallbackReason: string | null;
225
+ /** What the view actually shows, for diagnostics. */
226
+ weaveState(): CameraViewWeaveState;
204
227
  /** The wall's tile handle while woven (its `firstWoven` settles when the tile is safe to reveal), else null. */
205
228
  readonly handle: TileHandle | null;
206
229
  readonly mirror: boolean;
@@ -0,0 +1,76 @@
1
+ // Type definitions for @displayxr/inline3d/cursor-depth — the depth-aware cursor's placement
2
+ // maths, dependency-free. A port of the runtime's u_cursor_depth (ADR-046, XR_DXR_cursor_depth):
3
+ // same geometry, same defaults, same numbers. For three.js, ./three's `DepthCursor` wires it up.
4
+
5
+ /** A view as an XRView carries it: column-major projection + transform matrices. */
6
+ export interface CursorView {
7
+ projectionMatrix: ArrayLike<number>;
8
+ transformMatrix: ArrayLike<number>;
9
+ }
10
+
11
+ /** Placement policy. Disparities in eye-baseline units (0 = on the glass, < 0 = in front); times in seconds. */
12
+ export interface CursorTuning {
13
+ margin: number;
14
+ minDisparity: number;
15
+ maxDisparity: number;
16
+ riseTau: number;
17
+ sinkTau: number;
18
+ stale: number;
19
+ }
20
+
21
+ /** The runtime's defaults: margin 0.03, clamp ±0.6, rise 30 ms, sink 250 ms, stale 0.5 s. */
22
+ export const CURSOR_DEFAULT_TUNING: Readonly<CursorTuning>;
23
+ /** Default sprite height, as a fraction of the canvas height (0.03). */
24
+ export const CURSOR_DEFAULT_HEIGHT: number;
25
+
26
+ /** The cursor's line of sight for one frame. */
27
+ export interface CursorGeometry {
28
+ eye: number[];
29
+ canvasPoint: number[];
30
+ forward: number[];
31
+ eyeToCanvas: number;
32
+ canvasHeight: number;
33
+ /** The display plane's unit axes: draw the sprite in its X/Y plane, facing +Z. */
34
+ basis: { x: number[]; y: number[]; z: number[] };
35
+ }
36
+
37
+ /** The ray from a view through canvas point (u, v) (`[0,1]²`, origin top-left, v down). */
38
+ export function cursorViewRay(view: CursorView, u: number, v: number): { origin: number[]; direction: number[] };
39
+ /** Solve the line of sight from two views (the outermost pair); null when degenerate (2D, off-canvas). */
40
+ export function solveCursorGeometry(viewA: CursorView, viewB: CursorView, u: number, v: number): CursorGeometry | null;
41
+ /** A point's disparity; null when it is not in front of the eye. */
42
+ export function cursorPointDisparity(g: CursorGeometry, point: ArrayLike<number>): number | null;
43
+ /** Content minus the margin, clamped; 0 when nothing is under the cursor. */
44
+ export function cursorTarget(tuning: CursorTuning, hasContent: boolean, contentDisparity: number): number;
45
+ /** Advance a filter (`{}` to start; the first step snaps) toward `target` at `nowSec`. */
46
+ export function cursorFilterStep(filter: object, tuning: CursorTuning, target: number, nowSec: number): number;
47
+ /** Where to draw a sprite at `disparity`, and how tall. */
48
+ export function placeCursor(g: CursorGeometry, disparity: number, heightFraction: number): { position: number[]; height: number };
49
+
50
+ /** One frame's placement. `position`/`basis` are in the views' space; null when inactive. */
51
+ export interface CursorPlacement {
52
+ active: boolean;
53
+ position: number[] | null;
54
+ basis: { x: number[]; y: number[]; z: number[] } | null;
55
+ height: number;
56
+ disparity: number;
57
+ targetDisparity: number;
58
+ }
59
+
60
+ /** The whole pipeline with its filter state: one per cursor. Inactive with fewer than two views. */
61
+ export class CursorDepthPlacer {
62
+ constructor(tuning?: CursorTuning);
63
+ update(
64
+ views: ArrayLike<CursorView | XRView>,
65
+ hint: {
66
+ /** Canvas-normalised pointer, v down. */
67
+ u: number;
68
+ v: number;
69
+ /** Nearest content point under the cursor FOOTPRINT, in the views' space; null = nothing. */
70
+ nearestPoint: ArrayLike<number> | null;
71
+ /** Sprite height as a fraction of the canvas height (default 0.03). */
72
+ cursorHeight?: number;
73
+ },
74
+ nowSec: number,
75
+ ): CursorPlacement;
76
+ }