diff --git a/CHANGELOG.md b/CHANGELOG.md index d7752a5..7db31c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,20 @@ entry points (`.`, `./three`) are frozen for 1.x, while the **scene subpaths** ( `./splat`, `./model`) are a preview tier whose options may change in any release. Entries below say which tier they touch, because that is what tells you whether an upgrade can move your pixels. +## Unreleased + +Touches the **core** `./three` (`DepthCursor`) and the **preview tier** (`./model`, `./splat`, +`./call`, `./camera`: `cursor: 'depth'`), plus `./cursor-depth` (`CURSOR_DEFAULT_TUNING`). **One +deliberate default change.** + +- **Depth-cursor margin 0.03 → 0.015 eye baselines (default change),** about 1 mm of crossed + on-screen disparity at a 63–65 mm IPD instead of about 2 mm. Runtime ADR-046 Amendment 2 A.2 was + halved after a Windows panel check, because 0.03 floated visibly off the content. The runtime + makes the same change (`U_CURSOR_DEPTH_DEFAULT_MARGIN`), so web and native cursors still agree. + Every depth cursor takes the default from `CURSOR_DEFAULT_TUNING`: `DepthCursor`, + `cursor: 'depth'` on both engines, and the stereo-tile pair cursor. `margin` still overrides it: + pass `margin: 0.03` for the 1.41.0 look. + ## 1.41.0 — 2026-10-11 Touches the **core** `./three` (`DepthCursor`'s look and two defaults) and the **preview tier** diff --git a/cursor-depth.d.ts b/cursor-depth.d.ts index ca94dd6..12306ed 100644 --- a/cursor-depth.d.ts +++ b/cursor-depth.d.ts @@ -3,7 +3,7 @@ // same geometry, same defaults, same numbers. For three.js, ./three's `DepthCursor` wires it up. // // The policy (ADR-046 Amendment 2), the same for every DisplayXR depth cursor: never behind the -// glass (`cursorTarget`), margin 0.03 eye baselines, drawn only past the draw threshold +// glass (`cursorTarget`), margin 0.015 eye baselines, drawn only past the draw threshold // (`cursorShouldDraw`; otherwise the OS cursor is the cursor), the classic arrow at the OS cursor's // size (`CURSOR_ARROW`, `cursorArrowMesh`, `cursorDefaultHeight`). @@ -24,7 +24,7 @@ export interface CursorTuning { stale: number; } -/** The defaults: margin 0.03 (~2 mm at a 65 mm IPD), clamp [−0.6, 0], rise 30 ms, sink 250 ms, stale 0.5 s. */ +/** The defaults: margin 0.015 (~1 mm at a 63–65 mm IPD), clamp [−0.6, 0], rise 30 ms, sink 250 ms, stale 0.5 s. */ export const CURSOR_DEFAULT_TUNING: Readonly; /** Fallback sprite height, as a fraction of the canvas height (0.03), when the canvas's on-page size is unknown. */ export const CURSOR_DEFAULT_HEIGHT: number; @@ -79,7 +79,7 @@ export const CURSOR_ANCHOR_MODES: readonly CursorAnchorMode[]; export interface CursorOptions { /** Sprite height as a fraction of the canvas height (default: the OS arrow's 19 CSS px of the canvas). */ height?: number; - /** How far in front of out-of-screen content it floats, in eye-baseline units (default 0.03). */ + /** How far in front of out-of-screen content it floats, in eye-baseline units (default 0.015). */ margin?: number; /** Default `'hybrid'`. */ anchor?: CursorAnchorMode; diff --git a/docs/authoring-inline-3d.md b/docs/authoring-inline-3d.md index 7d91959..a05f99f 100644 --- a/docs/authoring-inline-3d.md +++ b/docs/authoring-inline-3d.md @@ -1073,8 +1073,8 @@ wall.addScene(canvas, (views, layer) => { - *On the plane it IS the OS cursor.* Nothing is drawn unless the cursor is at least about 0.3 mm (0.005 eye baselines, and half a panel pixel) in front of the glass. Otherwise the CSS cursor is the cursor, untouched, including any `cursor:` your page set on the canvas. - - *Margin.* `margin` (default 0.03 eye baselines, about 2 mm of on-screen disparity at a 65 mm - IPD) is how far in front of out-of-screen content it floats. + - *Margin.* `margin` (default 0.015 eye baselines, about 1 mm of on-screen disparity at a + 63–65 mm IPD) is how far in front of out-of-screen content it floats. - **What it draws.** The classic arrow (white, dark outline), at the OS cursor's size (19 CSS px of the canvas unless you pass `height`), with its tip on the click point. It hit-tests the arrow's footprint, not just the tip, so a nearer edge beside the hotspot can't cut through the arrow, and diff --git a/js/camera/pair-cursor.js b/js/camera/pair-cursor.js index 1f7a09a..2cb372a 100644 --- a/js/camera/pair-cursor.js +++ b/js/camera/pair-cursor.js @@ -10,7 +10,7 @@ // The policy is the one every DisplayXR depth cursor follows (runtime ADR-046 Amendment 2), taken // from the shared core (../inline3d-cursor-depth.js), not re-implemented here: // - never behind the glass: only content that comes OUT of the screen moves it, to the margin -// (0.03 eye baselines, ~2 mm) in front of that content (cursorTarget); +// (0.015 eye baselines, ~1 mm) in front of that content (cursorTarget); // - on the plane it IS the OS cursor: the arrow is drawn into both halves only while the filtered // disparity is past the draw threshold (-0.005 baselines, and >= 0.5 px; cursorShouldDraw), the // OS cursor hidden exactly then; @@ -109,7 +109,7 @@ export function pairPxPerBaseline(outW, tileCssWidth, metersPerCssPx, eyeSeparat /** * The arrow's target disparity in output px (+ = in front): the core's policy (cursorTarget) on * the content's disparity, converted through `pxPerBaseline`. In front of content that comes OUT - * of the screen by the margin (0.03 baselines), else the glass — never behind it. + * of the screen by the margin (0.015 baselines), else the glass — never behind it. */ export function pairCursorTarget(contentPx, pxPerBaseline, tuning = CURSOR_DEFAULT_TUNING) { if (contentPx === null || !Number.isFinite(contentPx) || !(pxPerBaseline > 0)) return 0; diff --git a/js/inline3d-cursor-depth.js b/js/inline3d-cursor-depth.js index 9409abb..5a0ddc8 100644 --- a/js/inline3d-cursor-depth.js +++ b/js/inline3d-cursor-depth.js @@ -46,7 +46,7 @@ // ./camera, which calls the pieces exported here): // - never behind the glass: target = d < 0 ? max(d - margin, dMin) : 0 — it only comes forward, // to stay ahead of content that comes OUT of the screen (cursorTarget); -// - margin 0.03 eye baselines (~2 mm of crossed on-screen disparity at a 65 mm IPD); +// - margin 0.015 eye baselines (~1 mm of crossed on-screen disparity at a 63-65 mm IPD); // - on the plane it IS the OS cursor: nothing is drawn while the filtered disparity is above // CURSOR_DRAW_THRESHOLD (-0.005, ~0.3 mm) or under CURSOR_DRAW_MIN_PX on the panel // (cursorShouldDraw), and the CSS cursor is left alone; @@ -99,10 +99,11 @@ export function cursorShouldDraw(disparity, pxPerBaseline) { */ export const CURSOR_DEFAULT_TUNING = Object.freeze({ /** - * How far in front of out-of-screen content the cursor floats: 0.03 eye baselines, ~2 mm of - * crossed on-screen disparity at a 65 mm IPD (ADR-046 Amendment 2; the reference app's value). + * How far in front of out-of-screen content the cursor floats: 0.015 eye baselines, ~1 mm of + * crossed on-screen disparity at a 63-65 mm IPD (ADR-046 Amendment 2 A.2; halved from 0.03 on + * 2026-10-11 after a Windows panel check: 0.03 floated visibly off the content). */ - margin: 0.03, + margin: 0.015, /** Comfort clamp: t >= 0.625, never more than ~3/8 of the way to the eye. */ minDisparity: -0.6, /** Never behind the glass: 0, and cursorTarget never goes above 0 whatever this says. */ diff --git a/js/inline3d-three.js b/js/inline3d-three.js index 094999d..374e88f 100644 --- a/js/inline3d-three.js +++ b/js/inline3d-three.js @@ -489,8 +489,8 @@ export class DepthCursor { * @param {number} [opts.height] sprite height as a fraction of the canvas height. Default: the * OS arrow's size, 19 CSS px of the canvas's on-page height (re-read every frame). * @param {number} [opts.color=0xffffff] the arrow's fill colour (the outline is always dark). - * @param {number} [opts.margin=0.03] how far in front of out-of-screen content it floats, in - * eye-baseline units (0.03 ≈ 2 mm of on-screen disparity at a 65 mm IPD). + * @param {number} [opts.margin=0.015] how far in front of out-of-screen content it floats, in + * eye-baseline units (0.015 ≈ 1 mm of on-screen disparity at a 63-65 mm IPD). * @param {'hybrid'|'screen'|'world'} [opts.anchor='hybrid'] where along that depth: 'hybrid' * follows the pointer exactly while it moves and stays world-fixed (parallaxes with the * content) while it is still; 'screen' never parallaxes; 'world' always does. diff --git a/model.d.ts b/model.d.ts index c682182..d9451df 100644 --- a/model.d.ts +++ b/model.d.ts @@ -53,7 +53,7 @@ export interface ModelOptions { * `'depth'`: a cursor that is never drawn on the glass behind content that pops out (ADR-046 + * Amendment 2, the same policy as native `XR_DXR_cursor_depth`). While the subject under the * pointer's footprint comes OUT of the screen, the classic arrow (the OS cursor's size, tip on - * the hotspot) is drawn the margin (0.03 eye baselines, ~2 mm) in front of it and the CSS cursor + * the hotspot) is drawn the margin (0.015 eye baselines, ~1 mm) in front of it and the CSS cursor * is hidden; over anything on or behind the glass nothing is drawn and the CSS cursor is the * cursor, untouched. Never behind the glass. OPT-IN: without it nothing * is loaded or run. PlayCanvas: hit-tested with the engine's depth picker (meshes and splats, diff --git a/splat.d.ts b/splat.d.ts index 19f2980..dd3675b 100644 --- a/splat.d.ts +++ b/splat.d.ts @@ -477,7 +477,7 @@ export interface SplatOptions { * `'depth'`: a cursor that is never drawn on the glass behind content that pops out (ADR-046 + * Amendment 2, the same policy as native `XR_DXR_cursor_depth`). While the subject under the * pointer's footprint comes OUT of the screen, the classic arrow (the OS cursor's size, tip on - * the hotspot) is drawn the margin (0.03 eye baselines, ~2 mm) in front of it and the CSS cursor + * the hotspot) is drawn the margin (0.015 eye baselines, ~1 mm) in front of it and the CSS cursor * is hidden; over anything on or behind the glass nothing is drawn and the CSS cursor is the * cursor, untouched. Never behind the glass. OPT-IN: without it nothing * is loaded or run. PlayCanvas: hit-tested with the engine's depth picker (meshes and splats, diff --git a/test/cursor-depth.test.mjs b/test/cursor-depth.test.mjs index 70d6d32..799408c 100644 --- a/test/cursor-depth.test.mjs +++ b/test/cursor-depth.test.mjs @@ -394,8 +394,11 @@ import { CURSOR_ANCHOR_MODES, CursorPointer, resolveCursorOption } from '../js/i const sub3 = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]]; -test('cursor-depth: the default margin is 0.03 baseline (~2 mm at 65 mm IPD), clamp [−0.6, 0]', () => { - assert.equal(T.margin, 0.03); +test('cursor-depth: the default margin is 0.015 baseline (~1 mm at 63–65 mm IPD), clamp [−0.6, 0]', () => { + // ADR-046 Amendment 2 A.2, halved from 0.03 on 2026-10-11 after a Windows panel check. + assert.equal(T.margin, 0.015); + // The behaviour, not just the field: content at d = −0.2 puts the cursor at −0.215. + assert.ok(Math.abs(cursorTarget(T, true, -0.2) - -0.215) < 1e-12); assert.equal(T.minDisparity, -0.6); assert.equal(T.maxDisparity, 0); }); diff --git a/test/e2e/cursor-depth.e2e.mjs b/test/e2e/cursor-depth.e2e.mjs index d1f90f3..f11f859 100644 --- a/test/e2e/cursor-depth.e2e.mjs +++ b/test/e2e/cursor-depth.e2e.mjs @@ -7,7 +7,7 @@ // viewer two hand-built off-axis views (what the DisplayXR Browser hands addScene) and a scripted // pointer. Pass = // model (a unit cube): over the cube the arrow's disparity (its tip) is the cube front's minus the -// margin (0.03 baselines — in front of it, by exactly the ADR-046 Amendment 2 policy), and off +// margin (0.015 baselines — in front of it, by exactly the ADR-046 Amendment 2 policy), and off // it NOTHING is drawn (the OS cursor is the cursor on the plane); // splat (butterfly.sog; 'three' = the Spark engine): over the subject the arrow is in front of the // glass, off it nothing is drawn; diff --git a/test/e2e/cursor-depth/index.html b/test/e2e/cursor-depth/index.html index e4aac48..963702a 100644 --- a/test/e2e/cursor-depth/index.html +++ b/test/e2e/cursor-depth/index.html @@ -11,7 +11,7 @@ DisplayXR Browser would hand addScene) and a scripted pointer, then checks the depth cursor: over the cube → the arrow's disparity ≈ (cube front disparity − margin), i.e. in front of it - (margin 0.03 baselines, ADR-046 Amendment 2), its TIP on the placement; + (margin 0.015 baselines, ADR-046 Amendment 2 A.2), its TIP on the placement; off the cube → NOTHING drawn (the sprite hidden) and the CSS cursor back: on the plane the OS cursor is the cursor (the policy's draw threshold). diff --git a/test/pair-cursor.test.mjs b/test/pair-cursor.test.mjs index 9231554..f835831 100644 --- a/test/pair-cursor.test.mjs +++ b/test/pair-cursor.test.mjs @@ -5,7 +5,7 @@ // - a mirrored view flips the pointer, never the disparity; // - the policy is the shared core's (ADR-046 Amendment 2), not a copy: the target is cursorTarget // on the content's disparity in eye baselines, converted through the panel's pixel pitch -// (displayMetrics) × the eye separation; margin 0.03 baselines; the draw threshold +// (displayMetrics) × the eye separation; margin 0.015 baselines; the draw threshold // (cursorShouldDraw: −0.005 baselines and ≥ 0.5 px) decides when the arrow is drawn; // - the cursor never goes INSIDE the display: content on or behind the glass → the plane; only // content that comes out moves it; @@ -123,9 +123,9 @@ test('footprint disparity: nothing to match (flat frame, no copy) → null; targ assert.equal(pairCursorTarget(null, 100), 0); }); -test('target: the core policy in px — content that comes out + 0.03 baselines (clamped); never behind the glass', () => { +test('target: the core policy in px — content that comes out + 0.015 baselines (clamped); never behind the glass', () => { const k = 100; // output px of on-screen disparity per eye baseline - assert.ok(Math.abs(pairCursorTarget(10, k) - (10 + 0.03 * k)) < 1e-9, 'the margin is 0.03 baselines'); + assert.ok(Math.abs(pairCursorTarget(10, k) - (10 + 0.015 * k)) < 1e-9, 'the margin is 0.015 baselines'); assert.ok(Math.abs(pairCursorTarget(500, k) - -T.minDisparity * k) < 1e-9, 'clamped at the core minDisparity'); assert.equal(pairCursorTarget(0, k), 0, 'content on the glass: the plane'); assert.equal(pairCursorTarget(-3, k), 0, 'content behind the glass: the cursor stays on the plane, never inside'); @@ -150,10 +150,10 @@ test('units: baseline ↔ px through the panel pixel pitch (displayMetrics) and const s = pairCursorScale(640, 320, 0.000225); assert.ok(Math.abs(0.002 * s - (0.002 / 0.000225) * 2) < 1e-9); assert.equal(pairCursorScale(640, 320, null), 2 / PAIR_CURSOR_CSS_PX_M, 'unknown pitch → the CSS reference px'); - // The margin, 0.03 of a 63 mm eye separation = 1.89 mm on the glass. + // The margin, 0.015 of a 63 mm eye separation = 0.945 mm on the glass. const k = pairPxPerBaseline(640, 320, 0.000225); assert.ok(Math.abs(k - s * DISPLAY_METRICS_DEFAULTS.eyeSeparationM) < 1e-9); - assert.ok(Math.abs(pairCursorTarget(1e-9, k) / s - 0.03 * 0.063) < 1e-9, 'margin ≈ 1.9 mm on the glass'); + assert.ok(Math.abs(pairCursorTarget(1e-9, k) / s - 0.015 * 0.063) < 1e-9, 'margin ≈ 0.95 mm on the glass'); }); /** A 2D context that records each filled path (the arrow's outline, then its fill). */ @@ -214,7 +214,7 @@ test('PairCursor: the OS arrow on the plane; the drawn arrow only over content t const k = pairPxPerBaseline(E, 384, null); assert.ok(Math.abs(p0b.pxPerBaseline - k) < 1e-9, 'px ↔ baselines through the tile scale (CSS reference px before displayMetrics answers)'); assert.ok(Math.abs(p0b.disparityPx - pairCursorTarget(12, k)) < 0.6, `caught up within ~100 ms (${p0.disparityPx} → ${p0b.disparityPx})`); - assert.ok(Math.abs(p0b.targetPx - (12 + 0.03 * k)) < 0.6, 'content + 0.03 baselines'); + assert.ok(Math.abs(p0b.targetPx - (12 + 0.015 * k)) < 0.6, 'content + 0.015 baselines'); const tipX = g.ops.at(-4).pts[0][0]; assert.ok(Math.abs(tipX - (0.5 * E + p0b.disparityPx / 2)) < 1e-9, 'left copy +d/2 about the hotspot'); assert.equal(g.ops.at(-4).pts[0][1], 0.5 * H, 'the TIP is on the hotspot row');