@displayxr/inline3d 1.44.0 → 1.45.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.
@@ -63,6 +63,7 @@ export {
63
63
  CHUNKAPI_2_8,
64
64
  // setSource's frame snapshot
65
65
  RenderTarget,
66
+ FILTER_LINEAR,
66
67
  FILTER_NEAREST,
67
68
  // setSource's live outgoing asset (./inline3d-splat-live.js)
68
69
  Layer,
@@ -570,6 +570,9 @@ export class LayerRigCameras {
570
570
  L.planeM = plane;
571
571
  L.photoConvergenceM = frame ? frame.D : null;
572
572
  const round = hasRun && frame ? roundViews(entries, frame, k, this._round, plane) : null;
573
+ // cursor: 'depth' picks the display run through these exact views (./inline3d-cursor-depth-playcanvas.js).
574
+ this.lastEntries = hasRun ? entries : null;
575
+ this.lastRound = round;
573
576
  let reason = null;
574
577
  if (!hasRun) reason = this._syncReason;
575
578
  else if (entries.length < 2) reason = 'mono (one view): nothing to round — identical to the photo rig';
@@ -55,13 +55,18 @@
55
55
  import {
56
56
  CursorDepthPlacer,
57
57
  CursorPointer,
58
- cursorArrowMesh,
59
58
  cursorArrowFootprint,
60
59
  cursorDefaultHeight,
61
60
  cursorModelMatrix,
62
61
  cursorPlacementSoften,
62
+ cursorArrowMesh,
63
63
  cursorSoftArrowBox,
64
64
  cursorSoftArrowGLSL,
65
+ CursorShapeSelector,
66
+ cursorShapeTexture,
67
+ cursorSpriteBox,
68
+ cursorSpriteGLSL,
69
+ cursorSpriteRect,
65
70
  cursorViewRay,
66
71
  resolveCursorSoften,
67
72
  CURSOR_DEFAULT_TUNING,
@@ -506,6 +511,12 @@ export class DepthCursor {
506
511
  * (capped at 6% of its height, 8 px) and an opacity easing to 0.5 over 0.03 baselines of
507
512
  * lift, both zero on the glass. A preset, or `{ mode, blurK, blurMaxPx, blurMaxFrac,
508
513
  * alphaMin, fullD }` over the defaults. Drawn in the arrow's own shader.
514
+ * @param {string} [opts.shape='auto'] the sprite: 'auto' mirrors the page's CSS cursor under the
515
+ * pointer — every standard keyword has a sprite (`grab` the open hand, `text` the I-beam,
516
+ * the resize arrows, …), `url(…)` images are drawn as images, `none` draws nothing; a
517
+ * CURSOR_SHAPES name (or 'none') fixes it. (`color` tints the arrow only.)
518
+ * @param {(hit: {u: number, v: number, point: number[]|null, css: string|null, cssValue: string|null}) => string|null|undefined} [opts.shapeFor]
519
+ * per-frame override while the sprite draws: return a shape name, or nothing for `shape`'s.
509
520
  * @param {object} [opts.viewSpace] Object3D whose world matrix maps view transforms to world.
510
521
  * @param {number} [opts.raysPerFrame=0] 0 = the whole footprint every frame (two eyes × 10
511
522
  * points), synchronously. N > 0 = amortised: N rays a frame, cycling the footprint
@@ -519,7 +530,7 @@ export class DepthCursor {
519
530
  */
520
531
  constructor(
521
532
  THREE,
522
- { canvas, hitTest, height = null, color = 0xffffff, viewSpace = null, raysPerFrame = 0, margin, anchor = 'hybrid', pointerScope = 'canvas', debug = false, soften },
533
+ { canvas, hitTest, height = null, color = 0xffffff, viewSpace = null, raysPerFrame = 0, margin, anchor = 'hybrid', pointerScope = 'canvas', debug = false, soften, shape = 'auto', shapeFor = null },
523
534
  ) {
524
535
  this._THREE = THREE;
525
536
  this.canvas = canvas;
@@ -554,6 +565,10 @@ export class DepthCursor {
554
565
  /** The last frame's softening (diagnostics): `{blurPx, alpha, ghostPx, spritePx, radius, aa, soft}`. */
555
566
  this.softening = null;
556
567
  this._soft = null; // { geometry, material } — built on the first softened frame
568
+ /** Which sprite (the page's CSS cursor / fixed / shapeFor); `.shape` = the one drawn last. */
569
+ this.shapes = new CursorShapeSelector(this.pointer, { shape, shapeFor });
570
+ this._tex = null; // { geometry, material } — the textured quad, built on the first non-arrow sprite
571
+ this._textures = new Map(); // sprite key → THREE texture
557
572
 
558
573
  // The arrow: a dark outline polygon, the fill inset over it; tip at the origin (the hotspot).
559
574
  const fill = [((color >> 16) & 255) / 255, ((color >> 8) & 255) / 255, (color & 255) / 255, 1];
@@ -579,6 +594,11 @@ export class DepthCursor {
579
594
  this._crisp = { geometry: geom, material: mat };
580
595
  }
581
596
 
597
+ /** The sprite drawn last: a CURSOR_SHAPES name, 'none' or 'image'. */
598
+ get shape() {
599
+ return this.shapes.current;
600
+ }
601
+
582
602
  /** Set the pointer position directly (canvas-normalised, v down), or null — for scripted input. */
583
603
  setPointer(u, v) {
584
604
  this.pointer.set(u, v);
@@ -615,23 +635,28 @@ export class DepthCursor {
615
635
  const uv = this.pointer.uv;
616
636
  let p = { active: false };
617
637
  let h = 0;
638
+ let nearestPoint = null;
618
639
  if (uv && vs.length >= 2) {
619
640
  const [u, v] = uv;
620
641
  h = this.height ?? cursorDefaultHeight(this.pointer.cssHeight());
621
- const nearestPoint = this._footprintHit(vs, u, v, h);
642
+ nearestPoint = this._footprintHit(vs, u, v, h);
622
643
  p = this.placer.update(vs, { u, v, nearestPoint, cursorHeight: h, canvasPx: this.pointer.panelPx() }, nowMs / 1000);
623
644
  }
624
645
  this.placement = p;
625
- this.object.visible = !!p.active;
646
+ let visible = !!p.active;
626
647
  if (p.active) {
627
648
  this.object.matrix.fromArray(cursorModelMatrix(p));
628
649
  this.object.matrixWorldNeedsUpdate = true;
629
650
  const [pw, ph] = this.pointer.panelSize();
630
651
  const s = (this.softening = cursorPlacementSoften(p, vs, pw, ph, h, this.soften));
631
- this._useSoft(s);
652
+ const sprite = this.shapes.pick({ u: uv[0], v: uv[1], point: nearestPoint }, nowMs);
653
+ if (sprite.kind === 'none') visible = false; // the page hides its cursor: so do we (and the OS one stays hidden)
654
+ else if (sprite.kind === 'shape' && sprite.name === 'arrow') this._useSoft(s);
655
+ else this._useSprite(s, sprite);
632
656
  } else {
633
657
  this.softening = null;
634
658
  }
659
+ this.object.visible = visible;
635
660
  this.pointer.hideCss(!!p.active);
636
661
  return p;
637
662
  }
@@ -694,15 +719,101 @@ export class DepthCursor {
694
719
  return { geometry, material };
695
720
  }
696
721
 
722
+ // Every sprite but the arrow: a texture (a keyword shape rasterised once, or a url() image) on
723
+ // one soft quad — the same 13-tap blur and opacity, crisp at radius 0. One program for all.
724
+ _useSprite(s, sprite) {
725
+ const tex = this._texture(sprite);
726
+ const q = this._tex || (this._tex = this._makeTex());
727
+ const u = q.material.uniforms;
728
+ const radius = s.soft ? s.radius : 0;
729
+ const [x0, y0, x1, y1] = cursorSpriteBox(sprite, radius, s.aa);
730
+ const r = cursorSpriteRect(sprite);
731
+ u.inline3dCursorBox.value.set(x0, y0, x1, y1);
732
+ u.inline3dCursorRect.value.set(r[0], r[1], r[2], r[3]);
733
+ u.inline3dCursorSoft.value.set(radius, s.soft ? s.alpha : 1, 0, 0);
734
+ u.inline3dCursorTex.value = tex;
735
+ this.object.geometry = q.geometry;
736
+ this.object.material = q.material;
737
+ }
738
+
739
+ _texture(sprite) {
740
+ let t = this._textures.get(sprite.key);
741
+ if (t) return t;
742
+ const THREE = this._THREE;
743
+ if (sprite.kind === 'image') {
744
+ t = new THREE.Texture(sprite.entry.image);
745
+ } else {
746
+ const d = cursorShapeTexture(sprite.name);
747
+ t = new THREE.DataTexture(d.data, d.width, d.height, THREE.RGBAFormat);
748
+ }
749
+ t.flipY = false; // row 0 = the sprite's top, as the shader reads it
750
+ t.generateMipmaps = false;
751
+ t.minFilter = THREE.LinearFilter;
752
+ t.magFilter = THREE.LinearFilter;
753
+ if (THREE.ClampToEdgeWrapping !== undefined) t.wrapS = t.wrapT = THREE.ClampToEdgeWrapping;
754
+ // The bytes are sRGB: decode on sampling, so the output transform does not encode them twice.
755
+ if ('colorSpace' in t && THREE.SRGBColorSpace) t.colorSpace = THREE.SRGBColorSpace;
756
+ else if (THREE.sRGBEncoding !== undefined) t.encoding = THREE.sRGBEncoding;
757
+ t.needsUpdate = true;
758
+ if (this._textures.size > 32) {
759
+ for (const old of this._textures.values()) old.dispose?.();
760
+ this._textures.clear();
761
+ }
762
+ this._textures.set(sprite.key, t);
763
+ return t;
764
+ }
765
+
766
+ _makeTex() {
767
+ const THREE = this._THREE;
768
+ const geometry = new THREE.BufferGeometry();
769
+ geometry.setAttribute('position', new THREE.Float32BufferAttribute([0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0], 3));
770
+ const chunk = THREE.ShaderChunk && THREE.ShaderChunk.colorspace_fragment !== undefined ? 'colorspace_fragment' : 'encodings_fragment';
771
+ const material = new THREE.ShaderMaterial({
772
+ uniforms: {
773
+ inline3dCursorBox: { value: new THREE.Vector4() },
774
+ inline3dCursorRect: { value: new THREE.Vector4(0, 0, 1, 1) },
775
+ inline3dCursorSoft: { value: new THREE.Vector4(0, 1, 0, 0) },
776
+ inline3dCursorTex: { value: null },
777
+ },
778
+ vertexShader: `
779
+ uniform vec4 inline3dCursorBox;
780
+ varying vec2 vSprite;
781
+ void main() {
782
+ vSprite = vec2(mix(inline3dCursorBox.x, inline3dCursorBox.z, position.x), mix(inline3dCursorBox.y, inline3dCursorBox.w, position.y));
783
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(vSprite.x, -vSprite.y, 0.0, 1.0);
784
+ }`,
785
+ fragmentShader: `
786
+ uniform vec4 inline3dCursorRect; // the area the texture covers (sprite units, y down)
787
+ uniform vec4 inline3dCursorSoft; // radius (sprite units), opacity
788
+ uniform sampler2D inline3dCursorTex;
789
+ varying vec2 vSprite;
790
+ ${cursorSpriteGLSL()}
791
+ void main() {
792
+ vec4 c = inline3dSoftSprite(inline3dCursorTex, vSprite, inline3dCursorRect, inline3dCursorSoft.x);
793
+ gl_FragColor = vec4(c.rgb, c.a * inline3dCursorSoft.y);
794
+ #include <${chunk}>
795
+ }`,
796
+ transparent: true,
797
+ depthTest: false,
798
+ depthWrite: false,
799
+ side: THREE.DoubleSide,
800
+ toneMapped: false,
801
+ });
802
+ return { geometry, material };
803
+ }
804
+
697
805
  /** Remove listeners, give the CSS cursor back, free the sprite's GPU resources. */
698
806
  dispose() {
699
807
  this.pointer.dispose();
808
+ this.shapes.dispose();
700
809
  this.object.visible = false;
701
- for (const r of [this._crisp, this._soft]) {
810
+ for (const r of [this._crisp, this._soft, this._tex]) {
702
811
  if (!r) continue;
703
812
  r.geometry.dispose();
704
813
  r.material.dispose();
705
814
  }
815
+ for (const t of this._textures.values()) t.dispose?.();
816
+ this._textures.clear();
706
817
  }
707
818
 
708
819
  // Nearest content point under the footprint, from the two outer views (or, amortised, from
package/model.d.ts CHANGED
@@ -58,7 +58,7 @@ export interface ModelOptions {
58
58
  * cursor, untouched. Never behind the glass. OPT-IN: without it nothing
59
59
  * is loaded or run. PlayCanvas: hit-tested with the engine's depth picker (meshes and splats,
60
60
  * one low-res pass every other frame while the pointer is over the tile). three: a raycast
61
- * against the subject. Pass an object to tune it: `{ margin, anchor, height, pointerScope, soften }`
61
+ * against the subject. Pass an object to tune it: `{ margin, anchor, height, pointerScope, soften, shape, shapeFor }`
62
62
  * (any object also means depth).
63
63
  */
64
64
  cursor?: 'depth' | CursorOptions;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@displayxr/inline3d",
3
- "version": "1.44.0",
3
+ "version": "1.45.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",
@@ -79,6 +79,7 @@
79
79
  "js/inline3d-cursor-depth-playcanvas.js",
80
80
  "js/inline3d-cursor-perf.js",
81
81
  "js/inline3d-cursor-bvh.js",
82
+ "js/inline3d-cursor-shape.js",
82
83
  "js/vendor/",
83
84
  "js/inline3d-viewer.js",
84
85
  "js/inline3d-splat.js",
package/splat.d.ts CHANGED
@@ -482,7 +482,7 @@ export interface SplatOptions {
482
482
  * cursor, untouched. Never behind the glass. OPT-IN: without it nothing
483
483
  * is loaded or run. PlayCanvas: hit-tested with the engine's depth picker (meshes and splats,
484
484
  * one low-res pass every other frame while the pointer is over the tile). three: a raycast
485
- * against the subject. Pass an object to tune it: `{ margin, anchor, height, pointerScope, soften }`
485
+ * against the subject. Pass an object to tune it: `{ margin, anchor, height, pointerScope, soften, shape, shapeFor }`
486
486
  * (any object also means depth).
487
487
  */
488
488
  cursor?: 'depth' | CursorOptions;
package/three.d.ts CHANGED
@@ -143,7 +143,7 @@ export class EdgeFeather {
143
143
  ): void;
144
144
  }
145
145
 
146
- import type { CursorAnchorMode, CursorPlacement, CursorSoftenOption, CursorSoftenParams, CursorSoftening, CursorView } from './cursor-depth.js';
146
+ import type { CursorAnchorMode, CursorPlacement, CursorShape, CursorShapeHit, CursorShapeOption, CursorSoftenOption, CursorSoftenParams, CursorSoftening, CursorView } from './cursor-depth.js';
147
147
 
148
148
  /** One second of `debug` stats: each figure as `{ mean, p95, max, n }`. */
149
149
  export interface CursorDebugSummary {
@@ -205,6 +205,14 @@ export class DepthCursor {
205
205
  * 0.5 over 0.03 baselines of lift; both zero on the glass. Default `'both'`; `'off'` = the crisp arrow.
206
206
  */
207
207
  soften?: CursorSoftenOption;
208
+ /**
209
+ * The sprite. Default `'auto'`: what the browser would show — the page's computed CSS cursor
210
+ * under the pointer: every standard keyword has a sprite, `url(…)` images are drawn as
211
+ * images, `none` draws nothing. A sprite name (or `'none'`) fixes it. `color` tints the arrow only.
212
+ */
213
+ shape?: CursorShapeOption;
214
+ /** Per-frame override while the sprite draws: a shape name, or nothing for `shape`'s answer. */
215
+ shapeFor?: (hit: CursorShapeHit) => CursorShape | 'none' | 'auto' | null | undefined;
208
216
  /** Attach pattern: the Object3D the eye cameras hang off; its world matrix carries the views to world. */
209
217
  viewSpace?: unknown;
210
218
  /** `true`: log update() cost, rays a frame and the frame interval once a second; `'both'`: also keep the OS cursor visible. */
@@ -224,6 +232,8 @@ export class DepthCursor {
224
232
  readonly soften: CursorSoftenParams;
225
233
  /** The last drawn frame's softening (diagnostics); null while nothing is drawn. */
226
234
  readonly softening: CursorSoftening | null;
235
+ /** The sprite drawn last: a name, 'none' or 'image'. */
236
+ readonly shape: CursorShape | 'none' | 'image';
227
237
  /** Place the sprite for this frame — call with the views you are about to render, before rendering. */
228
238
  update(views: ArrayLike<XRView | CursorView>, nowMs?: number): CursorPlacement | { active: false };
229
239
  /** Set the pointer directly (canvas-normalised, v down), or null — for scripted input. */