@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.
- package/CHANGELOG.md +66 -0
- package/camera.d.ts +5 -1
- package/cursor-depth.d.ts +151 -0
- package/dist/call.js +18 -4
- package/js/camera/pair-cursor.js +66 -32
- package/js/inline3d-cursor-depth-playcanvas.js +369 -69
- package/js/inline3d-cursor-depth.js +91 -20
- package/js/inline3d-cursor-option.js +24 -5
- package/js/inline3d-cursor-shape.js +962 -0
- package/js/inline3d-playcanvas-engine.js +1 -0
- package/js/inline3d-splat-layer-rig.js +3 -0
- package/js/inline3d-three.js +117 -6
- package/model.d.ts +1 -1
- package/package.json +2 -1
- package/splat.d.ts +1 -1
- package/three.d.ts +11 -1
|
@@ -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';
|
package/js/inline3d-three.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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. */
|