@displayxr/inline3d 1.40.2 → 1.42.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 +61 -0
- package/cursor-depth.d.ts +50 -9
- package/dist/call.js +4 -4
- package/js/camera/pair-cursor.js +94 -69
- package/js/inline3d-cursor-depth-playcanvas.js +21 -17
- package/js/inline3d-cursor-depth.js +184 -61
- package/js/inline3d-three.js +26 -18
- package/model.d.ts +6 -4
- package/package.json +1 -1
- package/splat.d.ts +6 -4
- package/three.d.ts +11 -8
|
@@ -40,28 +40,74 @@
|
|
|
40
40
|
// put; a depth change slides it along that line, so it stays on the click point
|
|
41
41
|
// for the viewer who placed it. Continuous at the stop frame.
|
|
42
42
|
// The height is scaled by t either way, so the apparent size is constant.
|
|
43
|
+
//
|
|
44
|
+
// THE POLICY (ADR-046 Amendment 2, the same for every DisplayXR depth cursor — this module, ./three's
|
|
45
|
+
// DepthCursor, `cursor: 'depth'` on ./model and ./splat, and the stereo-tile cursor on ./call and
|
|
46
|
+
// ./camera, which calls the pieces exported here):
|
|
47
|
+
// - never behind the glass: target = d < 0 ? max(d - margin, dMin) : 0 — it only comes forward,
|
|
48
|
+
// to stay ahead of content that comes OUT of the screen (cursorTarget);
|
|
49
|
+
// - margin 0.015 eye baselines (~1 mm of crossed on-screen disparity at a 63-65 mm IPD);
|
|
50
|
+
// - on the plane it IS the OS cursor: nothing is drawn while the filtered disparity is above
|
|
51
|
+
// CURSOR_DRAW_THRESHOLD (-0.005, ~0.3 mm) or under CURSOR_DRAW_MIN_PX on the panel
|
|
52
|
+
// (cursorShouldDraw), and the CSS cursor is left alone;
|
|
53
|
+
// - the look: the classic arrow (CURSOR_ARROW, tip = hotspot), at the OS cursor's size
|
|
54
|
+
// (CURSOR_ARROW_CSS_PX tall on the page, cursorDefaultHeight).
|
|
43
55
|
|
|
44
56
|
import { CURSOR_ANCHOR_MODES } from './inline3d-cursor-option.js';
|
|
45
57
|
|
|
46
58
|
export { CURSOR_ANCHOR_MODES, CURSOR_OPTION_KEYS, resolveCursorOption } from './inline3d-cursor-option.js';
|
|
47
59
|
|
|
48
|
-
/**
|
|
60
|
+
/**
|
|
61
|
+
* Fallback sprite height, as a fraction of the canvas height, when the canvas's on-page size is
|
|
62
|
+
* unknown. With a known size the default is the OS cursor's size (cursorDefaultHeight).
|
|
63
|
+
*/
|
|
49
64
|
export const CURSOR_DEFAULT_HEIGHT = 0.03;
|
|
50
65
|
|
|
66
|
+
/** The OS arrow's height in CSS px: the drawn arrow's default size, so the hand-over is seamless. */
|
|
67
|
+
export const CURSOR_ARROW_CSS_PX = 19;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The default sprite height for a canvas `cssHeight` CSS px tall: the OS arrow's 19 CSS px, as a
|
|
71
|
+
* fraction of it (capped at half the canvas). Recompute it when the canvas resizes.
|
|
72
|
+
*/
|
|
73
|
+
export function cursorDefaultHeight(cssHeight) {
|
|
74
|
+
return cssHeight > 0 && Number.isFinite(cssHeight) ? Math.min(0.5, CURSOR_ARROW_CSS_PX / cssHeight) : CURSOR_DEFAULT_HEIGHT;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The draw threshold (eye-baseline units): the sprite is drawn only while the FILTERED disparity is
|
|
79
|
+
* at or below this (~0.3 mm of on-screen disparity at a 65 mm IPD). Above it, the OS cursor is the
|
|
80
|
+
* cursor and nothing is drawn.
|
|
81
|
+
*/
|
|
82
|
+
export const CURSOR_DRAW_THRESHOLD = -0.005;
|
|
83
|
+
/** …and never while the on-screen disparity is under this many panel pixels (where they are knowable). */
|
|
84
|
+
export const CURSOR_DRAW_MIN_PX = 0.5;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Should the sprite be drawn at filtered `disparity`? Only while it is in front of the glass by at
|
|
88
|
+
* least the draw threshold, and — when `pxPerBaseline` (panel pixels of on-screen disparity per
|
|
89
|
+
* eye-baseline unit) is known — by at least CURSOR_DRAW_MIN_PX.
|
|
90
|
+
*/
|
|
91
|
+
export function cursorShouldDraw(disparity, pxPerBaseline) {
|
|
92
|
+
if (!(disparity <= CURSOR_DRAW_THRESHOLD)) return false;
|
|
93
|
+
return !(pxPerBaseline > 0 && Number.isFinite(pxPerBaseline)) || -disparity * pxPerBaseline >= CURSOR_DRAW_MIN_PX;
|
|
94
|
+
}
|
|
95
|
+
|
|
51
96
|
/**
|
|
52
97
|
* The runtime's defaults (u_cursor_depth_tuning_defaults), in the same units.
|
|
53
98
|
* Disparities are in eye-baseline units; times in seconds.
|
|
54
99
|
*/
|
|
55
100
|
export const CURSOR_DEFAULT_TUNING = Object.freeze({
|
|
56
101
|
/**
|
|
57
|
-
* How far in front of
|
|
58
|
-
*
|
|
59
|
-
*
|
|
102
|
+
* How far in front of out-of-screen content the cursor floats: 0.015 eye baselines, ~1 mm of
|
|
103
|
+
* crossed on-screen disparity at a 63-65 mm IPD (ADR-046 Amendment 2 A.2; halved from 0.03 on
|
|
104
|
+
* 2026-10-11 after a Windows panel check: 0.03 floated visibly off the content).
|
|
60
105
|
*/
|
|
61
|
-
margin: 0.
|
|
106
|
+
margin: 0.015,
|
|
62
107
|
/** Comfort clamp: t >= 0.625, never more than ~3/8 of the way to the eye. */
|
|
63
108
|
minDisparity: -0.6,
|
|
64
|
-
|
|
109
|
+
/** Never behind the glass: 0, and cursorTarget never goes above 0 whatever this says. */
|
|
110
|
+
maxDisparity: 0,
|
|
65
111
|
/** Rising toward the viewer is fast — never lag behind content that comes forward. */
|
|
66
112
|
riseTau: 0.03,
|
|
67
113
|
/** Sinking away is slow — no flicker as the footprint crosses an edge. */
|
|
@@ -161,10 +207,15 @@ export function cursorPointDisparity(g, point) {
|
|
|
161
207
|
return 1 - 1 / t;
|
|
162
208
|
}
|
|
163
209
|
|
|
164
|
-
/**
|
|
210
|
+
/**
|
|
211
|
+
* Target disparity — the policy: `d < 0 ? max(d - margin, dMin) : 0`. Content in front of the glass
|
|
212
|
+
* (d < 0) puts the cursor the margin in front of it (clamped); content on or behind the glass, or
|
|
213
|
+
* nothing under it, leaves it ON the glass. Never behind it: the result is never above 0.
|
|
214
|
+
*/
|
|
165
215
|
export function cursorTarget(tuning, hasContent, contentDisparity) {
|
|
166
|
-
if (!hasContent || !Number.isFinite(contentDisparity)) return 0;
|
|
167
|
-
|
|
216
|
+
if (!hasContent || !Number.isFinite(contentDisparity) || !(contentDisparity < 0)) return 0;
|
|
217
|
+
const hi = Math.min(0, Number.isFinite(tuning.maxDisparity) ? tuning.maxDisparity : 0);
|
|
218
|
+
return Math.min(hi, Math.max(tuning.minDisparity, contentDisparity - tuning.margin));
|
|
168
219
|
}
|
|
169
220
|
|
|
170
221
|
/**
|
|
@@ -204,12 +255,17 @@ const POINTER_STILL_EPS = 1e-4;
|
|
|
204
255
|
*
|
|
205
256
|
* const placer = new CursorDepthPlacer();
|
|
206
257
|
* // per frame, with the views you are about to render:
|
|
207
|
-
* const p = placer.update(views, { u, v, nearestPoint, cursorHeight
|
|
208
|
-
* if (p.active) drawSprite(p.position, p.basis, p.height);
|
|
258
|
+
* const p = placer.update(views, { u, v, nearestPoint, cursorHeight, canvasPx }, now / 1000);
|
|
259
|
+
* if (p.active) drawSprite(p.position, p.basis, p.height); // else: the OS cursor, untouched
|
|
209
260
|
*
|
|
210
261
|
* `views` are `{projectionMatrix, transformMatrix}` (or XRViews — `transform.matrix` is read
|
|
211
262
|
* when `transformMatrix` is absent). `nearestPoint` is the content point under the cursor
|
|
212
|
-
* FOOTPRINT nearest the viewer, in the views' space, or null for "nothing under it".
|
|
263
|
+
* FOOTPRINT nearest the viewer, in the views' space, or null for "nothing under it". `canvasPx`
|
|
264
|
+
* (optional) is the canvas's height in panel pixels, for the draw threshold's pixel floor.
|
|
265
|
+
*
|
|
266
|
+
* `active` is the DRAW decision (cursorShouldDraw on the filtered disparity): false on the plane,
|
|
267
|
+
* where the OS cursor is the cursor. The filter and the anchor keep running either way, and
|
|
268
|
+
* `solved` says the geometry was (position etc. are then filled for diagnostics).
|
|
213
269
|
*/
|
|
214
270
|
export class CursorDepthPlacer {
|
|
215
271
|
/**
|
|
@@ -228,7 +284,7 @@ export class CursorDepthPlacer {
|
|
|
228
284
|
}
|
|
229
285
|
|
|
230
286
|
update(views, hint, nowSec) {
|
|
231
|
-
const inactive = { active: false, position: null, basis: null, height: 0, disparity: 0, targetDisparity: 0 };
|
|
287
|
+
const inactive = { active: false, solved: false, position: null, basis: null, height: 0, disparity: 0, targetDisparity: 0 };
|
|
232
288
|
if (!views || views.length < 2 || !hint) {
|
|
233
289
|
this._line = null;
|
|
234
290
|
this._last = null;
|
|
@@ -270,7 +326,13 @@ export class CursorDepthPlacer {
|
|
|
270
326
|
}
|
|
271
327
|
}
|
|
272
328
|
this._last = [hint.u, hint.v];
|
|
273
|
-
|
|
329
|
+
// Panel px of on-screen disparity per baseline unit: the outer views' baseline over the canvas
|
|
330
|
+
// height, in the views' space, times the canvas height in panel px.
|
|
331
|
+
const ra = a.transformMatrix, rb = b.transformMatrix;
|
|
332
|
+
const baseline = Math.hypot(ra[12] - rb[12], ra[13] - rb[13], ra[14] - rb[14]);
|
|
333
|
+
const pxPerBaseline = hint.canvasPx > 0 ? (baseline / g.canvasHeight) * hint.canvasPx : null;
|
|
334
|
+
const active = cursorShouldDraw(disparity, pxPerBaseline);
|
|
335
|
+
return { active, solved: true, position, basis: g.basis, height, disparity, targetDisparity: target, anchored, geometry: g, pxPerBaseline };
|
|
274
336
|
}
|
|
275
337
|
}
|
|
276
338
|
|
|
@@ -336,7 +398,24 @@ export class CursorPointer {
|
|
|
336
398
|
return r.height > 0 ? r.width / r.height : 1;
|
|
337
399
|
}
|
|
338
400
|
|
|
339
|
-
/**
|
|
401
|
+
/** The canvas's on-page height, CSS px (0 when unknown). */
|
|
402
|
+
cssHeight() {
|
|
403
|
+
const r = this.canvas.getBoundingClientRect();
|
|
404
|
+
return r.height > 0 ? r.height : 0;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/** The canvas's height in panel (device) pixels, or 0 when unknown. */
|
|
408
|
+
panelPx() {
|
|
409
|
+
return this.cssHeight() * (globalThis.devicePixelRatio > 0 ? globalThis.devicePixelRatio : 1);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Hide (true) or restore (false) the CSS cursor; idempotent. Only ever while a sprite replaces
|
|
414
|
+
* it: whatever cursor the page set (`pointer`, `grab`, a `url(…)` image) is the cursor the rest of
|
|
415
|
+
* the time, and a cursor the page changes while it is hidden is not overwritten on restore.
|
|
416
|
+
* (Mirroring the page's CSS cursor INTO the drawn sprite is future work — ADR-046 Amendment 2 C,
|
|
417
|
+
* the browser feeding the computed CSS cursor to the runtime; today the sprite is the arrow.)
|
|
418
|
+
*/
|
|
340
419
|
hideCss(hide) {
|
|
341
420
|
if (hide === this._hidden) return;
|
|
342
421
|
if (this.scope === 'window') {
|
|
@@ -344,8 +423,8 @@ export class CursorPointer {
|
|
|
344
423
|
} else if (hide) {
|
|
345
424
|
this._prev = this.canvas.style.cursor || '';
|
|
346
425
|
this.canvas.style.cursor = 'none';
|
|
347
|
-
} else {
|
|
348
|
-
this.canvas.style.cursor = this._prev;
|
|
426
|
+
} else if (this.canvas.style.cursor === 'none') {
|
|
427
|
+
this.canvas.style.cursor = this._prev; // only if it is still ours
|
|
349
428
|
}
|
|
350
429
|
this._hidden = hide;
|
|
351
430
|
}
|
|
@@ -380,9 +459,9 @@ function pageCursorHidden(hide) {
|
|
|
380
459
|
}
|
|
381
460
|
|
|
382
461
|
/**
|
|
383
|
-
*
|
|
384
|
-
*
|
|
385
|
-
* @returns {Array<[number, number]>} canvas-normalised points,
|
|
462
|
+
* A footprint ring: the point (u, v) plus 8 points on a circle of radius 0.75 × the sprite
|
|
463
|
+
* height (canvas heights) around it.
|
|
464
|
+
* @returns {Array<[number, number]>} canvas-normalised points, (u, v) first.
|
|
386
465
|
*/
|
|
387
466
|
export function cursorFootprint(u, v, height, aspect) {
|
|
388
467
|
const r = 0.75 * height;
|
|
@@ -395,34 +474,98 @@ export function cursorFootprint(u, v, height, aspect) {
|
|
|
395
474
|
}
|
|
396
475
|
|
|
397
476
|
/**
|
|
398
|
-
* The
|
|
399
|
-
*
|
|
477
|
+
* The footprint every backend samples for the ARROW: the hotspot (its tip) first, then a ring
|
|
478
|
+
* around the arrow's BODY, which hangs down-right of the tip — that is what content must not
|
|
479
|
+
* overlap. Canvas-normalised, v down.
|
|
480
|
+
* @returns {Array<[number, number]>}
|
|
400
481
|
*/
|
|
401
|
-
export
|
|
402
|
-
[
|
|
403
|
-
|
|
404
|
-
|
|
482
|
+
export function cursorArrowFootprint(u, v, height, aspect) {
|
|
483
|
+
return [[u, v], ...cursorFootprint(u + (0.3 * height) / aspect, v + 0.5 * height, height, aspect)];
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* The classic pointer arrow every backend draws (ADR-046 Amendment 2: the depth cursor is the
|
|
488
|
+
* cursor the user already has; where its shape cannot be read, this). The OS arrow's outline,
|
|
489
|
+
* 12 × 19 px normalised: unit = its height, x right, y DOWN, the hotspot = the TIP at (0, 0). This
|
|
490
|
+
* is the DARK outline's polygon; the white fill is it inset by one px (CURSOR_ARROW_FILL).
|
|
491
|
+
*/
|
|
492
|
+
export const CURSOR_ARROW = Object.freeze(
|
|
493
|
+
[[0, 0], [0, 16], [4.2, 12.4], [6.9, 18.6], [9.5, 17.5], [6.8, 11.5], [11.7, 11.5]].map(([x, y]) => Object.freeze([x / 19, y / 19])),
|
|
494
|
+
);
|
|
495
|
+
|
|
496
|
+
/** Triangles over CURSOR_ARROW's vertices (indices): a fan from the tip over the head, the tail as two. */
|
|
497
|
+
const ARROW_TRIS = Object.freeze([[0, 1, 2], [0, 2, 5], [0, 5, 6], [2, 3, 4], [2, 4, 5]]);
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* CURSOR_ARROW inset by `d` (units of its height) — mitred, so every edge moves in by exactly d.
|
|
501
|
+
* @returns {Array<[number, number]>}
|
|
502
|
+
*/
|
|
503
|
+
export function cursorArrowInset(d, poly = CURSOR_ARROW) {
|
|
504
|
+
let area = 0;
|
|
505
|
+
for (let i = 0; i < poly.length; i++) {
|
|
506
|
+
const [x0, y0] = poly[i], [x1, y1] = poly[(i + 1) % poly.length];
|
|
507
|
+
area += x0 * y1 - x1 * y0;
|
|
508
|
+
}
|
|
509
|
+
const sgn = area > 0 ? 1 : -1;
|
|
510
|
+
const n = poly.map((p, i) => {
|
|
511
|
+
const q = poly[(i + 1) % poly.length];
|
|
512
|
+
const dx = q[0] - p[0], dy = q[1] - p[1];
|
|
513
|
+
const l = Math.hypot(dx, dy) || 1;
|
|
514
|
+
return [(-dy / l) * sgn, (dx / l) * sgn]; // inward normal of edge i → i+1
|
|
515
|
+
});
|
|
516
|
+
return poly.map((p, i) => {
|
|
517
|
+
const a = n[(i + poly.length - 1) % poly.length], b = n[i];
|
|
518
|
+
const k = d / (1 + a[0] * b[0] + a[1] * b[1]);
|
|
519
|
+
return [p[0] + (a[0] + b[0]) * k, p[1] + (a[1] + b[1]) * k];
|
|
520
|
+
});
|
|
521
|
+
}
|
|
405
522
|
|
|
406
|
-
/**
|
|
407
|
-
const
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
export const
|
|
523
|
+
/** The white fill: the arrow inset by one px of its 19. */
|
|
524
|
+
export const CURSOR_ARROW_FILL = Object.freeze(cursorArrowInset(1 / 19).map((p) => Object.freeze(p)));
|
|
525
|
+
|
|
526
|
+
/** The arrow's colours (RGBA, 0..1): white fill, dark outline — the OS arrow. */
|
|
527
|
+
export const CURSOR_FILL_COLOR = Object.freeze([1, 1, 1, 1]);
|
|
528
|
+
export const CURSOR_OUTLINE_COLOR = Object.freeze([0.02, 0.02, 0.03, 1]);
|
|
411
529
|
|
|
412
530
|
/**
|
|
413
|
-
* The
|
|
414
|
-
*
|
|
415
|
-
*
|
|
531
|
+
* The arrow as a filled 3D mesh in its own XY plane (unit = sprite height, x right, y UP, the tip
|
|
532
|
+
* = the hotspot at the origin, so cursorModelMatrix puts the tip on the placement): the dark
|
|
533
|
+
* outline polygon first, the white fill over it — the order a depth-test-off draw needs.
|
|
534
|
+
* Triangles, non-indexed. Shared by three, PlayCanvas and (in 2D) the stereo-tile cursor.
|
|
416
535
|
* @returns {{positions: Float32Array, colors: Float32Array, count: number}}
|
|
417
536
|
*/
|
|
418
|
-
export function
|
|
537
|
+
export function cursorArrowMesh(fill = CURSOR_FILL_COLOR, outline = CURSOR_OUTLINE_COLOR) {
|
|
538
|
+
const pos = [];
|
|
539
|
+
const col = [];
|
|
540
|
+
for (const [poly, c] of [[CURSOR_ARROW, outline], [CURSOR_ARROW_FILL, fill]]) {
|
|
541
|
+
for (const tri of ARROW_TRIS) {
|
|
542
|
+
for (const k of tri) {
|
|
543
|
+
pos.push(poly[k][0], -poly[k][1], 0);
|
|
544
|
+
col.push(c[0], c[1], c[2], c[3]);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
return { positions: new Float32Array(pos), colors: new Float32Array(col), count: pos.length / 3 };
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// The retired crosshair (1.36–1.40), kept only behind the deprecated cursorCrosshairMesh.
|
|
552
|
+
const CROSSHAIR = [
|
|
553
|
+
[-0.5, 0, -0.15, 0], [0.15, 0, 0.5, 0], [0, -0.5, 0, -0.15], [0, 0.15, 0, 0.5],
|
|
554
|
+
[-0.15, -0.15, 0.15, -0.15], [0.15, -0.15, 0.15, 0.15], [0.15, 0.15, -0.15, 0.15], [-0.15, 0.15, -0.15, -0.15],
|
|
555
|
+
];
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* @deprecated The crosshair is retired (ADR-046 Amendment 2): every depth cursor draws the arrow,
|
|
559
|
+
* cursorArrowMesh(). Kept for pages that built their own sprite from it; removed in 2.0.
|
|
560
|
+
*/
|
|
561
|
+
export function cursorCrosshairMesh(fill = [1, 0.84, 0.1, 1], outline = [0.04, 0.04, 0.06, 0.85]) {
|
|
419
562
|
const pos = [];
|
|
420
563
|
const col = [];
|
|
421
564
|
const quad = ([x0, y0, x1, y1], w, ext, c) => {
|
|
422
565
|
const dx = x1 - x0, dy = y1 - y0;
|
|
423
566
|
const l = Math.hypot(dx, dy) || 1;
|
|
424
|
-
const ux = dx / l, uy = dy / l;
|
|
425
|
-
const nx = -uy * (w / 2), ny = ux * (w / 2);
|
|
567
|
+
const ux = dx / l, uy = dy / l;
|
|
568
|
+
const nx = -uy * (w / 2), ny = ux * (w / 2);
|
|
426
569
|
const ax = x0 - ux * ext, ay = y0 - uy * ext, bx = x1 + ux * ext, by = y1 + uy * ext;
|
|
427
570
|
const v = [[ax - nx, ay - ny], [bx - nx, by - ny], [bx + nx, by + ny], [ax - nx, ay - ny], [bx + nx, by + ny], [ax + nx, ay + ny]];
|
|
428
571
|
for (const [x, y] of v) {
|
|
@@ -430,14 +573,14 @@ export function cursorCrosshairMesh(fill = CURSOR_FILL_COLOR, outline = CURSOR_O
|
|
|
430
573
|
col.push(c[0], c[1], c[2], c[3]);
|
|
431
574
|
}
|
|
432
575
|
};
|
|
433
|
-
for (const seg of
|
|
434
|
-
for (const seg of
|
|
576
|
+
for (const seg of CROSSHAIR) quad(seg, 0.15, 0.04, outline);
|
|
577
|
+
for (const seg of CROSSHAIR) quad(seg, 0.07, 0, fill);
|
|
435
578
|
return { positions: new Float32Array(pos), colors: new Float32Array(col), count: pos.length / 3 };
|
|
436
579
|
}
|
|
437
580
|
|
|
438
581
|
/**
|
|
439
|
-
* A column-major model matrix placing the unit
|
|
440
|
-
* scaled by its height, at its position.
|
|
582
|
+
* A column-major model matrix placing the unit sprite (the arrow, tip at its origin) for a
|
|
583
|
+
* placement: the sprite's basis scaled by its height, its origin (the hotspot) at its position.
|
|
441
584
|
*/
|
|
442
585
|
export function cursorModelMatrix(placement, out = new Array(16)) {
|
|
443
586
|
const { x, y, z } = placement.basis;
|
|
@@ -449,23 +592,3 @@ export function cursorModelMatrix(placement, out = new Array(16)) {
|
|
|
449
592
|
out[12] = e[0]; out[13] = e[1]; out[14] = e[2]; out[15] = 1;
|
|
450
593
|
return out;
|
|
451
594
|
}
|
|
452
|
-
|
|
453
|
-
/**
|
|
454
|
-
* The crosshair's segment endpoints in the views' space for a placement, as a flat
|
|
455
|
-
* [x, y, z, x, y, z, …] array (16 points) — what a line renderer takes.
|
|
456
|
-
*/
|
|
457
|
-
export function cursorCrosshairPoints(placement, out = []) {
|
|
458
|
-
out.length = 0;
|
|
459
|
-
const { position: p, basis: b, height: h } = placement;
|
|
460
|
-
for (const [x0, y0, x1, y1] of CURSOR_CROSSHAIR) {
|
|
461
|
-
for (const [x, y] of [[x0, y0], [x1, y1]]) {
|
|
462
|
-
out.push(
|
|
463
|
-
p[0] + (b.x[0] * x + b.y[0] * y) * h,
|
|
464
|
-
p[1] + (b.x[1] * x + b.y[1] * y) * h,
|
|
465
|
-
p[2] + (b.x[2] * x + b.y[2] * y) * h,
|
|
466
|
-
);
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
return out;
|
|
470
|
-
}
|
|
471
|
-
|
package/js/inline3d-three.js
CHANGED
|
@@ -55,12 +55,13 @@
|
|
|
55
55
|
import {
|
|
56
56
|
CursorDepthPlacer,
|
|
57
57
|
CursorPointer,
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
cursorArrowMesh,
|
|
59
|
+
cursorArrowFootprint,
|
|
60
|
+
cursorDefaultHeight,
|
|
60
61
|
cursorModelMatrix,
|
|
61
62
|
cursorViewRay,
|
|
62
|
-
CURSOR_DEFAULT_HEIGHT,
|
|
63
63
|
CURSOR_DEFAULT_TUNING,
|
|
64
|
+
CURSOR_OUTLINE_COLOR,
|
|
64
65
|
} from './inline3d-cursor-depth.js';
|
|
65
66
|
|
|
66
67
|
/**
|
|
@@ -451,9 +452,13 @@ export function raycastHitTest(THREE, targets) {
|
|
|
451
452
|
* A cursor that rises to the depth of the content under it, so it is never drawn behind
|
|
452
453
|
* something that pops out of the glass (a depth violation). The page supplies the content —
|
|
453
454
|
* 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
|
|
455
|
-
*
|
|
456
|
-
* while the sprite replaces it.
|
|
455
|
+
* cursor FOOTPRINT (not just the hotspot), places the arrow sprite with the same maths and
|
|
456
|
+
* policy as the runtime's XR_DXR_cursor_depth (ADR-046 Amendment 2), and hides the CSS cursor
|
|
457
|
+
* exactly while the sprite replaces it.
|
|
458
|
+
*
|
|
459
|
+
* Front-only: the sprite is drawn only while content under it comes OUT of the glass, the margin
|
|
460
|
+
* in front of it. Over content on or behind the glass nothing is drawn — the CSS/OS cursor is the
|
|
461
|
+
* cursor, untouched. The sprite is the classic arrow at the OS cursor's size, tip on the hotspot.
|
|
457
462
|
*
|
|
458
463
|
* Opt-in: nothing runs unless you construct one and call update(). In 2D (one view) or with the
|
|
459
464
|
* pointer off the canvas it is inactive, the sprite is hidden and the CSS cursor is back.
|
|
@@ -481,10 +486,11 @@ export class DepthCursor {
|
|
|
481
486
|
* @param {(origin:number[], direction:number[]) => number[]|null} opts.hitTest nearest
|
|
482
487
|
* content point along a ray in WORLD space, or null. {@link raycastHitTest} for meshes;
|
|
483
488
|
* for splats return the renderer's expected depth along the ray (never raycast splats).
|
|
484
|
-
* @param {number} [opts.height
|
|
485
|
-
*
|
|
486
|
-
* @param {number} [opts.
|
|
487
|
-
*
|
|
489
|
+
* @param {number} [opts.height] sprite height as a fraction of the canvas height. Default: the
|
|
490
|
+
* OS arrow's size, 19 CSS px of the canvas's on-page height (re-read every frame).
|
|
491
|
+
* @param {number} [opts.color=0xffffff] the arrow's fill colour (the outline is always dark).
|
|
492
|
+
* @param {number} [opts.margin=0.015] how far in front of out-of-screen content it floats, in
|
|
493
|
+
* eye-baseline units (0.015 ≈ 1 mm of on-screen disparity at a 63-65 mm IPD).
|
|
488
494
|
* @param {'hybrid'|'screen'|'world'} [opts.anchor='hybrid'] where along that depth: 'hybrid'
|
|
489
495
|
* follows the pointer exactly while it moves and stays world-fixed (parallaxes with the
|
|
490
496
|
* content) while it is still; 'screen' never parallaxes; 'world' always does.
|
|
@@ -498,12 +504,13 @@ export class DepthCursor {
|
|
|
498
504
|
*/
|
|
499
505
|
constructor(
|
|
500
506
|
THREE,
|
|
501
|
-
{ canvas, hitTest, height =
|
|
507
|
+
{ canvas, hitTest, height = null, color = 0xffffff, viewSpace = null, raysPerFrame = 0, margin, anchor = 'hybrid', pointerScope = 'canvas' },
|
|
502
508
|
) {
|
|
503
509
|
this._THREE = THREE;
|
|
504
510
|
this.canvas = canvas;
|
|
505
511
|
this.hitTest = hitTest;
|
|
506
|
-
|
|
512
|
+
/** Sprite height (canvas heights) when set; null = the OS arrow's size, from the canvas's on-page height. */
|
|
513
|
+
this.height = height > 0 && Number.isFinite(height) ? height : null;
|
|
507
514
|
this.viewSpace = viewSpace;
|
|
508
515
|
this.raysPerFrame = raysPerFrame;
|
|
509
516
|
this._ring = []; // amortised mode: the last hit per footprint point (null = a miss)
|
|
@@ -514,9 +521,9 @@ export class DepthCursor {
|
|
|
514
521
|
/** The last placement (diagnostics): `{active, position, height, disparity, targetDisparity, anchored}`. */
|
|
515
522
|
this.placement = { active: false };
|
|
516
523
|
|
|
517
|
-
//
|
|
524
|
+
// The arrow: a dark outline polygon, the fill inset over it; tip at the origin (the hotspot).
|
|
518
525
|
const fill = [((color >> 16) & 255) / 255, ((color >> 8) & 255) / 255, (color & 255) / 255, 1];
|
|
519
|
-
const m =
|
|
526
|
+
const m = cursorArrowMesh(fill, CURSOR_OUTLINE_COLOR);
|
|
520
527
|
const geom = new THREE.BufferGeometry();
|
|
521
528
|
geom.setAttribute('position', new THREE.Float32BufferAttribute(m.positions, 3));
|
|
522
529
|
geom.setAttribute('color', new THREE.Float32BufferAttribute(m.colors, 4));
|
|
@@ -552,8 +559,9 @@ export class DepthCursor {
|
|
|
552
559
|
let p = { active: false };
|
|
553
560
|
if (uv && vs.length >= 2) {
|
|
554
561
|
const [u, v] = uv;
|
|
555
|
-
const
|
|
556
|
-
|
|
562
|
+
const h = this.height ?? cursorDefaultHeight(this.pointer.cssHeight());
|
|
563
|
+
const nearestPoint = this._footprintHit(vs, u, v, h);
|
|
564
|
+
p = this.placer.update(vs, { u, v, nearestPoint, cursorHeight: h, canvasPx: this.pointer.panelPx() }, nowMs / 1000);
|
|
557
565
|
}
|
|
558
566
|
this.placement = p;
|
|
559
567
|
this.object.visible = !!p.active;
|
|
@@ -575,12 +583,12 @@ export class DepthCursor {
|
|
|
575
583
|
|
|
576
584
|
// Nearest content point under the footprint, from the two outer views (or, amortised, from
|
|
577
585
|
// the first eye over the last cycle of footprint points).
|
|
578
|
-
_footprintHit(views, u, v) {
|
|
586
|
+
_footprintHit(views, u, v, height) {
|
|
579
587
|
const a = views[0], b = views[views.length - 1];
|
|
580
588
|
const m = a.transformMatrix;
|
|
581
589
|
const fl = Math.hypot(m[8], m[9], m[10]) || 1;
|
|
582
590
|
const f = [-m[8] / fl, -m[9] / fl, -m[10] / fl];
|
|
583
|
-
const fp =
|
|
591
|
+
const fp = cursorArrowFootprint(u, v, height, this.pointer.aspect());
|
|
584
592
|
let candidates;
|
|
585
593
|
if (this.raysPerFrame > 0) {
|
|
586
594
|
const ring = this._ring;
|
package/model.d.ts
CHANGED
|
@@ -50,10 +50,12 @@ export interface ModelOptions {
|
|
|
50
50
|
*/
|
|
51
51
|
viewerEase?: ViewerEaseOption;
|
|
52
52
|
/**
|
|
53
|
-
* `'depth'`: a cursor that
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
* is
|
|
53
|
+
* `'depth'`: a cursor that is never drawn on the glass behind content that pops out (ADR-046 +
|
|
54
|
+
* Amendment 2, the same policy as native `XR_DXR_cursor_depth`). While the subject under the
|
|
55
|
+
* pointer's footprint comes OUT of the screen, the classic arrow (the OS cursor's size, tip on
|
|
56
|
+
* the hotspot) is drawn the margin (0.015 eye baselines, ~1 mm) in front of it and the CSS cursor
|
|
57
|
+
* is hidden; over anything on or behind the glass nothing is drawn and the CSS cursor is the
|
|
58
|
+
* cursor, untouched. Never behind the glass. OPT-IN: without it nothing
|
|
57
59
|
* is loaded or run. PlayCanvas: hit-tested with the engine's depth picker (meshes and splats,
|
|
58
60
|
* one low-res pass every other frame while the pointer is over the tile). three: a raycast
|
|
59
61
|
* against the subject. Pass an object to tune it: `{ margin, anchor, height, pointerScope }`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@displayxr/inline3d",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.42.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",
|
package/splat.d.ts
CHANGED
|
@@ -474,10 +474,12 @@ export interface SplatOptions {
|
|
|
474
474
|
*/
|
|
475
475
|
viewerEase?: ViewerEaseOption;
|
|
476
476
|
/**
|
|
477
|
-
* `'depth'`: a cursor that
|
|
478
|
-
*
|
|
479
|
-
*
|
|
480
|
-
* is
|
|
477
|
+
* `'depth'`: a cursor that is never drawn on the glass behind content that pops out (ADR-046 +
|
|
478
|
+
* Amendment 2, the same policy as native `XR_DXR_cursor_depth`). While the subject under the
|
|
479
|
+
* pointer's footprint comes OUT of the screen, the classic arrow (the OS cursor's size, tip on
|
|
480
|
+
* the hotspot) is drawn the margin (0.015 eye baselines, ~1 mm) in front of it and the CSS cursor
|
|
481
|
+
* is hidden; over anything on or behind the glass nothing is drawn and the CSS cursor is the
|
|
482
|
+
* cursor, untouched. Never behind the glass. OPT-IN: without it nothing
|
|
481
483
|
* is loaded or run. PlayCanvas: hit-tested with the engine's depth picker (meshes and splats,
|
|
482
484
|
* one low-res pass every other frame while the pointer is over the tile). three: a raycast
|
|
483
485
|
* against the subject. Pass an object to tune it: `{ margin, anchor, height, pointerScope }`
|
package/three.d.ts
CHANGED
|
@@ -157,12 +157,15 @@ export function raycastHitTest(
|
|
|
157
157
|
/**
|
|
158
158
|
* A cursor that rises to the depth of the content under it, so it is never drawn behind something
|
|
159
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,
|
|
161
|
-
*
|
|
160
|
+
* (ADR-046, Amendment 2's policy). Tracks the pointer over `canvas`, hit-tests the cursor FOOTPRINT,
|
|
161
|
+
* and — only while content under it comes OUT of the glass — draws the classic arrow (`object`, add
|
|
162
|
+
* it to your scene; tip on the hotspot, the OS cursor's size) the margin in front of it, hiding the
|
|
163
|
+
* CSS cursor exactly then. Over content on or behind the glass nothing is drawn: the CSS cursor is
|
|
164
|
+
* the cursor, untouched. Never behind the glass.
|
|
162
165
|
* Opt-in: nothing runs unless you construct one and call `update(views)` each frame.
|
|
163
166
|
*
|
|
164
|
-
* Head motion
|
|
165
|
-
*
|
|
167
|
+
* Head motion: while the pointer moves the arrow sits on the line of sight through it; while it is
|
|
168
|
+
* still, it is held world-fixed (default `anchor: 'hybrid'`), so it parallaxes like content.
|
|
166
169
|
*/
|
|
167
170
|
export class DepthCursor {
|
|
168
171
|
constructor(
|
|
@@ -172,11 +175,11 @@ export class DepthCursor {
|
|
|
172
175
|
canvas: HTMLElement;
|
|
173
176
|
/** Nearest content point along a WORLD ray, or null. For splats return the expected depth along the ray. */
|
|
174
177
|
hitTest: (origin: number[], direction: number[]) => number[] | null;
|
|
175
|
-
/** Sprite height as a fraction of the canvas height (default
|
|
178
|
+
/** Sprite height as a fraction of the canvas height (default: the OS arrow's 19 CSS px of the canvas, re-read each frame). */
|
|
176
179
|
height?: number;
|
|
177
|
-
/** Fill colour of the
|
|
180
|
+
/** Fill colour of the arrow (default 0xffffff; the outline is always dark). */
|
|
178
181
|
color?: number;
|
|
179
|
-
/** How far in front of
|
|
182
|
+
/** How far in front of out-of-screen content it floats, in eye-baseline units (default 0.03 ≈ 2 mm at a 65 mm IPD). */
|
|
180
183
|
margin?: number;
|
|
181
184
|
/** Default `'hybrid'`: exact while the pointer moves, world-fixed (parallaxing) while it is still. */
|
|
182
185
|
anchor?: CursorAnchorMode;
|
|
@@ -188,7 +191,7 @@ export class DepthCursor {
|
|
|
188
191
|
viewSpace?: unknown;
|
|
189
192
|
},
|
|
190
193
|
);
|
|
191
|
-
/** The
|
|
194
|
+
/** The arrow (a `THREE.Mesh`: dark outline, white fill, tip at its origin; depth test off, drawn last). Add it to the scene root. */
|
|
192
195
|
readonly object: unknown;
|
|
193
196
|
/** The last placement, for diagnostics. */
|
|
194
197
|
readonly placement: CursorPlacement | { active: false };
|