@displayxr/inline3d 1.45.1 → 1.46.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 +33 -0
- package/cursor-depth.d.ts +41 -0
- package/dist/call.js +6 -6
- package/index.d.ts +31 -0
- package/js/camera/pair-cursor.js +27 -1
- package/js/inline3d-cursor-depth-playcanvas.js +20 -2
- package/js/inline3d-cursor-depth.js +160 -1
- package/js/inline3d-splat-playcanvas.js +4 -1
- package/js/inline3d-three.js +45 -2
- package/js/inline3d-viewer.js +2 -1
- package/js/inline3d.js +36 -0
- package/package.json +1 -1
- package/three.d.ts +12 -2
package/index.d.ts
CHANGED
|
@@ -449,6 +449,14 @@ export interface TileHandle {
|
|
|
449
449
|
* The canvas size is read at the call: call again after a resize.
|
|
450
450
|
*/
|
|
451
451
|
displayMetrics(): Promise<DisplayMetrics>;
|
|
452
|
+
/**
|
|
453
|
+
* Runtime ADR-046 Amendment 4: hand this window's depth under the pointer to its live layer
|
|
454
|
+
* (`XRDisplayLayer.setCursorDepth`); nothing / null withdraws. Returns whether a live layer took
|
|
455
|
+
* it. `cursor: 'depth'` calls this for you; a page drawing its own pair cursor can too.
|
|
456
|
+
*/
|
|
457
|
+
setCursorDepth(depth?: XRCursorDepthInit | null): boolean;
|
|
458
|
+
/** "runtime" when the browser and the runtime draw this window's cursor; "none" otherwise (incl. no live layer yet). Never rejects. */
|
|
459
|
+
getCursorDepthSupport(): Promise<'runtime' | 'none'>;
|
|
452
460
|
/**
|
|
453
461
|
* Every rendering mode the display can be put in. See {@link XRDisplayRenderingMode}.
|
|
454
462
|
*
|
|
@@ -992,5 +1000,28 @@ export interface XRDisplayLayer {
|
|
|
992
1000
|
/** Undock this layer's asset into the floating native viewer. Optional; same probe rule. */
|
|
993
1001
|
undock?(init: UndockOptions): Promise<unknown>;
|
|
994
1002
|
getUndockCapabilities?(): Promise<UndockCapabilities>;
|
|
1003
|
+
/**
|
|
1004
|
+
* Runtime ADR-046 Amendment 4: the depth under the pointer, per animation frame while it is over
|
|
1005
|
+
* this layer's element; no argument / null / `{}` withdraws. Optional; same probe rule.
|
|
1006
|
+
*/
|
|
1007
|
+
setCursorDepth?(depth?: XRCursorDepthInit | null): void;
|
|
1008
|
+
/** "runtime" when the browser and the runtime draw this page's cursor; else "none". Optional; same probe rule. */
|
|
1009
|
+
getCursorDepthSupport?(): Promise<'runtime' | 'none'>;
|
|
995
1010
|
close(): void;
|
|
996
1011
|
}
|
|
1012
|
+
|
|
1013
|
+
/**
|
|
1014
|
+
* What a page passes to `setCursorDepth` (runtime ADR-046 Amendment 4). The runtime does the rest:
|
|
1015
|
+
* the pointer (read at draw time), the policy, the anchor, the look (the browser's own cursor).
|
|
1016
|
+
*/
|
|
1017
|
+
export interface XRCursorDepthInit {
|
|
1018
|
+
/**
|
|
1019
|
+
* On-screen disparity in DEVICE px between the two outermost rendered views, of the nearest
|
|
1020
|
+
* content under the cursor footprint; < 0 = crossed = in front of the glass. RAW content
|
|
1021
|
+
* disparity, not a target. Absent / null = over the element, nothing 3D under it.
|
|
1022
|
+
*/
|
|
1023
|
+
disparityPx?: number | null;
|
|
1024
|
+
/** Where it was sampled: element-normalised, v down. */
|
|
1025
|
+
u: number;
|
|
1026
|
+
v: number;
|
|
1027
|
+
}
|
package/js/camera/pair-cursor.js
CHANGED
|
@@ -26,9 +26,15 @@
|
|
|
26
26
|
// buffer px per metre = outW / tileCssWidth / metersPerCssPx (the tile handle's displayMetrics()),
|
|
27
27
|
// times the eye separation (displayMetrics' default, 63 mm). Loaded only when a page asks for
|
|
28
28
|
// `cursor: 'depth'` — zero cost otherwise.
|
|
29
|
+
//
|
|
30
|
+
// Where the browser and the runtime draw the web cursor (runtime ADR-046 Amendment 4: the tile's
|
|
31
|
+
// layer answers getCursorDepthSupport() "runtime"), nothing is drawn into the buffer: the matched
|
|
32
|
+
// content disparity goes to the layer RAW, in device px (−contentPx × device px per output px), and
|
|
33
|
+
// the runtime draws the browser's own cursor with the one policy. Elsewhere: the arrow, as above.
|
|
29
34
|
|
|
30
35
|
import {
|
|
31
36
|
CursorPointer,
|
|
37
|
+
CursorRuntimeLink,
|
|
32
38
|
CursorShapeSelector,
|
|
33
39
|
cursorArrowFootprint,
|
|
34
40
|
cursorShapePolys,
|
|
@@ -357,6 +363,8 @@ export class PairCursor {
|
|
|
357
363
|
this.eyeSeparationM = DISPLAY_METRICS_DEFAULTS.eyeSeparationM;
|
|
358
364
|
/** The last frame's placement, for diagnostics: `{ contentPx, targetPx, disparityPx, filteredPx, drawn, pxPerBaseline, soften }` (output px, + = in front) or null. */
|
|
359
365
|
this.last = null;
|
|
366
|
+
/** The runtime-drawn cursor (ADR-046 Amendment 4), bound to the tile's handle each frame. */
|
|
367
|
+
this.runtime = new CursorRuntimeLink();
|
|
360
368
|
}
|
|
361
369
|
|
|
362
370
|
get hovering() {
|
|
@@ -373,6 +381,7 @@ export class PairCursor {
|
|
|
373
381
|
* @param {number} nowSec
|
|
374
382
|
*/
|
|
375
383
|
draw(g, luma, geo, nowSec) {
|
|
384
|
+
const runtime = this.runtime.bind(geo && geo.handle);
|
|
376
385
|
const uv = this.pointer.uv;
|
|
377
386
|
if (!uv) return this.idle();
|
|
378
387
|
const [u, v] = uv;
|
|
@@ -383,8 +392,9 @@ export class PairCursor {
|
|
|
383
392
|
this._luma = luma;
|
|
384
393
|
this._uv = uv;
|
|
385
394
|
}
|
|
386
|
-
this._askMetrics(geo.handle);
|
|
387
395
|
const r = this.pointer.canvas.getBoundingClientRect?.();
|
|
396
|
+
if (runtime) return this._drawRuntime(u, v, r, geo);
|
|
397
|
+
this._askMetrics(geo.handle);
|
|
388
398
|
const pxPerBaseline = pairPxPerBaseline(geo.outW, r && r.width, this.metersPerCssPx, this.eyeSeparationM);
|
|
389
399
|
// The core's policy and filter, in its units (eye baselines, < 0 = in front).
|
|
390
400
|
const content = this._content === null ? null : -this._content / pxPerBaseline;
|
|
@@ -416,6 +426,20 @@ export class PairCursor {
|
|
|
416
426
|
return this.last;
|
|
417
427
|
}
|
|
418
428
|
|
|
429
|
+
// The runtime draws (ADR-046 Amendment 4): the matched content disparity, RAW, in device px
|
|
430
|
+
// between the two views (the two halves), < 0 crossed: one output px of an eye spans
|
|
431
|
+
// (tile CSS width × dpr) / outW device px. Nothing drawn into the buffer, the CSS cursor alone.
|
|
432
|
+
_drawRuntime(u, v, r, geo) {
|
|
433
|
+
this.pointer.hideCss(false);
|
|
434
|
+
this.filter = {};
|
|
435
|
+
const dpr = globalThis.devicePixelRatio > 0 ? globalThis.devicePixelRatio : 1;
|
|
436
|
+
const k = r && r.width > 0 && geo.outW > 0 ? (r.width * dpr) / geo.outW : null;
|
|
437
|
+
const px = this._content === null || k === null ? null : -this._content * k;
|
|
438
|
+
this.runtime.report(u, v, px);
|
|
439
|
+
this.last = { contentPx: this._content, targetPx: 0, disparityPx: 0, filteredPx: 0, drawn: false, runtime: true, runtimeDisparityPx: px, pxPerBaseline: null, soften: null, shape: null };
|
|
440
|
+
return this.last;
|
|
441
|
+
}
|
|
442
|
+
|
|
419
443
|
/** Ask the tile's handle once for the panel's pixel pitch; until it answers, the CSS reference px. */
|
|
420
444
|
_askMetrics(handle) {
|
|
421
445
|
if (!handle || handle === this._metricsFor || typeof handle.displayMetrics !== 'function') return;
|
|
@@ -438,6 +462,7 @@ export class PairCursor {
|
|
|
438
462
|
|
|
439
463
|
/** Not drawing this frame (pointer away, tile not woven): the CSS cursor comes back. */
|
|
440
464
|
idle() {
|
|
465
|
+
this.runtime.withdraw();
|
|
441
466
|
this.pointer.hideCss(false);
|
|
442
467
|
this.filter = {};
|
|
443
468
|
this._uv = null;
|
|
@@ -446,6 +471,7 @@ export class PairCursor {
|
|
|
446
471
|
}
|
|
447
472
|
|
|
448
473
|
dispose() {
|
|
474
|
+
this.runtime.dispose();
|
|
449
475
|
this.pointer.dispose();
|
|
450
476
|
this.shapes.dispose();
|
|
451
477
|
}
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
import {
|
|
27
27
|
CursorDepthPlacer,
|
|
28
28
|
CursorPointer,
|
|
29
|
+
CursorRuntimeLink,
|
|
29
30
|
CURSOR_DEFAULT_TUNING,
|
|
30
31
|
cursorArrowFootprint,
|
|
31
32
|
cursorDefaultHeight,
|
|
@@ -85,6 +86,8 @@ export class PlayCanvasDepthCursor {
|
|
|
85
86
|
this._sprite = null; // { mi, node, mat } — built on first use
|
|
86
87
|
/** Which sprite (CSS mirror / fixed / shapeFor); `.current` = the one drawn last. */
|
|
87
88
|
this.shapes = new CursorShapeSelector(this.pointer, { shape, shapeFor });
|
|
89
|
+
/** The runtime-drawn cursor (runtime ADR-046 Amendment 4): the depth goes to the frame's layer when it says "runtime". */
|
|
90
|
+
this.runtime = new CursorRuntimeLink();
|
|
88
91
|
}
|
|
89
92
|
|
|
90
93
|
/** The sprite drawn last: a CURSOR_SHAPES name, 'none' or 'image'. */
|
|
@@ -96,11 +99,14 @@ export class PlayCanvasDepthCursor {
|
|
|
96
99
|
* One 3D frame, before app.tick: schedule a pick, place the sprite, queue its lines.
|
|
97
100
|
* @param {Array<{proj: ArrayLike<number>, pose: ArrayLike<number>, width: number, height: number}>} entries
|
|
98
101
|
* this frame's views, poses in RIG space (the eye cameras' parent).
|
|
102
|
+
* @param {object|null} [layer] the frame's XRDisplayLayer: where it says "runtime" (ADR-046
|
|
103
|
+
* Amendment 4) the raw depth goes to it and no sprite is drawn.
|
|
99
104
|
*/
|
|
100
|
-
frame(entries) {
|
|
105
|
+
frame(entries, layer = null) {
|
|
101
106
|
const app = this.viewer.app;
|
|
102
107
|
const rig = this.viewer.rigNode;
|
|
103
108
|
const uv = this.pointer.uv;
|
|
109
|
+
const runtime = this.runtime.bind(layer);
|
|
104
110
|
if (this._disposed || !app || !rig || !uv || !entries || entries.length < 2) {
|
|
105
111
|
this.inactive();
|
|
106
112
|
return;
|
|
@@ -122,6 +128,16 @@ export class PlayCanvasDepthCursor {
|
|
|
122
128
|
if (this._frame++ % PICK_EVERY === 0 && !this._inFlight) this._pick(app, rig, entries[0], uv, h);
|
|
123
129
|
const now = (globalThis.performance ? globalThis.performance.now() : Date.now()) / 1000;
|
|
124
130
|
const p = this.placer.update(views, { u: uv[0], v: uv[1], nearestPoint: this.hit, cursorHeight: h, canvasPx: this.pointer.panelPx() }, now);
|
|
131
|
+
if (runtime) {
|
|
132
|
+
// The runtime draws the browser's own cursor at this depth: the raw content disparity in
|
|
133
|
+
// device px; no sprite, no CSS hide (the browser hides the OS cursor over what it drew).
|
|
134
|
+
this.runtime.report(uv[0], uv[1], p.solved ? p.contentDisparityPx : null);
|
|
135
|
+
this.placement = { ...p, active: false, runtime: true };
|
|
136
|
+
this.softening = null;
|
|
137
|
+
this.pointer.hideCss(false);
|
|
138
|
+
if (this._sprite) this._sprite.mi.visible = false;
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
125
141
|
this.placement = p;
|
|
126
142
|
this.pointer.hideCss(!!p.active);
|
|
127
143
|
const sprite = this._sprite || (p.active ? this._makeSprite(app) : null);
|
|
@@ -145,7 +161,8 @@ export class PlayCanvasDepthCursor {
|
|
|
145
161
|
|
|
146
162
|
/** Mono / no pointer / no views: no sprite, the CSS cursor back. */
|
|
147
163
|
inactive() {
|
|
148
|
-
|
|
164
|
+
this.runtime.withdraw();
|
|
165
|
+
if (this.placement.active || this.placement.runtime) this.placement = { active: false };
|
|
149
166
|
this.softening = null;
|
|
150
167
|
this.placer.update(null); // forget the hybrid anchor
|
|
151
168
|
if (this._sprite) this._sprite.mi.visible = false;
|
|
@@ -356,6 +373,7 @@ export class PlayCanvasDepthCursor {
|
|
|
356
373
|
|
|
357
374
|
dispose() {
|
|
358
375
|
this._disposed = true;
|
|
376
|
+
this.runtime.dispose();
|
|
359
377
|
this.pointer.dispose();
|
|
360
378
|
this.shapes.dispose();
|
|
361
379
|
try {
|
|
@@ -415,7 +415,12 @@ export class CursorDepthPlacer {
|
|
|
415
415
|
const baseline = Math.hypot(ra[12] - rb[12], ra[13] - rb[13], ra[14] - rb[14]);
|
|
416
416
|
const pxPerBaseline = hint.canvasPx > 0 ? (baseline / g.canvasHeight) * hint.canvasPx : null;
|
|
417
417
|
const active = cursorShouldDraw(disparity, pxPerBaseline);
|
|
418
|
-
|
|
418
|
+
// The RAW content disparity, before the policy (ADR-046 Amendment 4 sends this, in device px).
|
|
419
|
+
const contentDisparityPx = cursorDisparityPx(content, pxPerBaseline);
|
|
420
|
+
return {
|
|
421
|
+
active, solved: true, position, basis: g.basis, height, disparity, targetDisparity: target, anchored, geometry: g, pxPerBaseline,
|
|
422
|
+
contentDisparity: content, contentDisparityPx,
|
|
423
|
+
};
|
|
419
424
|
}
|
|
420
425
|
}
|
|
421
426
|
|
|
@@ -626,6 +631,160 @@ function pageCursorHidden(hide) {
|
|
|
626
631
|
else el.removeAttribute?.(CURSOR_HIDDEN_ATTR);
|
|
627
632
|
}
|
|
628
633
|
|
|
634
|
+
// ── The runtime draws the cursor (runtime ADR-046 Amendment 4) ──────────────────────────────
|
|
635
|
+
//
|
|
636
|
+
// In a DisplayXR Browser whose runtime can draw the web cursor (Windows, XR_DXR_cursor_depth
|
|
637
|
+
// spec >= 8), the page supplies ONLY the depth under the pointer: `layer.setCursorDepth({
|
|
638
|
+
// disparityPx, u, v })` per animation frame while the pointer is over the layer's element, and
|
|
639
|
+
// `layer.setCursorDepth()` to withdraw. The runtime reads the pointer itself at weave time,
|
|
640
|
+
// applies the one policy (margin, clamp, threshold, filter, hybrid anchor), draws the BROWSER's
|
|
641
|
+
// own cursor (so `grab`, `pointer`, `url(…)` look exactly as they would) with the B.7 softening,
|
|
642
|
+
// and the browser hides the OS cursor over what it drew. The SDK then draws no sprite and never
|
|
643
|
+
// touches the CSS cursor. Anywhere else (`getCursorDepthSupport()` says "none", or the methods do
|
|
644
|
+
// not exist) every backend keeps drawing its own sprite exactly as before — the fallback ladder.
|
|
645
|
+
|
|
646
|
+
/** Re-ask a target that answered "none" at most this often (ms) while the cursor runs… */
|
|
647
|
+
export const CURSOR_RUNTIME_RETRY_MS = 2000;
|
|
648
|
+
/** …this many times per target. The browser answers "none" until its weave session is live. */
|
|
649
|
+
export const CURSOR_RUNTIME_PROBES = 6;
|
|
650
|
+
/**
|
|
651
|
+
* After those, a "none" is still re-asked this often, without a cap: the weave session can take tens
|
|
652
|
+
* of seconds to report (a cold GPU, a slow first frame), longer than the fast probes cover. Only a
|
|
653
|
+
* browser that HAS the API and says "none" is ever asked; one without it is never probed at all.
|
|
654
|
+
*/
|
|
655
|
+
export const CURSOR_RUNTIME_SLOW_RETRY_MS = 10000;
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Does `target` (an XRDisplayLayer, or a tile handle that forwards to one) take a cursor depth?
|
|
659
|
+
* Method presence only — never reads an attribute off a prototype (the `Illegal invocation` trap).
|
|
660
|
+
*/
|
|
661
|
+
export function cursorRuntimeCapable(target) {
|
|
662
|
+
return !!target && typeof target.setCursorDepth === 'function' && typeof target.getCursorDepthSupport === 'function';
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Content disparity in eye-baseline units (< 0 in front) → on-screen device px between the two
|
|
667
|
+
* outermost rendered views (< 0 crossed), the unit `setCursorDepth` takes: `d × pxPerBaseline`.
|
|
668
|
+
* @returns {number|null} null when either is unknown (nothing under the pointer, canvas size unknown).
|
|
669
|
+
*/
|
|
670
|
+
export function cursorDisparityPx(contentDisparity, pxPerBaseline) {
|
|
671
|
+
if (contentDisparity === null || contentDisparity === undefined || !Number.isFinite(contentDisparity)) return null;
|
|
672
|
+
if (!(pxPerBaseline > 0) || !Number.isFinite(pxPerBaseline)) return null;
|
|
673
|
+
const px = contentDisparity * pxPerBaseline;
|
|
674
|
+
return Number.isFinite(px) ? px : null;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// What each target answered, shared by every link (one support probe per layer, not per cursor).
|
|
678
|
+
const _runtimeSupport = new WeakMap();
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* The page half of a runtime-drawn cursor, shared by every backend. Per frame: `bind(target)` with
|
|
682
|
+
* the layer the cursor's element weaves through (true = the runtime draws, so draw NO sprite and
|
|
683
|
+
* leave the CSS cursor alone), then either `report(u, v, disparityPx)` while the pointer is over
|
|
684
|
+
* the element or `withdraw()` when it is not. Constructing one costs nothing; a cursor builds it
|
|
685
|
+
* only under `cursor: 'depth'` (ADR-046 §0).
|
|
686
|
+
*/
|
|
687
|
+
export class CursorRuntimeLink {
|
|
688
|
+
constructor() {
|
|
689
|
+
/** The target bound last (an XRDisplayLayer or a tile handle), or null. */
|
|
690
|
+
this.target = null;
|
|
691
|
+
this._sent = false;
|
|
692
|
+
/** The last depth sent (diagnostics): `{ u, v, disparityPx }`, or null after a withdrawal. */
|
|
693
|
+
this.last = null;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/** "runtime" | "none" | "pending" for the bound target. */
|
|
697
|
+
get support() {
|
|
698
|
+
const t = this.target;
|
|
699
|
+
if (!cursorRuntimeCapable(t)) return 'none';
|
|
700
|
+
const s = _runtimeSupport.get(t);
|
|
701
|
+
return s ? s.state : 'pending';
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/** True while the bound target's runtime draws the cursor. */
|
|
705
|
+
get drawing() {
|
|
706
|
+
return this.support === 'runtime';
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* This frame's target. A new target withdraws the depth from the old one. Starts (or re-tries,
|
|
711
|
+
* rate-limited) the support probe. Returns `drawing`.
|
|
712
|
+
*/
|
|
713
|
+
bind(target) {
|
|
714
|
+
const t = target || null;
|
|
715
|
+
if (t !== this.target) {
|
|
716
|
+
this.withdraw();
|
|
717
|
+
this.target = t;
|
|
718
|
+
}
|
|
719
|
+
if (!cursorRuntimeCapable(t)) return false;
|
|
720
|
+
let s = _runtimeSupport.get(t);
|
|
721
|
+
if (!s) _runtimeSupport.set(t, (s = { state: 'pending', probes: 0, inFlight: false, at: 0 }));
|
|
722
|
+
if (s.state === 'runtime' || s.inFlight) return s.state === 'runtime';
|
|
723
|
+
const now = globalThis.performance ? globalThis.performance.now() : Date.now();
|
|
724
|
+
const wait = s.probes < CURSOR_RUNTIME_PROBES ? CURSOR_RUNTIME_RETRY_MS : CURSOR_RUNTIME_SLOW_RETRY_MS;
|
|
725
|
+
if (s.probes === 0 || now - s.at >= wait) {
|
|
726
|
+
s.probes++;
|
|
727
|
+
s.at = now;
|
|
728
|
+
s.inFlight = true;
|
|
729
|
+
let p;
|
|
730
|
+
try {
|
|
731
|
+
p = Promise.resolve(t.getCursorDepthSupport());
|
|
732
|
+
} catch (e) {
|
|
733
|
+
p = Promise.reject(e);
|
|
734
|
+
}
|
|
735
|
+
p.then(
|
|
736
|
+
(r) => {
|
|
737
|
+
s.inFlight = false;
|
|
738
|
+
s.state = r === 'runtime' ? 'runtime' : 'none';
|
|
739
|
+
},
|
|
740
|
+
() => {
|
|
741
|
+
s.inFlight = false;
|
|
742
|
+
s.state = 'none';
|
|
743
|
+
},
|
|
744
|
+
);
|
|
745
|
+
}
|
|
746
|
+
return s.state === 'runtime';
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* The depth under the pointer at element-normalised (u, v), v down: `disparityPx` in device px
|
|
751
|
+
* between the outermost views (< 0 = in front), RAW — the runtime applies the policy — or null
|
|
752
|
+
* for "over the element, nothing 3D under it". A no-op unless the runtime draws.
|
|
753
|
+
*/
|
|
754
|
+
report(u, v, disparityPx) {
|
|
755
|
+
if (!this.drawing) return false;
|
|
756
|
+
const d = { u: clamp01(u), v: clamp01(v) };
|
|
757
|
+
if (disparityPx !== null && disparityPx !== undefined && Number.isFinite(disparityPx)) d.disparityPx = disparityPx;
|
|
758
|
+
try {
|
|
759
|
+
this.target.setCursorDepth(d);
|
|
760
|
+
} catch {
|
|
761
|
+
return false;
|
|
762
|
+
}
|
|
763
|
+
this._sent = true;
|
|
764
|
+
this.last = { u: d.u, v: d.v, disparityPx: d.disparityPx ?? null };
|
|
765
|
+
return true;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/** Withdraw the depth (pointer off the element, 2D, teardown). Idempotent: one call per withdrawal. */
|
|
769
|
+
withdraw() {
|
|
770
|
+
this.last = null;
|
|
771
|
+
if (!this._sent) return;
|
|
772
|
+
this._sent = false;
|
|
773
|
+
try {
|
|
774
|
+
this.target?.setCursorDepth();
|
|
775
|
+
} catch {
|
|
776
|
+
/* a closed layer: its depth went with it */
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
dispose() {
|
|
781
|
+
this.withdraw();
|
|
782
|
+
this.target = null;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
const clamp01 = (x) => (x > 0 ? (x < 1 ? x : 1) : 0);
|
|
787
|
+
|
|
629
788
|
/**
|
|
630
789
|
* A footprint ring: the point (u, v) plus 8 points on a circle of radius 0.75 × the sprite
|
|
631
790
|
* height (canvas heights) around it.
|
|
@@ -1306,6 +1306,8 @@ export class PlayCanvasSplatViewer {
|
|
|
1306
1306
|
onFrame(views, layer, frame) {
|
|
1307
1307
|
if (this._disposed) return;
|
|
1308
1308
|
if (this._mode !== '3d') this.stopMono();
|
|
1309
|
+
// cursor: 'depth' hands its depth to this layer where the runtime draws the web cursor (ADR-046 Amendment 4).
|
|
1310
|
+
this._frameLayer = layer || null;
|
|
1309
1311
|
// The rig these views were located with: Blink chained the rig declared BEFORE this callback,
|
|
1310
1312
|
// and the tick below may declare a new one (a focus ease) for the NEXT locate.
|
|
1311
1313
|
if (this.layerRigs?.active) {
|
|
@@ -1348,6 +1350,7 @@ export class PlayCanvasSplatViewer {
|
|
|
1348
1350
|
this._monoRaf = requestAnimationFrame(loop);
|
|
1349
1351
|
this._beforeFrame?.(null);
|
|
1350
1352
|
if (this._disposed || this._mode !== 'mono') return; // the callback removed or re-wove us
|
|
1353
|
+
this._frameLayer = null;
|
|
1351
1354
|
this.cursorDepth?.inactive(); // 2D: the normal cursor
|
|
1352
1355
|
this._tick();
|
|
1353
1356
|
this._drawMono();
|
|
@@ -2221,7 +2224,7 @@ export class PlayCanvasSplatViewer {
|
|
|
2221
2224
|
// handle.setVideo's plane: size, eye split, and a new frame's upload (./inline3d-splat-video.js).
|
|
2222
2225
|
this._videoPlane?.beforeDraw(entries, rect);
|
|
2223
2226
|
// cursor: 'depth' — place the sprite and queue its lines for the tick below (no-op unless opted in).
|
|
2224
|
-
this.cursorDepth?.frame(entries);
|
|
2227
|
+
this.cursorDepth?.frame(entries, this._frameLayer);
|
|
2225
2228
|
app.tick(now());
|
|
2226
2229
|
this._afterTick();
|
|
2227
2230
|
return true;
|
package/js/inline3d-three.js
CHANGED
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
import {
|
|
56
56
|
CursorDepthPlacer,
|
|
57
57
|
CursorPointer,
|
|
58
|
+
CursorRuntimeLink,
|
|
58
59
|
cursorArrowFootprint,
|
|
59
60
|
cursorDefaultHeight,
|
|
60
61
|
cursorModelMatrix,
|
|
@@ -569,6 +570,13 @@ export class DepthCursor {
|
|
|
569
570
|
this.shapes = new CursorShapeSelector(this.pointer, { shape, shapeFor });
|
|
570
571
|
this._tex = null; // { geometry, material } — the textured quad, built on the first non-arrow sprite
|
|
571
572
|
this._textures = new Map(); // sprite key → THREE texture
|
|
573
|
+
/**
|
|
574
|
+
* The runtime-drawn cursor (runtime ADR-046 Amendment 4): when the frame's layer says
|
|
575
|
+
* "runtime", the depth goes to the browser and no sprite is drawn. Inert without a layer.
|
|
576
|
+
*/
|
|
577
|
+
this.runtime = new CursorRuntimeLink();
|
|
578
|
+
/** The XRDisplayLayer this canvas weaves through; `update(views, { layer })` sets it per frame. */
|
|
579
|
+
this.layer = null;
|
|
572
580
|
|
|
573
581
|
// The arrow: a dark outline polygon, the fill inset over it; tip at the origin (the hotspot).
|
|
574
582
|
const fill = [((color >> 16) & 255) / 255, ((color >> 8) & 255) / 255, (color & 255) / 255, 1];
|
|
@@ -606,10 +614,22 @@ export class DepthCursor {
|
|
|
606
614
|
|
|
607
615
|
/**
|
|
608
616
|
* Place the sprite for this frame. Call with the views you are about to render.
|
|
617
|
+
*
|
|
618
|
+
* Pass the frame's `layer` (the second argument of wall.addScene's callback) as
|
|
619
|
+
* `update(views, { layer })`: where the browser and the runtime draw the web cursor
|
|
620
|
+
* (`layer.getCursorDepthSupport()` → "runtime", runtime ADR-046 Amendment 4) the depth under the
|
|
621
|
+
* pointer is handed to the layer and no sprite is drawn — the browser's own cursor, lifted by
|
|
622
|
+
* the runtime. Without a layer, or where it says "none", the sprite is drawn as always.
|
|
609
623
|
* @param {ArrayLike<XRView|{projectionMatrix:ArrayLike<number>,transformMatrix:ArrayLike<number>}>} views
|
|
610
|
-
* @param {number} [
|
|
624
|
+
* @param {number|{nowMs?: number, layer?: object|null}} [opts] the time (ms), or `{ nowMs, layer }`.
|
|
611
625
|
*/
|
|
612
|
-
update(views,
|
|
626
|
+
update(views, opts) {
|
|
627
|
+
let nowMs = opts;
|
|
628
|
+
if (opts && typeof opts === 'object') {
|
|
629
|
+
if ('layer' in opts) this.layer = opts.layer || null;
|
|
630
|
+
nowMs = opts.nowMs;
|
|
631
|
+
}
|
|
632
|
+
if (!(typeof nowMs === 'number' && Number.isFinite(nowMs))) nowMs = globalThis.performance ? globalThis.performance.now() : Date.now();
|
|
613
633
|
if (this.debugStats) return this._updateMeasured(views, nowMs);
|
|
614
634
|
return this._update(views, nowMs);
|
|
615
635
|
}
|
|
@@ -631,6 +651,7 @@ export class DepthCursor {
|
|
|
631
651
|
}
|
|
632
652
|
|
|
633
653
|
_update(views, nowMs) {
|
|
654
|
+
if (this.runtime.bind(this.layer)) return this._updateRuntime(views, nowMs);
|
|
634
655
|
const vs = this._worldViews(views);
|
|
635
656
|
const uv = this.pointer.uv;
|
|
636
657
|
let p = { active: false };
|
|
@@ -661,6 +682,27 @@ export class DepthCursor {
|
|
|
661
682
|
return p;
|
|
662
683
|
}
|
|
663
684
|
|
|
685
|
+
// The runtime draws (ADR-046 Amendment 4): the same pick and the same placement maths, but the
|
|
686
|
+
// RAW content disparity goes to the layer in device px, and nothing is drawn or hidden here.
|
|
687
|
+
_updateRuntime(views, nowMs) {
|
|
688
|
+
this.object.visible = false;
|
|
689
|
+
this.softening = null;
|
|
690
|
+
this.pointer.hideCss(false);
|
|
691
|
+
const uv = this.pointer.uv;
|
|
692
|
+
const vs = uv && views && views.length >= 2 ? this._worldViews(views) : null;
|
|
693
|
+
if (!vs) {
|
|
694
|
+
this.placer.update(null); // forget the hybrid anchor
|
|
695
|
+
this.runtime.withdraw();
|
|
696
|
+
return (this.placement = { active: false, runtime: true });
|
|
697
|
+
}
|
|
698
|
+
const [u, v] = uv;
|
|
699
|
+
const h = this.height ?? cursorDefaultHeight(this.pointer.cssHeight());
|
|
700
|
+
const nearestPoint = this._footprintHit(vs, u, v, h);
|
|
701
|
+
const p = this.placer.update(vs, { u, v, nearestPoint, cursorHeight: h, canvasPx: this.pointer.panelPx() }, nowMs / 1000);
|
|
702
|
+
this.runtime.report(u, v, p.solved ? p.contentDisparityPx : null);
|
|
703
|
+
return (this.placement = { ...p, active: false, runtime: true });
|
|
704
|
+
}
|
|
705
|
+
|
|
664
706
|
// Crisp frames draw the arrow mesh exactly as without softening; softened ones swap in a quad
|
|
665
707
|
// whose shader evaluates the arrow at the runtime's 13 blur taps, times the opacity.
|
|
666
708
|
_useSoft(s) {
|
|
@@ -804,6 +846,7 @@ export class DepthCursor {
|
|
|
804
846
|
|
|
805
847
|
/** Remove listeners, give the CSS cursor back, free the sprite's GPU resources. */
|
|
806
848
|
dispose() {
|
|
849
|
+
this.runtime.dispose();
|
|
807
850
|
this.pointer.dispose();
|
|
808
851
|
this.shapes.dispose();
|
|
809
852
|
this.object.visible = false;
|
package/js/inline3d-viewer.js
CHANGED
|
@@ -740,7 +740,8 @@ export class SceneViewer {
|
|
|
740
740
|
// cursor: 'depth' (ADR-046) — only when a page opted in; null otherwise.
|
|
741
741
|
if (this.cursorDepth) {
|
|
742
742
|
this.scene.updateMatrixWorld(); // the hit test reads THIS frame's subject pose
|
|
743
|
-
|
|
743
|
+
// The layer too: where the runtime draws the web cursor (ADR-046 Amendment 4) the depth goes to it.
|
|
744
|
+
this.cursorDepth.update(g.entries.map((e) => ({ projectionMatrix: e.proj, transformMatrix: e.pose })), { layer });
|
|
744
745
|
}
|
|
745
746
|
const r = this.renderer;
|
|
746
747
|
// getViewport() splits canvas.width; a drawing buffer the browser clamped behind our back
|
package/js/inline3d.js
CHANGED
|
@@ -1274,6 +1274,42 @@ class Inline3D {
|
|
|
1274
1274
|
win.viewRig = rig || null;
|
|
1275
1275
|
return this._pushViewRig(win);
|
|
1276
1276
|
},
|
|
1277
|
+
/**
|
|
1278
|
+
* Runtime ADR-046 Amendment 4: the depth under the pointer for THIS window, forwarded to its
|
|
1279
|
+
* live layer's `setCursorDepth` — `{ disparityPx, u, v }` (device px between the outermost
|
|
1280
|
+
* views, < 0 in front, RAW content disparity; u/v element-normalised, v down; no
|
|
1281
|
+
* `disparityPx` = nothing 3D there), or nothing / null to withdraw. Returns whether a live
|
|
1282
|
+
* layer took it. The depth cursors call this for you under `cursor: 'depth'`.
|
|
1283
|
+
*/
|
|
1284
|
+
setCursorDepth: (depth) => {
|
|
1285
|
+
const layer = win.layer;
|
|
1286
|
+
if (!layer || typeof layer.setCursorDepth !== 'function') return false;
|
|
1287
|
+
try {
|
|
1288
|
+
if (depth) layer.setCursorDepth(depth);
|
|
1289
|
+
else layer.setCursorDepth();
|
|
1290
|
+
return true;
|
|
1291
|
+
} catch {
|
|
1292
|
+
return false;
|
|
1293
|
+
}
|
|
1294
|
+
},
|
|
1295
|
+
/**
|
|
1296
|
+
* "runtime" when the browser AND the runtime draw this window's cursor (the page then supplies
|
|
1297
|
+
* only the depth, setCursorDepth); "none" otherwise — including no live layer yet, so ask
|
|
1298
|
+
* again later. Never rejects.
|
|
1299
|
+
* @returns {Promise<'runtime'|'none'>}
|
|
1300
|
+
*/
|
|
1301
|
+
getCursorDepthSupport: () => {
|
|
1302
|
+
const layer = win.layer;
|
|
1303
|
+
if (!layer || typeof layer.getCursorDepthSupport !== 'function') return Promise.resolve('none');
|
|
1304
|
+
try {
|
|
1305
|
+
return Promise.resolve(layer.getCursorDepthSupport()).then(
|
|
1306
|
+
(s) => (s === 'runtime' ? 'runtime' : 'none'),
|
|
1307
|
+
() => 'none',
|
|
1308
|
+
);
|
|
1309
|
+
} catch {
|
|
1310
|
+
return Promise.resolve('none');
|
|
1311
|
+
}
|
|
1312
|
+
},
|
|
1277
1313
|
/**
|
|
1278
1314
|
* The panel this window is weaving on: physical size in metres, pixel size, and the view
|
|
1279
1315
|
* scale the runtime RECOMMENDS. Resolves null on a machine with no glasses-free display.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@displayxr/inline3d",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.46.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",
|
package/three.d.ts
CHANGED
|
@@ -143,6 +143,7 @@ export class EdgeFeather {
|
|
|
143
143
|
): void;
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
+
import type { CursorRuntimeLink } from './cursor-depth.js';
|
|
146
147
|
import type { CursorAnchorMode, CursorPlacement, CursorShape, CursorShapeHit, CursorShapeOption, CursorSoftenOption, CursorSoftenParams, CursorSoftening, CursorView } from './cursor-depth.js';
|
|
147
148
|
|
|
148
149
|
/** One second of `debug` stats: each figure as `{ mean, p95, max, n }`. */
|
|
@@ -234,8 +235,17 @@ export class DepthCursor {
|
|
|
234
235
|
readonly softening: CursorSoftening | null;
|
|
235
236
|
/** The sprite drawn last: a name, 'none' or 'image'. */
|
|
236
237
|
readonly shape: CursorShape | 'none' | 'image';
|
|
237
|
-
/**
|
|
238
|
-
|
|
238
|
+
/** The runtime-drawn cursor link (runtime ADR-046 Amendment 4). */
|
|
239
|
+
readonly runtime: CursorRuntimeLink;
|
|
240
|
+
/** The layer this canvas weaves through, as last passed to `update(views, { layer })`. */
|
|
241
|
+
layer: unknown;
|
|
242
|
+
/**
|
|
243
|
+
* Place the sprite for this frame — call with the views you are about to render, before rendering.
|
|
244
|
+
* Pass `{ layer }` (addScene's second callback argument): where it says "runtime" (ADR-046
|
|
245
|
+
* Amendment 4) the depth goes to the layer and the browser's own cursor is lifted by the runtime;
|
|
246
|
+
* no sprite is drawn. A number is the time in ms, as before.
|
|
247
|
+
*/
|
|
248
|
+
update(views: ArrayLike<XRView | CursorView> | null, opts?: number | { nowMs?: number; layer?: unknown }): CursorPlacement | { active: false; runtime?: boolean };
|
|
239
249
|
/** Set the pointer directly (canvas-normalised, v down), or null — for scripted input. */
|
|
240
250
|
setPointer(u: number | null, v?: number): void;
|
|
241
251
|
/** Remove listeners, restore the CSS cursor, free the sprite's GPU resources. */
|