@displayxr/inline3d 1.35.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.
@@ -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
@@ -48,6 +48,16 @@ export interface ModelOptions {
48
48
  * `createInline3D({ viewerEase })`, else on. `false` restores the snap.
49
49
  */
50
50
  viewerEase?: ViewerEaseOption;
51
+ /**
52
+ * `'depth'`: a cursor that rises to the subject under it instead of being drawn on the glass,
53
+ * behind content that pops out (ADR-046, same maths as native `XR_DXR_cursor_depth`). A
54
+ * crosshair just in front of the nearest content under the pointer's footprint; the CSS cursor
55
+ * is hidden while it shows and comes back in 2D and off the canvas. OPT-IN: without it nothing
56
+ * is loaded or run. PlayCanvas: hit-tested with the engine's depth picker (meshes and splats,
57
+ * one low-res pass every other frame while the pointer is over the tile). three: a raycast
58
+ * against the subject.
59
+ */
60
+ cursor?: 'depth';
51
61
  /**
52
62
  * How the mesh is lit.
53
63
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@displayxr/inline3d",
3
- "version": "1.35.0",
3
+ "version": "1.36.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,8 @@
70
74
  "js/inline3d-buffer-limit.js",
71
75
  "js/inline3d-viewer-ease.js",
72
76
  "js/inline3d-three.js",
77
+ "js/inline3d-cursor-depth.js",
78
+ "js/inline3d-cursor-depth-playcanvas.js",
73
79
  "js/inline3d-viewer.js",
74
80
  "js/inline3d-splat.js",
75
81
  "js/inline3d-splat-pc-entry.js",
@@ -100,6 +106,7 @@
100
106
  "dist/",
101
107
  "index.d.ts",
102
108
  "three.d.ts",
109
+ "cursor-depth.d.ts",
103
110
  "viewer.d.ts",
104
111
  "splat.d.ts",
105
112
  "splat-playcanvas.d.ts",
@@ -167,6 +174,7 @@
167
174
  "prepack": "node tools/dist/build.mjs",
168
175
  "test:e2e": "node test/e2e/call-element.e2e.mjs",
169
176
  "test:e2e:camera": "node test/e2e/camera.e2e.mjs",
177
+ "test:e2e:cursor": "node test/e2e/cursor-depth.e2e.mjs",
170
178
  "api:check": "node tools/api-surface.mjs --check",
171
179
  "api:update": "node tools/api-surface.mjs --update",
172
180
  "check:snippets": "node tools/check-snippets.mjs",
package/splat.d.ts CHANGED
@@ -472,6 +472,16 @@ export interface SplatOptions {
472
472
  * `createInline3D({ viewerEase })`, else on. `false` restores the snap.
473
473
  */
474
474
  viewerEase?: ViewerEaseOption;
475
+ /**
476
+ * `'depth'`: a cursor that rises to the subject under it instead of being drawn on the glass,
477
+ * behind content that pops out (ADR-046, same maths as native `XR_DXR_cursor_depth`). A
478
+ * crosshair just in front of the nearest content under the pointer's footprint; the CSS cursor
479
+ * is hidden while it shows and comes back in 2D and off the canvas. OPT-IN: without it nothing
480
+ * is loaded or run. PlayCanvas: hit-tested with the engine's depth picker (meshes and splats,
481
+ * one low-res pass every other frame while the pointer is over the tile). three: a raycast
482
+ * against the subject.
483
+ */
484
+ cursor?: 'depth';
475
485
  /** Camera rig only: the distance in world metres that sits ON the glass. */
476
486
  convergence?: number;
477
487
  /**
package/three.d.ts CHANGED
@@ -142,3 +142,52 @@ export class EdgeFeather {
142
142
  vp: { x: number; y: number; width: number; height: number },
143
143
  ): void;
144
144
  }
145
+
146
+ import type { 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
+ /** Crosshair colour (default 0xffd419). */
178
+ color?: number;
179
+ /** Attach pattern: the Object3D the eye cameras hang off; its world matrix carries the views to world. */
180
+ viewSpace?: unknown;
181
+ },
182
+ );
183
+ /** The crosshair (a `THREE.LineSegments`, depth test off, drawn last). Add it to the scene root. */
184
+ readonly object: unknown;
185
+ /** The last placement, for diagnostics. */
186
+ readonly placement: CursorPlacement | { active: false };
187
+ /** Place the sprite for this frame — call with the views you are about to render, before rendering. */
188
+ update(views: ArrayLike<XRView | CursorView>, nowMs?: number): CursorPlacement | { active: false };
189
+ /** Set the pointer directly (canvas-normalised, v down), or null — for scripted input. */
190
+ setPointer(u: number | null, v?: number): void;
191
+ /** Remove listeners, restore the CSS cursor, free the sprite's GPU resources. */
192
+ dispose(): void;
193
+ }