@displayxr/inline3d 1.40.0 → 1.40.2
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 +30 -0
- package/call.d.ts +5 -4
- package/camera.d.ts +5 -4
- package/dist/call.js +4 -4
- package/js/call/options.js +3 -2
- package/js/camera/pair-cursor.js +112 -49
- package/js/inline3d-call.js +1 -1
- package/js/inline3d-camera.js +1 -1
- package/package.json +1 -1
package/js/call/options.js
CHANGED
|
@@ -90,8 +90,9 @@ export function normalizeCallOptions(opts = {}) {
|
|
|
90
90
|
// peer's eyes and shift the eyes so it sits at the display plane. The depth slider stays an
|
|
91
91
|
// offset on top. Off = the pair as sent (plus any `hint`).
|
|
92
92
|
autoConverge: opts.autoConverge === undefined ? true : !!opts.autoConverge,
|
|
93
|
-
// `cursor: 'depth'` (runtime ADR-046 on a pair):
|
|
94
|
-
// the
|
|
93
|
+
// `cursor: 'depth'` (runtime ADR-046 on a pair): the arrow comes in front of a person who pops
|
|
94
|
+
// out of the screen under it, measured off each stereo tile's two halves; else the plain OS
|
|
95
|
+
// arrow on the plane. Off by default (zero cost).
|
|
95
96
|
cursor: resolvePairCursorOption(opts.cursor, '@displayxr/inline3d/call'),
|
|
96
97
|
mono3D: normalizeMono3D(opts.mono3D),
|
|
97
98
|
maxPeers: clampMaxPeers(opts.maxPeers === undefined ? DEFAULT_MAX_PEERS : opts.maxPeers),
|
package/js/camera/pair-cursor.js
CHANGED
|
@@ -5,23 +5,48 @@
|
|
|
5
5
|
// up are all in front), it is drawn on top of pixels that say they are nearer — the depth
|
|
6
6
|
// violation runtime ADR-046 is about. A pair has no scene to raycast and no depth buffer, but its
|
|
7
7
|
// two halves ARE the depth: this is ADR-046 Phase 3b done in the page. Block-match a cursor-sized
|
|
8
|
-
// footprint between the eyes
|
|
9
|
-
// halves with that disparity plus a small margin. The browser weaves the pair as usual, so the
|
|
10
|
-
// sprite lands just in front of the content under it.
|
|
8
|
+
// footprint between the eyes and take the nearest confident match.
|
|
11
9
|
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
10
|
+
// The cursor only ever comes FORWARD (David, 2026-10-10): it never goes inside the display. While
|
|
11
|
+
// what is under it sits at or behind the glass, it is simply the OS arrow, on the screen plane.
|
|
12
|
+
// Only when content comes out of the screen under it is the arrow drawn into both halves, with that
|
|
13
|
+
// content's disparity plus a small margin (the OS cursor hidden meanwhile), so it is never overlapped
|
|
14
|
+
// by what pops out. It is the normal arrow either way, not a special sprite.
|
|
15
|
+
//
|
|
16
|
+
// Same filter as the scene cursor (./inline3d-cursor-depth.js): fast rise, slow sink, the plane when
|
|
17
|
+
// nothing matches. Disparities here are in OUTPUT pixels of one eye of the woven buffer, + = crossed
|
|
18
|
+
// (in front of the glass), the sign the rest of camera/ uses. Loaded only when a page asks for
|
|
19
|
+
// `cursor: 'depth'` — zero cost otherwise.
|
|
16
20
|
|
|
17
|
-
import { CursorPointer, cursorFootprint, cursorFilterStep, CURSOR_DEFAULT_TUNING
|
|
21
|
+
import { CursorPointer, cursorFootprint, cursorFilterStep, CURSOR_DEFAULT_TUNING } from '../inline3d-cursor-depth.js';
|
|
18
22
|
import { matchBlock, MIN_NCC } from './disparity.js';
|
|
19
23
|
|
|
20
|
-
/**
|
|
24
|
+
/** The arrow's height in CSS px — the OS arrow's apparent size, so the swap to the drawn one is seamless. */
|
|
25
|
+
export const PAIR_CURSOR_CSS_PX = 19;
|
|
26
|
+
/** Arrow height as a fraction of the tile height when the tile's on-page size is unknown. */
|
|
21
27
|
export const PAIR_CURSOR_HEIGHT = 0.06;
|
|
22
|
-
/**
|
|
23
|
-
|
|
24
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Below this far in front of the glass (metres of on-screen disparity, ~0.3 mm = 0.005 of the eye
|
|
30
|
+
* baseline, the runtime's rule) the drawn arrow hands back to the OS cursor. Never under 0.5 px.
|
|
31
|
+
*/
|
|
32
|
+
export const PAIR_CURSOR_SHOW_M = 0.0003;
|
|
33
|
+
export const PAIR_CURSOR_SHOW_MIN_PX = 0.5;
|
|
34
|
+
/**
|
|
35
|
+
* The standard pointer arrow, hotspot at the tip (0, 0), unit = its height, x right, y down: the
|
|
36
|
+
* OS arrow's outline (a 19-px-tall Windows/macOS arrow, normalised).
|
|
37
|
+
*/
|
|
38
|
+
export const PAIR_CURSOR_ARROW = Object.freeze([
|
|
39
|
+
[0, 0], [0, 16 / 19], [4.2 / 19, 12.4 / 19], [6.9 / 19, 18.6 / 19], [9.5 / 19, 17.5 / 19], [6.8 / 19, 11.5 / 19], [11.7 / 19, 11.5 / 19],
|
|
40
|
+
]);
|
|
41
|
+
/**
|
|
42
|
+
* How far in front of out-of-screen content the arrow floats: 2 mm of on-screen disparity — the
|
|
43
|
+
* runtime's 0.03 of the eye baseline (ADR-046 / runtime #1908), so a lifted tile's cursor and a
|
|
44
|
+
* stereo tile's sit the same way. Converted to buffer px through the tile's `displayMetrics()`.
|
|
45
|
+
*/
|
|
46
|
+
export const PAIR_CURSOR_MARGIN_M = 0.002;
|
|
47
|
+
/** One CSS px in metres when the panel cannot say (the CSS reference pixel, 1/96 in). */
|
|
48
|
+
export const PAIR_CURSOR_CSS_PX_M = 0.0254 / 96;
|
|
49
|
+
/** Comfort cap on how far in front the arrow comes, as a fraction of the eye width. */
|
|
25
50
|
export const PAIR_CURSOR_CLAMP = 0.08;
|
|
26
51
|
|
|
27
52
|
/**
|
|
@@ -33,9 +58,10 @@ export const PAIR_CURSOR_CLAMP = 0.08;
|
|
|
33
58
|
* @param {{rL: {sx:number, sy:number, sw:number, sh:number}, rR: {sx:number}, outW: number, mirror?: boolean}} geo
|
|
34
59
|
* each eye's source crop (relative to its half, eyeCropRect) and the output eye width; a
|
|
35
60
|
* mirrored view (the self view) flips the pointer, never the disparity.
|
|
36
|
-
* @param {number} u pointer, tile-normalised (0..1, left → right as the viewer sees it)
|
|
37
|
-
* @param {number} v pointer, tile-normalised (0..1, top → bottom)
|
|
38
|
-
* @param {number} height
|
|
61
|
+
* @param {number} u pointer hotspot, tile-normalised (0..1, left → right as the viewer sees it)
|
|
62
|
+
* @param {number} v pointer hotspot, tile-normalised (0..1, top → bottom)
|
|
63
|
+
* @param {number} height arrow height (fraction of the tile height); the footprint covers the
|
|
64
|
+
* arrow's body, which hangs down-right of the hotspot
|
|
39
65
|
* @param {number} aspect the tile's width / height, for a round footprint
|
|
40
66
|
* @returns {number|null} output px, + = in front of the glass
|
|
41
67
|
*/
|
|
@@ -49,7 +75,8 @@ export function pairFootprintDisparity(luma, geo, u, v, height, aspect) {
|
|
|
49
75
|
const { rL, rR } = geo;
|
|
50
76
|
const k = geo.outW / rL.sw; // output px per source px
|
|
51
77
|
let best = null;
|
|
52
|
-
|
|
78
|
+
// The ring is centred on the arrow's body, not its tip: that is what content must not overlap.
|
|
79
|
+
for (const [pu, pv] of cursorFootprint(u + (0.3 * height) / aspect, v + 0.5 * height, height, aspect)) {
|
|
53
80
|
if (!(pu >= 0 && pu <= 1 && pv >= 0 && pv <= 1)) continue;
|
|
54
81
|
const su = geo.mirror ? 1 - pu : pu;
|
|
55
82
|
const x = Math.round((rL.sx + su * rL.sw) * scale - b / 2);
|
|
@@ -64,42 +91,49 @@ export function pairFootprintDisparity(luma, geo, u, v, height, aspect) {
|
|
|
64
91
|
return best;
|
|
65
92
|
}
|
|
66
93
|
|
|
67
|
-
/**
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
94
|
+
/**
|
|
95
|
+
* The arrow's target disparity: in front of content that comes OUT of the screen (its disparity
|
|
96
|
+
* plus the margin, clamped), else the glass. Never behind the glass — the cursor does not go inside
|
|
97
|
+
* the display.
|
|
98
|
+
* @param {number} marginPx the margin in output px (pairCursorScale × PAIR_CURSOR_MARGIN_M)
|
|
99
|
+
*/
|
|
100
|
+
export function pairCursorTarget(contentPx, outW, marginPx) {
|
|
101
|
+
if (contentPx === null || !Number.isFinite(contentPx) || !(contentPx > 0)) return 0;
|
|
102
|
+
return Math.min(PAIR_CURSOR_CLAMP * outW, contentPx + marginPx);
|
|
72
103
|
}
|
|
73
104
|
|
|
74
105
|
/**
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
* its own half, so a sprite at the edge never bleeds into the other eye.
|
|
106
|
+
* Output px of one eye per metre on the glass: the buffer eye is stretched over the tile's CSS
|
|
107
|
+
* width, and one CSS px is `metersPerCssPx` on the panel.
|
|
78
108
|
*/
|
|
79
|
-
export function
|
|
80
|
-
const
|
|
109
|
+
export function pairCursorScale(outW, tileCssWidth, metersPerCssPx) {
|
|
110
|
+
const m = metersPerCssPx > 0 && Number.isFinite(metersPerCssPx) ? metersPerCssPx : PAIR_CURSOR_CSS_PX_M;
|
|
111
|
+
return tileCssWidth > 0 ? outW / tileCssWidth / m : outW / (1000 * m);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Draw the arrow into both halves of a woven buffer: the left eye's copy `disparityPx / 2` right of
|
|
116
|
+
* the pointer, the right eye's as far left (crossed = in front). White with a dark outline, like the
|
|
117
|
+
* OS arrow. Each copy is clipped to its own half, so an arrow at the edge never bleeds into the other eye.
|
|
118
|
+
* @param {number} sizePx arrow height in output px
|
|
119
|
+
*/
|
|
120
|
+
export function drawPairArrow(g, outW, outH, u, v, disparityPx, sizePx) {
|
|
81
121
|
const cy = v * outH;
|
|
82
|
-
const rgba = (c) => `rgba(${Math.round(c[0] * 255)},${Math.round(c[1] * 255)},${Math.round(c[2] * 255)},${c[3]})`;
|
|
83
122
|
for (const eye of [0, 1]) {
|
|
84
123
|
const cx = eye * outW + u * outW + (eye === 0 ? disparityPx / 2 : -disparityPx / 2);
|
|
85
124
|
g.save();
|
|
86
125
|
g.beginPath();
|
|
87
126
|
g.rect(eye * outW, 0, outW, outH);
|
|
88
127
|
g.clip();
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
g.moveTo(cx + x0 * size, cy + y0 * size);
|
|
99
|
-
g.lineTo(cx + x1 * size, cy + y1 * size);
|
|
100
|
-
}
|
|
101
|
-
g.stroke();
|
|
102
|
-
}
|
|
128
|
+
g.beginPath();
|
|
129
|
+
PAIR_CURSOR_ARROW.forEach(([x, y], k) => (k ? g.lineTo : g.moveTo).call(g, cx + x * sizePx, cy + y * sizePx));
|
|
130
|
+
g.closePath();
|
|
131
|
+
g.fillStyle = '#fff';
|
|
132
|
+
g.fill();
|
|
133
|
+
g.lineWidth = Math.max(1, sizePx / 19);
|
|
134
|
+
g.lineJoin = 'miter';
|
|
135
|
+
g.strokeStyle = '#000';
|
|
136
|
+
g.stroke();
|
|
103
137
|
g.restore();
|
|
104
138
|
}
|
|
105
139
|
}
|
|
@@ -113,12 +147,16 @@ export class PairCursor {
|
|
|
113
147
|
/** @param {HTMLElement} canvas the tile — the pointer counts over its box */
|
|
114
148
|
constructor(canvas, { height, pointerScope = 'window' } = {}) {
|
|
115
149
|
this.pointer = new CursorPointer(canvas, { scope: pointerScope });
|
|
116
|
-
|
|
150
|
+
// A fraction of the tile height when given; by default the OS arrow's size (CSS px → tile).
|
|
151
|
+
this.height = height > 0 ? height : null;
|
|
117
152
|
this.filter = {};
|
|
118
153
|
this._luma = null;
|
|
119
154
|
this._uv = null;
|
|
120
155
|
this._content = null;
|
|
121
|
-
|
|
156
|
+
this._metricsFor = null; // the tile handle whose displayMetrics() was asked for
|
|
157
|
+
/** One CSS px on the panel, metres, once the tile's handle has said (else the CSS reference px). */
|
|
158
|
+
this.metersPerCssPx = null;
|
|
159
|
+
/** The last frame's placement, for diagnostics: `{ contentPx, targetPx, disparityPx, drawn }` or null. */
|
|
122
160
|
this.last = null;
|
|
123
161
|
}
|
|
124
162
|
|
|
@@ -127,30 +165,55 @@ export class PairCursor {
|
|
|
127
165
|
}
|
|
128
166
|
|
|
129
167
|
/**
|
|
168
|
+
* Draws the arrow only while content in front of the glass is under it (the OS cursor is hidden
|
|
169
|
+
* exactly then); otherwise leaves the buffer alone and the OS arrow shows, on the plane.
|
|
130
170
|
* @param {CanvasRenderingContext2D} g the woven buffer (2·outW × outH), already painted
|
|
131
171
|
* @param {object|null} luma the latest grayscale source copy (pairFootprintDisparity)
|
|
132
|
-
* @param {{rL: object, rR: object, outW: number, outH: number, mirror?: boolean}} geo
|
|
172
|
+
* @param {{rL: object, rR: object, outW: number, outH: number, mirror?: boolean, handle?: object}} geo
|
|
173
|
+
* `handle` = the tile's woven handle: its `displayMetrics()` gives the panel's pixel pitch
|
|
133
174
|
* @param {number} nowSec
|
|
134
175
|
*/
|
|
135
176
|
draw(g, luma, geo, nowSec) {
|
|
136
177
|
const uv = this.pointer.uv;
|
|
137
178
|
if (!uv) return this.idle();
|
|
138
179
|
const [u, v] = uv;
|
|
180
|
+
const hf = this.height ?? this._osArrowFraction();
|
|
139
181
|
// Match only when there is something new to match: a new frame copy or a moved pointer.
|
|
140
182
|
if (luma !== this._luma || !this._uv || this._uv[0] !== u || this._uv[1] !== v) {
|
|
141
|
-
this._content = pairFootprintDisparity(luma, geo, u, v,
|
|
183
|
+
this._content = pairFootprintDisparity(luma, geo, u, v, hf, geo.outW / geo.outH);
|
|
142
184
|
this._luma = luma;
|
|
143
185
|
this._uv = uv;
|
|
144
186
|
}
|
|
145
|
-
|
|
187
|
+
this._askMetrics(geo.handle);
|
|
188
|
+
const r = this.pointer.canvas.getBoundingClientRect?.();
|
|
189
|
+
const pxPerM = pairCursorScale(geo.outW, r && r.width, this.metersPerCssPx);
|
|
190
|
+
const target = pairCursorTarget(this._content, geo.outW, PAIR_CURSOR_MARGIN_M * pxPerM);
|
|
146
191
|
// The shared filter rises fast toward SMALLER values (nearer, in its convention): negate.
|
|
147
192
|
const disparity = -cursorFilterStep(this.filter, CURSOR_DEFAULT_TUNING, -target, nowSec);
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
this.
|
|
193
|
+
const drawn = disparity >= Math.max(PAIR_CURSOR_SHOW_MIN_PX, PAIR_CURSOR_SHOW_M * pxPerM);
|
|
194
|
+
if (drawn) drawPairArrow(g, geo.outW, geo.outH, u, v, disparity, hf * geo.outH);
|
|
195
|
+
this.pointer.hideCss(drawn); // the OS arrow IS the cursor on the plane
|
|
196
|
+
this.last = { contentPx: this._content, targetPx: target, disparityPx: drawn ? disparity : 0, drawn };
|
|
151
197
|
return this.last;
|
|
152
198
|
}
|
|
153
199
|
|
|
200
|
+
/** Ask the tile's handle once for the panel's pixel pitch; until it answers, the CSS reference px. */
|
|
201
|
+
_askMetrics(handle) {
|
|
202
|
+
if (!handle || handle === this._metricsFor || typeof handle.displayMetrics !== 'function') return;
|
|
203
|
+
this._metricsFor = handle;
|
|
204
|
+
Promise.resolve(handle.displayMetrics())
|
|
205
|
+
.then((m) => {
|
|
206
|
+
if (this._metricsFor === handle && m && m.metersPerCssPx > 0) this.metersPerCssPx = m.metersPerCssPx;
|
|
207
|
+
})
|
|
208
|
+
.catch(() => {});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** The OS arrow's height as a fraction of the tile's on-page height. */
|
|
212
|
+
_osArrowFraction() {
|
|
213
|
+
const r = this.pointer.canvas.getBoundingClientRect?.();
|
|
214
|
+
return r && r.height > 0 ? Math.min(0.5, PAIR_CURSOR_CSS_PX / r.height) : PAIR_CURSOR_HEIGHT;
|
|
215
|
+
}
|
|
216
|
+
|
|
154
217
|
/** Not drawing this frame (pointer away, tile not woven): the CSS cursor comes back. */
|
|
155
218
|
idle() {
|
|
156
219
|
this.pointer.hideCss(false);
|
package/js/inline3d-call.js
CHANGED
|
@@ -1715,7 +1715,7 @@ class Tile {
|
|
|
1715
1715
|
const r = (rects[eye] = wire.eyeCropRect(eyeW, H, A, shift, eye));
|
|
1716
1716
|
g.drawImage(v, eye * eyeW + r.sx, r.sy, r.sw, r.sh, eye * outW, 0, outW, outH);
|
|
1717
1717
|
}
|
|
1718
|
-
if (this.cursor) this.cursor.draw(g, this.luma, { rL: rects[0], rR: rects[1], outW, outH }, performance.now() / 1000);
|
|
1718
|
+
if (this.cursor) this.cursor.draw(g, this.luma, { rL: rects[0], rR: rects[1], outW, outH, handle: this.handle }, performance.now() / 1000);
|
|
1719
1719
|
}
|
|
1720
1720
|
|
|
1721
1721
|
/**
|
package/js/inline3d-camera.js
CHANGED
|
@@ -762,7 +762,7 @@ class CameraView {
|
|
|
762
762
|
}
|
|
763
763
|
}
|
|
764
764
|
if (this.cursor) {
|
|
765
|
-
const geo = { rL: eyeCropRect(eyeW, H, A, shift, 0), rR: eyeCropRect(eyeW, H, A, shift, 1), outW, outH, mirror: this.mirror };
|
|
765
|
+
const geo = { rL: eyeCropRect(eyeW, H, A, shift, 0), rR: eyeCropRect(eyeW, H, A, shift, 1), outW, outH, mirror: this.mirror, handle: this.handle };
|
|
766
766
|
this.cursor.draw(g, this._luma, geo, performance.now() / 1000);
|
|
767
767
|
}
|
|
768
768
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@displayxr/inline3d",
|
|
3
|
-
"version": "1.40.
|
|
3
|
+
"version": "1.40.2",
|
|
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",
|