@displayxr/inline3d 1.35.0 → 1.37.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 +68 -0
- package/cursor-depth.d.ts +113 -0
- package/dist/call.js +1 -1
- package/js/inline3d-cursor-depth-playcanvas.js +337 -0
- package/js/inline3d-cursor-depth.js +466 -0
- package/js/inline3d-cursor-option.js +38 -0
- package/js/inline3d-model-playcanvas.js +1 -0
- package/js/inline3d-model.js +4 -1
- package/js/inline3d-playcanvas-engine.js +4 -0
- package/js/inline3d-splat-playcanvas.js +21 -0
- package/js/inline3d-splat.js +6 -1
- package/js/inline3d-three.js +223 -0
- package/js/inline3d-viewer.js +24 -0
- package/model.d.ts +12 -0
- package/package.json +10 -1
- package/splat.d.ts +12 -0
- package/three.d.ts +57 -0
package/js/inline3d-three.js
CHANGED
|
@@ -52,6 +52,17 @@
|
|
|
52
52
|
// descriptor and nothing else: no Kooima, no off-axis math, no scale, here or anywhere in this
|
|
53
53
|
// SDK. That stays in the runtime, which is the point of the extension.
|
|
54
54
|
|
|
55
|
+
import {
|
|
56
|
+
CursorDepthPlacer,
|
|
57
|
+
CursorPointer,
|
|
58
|
+
cursorCrosshairMesh,
|
|
59
|
+
cursorFootprint,
|
|
60
|
+
cursorModelMatrix,
|
|
61
|
+
cursorViewRay,
|
|
62
|
+
CURSOR_DEFAULT_HEIGHT,
|
|
63
|
+
CURSOR_DEFAULT_TUNING,
|
|
64
|
+
} from './inline3d-cursor-depth.js';
|
|
65
|
+
|
|
55
66
|
/**
|
|
56
67
|
* A reusable three.js camera driven directly by an XRView's matrices. Construct once with
|
|
57
68
|
* your THREE namespace and reuse across frames/windows.
|
|
@@ -413,3 +424,215 @@ export class EdgeFeather {
|
|
|
413
424
|
renderer.autoClear = prevAutoClear;
|
|
414
425
|
}
|
|
415
426
|
}
|
|
427
|
+
|
|
428
|
+
// ── Depth-aware cursor ─────────────────────────────────────────────────────────────────────
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* A hit test for {@link DepthCursor} that raycasts three.js objects: the first hit along the
|
|
432
|
+
* ray, as a world point, or null.
|
|
433
|
+
*
|
|
434
|
+
* @param {object} THREE your imported three.js module namespace.
|
|
435
|
+
* @param {object[]} targets the Object3Ds that count as content (searched recursively).
|
|
436
|
+
*/
|
|
437
|
+
export function raycastHitTest(THREE, targets) {
|
|
438
|
+
const raycaster = new THREE.Raycaster();
|
|
439
|
+
const o = new THREE.Vector3();
|
|
440
|
+
const d = new THREE.Vector3();
|
|
441
|
+
return (origin, direction) => {
|
|
442
|
+
o.set(origin[0], origin[1], origin[2]);
|
|
443
|
+
d.set(direction[0], direction[1], direction[2]).normalize();
|
|
444
|
+
raycaster.set(o, d);
|
|
445
|
+
const hit = raycaster.intersectObjects(targets, true)[0];
|
|
446
|
+
return hit ? [hit.point.x, hit.point.y, hit.point.z] : null;
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* A cursor that rises to the depth of the content under it, so it is never drawn behind
|
|
452
|
+
* something that pops out of the glass (a depth violation). The page supplies the content —
|
|
453
|
+
* a hit test — and this does the rest: tracks the pointer over the canvas, hit-tests the
|
|
454
|
+
* cursor FOOTPRINT (not just the hotspot), places a crosshair sprite with the same maths and
|
|
455
|
+
* defaults as the runtime's XR_DXR_cursor_depth (ADR-046), and hides the CSS cursor exactly
|
|
456
|
+
* while the sprite replaces it.
|
|
457
|
+
*
|
|
458
|
+
* Opt-in: nothing runs unless you construct one and call update(). In 2D (one view) or with the
|
|
459
|
+
* pointer off the canvas it is inactive, the sprite is hidden and the CSS cursor is back.
|
|
460
|
+
*
|
|
461
|
+
* const cursor = new DepthCursor(THREE, { canvas, hitTest: raycastHitTest(THREE, [model]) });
|
|
462
|
+
* scene.add(cursor.object);
|
|
463
|
+
* wall.addScene(canvas, (views, layer) => {
|
|
464
|
+
* cursor.update(views); // first, with THIS frame's views
|
|
465
|
+
* // ... render every view as usual; the sprite draws last, depth test off ...
|
|
466
|
+
* });
|
|
467
|
+
*
|
|
468
|
+
* Head motion: the sprite sits on the line from the viewer through the pointer's point on the
|
|
469
|
+
* canvas, so its image on the glass does not move when the head does — it stays under the
|
|
470
|
+
* mouse like the OS cursor, and only its depth follows the content under that line of sight.
|
|
471
|
+
*
|
|
472
|
+
* The attach pattern (eye cameras parented under an app camera): pass `viewSpace` = that
|
|
473
|
+
* parent, so the views' local transforms are carried into world space; keep `cursor.object` at
|
|
474
|
+
* the scene root.
|
|
475
|
+
*/
|
|
476
|
+
export class DepthCursor {
|
|
477
|
+
/**
|
|
478
|
+
* @param {object} THREE your imported three.js module namespace.
|
|
479
|
+
* @param {object} opts
|
|
480
|
+
* @param {HTMLElement} opts.canvas the woven canvas (pointer is tracked over it).
|
|
481
|
+
* @param {(origin:number[], direction:number[]) => number[]|null} opts.hitTest nearest
|
|
482
|
+
* content point along a ray in WORLD space, or null. {@link raycastHitTest} for meshes;
|
|
483
|
+
* for splats return the renderer's expected depth along the ray (never raycast splats).
|
|
484
|
+
* @param {number} [opts.height=0.03] sprite height as a fraction of the canvas height.
|
|
485
|
+
* @param {number} [opts.color=0xffd61a] fill colour (the outline is always dark).
|
|
486
|
+
* @param {number} [opts.margin=0.005] how far in front of the content it floats, in eye-baseline
|
|
487
|
+
* units (0.005 ≈ 1.5 mm at 60 cm: it rests on the content).
|
|
488
|
+
* @param {'hybrid'|'screen'|'world'} [opts.anchor='hybrid'] where along that depth: 'hybrid'
|
|
489
|
+
* follows the pointer exactly while it moves and stays world-fixed (parallaxes with the
|
|
490
|
+
* content) while it is still; 'screen' never parallaxes; 'world' always does.
|
|
491
|
+
* @param {'canvas'|'window'} [opts.pointerScope='canvas'] 'window' keeps the cursor over DOM
|
|
492
|
+
* layered on the canvas (overlay buttons) and hides the CSS cursor page-wide meanwhile.
|
|
493
|
+
* @param {object} [opts.viewSpace] Object3D whose world matrix maps view transforms to world.
|
|
494
|
+
* @param {number} [opts.raysPerFrame=0] 0 = the whole footprint every frame (two eyes × 9
|
|
495
|
+
* points). N > 0 = an EXPENSIVE hit test (e.g. a gaussian-splat raycast, ~8 ms a ray):
|
|
496
|
+
* cast N rays a frame from the first eye, cycling through the footprint, and take the
|
|
497
|
+
* nearest of the most recent full cycle.
|
|
498
|
+
*/
|
|
499
|
+
constructor(
|
|
500
|
+
THREE,
|
|
501
|
+
{ canvas, hitTest, height = CURSOR_DEFAULT_HEIGHT, color = 0xffd61a, viewSpace = null, raysPerFrame = 0, margin, anchor = 'hybrid', pointerScope = 'canvas' },
|
|
502
|
+
) {
|
|
503
|
+
this._THREE = THREE;
|
|
504
|
+
this.canvas = canvas;
|
|
505
|
+
this.hitTest = hitTest;
|
|
506
|
+
this.height = height;
|
|
507
|
+
this.viewSpace = viewSpace;
|
|
508
|
+
this.raysPerFrame = raysPerFrame;
|
|
509
|
+
this._ring = []; // amortised mode: the last hit per footprint point (null = a miss)
|
|
510
|
+
this._next = 0;
|
|
511
|
+
const tuning = margin > 0 && Number.isFinite(margin) ? { ...CURSOR_DEFAULT_TUNING, margin } : CURSOR_DEFAULT_TUNING;
|
|
512
|
+
this.placer = new CursorDepthPlacer(tuning, { anchor });
|
|
513
|
+
this.pointer = new CursorPointer(canvas, { scope: pointerScope });
|
|
514
|
+
/** The last placement (diagnostics): `{active, position, height, disparity, targetDisparity, anchored}`. */
|
|
515
|
+
this.placement = { active: false };
|
|
516
|
+
|
|
517
|
+
// Filled strokes with a dark outline (a 1 px line reads too thin through the lens).
|
|
518
|
+
const fill = [((color >> 16) & 255) / 255, ((color >> 8) & 255) / 255, (color & 255) / 255, 1];
|
|
519
|
+
const m = cursorCrosshairMesh(fill);
|
|
520
|
+
const geom = new THREE.BufferGeometry();
|
|
521
|
+
geom.setAttribute('position', new THREE.Float32BufferAttribute(m.positions, 3));
|
|
522
|
+
geom.setAttribute('color', new THREE.Float32BufferAttribute(m.colors, 4));
|
|
523
|
+
const mat = new THREE.MeshBasicMaterial({
|
|
524
|
+
vertexColors: true,
|
|
525
|
+
transparent: true,
|
|
526
|
+
depthTest: false,
|
|
527
|
+
depthWrite: false,
|
|
528
|
+
side: THREE.DoubleSide,
|
|
529
|
+
toneMapped: false,
|
|
530
|
+
});
|
|
531
|
+
this.object = new THREE.Mesh(geom, mat);
|
|
532
|
+
this.object.name = 'inline3d-cursor';
|
|
533
|
+
this.object.matrixAutoUpdate = false;
|
|
534
|
+
this.object.frustumCulled = false;
|
|
535
|
+
this.object.renderOrder = 1e9; // last: never occluded, it is in front by construction
|
|
536
|
+
this.object.visible = false;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/** Set the pointer position directly (canvas-normalised, v down), or null — for scripted input. */
|
|
540
|
+
setPointer(u, v) {
|
|
541
|
+
this.pointer.set(u, v);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Place the sprite for this frame. Call with the views you are about to render.
|
|
546
|
+
* @param {ArrayLike<XRView|{projectionMatrix:ArrayLike<number>,transformMatrix:ArrayLike<number>}>} views
|
|
547
|
+
* @param {number} [nowMs=performance.now()]
|
|
548
|
+
*/
|
|
549
|
+
update(views, nowMs = globalThis.performance ? globalThis.performance.now() : Date.now()) {
|
|
550
|
+
const vs = this._worldViews(views);
|
|
551
|
+
const uv = this.pointer.uv;
|
|
552
|
+
let p = { active: false };
|
|
553
|
+
if (uv && vs.length >= 2) {
|
|
554
|
+
const [u, v] = uv;
|
|
555
|
+
const nearestPoint = this._footprintHit(vs, u, v);
|
|
556
|
+
p = this.placer.update(vs, { u, v, nearestPoint, cursorHeight: this.height }, nowMs / 1000);
|
|
557
|
+
}
|
|
558
|
+
this.placement = p;
|
|
559
|
+
this.object.visible = !!p.active;
|
|
560
|
+
if (p.active) {
|
|
561
|
+
this.object.matrix.fromArray(cursorModelMatrix(p));
|
|
562
|
+
this.object.matrixWorldNeedsUpdate = true;
|
|
563
|
+
}
|
|
564
|
+
this.pointer.hideCss(!!p.active);
|
|
565
|
+
return p;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/** Remove listeners, give the CSS cursor back, free the sprite's GPU resources. */
|
|
569
|
+
dispose() {
|
|
570
|
+
this.pointer.dispose();
|
|
571
|
+
this.object.visible = false;
|
|
572
|
+
this.object.geometry.dispose();
|
|
573
|
+
this.object.material.dispose();
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Nearest content point under the footprint, from the two outer views (or, amortised, from
|
|
577
|
+
// the first eye over the last cycle of footprint points).
|
|
578
|
+
_footprintHit(views, u, v) {
|
|
579
|
+
const a = views[0], b = views[views.length - 1];
|
|
580
|
+
const m = a.transformMatrix;
|
|
581
|
+
const fl = Math.hypot(m[8], m[9], m[10]) || 1;
|
|
582
|
+
const f = [-m[8] / fl, -m[9] / fl, -m[10] / fl];
|
|
583
|
+
const fp = cursorFootprint(u, v, this.height, this.pointer.aspect());
|
|
584
|
+
let candidates;
|
|
585
|
+
if (this.raysPerFrame > 0) {
|
|
586
|
+
const ring = this._ring;
|
|
587
|
+
ring.length = fp.length;
|
|
588
|
+
for (let i = 0; i < this.raysPerFrame; i++) {
|
|
589
|
+
const k = this._next++ % fp.length;
|
|
590
|
+
const ray = cursorViewRay(a, fp[k][0], fp[k][1]);
|
|
591
|
+
ring[k] = this.hitTest(ray.origin, ray.direction);
|
|
592
|
+
}
|
|
593
|
+
candidates = ring;
|
|
594
|
+
} else {
|
|
595
|
+
candidates = [];
|
|
596
|
+
for (const [su, sv] of fp) {
|
|
597
|
+
for (const view of [a, b]) {
|
|
598
|
+
const ray = cursorViewRay(view, su, sv);
|
|
599
|
+
candidates.push(this.hitTest(ray.origin, ray.direction));
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
let best = Infinity, nearest = null;
|
|
604
|
+
for (const p of candidates) {
|
|
605
|
+
if (!p) continue;
|
|
606
|
+
const depth = p[0] * f[0] + p[1] * f[1] + p[2] * f[2]; // smaller = nearer the viewer
|
|
607
|
+
if (depth < best) {
|
|
608
|
+
best = depth;
|
|
609
|
+
nearest = p;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
return nearest;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
_worldViews(views) {
|
|
616
|
+
const out = [];
|
|
617
|
+
if (!views) return out;
|
|
618
|
+
const parent = this.viewSpace ? this.viewSpace.matrixWorld.elements : null;
|
|
619
|
+
for (const v of views) {
|
|
620
|
+
const t = v.transformMatrix || v.transform.matrix;
|
|
621
|
+
out.push({ projectionMatrix: v.projectionMatrix, transformMatrix: parent ? mul4(parent, t) : t });
|
|
622
|
+
}
|
|
623
|
+
return out;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/** Column-major 4x4 multiply, a × b. */
|
|
628
|
+
function mul4(a, b) {
|
|
629
|
+
const o = new Array(16);
|
|
630
|
+
for (let c = 0; c < 4; c++) {
|
|
631
|
+
for (let r = 0; r < 4; r++) {
|
|
632
|
+
let s = 0;
|
|
633
|
+
for (let k = 0; k < 4; k++) s += a[k * 4 + r] * b[c * 4 + k];
|
|
634
|
+
o[c * 4 + r] = s;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
return o;
|
|
638
|
+
}
|
package/js/inline3d-viewer.js
CHANGED
|
@@ -737,6 +737,11 @@ export class SceneViewer {
|
|
|
737
737
|
this._cacheGood(views, vps, !eye);
|
|
738
738
|
const g = this._lastGood;
|
|
739
739
|
if (eye) (this.viewerEase ||= viewerEaseFor(frame, this._viewerEaseOpt)).apply(g.entries, frameTrackingState(frame));
|
|
740
|
+
// cursor: 'depth' (ADR-046) — only when a page opted in; null otherwise.
|
|
741
|
+
if (this.cursorDepth) {
|
|
742
|
+
this.scene.updateMatrixWorld(); // the hit test reads THIS frame's subject pose
|
|
743
|
+
this.cursorDepth.update(g.entries.map((e) => ({ projectionMatrix: e.proj, transformMatrix: e.pose })));
|
|
744
|
+
}
|
|
740
745
|
const r = this.renderer;
|
|
741
746
|
// getViewport() splits canvas.width; a drawing buffer the browser clamped behind our back
|
|
742
747
|
// (bufferScale().mismatch) gets the same split mapped onto its real size, never canvas.width/2.
|
|
@@ -771,6 +776,22 @@ export class SceneViewer {
|
|
|
771
776
|
return this;
|
|
772
777
|
}
|
|
773
778
|
|
|
779
|
+
/**
|
|
780
|
+
* `cursor: 'depth'` (ADR-046): a cursor that rises to the subject under it instead of being
|
|
781
|
+
* drawn on the glass behind content that pops out. Called by ./model and ./splat on opt-in
|
|
782
|
+
* only — without it nothing is built and every hook below is skipped. Injected, like
|
|
783
|
+
* useEyeCamera, so this module never imports three.js itself.
|
|
784
|
+
* @param {Function} DepthCursorClass ./three's DepthCursor.
|
|
785
|
+
* @param {(content: object) => Function} hitTestFor builds the hit test over the subject group.
|
|
786
|
+
* @param {object} [opts] extra DepthCursor options (./splat passes `raysPerFrame`).
|
|
787
|
+
*/
|
|
788
|
+
useDepthCursor(DepthCursorClass, hitTestFor, opts = {}) {
|
|
789
|
+
if (this.cursorDepth) return this;
|
|
790
|
+
this.cursorDepth = new DepthCursorClass(this._THREE, { ...opts, canvas: this.canvas, hitTest: hitTestFor(this.content) });
|
|
791
|
+
this.scene.add(this.cursorDepth.object);
|
|
792
|
+
return this;
|
|
793
|
+
}
|
|
794
|
+
|
|
774
795
|
/** Drive a flat, single-camera render loop for browsers without inline-3D. */
|
|
775
796
|
startMono() {
|
|
776
797
|
if (this._monoRaf || this._disposed) return;
|
|
@@ -780,6 +801,7 @@ export class SceneViewer {
|
|
|
780
801
|
if (this._disposed) return;
|
|
781
802
|
this._monoRaf = requestAnimationFrame(loop);
|
|
782
803
|
this._tick();
|
|
804
|
+
this.cursorDepth?.update(null); // 2D: no sprite, the normal cursor
|
|
783
805
|
const r = this.renderer;
|
|
784
806
|
r.clear();
|
|
785
807
|
const b = this._bufScale();
|
|
@@ -809,6 +831,8 @@ export class SceneViewer {
|
|
|
809
831
|
if (this._ro) this._ro.disconnect();
|
|
810
832
|
else removeEventListener('resize', this._onResize);
|
|
811
833
|
this._unbindOrbit();
|
|
834
|
+
this.cursorDepth?.dispose();
|
|
835
|
+
this.cursorDepth = null;
|
|
812
836
|
this.renderer.dispose();
|
|
813
837
|
}
|
|
814
838
|
|
package/model.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Type definitions for @displayxr/inline3d/model (and ./model/three).
|
|
2
2
|
// EXPERIMENTAL — not covered by the 1.x semver promise. See docs/sdk-stability.md.
|
|
3
3
|
|
|
4
|
+
import type { CursorOptions } from './cursor-depth.js';
|
|
4
5
|
import type { SceneViewer, SubjectBounds, OrbitPose } from './viewer.js';
|
|
5
6
|
import type { FirstWovenResult, ViewerEaseOption } from './index.js';
|
|
6
7
|
import type { PlayCanvasSplatViewer, SplatCameraPose, SplatFrameInfo } from './splat.js';
|
|
@@ -48,6 +49,17 @@ export interface ModelOptions {
|
|
|
48
49
|
* `createInline3D({ viewerEase })`, else on. `false` restores the snap.
|
|
49
50
|
*/
|
|
50
51
|
viewerEase?: ViewerEaseOption;
|
|
52
|
+
/**
|
|
53
|
+
* `'depth'`: a cursor that rises to the subject under it instead of being drawn on the glass,
|
|
54
|
+
* behind content that pops out (ADR-046, same maths as native `XR_DXR_cursor_depth`). A
|
|
55
|
+
* crosshair just in front of the nearest content under the pointer's footprint; the CSS cursor
|
|
56
|
+
* is hidden while it shows and comes back in 2D and off the canvas. OPT-IN: without it nothing
|
|
57
|
+
* is loaded or run. PlayCanvas: hit-tested with the engine's depth picker (meshes and splats,
|
|
58
|
+
* one low-res pass every other frame while the pointer is over the tile). three: a raycast
|
|
59
|
+
* against the subject. Pass an object to tune it: `{ margin, anchor, height, pointerScope }`
|
|
60
|
+
* (any object also means depth).
|
|
61
|
+
*/
|
|
62
|
+
cursor?: 'depth' | CursorOptions;
|
|
51
63
|
/**
|
|
52
64
|
* How the mesh is lit.
|
|
53
65
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@displayxr/inline3d",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.37.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",
|
|
@@ -15,6 +15,10 @@
|
|
|
15
15
|
"types": "./three.d.ts",
|
|
16
16
|
"import": "./js/inline3d-three.js"
|
|
17
17
|
},
|
|
18
|
+
"./cursor-depth": {
|
|
19
|
+
"types": "./cursor-depth.d.ts",
|
|
20
|
+
"import": "./js/inline3d-cursor-depth.js"
|
|
21
|
+
},
|
|
18
22
|
"./viewer": {
|
|
19
23
|
"types": "./viewer.d.ts",
|
|
20
24
|
"import": "./js/inline3d-viewer.js"
|
|
@@ -70,6 +74,9 @@
|
|
|
70
74
|
"js/inline3d-buffer-limit.js",
|
|
71
75
|
"js/inline3d-viewer-ease.js",
|
|
72
76
|
"js/inline3d-three.js",
|
|
77
|
+
"js/inline3d-cursor-option.js",
|
|
78
|
+
"js/inline3d-cursor-depth.js",
|
|
79
|
+
"js/inline3d-cursor-depth-playcanvas.js",
|
|
73
80
|
"js/inline3d-viewer.js",
|
|
74
81
|
"js/inline3d-splat.js",
|
|
75
82
|
"js/inline3d-splat-pc-entry.js",
|
|
@@ -100,6 +107,7 @@
|
|
|
100
107
|
"dist/",
|
|
101
108
|
"index.d.ts",
|
|
102
109
|
"three.d.ts",
|
|
110
|
+
"cursor-depth.d.ts",
|
|
103
111
|
"viewer.d.ts",
|
|
104
112
|
"splat.d.ts",
|
|
105
113
|
"splat-playcanvas.d.ts",
|
|
@@ -167,6 +175,7 @@
|
|
|
167
175
|
"prepack": "node tools/dist/build.mjs",
|
|
168
176
|
"test:e2e": "node test/e2e/call-element.e2e.mjs",
|
|
169
177
|
"test:e2e:camera": "node test/e2e/camera.e2e.mjs",
|
|
178
|
+
"test:e2e:cursor": "node test/e2e/cursor-depth.e2e.mjs",
|
|
170
179
|
"api:check": "node tools/api-surface.mjs --check",
|
|
171
180
|
"api:update": "node tools/api-surface.mjs --update",
|
|
172
181
|
"check:snippets": "node tools/check-snippets.mjs",
|
package/splat.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Type definitions for @displayxr/inline3d/splat.
|
|
2
2
|
// EXPERIMENTAL — not covered by the 1.x semver promise. See docs/sdk-stability.md.
|
|
3
3
|
|
|
4
|
+
import type { CursorOptions } from './cursor-depth.js';
|
|
4
5
|
import type { SceneViewer, SubjectBounds, OrbitPose } from './viewer.js';
|
|
5
6
|
import type { FirstWovenResult, ViewerEaseOption, DisplayMetrics } from './index.js';
|
|
6
7
|
|
|
@@ -472,6 +473,17 @@ export interface SplatOptions {
|
|
|
472
473
|
* `createInline3D({ viewerEase })`, else on. `false` restores the snap.
|
|
473
474
|
*/
|
|
474
475
|
viewerEase?: ViewerEaseOption;
|
|
476
|
+
/**
|
|
477
|
+
* `'depth'`: a cursor that rises to the subject under it instead of being drawn on the glass,
|
|
478
|
+
* behind content that pops out (ADR-046, same maths as native `XR_DXR_cursor_depth`). A
|
|
479
|
+
* crosshair just in front of the nearest content under the pointer's footprint; the CSS cursor
|
|
480
|
+
* is hidden while it shows and comes back in 2D and off the canvas. OPT-IN: without it nothing
|
|
481
|
+
* is loaded or run. PlayCanvas: hit-tested with the engine's depth picker (meshes and splats,
|
|
482
|
+
* one low-res pass every other frame while the pointer is over the tile). three: a raycast
|
|
483
|
+
* against the subject. Pass an object to tune it: `{ margin, anchor, height, pointerScope }`
|
|
484
|
+
* (any object also means depth).
|
|
485
|
+
*/
|
|
486
|
+
cursor?: 'depth' | CursorOptions;
|
|
475
487
|
/** Camera rig only: the distance in world metres that sits ON the glass. */
|
|
476
488
|
convergence?: number;
|
|
477
489
|
/**
|
package/three.d.ts
CHANGED
|
@@ -142,3 +142,60 @@ export class EdgeFeather {
|
|
|
142
142
|
vp: { x: number; y: number; width: number; height: number },
|
|
143
143
|
): void;
|
|
144
144
|
}
|
|
145
|
+
|
|
146
|
+
import type { CursorAnchorMode, CursorPlacement, CursorView } from './cursor-depth.js';
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* A hit test for {@link DepthCursor} that raycasts three.js objects (recursively): the first hit
|
|
150
|
+
* along the ray as a world point, or null.
|
|
151
|
+
*/
|
|
152
|
+
export function raycastHitTest(
|
|
153
|
+
THREE: unknown,
|
|
154
|
+
targets: unknown[],
|
|
155
|
+
): (origin: number[], direction: number[]) => number[] | null;
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* A cursor that rises to the depth of the content under it, so it is never drawn behind something
|
|
159
|
+
* that pops out of the glass. Same maths and defaults as the runtime's XR_DXR_cursor_depth
|
|
160
|
+
* (ADR-046). Tracks the pointer over `canvas`, hit-tests the cursor FOOTPRINT, places a crosshair
|
|
161
|
+
* (`object`, add it to your scene) and hides the CSS cursor exactly while the crosshair replaces it.
|
|
162
|
+
* Opt-in: nothing runs unless you construct one and call `update(views)` each frame.
|
|
163
|
+
*
|
|
164
|
+
* Head motion leaves the crosshair's image on the glass where it is (it stays under the mouse like
|
|
165
|
+
* the OS cursor); only its depth follows the content under that line of sight.
|
|
166
|
+
*/
|
|
167
|
+
export class DepthCursor {
|
|
168
|
+
constructor(
|
|
169
|
+
THREE: unknown,
|
|
170
|
+
opts: {
|
|
171
|
+
/** The woven canvas; the pointer is tracked over it. */
|
|
172
|
+
canvas: HTMLElement;
|
|
173
|
+
/** Nearest content point along a WORLD ray, or null. For splats return the expected depth along the ray. */
|
|
174
|
+
hitTest: (origin: number[], direction: number[]) => number[] | null;
|
|
175
|
+
/** Sprite height as a fraction of the canvas height (default 0.03). */
|
|
176
|
+
height?: number;
|
|
177
|
+
/** Fill colour of the crosshair (default 0xffd61a; the outline is always dark). */
|
|
178
|
+
color?: number;
|
|
179
|
+
/** How far in front of the content it floats, in eye-baseline units (default 0.005 ≈ 1.5 mm at 60 cm). */
|
|
180
|
+
margin?: number;
|
|
181
|
+
/** Default `'hybrid'`: exact while the pointer moves, world-fixed (parallaxing) while it is still. */
|
|
182
|
+
anchor?: CursorAnchorMode;
|
|
183
|
+
/** `'window'`: keep the cursor over DOM layered on the canvas; hides the CSS cursor page-wide meanwhile. */
|
|
184
|
+
pointerScope?: 'canvas' | 'window';
|
|
185
|
+
/** An EXPENSIVE hit test: N rays a frame from the first eye, cycling the footprint (0 = all). */
|
|
186
|
+
raysPerFrame?: number;
|
|
187
|
+
/** Attach pattern: the Object3D the eye cameras hang off; its world matrix carries the views to world. */
|
|
188
|
+
viewSpace?: unknown;
|
|
189
|
+
},
|
|
190
|
+
);
|
|
191
|
+
/** The crosshair (a `THREE.Mesh` of filled, outlined strokes; depth test off, drawn last). Add it to the scene root. */
|
|
192
|
+
readonly object: unknown;
|
|
193
|
+
/** The last placement, for diagnostics. */
|
|
194
|
+
readonly placement: CursorPlacement | { active: false };
|
|
195
|
+
/** Place the sprite for this frame — call with the views you are about to render, before rendering. */
|
|
196
|
+
update(views: ArrayLike<XRView | CursorView>, nowMs?: number): CursorPlacement | { active: false };
|
|
197
|
+
/** Set the pointer directly (canvas-normalised, v down), or null — for scripted input. */
|
|
198
|
+
setPointer(u: number | null, v?: number): void;
|
|
199
|
+
/** Remove listeners, restore the CSS cursor, free the sprite's GPU resources. */
|
|
200
|
+
dispose(): void;
|
|
201
|
+
}
|