@displayxr/inline3d 1.44.0 → 1.45.1
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 +80 -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 +118 -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
|
@@ -53,7 +53,8 @@
|
|
|
53
53
|
// CURSOR_DRAW_THRESHOLD (-0.005, ~0.3 mm) or under CURSOR_DRAW_MIN_PX on the panel
|
|
54
54
|
// (cursorShouldDraw), and the CSS cursor is left alone;
|
|
55
55
|
// - the look: the classic arrow (CURSOR_ARROW, tip = hotspot), at the OS cursor's size
|
|
56
|
-
// (CURSOR_ARROW_CSS_PX tall on the page, cursorDefaultHeight)
|
|
56
|
+
// (CURSOR_ARROW_CSS_PX tall on the page, cursorDefaultHeight) — or, mirroring the page's CSS
|
|
57
|
+
// cursor: every CSS keyword and url() images (./inline3d-cursor-shape.js);
|
|
57
58
|
// - crosstalk softening (Amendment 2 §B.7): a LIFTED arrow is blurred by k × its ghost offset and
|
|
58
59
|
// faded toward alphaMin with its lift, both exactly zero on the glass (cursorSoften and below).
|
|
59
60
|
|
|
@@ -70,6 +71,43 @@ export {
|
|
|
70
71
|
resolveCursorSoften,
|
|
71
72
|
} from './inline3d-cursor-option.js';
|
|
72
73
|
import { CURSOR_SOFTEN_DEFAULTS } from './inline3d-cursor-option.js';
|
|
74
|
+
import { CURSOR_HIDDEN_ATTR, ensureCursorHiddenRule } from './inline3d-cursor-shape.js';
|
|
75
|
+
|
|
76
|
+
// Which sprite: the page's CSS cursor mirrored (shapes, read-back) — its own module, re-exported.
|
|
77
|
+
export {
|
|
78
|
+
CURSOR_CSS_KEYWORDS,
|
|
79
|
+
CURSOR_CSS_READ_MS,
|
|
80
|
+
CURSOR_HIDDEN_ATTR,
|
|
81
|
+
CURSOR_HIDDEN_RULE,
|
|
82
|
+
CURSOR_IMAGE_MAX_PX,
|
|
83
|
+
CURSOR_SHAPES,
|
|
84
|
+
CURSOR_SHAPE_OPTIONS,
|
|
85
|
+
CURSOR_SHAPE_TEXTURE_PX,
|
|
86
|
+
CursorCssReader,
|
|
87
|
+
CursorImageCache,
|
|
88
|
+
CursorShapeSelector,
|
|
89
|
+
cssCursorToShape,
|
|
90
|
+
cssKeywordToShape,
|
|
91
|
+
curFileHotspot,
|
|
92
|
+
cursorImageCache,
|
|
93
|
+
cursorPickShape,
|
|
94
|
+
cursorShape,
|
|
95
|
+
cursorShapeBox,
|
|
96
|
+
cursorShapePolys,
|
|
97
|
+
cursorShapeRaster,
|
|
98
|
+
cursorShapeSample,
|
|
99
|
+
cursorShapeTexture,
|
|
100
|
+
cursorSoftenPixels,
|
|
101
|
+
cursorSpriteBox,
|
|
102
|
+
cursorSpriteGLSL,
|
|
103
|
+
cursorSpriteRect,
|
|
104
|
+
drawCursorShape2D,
|
|
105
|
+
ensureCursorHiddenRule,
|
|
106
|
+
parseCssCursor,
|
|
107
|
+
readPageCursor,
|
|
108
|
+
resolveCursorSprite,
|
|
109
|
+
spriteForName,
|
|
110
|
+
} from './inline3d-cursor-shape.js';
|
|
73
111
|
|
|
74
112
|
/**
|
|
75
113
|
* Fallback sprite height, as a fraction of the canvas height, when the canvas's on-page size is
|
|
@@ -413,7 +451,6 @@ export class CursorPointer {
|
|
|
413
451
|
/** Pointer samples received so far (diagnostics). */
|
|
414
452
|
this.samples = 0;
|
|
415
453
|
this._hidden = false;
|
|
416
|
-
this._prev = '';
|
|
417
454
|
this._cx = 0;
|
|
418
455
|
this._cy = 0;
|
|
419
456
|
this._pending = false; // a sample not yet resolved to uv (resolved once, on the next read)
|
|
@@ -421,6 +458,11 @@ export class CursorPointer {
|
|
|
421
458
|
this._onLeave = () => {
|
|
422
459
|
this.uv = null;
|
|
423
460
|
};
|
|
461
|
+
// A touch (or a pen in contact) shows no cursor in the browser, so it must never drive the
|
|
462
|
+
// sprite: clear it on contact too, so a tap without a move cannot leave the last mouse hover up.
|
|
463
|
+
this._onDown = (e) => {
|
|
464
|
+
if (!cursorPointerHovers(e)) this.uv = null;
|
|
465
|
+
};
|
|
424
466
|
this._target = scope === 'window' ? globalThis : canvas;
|
|
425
467
|
// Freshest pointer: `pointerrawupdate` (Chromium, secure contexts) fires as each input arrives,
|
|
426
468
|
// not once per frame, so the frame reads the newest position instead of one up to a frame old.
|
|
@@ -430,6 +472,7 @@ export class CursorPointer {
|
|
|
430
472
|
this.source = supportsRawUpdate(this._target) ? 'pointerrawupdate' : 'pointermove';
|
|
431
473
|
this._target.addEventListener('pointermove', this._onMove);
|
|
432
474
|
if (this.source === 'pointerrawupdate') this._target.addEventListener('pointerrawupdate', this._onMove);
|
|
475
|
+
this._target.addEventListener('pointerdown', this._onDown);
|
|
433
476
|
if (scope === 'window') {
|
|
434
477
|
// Leaving the window (relatedTarget null) or losing focus mid-hover: nothing to hover.
|
|
435
478
|
this._onOut = (e) => {
|
|
@@ -466,6 +509,12 @@ export class CursorPointer {
|
|
|
466
509
|
// One pointer event: keep its NEWEST coalesced sample (getCoalescedEvents(), last entry), unless
|
|
467
510
|
// a newer one is already held. No layout read here — uv resolves on the next read, once a frame.
|
|
468
511
|
_sample(e) {
|
|
512
|
+
// Only a pointer the browser draws a cursor for: a mouse, or a pen hovering. A touch drag (or a
|
|
513
|
+
// pen stroke) clears the sprite — the browser shows no cursor there, so neither do we.
|
|
514
|
+
if (!cursorPointerHovers(e)) {
|
|
515
|
+
this.uv = null;
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
469
518
|
const list = typeof e.getCoalescedEvents === 'function' ? e.getCoalescedEvents() : null;
|
|
470
519
|
const s = list && list.length ? list[list.length - 1] : e;
|
|
471
520
|
const t = s.timeStamp > 0 ? s.timeStamp : e.timeStamp > 0 ? e.timeStamp : globalThis.performance ? globalThis.performance.now() : Date.now();
|
|
@@ -508,21 +557,24 @@ export class CursorPointer {
|
|
|
508
557
|
|
|
509
558
|
/**
|
|
510
559
|
* Hide (true) or restore (false) the CSS cursor; idempotent. Only ever while a sprite replaces
|
|
511
|
-
* it
|
|
512
|
-
*
|
|
513
|
-
*
|
|
514
|
-
*
|
|
560
|
+
* it. The hide is a data attribute (CURSOR_HIDDEN_ATTR) on the canvas — on <html> for
|
|
561
|
+
* pointerScope 'window' — plus one injected `cursor: none !important` rule for it: the page's
|
|
562
|
+
* own cursor (`pointer`, `grab`, a `url(…)` image, inline or from a stylesheet) is never
|
|
563
|
+
* written, so it is what shows the rest of the time, and what the sprite mirrors (CursorCssReader
|
|
564
|
+
* lifts the attribute for its read).
|
|
515
565
|
*/
|
|
516
566
|
hideCss(hide) {
|
|
517
567
|
if (this.keepCss) hide = false;
|
|
518
568
|
if (hide === this._hidden) return;
|
|
519
569
|
if (this.scope === 'window') {
|
|
520
570
|
pageCursorHidden(hide);
|
|
521
|
-
} else if (
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
571
|
+
} else if (this.canvas.setAttribute) {
|
|
572
|
+
if (hide) {
|
|
573
|
+
ensureCursorHiddenRule();
|
|
574
|
+
this.canvas.setAttribute(CURSOR_HIDDEN_ATTR, '');
|
|
575
|
+
} else {
|
|
576
|
+
this.canvas.removeAttribute(CURSOR_HIDDEN_ATTR);
|
|
577
|
+
}
|
|
526
578
|
}
|
|
527
579
|
this._hidden = hide;
|
|
528
580
|
}
|
|
@@ -530,6 +582,7 @@ export class CursorPointer {
|
|
|
530
582
|
dispose() {
|
|
531
583
|
this._target.removeEventListener('pointermove', this._onMove);
|
|
532
584
|
if (this.source === 'pointerrawupdate') this._target.removeEventListener('pointerrawupdate', this._onMove);
|
|
585
|
+
this._target.removeEventListener('pointerdown', this._onDown);
|
|
533
586
|
if (this.scope === 'window') {
|
|
534
587
|
globalThis.removeEventListener('pointerout', this._onOut);
|
|
535
588
|
globalThis.removeEventListener('blur', this._onLeave);
|
|
@@ -540,26 +593,37 @@ export class CursorPointer {
|
|
|
540
593
|
}
|
|
541
594
|
}
|
|
542
595
|
|
|
596
|
+
/**
|
|
597
|
+
* Does this pointer event come from a pointer the browser shows a cursor for? A mouse, or a pen
|
|
598
|
+
* hovering in range (no buttons pressed). A touch never; a pen in contact neither. An event with
|
|
599
|
+
* no `pointerType` (a synthetic `new PointerEvent('pointermove')`, an old engine) counts as a mouse.
|
|
600
|
+
* Coalesced samples belong to their parent event, so the parent is what is tested.
|
|
601
|
+
* @param {{pointerType?: string, buttons?: number}} e
|
|
602
|
+
*/
|
|
603
|
+
export function cursorPointerHovers(e) {
|
|
604
|
+
const t = e && e.pointerType;
|
|
605
|
+
if (!t || t === 'mouse') return true;
|
|
606
|
+
if (t === 'pen') return !(e.buttons > 0);
|
|
607
|
+
return false;
|
|
608
|
+
}
|
|
609
|
+
|
|
543
610
|
/** Does `target` dispatch `pointerrawupdate`? (Chromium; only in a secure context.) */
|
|
544
611
|
export function supportsRawUpdate(target) {
|
|
545
612
|
return !!target && 'onpointerrawupdate' in target && globalThis.isSecureContext !== false;
|
|
546
613
|
}
|
|
547
614
|
|
|
548
615
|
// pointerScope 'window': the overlay DOM has its own cursor, so hiding it on the canvas alone is
|
|
549
|
-
// not enough.
|
|
550
|
-
// shows; a count keeps two tiles from un-hiding each other.
|
|
616
|
+
// not enough. The attribute on <html> (the injected rule covers its descendants) hides it
|
|
617
|
+
// everywhere while any depth cursor shows; a count keeps two tiles from un-hiding each other.
|
|
551
618
|
let _pageHides = 0;
|
|
552
619
|
function pageCursorHidden(hide) {
|
|
553
620
|
const doc = globalThis.document;
|
|
554
621
|
if (!doc || !doc.documentElement) return;
|
|
555
|
-
if (hide
|
|
556
|
-
const st = doc.createElement('style');
|
|
557
|
-
st.id = 'inline3d-cursor-style';
|
|
558
|
-
st.textContent = 'html.inline3d-cursor-hidden, html.inline3d-cursor-hidden * { cursor: none !important; }';
|
|
559
|
-
(doc.head || doc.documentElement).appendChild(st);
|
|
560
|
-
}
|
|
622
|
+
if (hide) ensureCursorHiddenRule(doc);
|
|
561
623
|
_pageHides = Math.max(0, _pageHides + (hide ? 1 : -1));
|
|
562
|
-
doc.documentElement
|
|
624
|
+
const el = doc.documentElement;
|
|
625
|
+
if (_pageHides > 0) el.setAttribute?.(CURSOR_HIDDEN_ATTR, '');
|
|
626
|
+
else el.removeAttribute?.(CURSOR_HIDDEN_ATTR);
|
|
563
627
|
}
|
|
564
628
|
|
|
565
629
|
/**
|
|
@@ -975,3 +1039,37 @@ export function cursorSoftArrowRaster(cx, cy, sizePx, radius, alpha, fill = CURS
|
|
|
975
1039
|
}
|
|
976
1040
|
return { x0, y0, width, height, data };
|
|
977
1041
|
}
|
|
1042
|
+
|
|
1043
|
+
/**
|
|
1044
|
+
* PlayCanvas `pc.ShaderMaterial` opt-in to the depth cursor's pick (`cursor: 'depth'` on ./model
|
|
1045
|
+
* and ./splat). The pick pass draws into TWO colour targets — the pick id, then the pick depth — and
|
|
1046
|
+
* leaves out any ShaderMaterial whose fragment code does not write the second under
|
|
1047
|
+
* DEPTH_PICK_PASS (WebGL would reject the draw), so such a mesh is never under the cursor. Paste
|
|
1048
|
+
* `declarations` above `main` and `output` inside `#ifdef PICK_PASS … #else <your colour> #endif`:
|
|
1049
|
+
*
|
|
1050
|
+
* fragmentGLSL: `${CURSOR_DEPTH_PICK_GLSL.declarations}
|
|
1051
|
+
* void main() {
|
|
1052
|
+
* #ifdef PICK_PASS
|
|
1053
|
+
* ${CURSOR_DEPTH_PICK_GLSL.output}
|
|
1054
|
+
* #else
|
|
1055
|
+
* gl_FragColor = vec4(0.2, 0.3, 0.4, 1.0);
|
|
1056
|
+
* #endif
|
|
1057
|
+
* }`
|
|
1058
|
+
*
|
|
1059
|
+
* The engine's own `pickPS` chunk does the work (getPickOutput / getPickDepth), so the id and the
|
|
1060
|
+
* linear depth match its lit and gsplat materials exactly. Discard where your shader discards.
|
|
1061
|
+
*/
|
|
1062
|
+
export const CURSOR_DEPTH_PICK_GLSL = Object.freeze({
|
|
1063
|
+
declarations: '#ifdef PICK_PASS\n#include "pickPS"\n#endif\n',
|
|
1064
|
+
output: 'pcFragColor0 = getPickOutput();\n#ifdef DEPTH_PICK_PASS\npcFragColor1 = getPickDepth();\n#endif\n',
|
|
1065
|
+
});
|
|
1066
|
+
|
|
1067
|
+
/**
|
|
1068
|
+
* The WGSL twin of CURSOR_DEPTH_PICK_GLSL (a ShaderMaterial with `fragmentWGSL`, WebGPU): `output`
|
|
1069
|
+
* writes `output.color` / `output.color1` of the engine's `FragmentOutput`, inside
|
|
1070
|
+
* `#ifdef PICK_PASS … #else … #endif` in `fragmentMain`.
|
|
1071
|
+
*/
|
|
1072
|
+
export const CURSOR_DEPTH_PICK_WGSL = Object.freeze({
|
|
1073
|
+
declarations: '#ifdef PICK_PASS\n#include "pickPS"\n#endif\n',
|
|
1074
|
+
output: 'output.color = getPickOutput();\n#ifdef DEPTH_PICK_PASS\noutput.color1 = getPickDepth();\n#endif\n',
|
|
1075
|
+
});
|
|
@@ -8,7 +8,20 @@
|
|
|
8
8
|
export const CURSOR_ANCHOR_MODES = Object.freeze(['hybrid', 'screen', 'world']);
|
|
9
9
|
|
|
10
10
|
/** The keys `cursor: { … }` accepts on ./model and ./splat. */
|
|
11
|
-
export const CURSOR_OPTION_KEYS = Object.freeze(['height', 'margin', 'anchor', 'pointerScope', 'debug', 'soften']);
|
|
11
|
+
export const CURSOR_OPTION_KEYS = Object.freeze(['height', 'margin', 'anchor', 'pointerScope', 'debug', 'soften', 'shape', 'shapeFor']);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The procedural sprites the depth cursor can draw (see ./inline3d-cursor-shape.js): one per
|
|
15
|
+
* standard CSS cursor keyword (aliases such as `n-resize` → `ns-resize` share one).
|
|
16
|
+
*/
|
|
17
|
+
export const CURSOR_SHAPES = Object.freeze([
|
|
18
|
+
'arrow', 'pointer', 'grab', 'grabbing', 'move', 'text', 'vertical-text', 'crosshair', 'not-allowed',
|
|
19
|
+
'help', 'wait', 'progress', 'copy', 'alias', 'context-menu', 'cell', 'ns-resize', 'ew-resize',
|
|
20
|
+
'nesw-resize', 'nwse-resize', 'col-resize', 'row-resize', 'zoom-in', 'zoom-out',
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
/** `cursor.shape` values: a sprite, `'none'` (draw nothing), or `'auto'` (default) — mirror the page's CSS cursor. */
|
|
24
|
+
export const CURSOR_SHAPE_OPTIONS = Object.freeze(['auto', 'none', ...CURSOR_SHAPES]);
|
|
12
25
|
|
|
13
26
|
// ── Crosstalk softening (runtime ADR-046 Amendment 2 §B.7) ─────────────────────────────────────
|
|
14
27
|
// On a lenticular panel a little of every view leaks into its neighbours. A LIFTED cursor's leaked
|
|
@@ -124,13 +137,13 @@ export function parseCursorSoften(mode, blurK, blurMaxPx, blurMaxFrac, alphaMin,
|
|
|
124
137
|
* depth). Anything falsy is OFF — the zero-cost default. Validated here, at the call.
|
|
125
138
|
* `debug`: true logs the cursor's per-frame cost (rolling stats, at most once a second); 'both' also
|
|
126
139
|
* keeps the OS cursor visible beside the sprite (the latency check). Off by default, and free.
|
|
127
|
-
* @returns {null | {height?: number, margin?: number, anchor?: string, pointerScope?: string, debug?: boolean|'both', soften?: string | object}}
|
|
140
|
+
* @returns {null | {height?: number, margin?: number, anchor?: string, pointerScope?: string, debug?: boolean|'both', soften?: string | object, shape?: string, shapeFor?: Function}}
|
|
128
141
|
*/
|
|
129
142
|
export function resolveCursorOption(opt, who = '@displayxr/inline3d') {
|
|
130
143
|
if (opt === undefined || opt === null || opt === false) return null;
|
|
131
144
|
if (opt === 'depth' || opt === true) return {};
|
|
132
145
|
if (typeof opt !== 'object') {
|
|
133
|
-
throw new Error(`${who}: cursor ${JSON.stringify(opt)} — expected 'depth' or {
|
|
146
|
+
throw new Error(`${who}: cursor ${JSON.stringify(opt)} — expected 'depth' or { ${CURSOR_OPTION_KEYS.join(', ')} }.`);
|
|
134
147
|
}
|
|
135
148
|
const unknown = Object.keys(opt).filter((k) => !CURSOR_OPTION_KEYS.includes(k));
|
|
136
149
|
if (unknown.length) throw new Error(`${who}: cursor — unknown option(s) ${unknown.join(', ')}; expected ${CURSOR_OPTION_KEYS.join(', ')}.`);
|
|
@@ -149,14 +162,20 @@ export function resolveCursorOption(opt, who = '@displayxr/inline3d') {
|
|
|
149
162
|
}
|
|
150
163
|
}
|
|
151
164
|
if (opt.soften !== undefined) resolveCursorSoften(opt.soften, who); // validate at the call
|
|
165
|
+
if (opt.shape !== undefined && !CURSOR_SHAPE_OPTIONS.includes(opt.shape)) {
|
|
166
|
+
throw new Error(`${who}: cursor.shape ${JSON.stringify(opt.shape)} — expected ${CURSOR_SHAPE_OPTIONS.map((m) => `'${m}'`).join(' | ')}.`);
|
|
167
|
+
}
|
|
168
|
+
if (opt.shapeFor !== undefined && opt.shapeFor !== null && typeof opt.shapeFor !== 'function') {
|
|
169
|
+
throw new Error(`${who}: cursor.shapeFor must be a function (hit) => shape.`);
|
|
170
|
+
}
|
|
152
171
|
return { ...opt };
|
|
153
172
|
}
|
|
154
173
|
|
|
155
174
|
/** The keys `cursor: { … }` accepts on a stereo PAIR (./camera's addCameraView, ./call's tiles). */
|
|
156
|
-
export const PAIR_CURSOR_OPTION_KEYS = Object.freeze(['height', 'pointerScope', 'soften']);
|
|
175
|
+
export const PAIR_CURSOR_OPTION_KEYS = Object.freeze(['height', 'pointerScope', 'soften', 'shape', 'shapeFor']);
|
|
157
176
|
|
|
158
177
|
/**
|
|
159
|
-
* `cursor` on a stereo pair — a camera view or a call tile: `'depth'`, or `{ height, pointerScope, soften }`.
|
|
178
|
+
* `cursor` on a stereo pair — a camera view or a call tile: `'depth'`, or `{ height, pointerScope, soften, shape, shapeFor }`.
|
|
160
179
|
* A pair has no scene: its depth is MEASURED off the two halves, and the sprite is drawn into them,
|
|
161
180
|
* so there is no anchor mode (it is always on the line of sight) and the margin is the SDK's. Falsy
|
|
162
181
|
* is OFF, the zero-cost default.
|