@displayxr/inline3d 1.43.1 → 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 +129 -0
- package/THIRD_PARTY_NOTICES.md +26 -2
- package/camera.d.ts +7 -0
- package/cursor-depth.d.ts +275 -1
- package/dist/call.js +18 -4
- package/js/camera/pair-cursor.js +129 -26
- package/js/inline3d-cursor-bvh.js +147 -0
- package/js/inline3d-cursor-depth-playcanvas.js +507 -61
- package/js/inline3d-cursor-depth.js +497 -43
- package/js/inline3d-cursor-option.js +140 -6
- package/js/inline3d-cursor-perf.js +199 -0
- package/js/inline3d-cursor-shape.js +962 -0
- package/js/inline3d-model.js +30 -1
- package/js/inline3d-playcanvas-engine.js +2 -0
- package/js/inline3d-splat-layer-rig.js +3 -0
- package/js/inline3d-three.js +263 -30
- package/js/vendor/three-mesh-bvh.js +3 -0
- package/model.d.ts +1 -1
- package/package.json +7 -1
- package/splat.d.ts +1 -1
- package/three.d.ts +43 -4
package/js/camera/pair-cursor.js
CHANGED
|
@@ -15,7 +15,11 @@
|
|
|
15
15
|
// disparity is past the draw threshold (-0.005 baselines, and >= 0.5 px; cursorShouldDraw), the
|
|
16
16
|
// OS cursor hidden exactly then;
|
|
17
17
|
// - the same filter (fast rise, slow sink) and the same arrow (CURSOR_ARROW, tip = hotspot, the
|
|
18
|
-
// OS arrow's 19 CSS px)
|
|
18
|
+
// OS arrow's 19 CSS px);
|
|
19
|
+
// - the same crosstalk softening (Amendment 2 §B.7, cursorSoften): a lifted arrow is blurred by
|
|
20
|
+
// 0.1 × its ghost offset (here: its disparity, the two halves being the two views) and faded
|
|
21
|
+
// toward 0.5, both zero on the glass. Rasterised into the buffer from the core's 13-tap kernel
|
|
22
|
+
// (cursorSoftArrowRaster) — inside the woven pixels, never a CSS filter or CSS opacity.
|
|
19
23
|
// What is specific to a pair: it has no 3D views, so it works in tile PIXEL disparity — OUTPUT
|
|
20
24
|
// pixels of one eye of the woven buffer, + = crossed (in front of the glass), the sign the rest of
|
|
21
25
|
// camera/ uses — and converts to the core's eye-baseline units through the panel's pixel pitch:
|
|
@@ -25,12 +29,18 @@
|
|
|
25
29
|
|
|
26
30
|
import {
|
|
27
31
|
CursorPointer,
|
|
32
|
+
CursorShapeSelector,
|
|
28
33
|
cursorArrowFootprint,
|
|
29
|
-
|
|
34
|
+
cursorShapePolys,
|
|
35
|
+
cursorShapeRaster,
|
|
36
|
+
cursorSoftenPixels,
|
|
37
|
+
drawCursorShape2D,
|
|
30
38
|
cursorDefaultHeight,
|
|
31
39
|
cursorFilterStep,
|
|
32
40
|
cursorShouldDraw,
|
|
41
|
+
cursorSoften,
|
|
33
42
|
cursorTarget,
|
|
43
|
+
resolveCursorSoften,
|
|
34
44
|
CURSOR_ARROW,
|
|
35
45
|
CURSOR_ARROW_CSS_PX,
|
|
36
46
|
CURSOR_DEFAULT_TUNING,
|
|
@@ -211,44 +221,114 @@ export function pairCursorTarget(contentPx, pxPerBaseline, tuning = CURSOR_DEFAU
|
|
|
211
221
|
return -cursorTarget(tuning, true, -contentPx / pxPerBaseline) * pxPerBaseline || 0;
|
|
212
222
|
}
|
|
213
223
|
|
|
214
|
-
/** The
|
|
215
|
-
const
|
|
216
|
-
function
|
|
224
|
+
/** The shape's outline + fill inset by one output px (at least 1/19 of its height), cached per size. */
|
|
225
|
+
const _polysBySize = new Map();
|
|
226
|
+
function shapePolys(name, sizePx) {
|
|
217
227
|
const d = sizePx > 0 ? Math.max(1 / 19, 1 / sizePx) : 1 / 19;
|
|
218
|
-
const key = Math.round(d * 1e4)
|
|
219
|
-
let f =
|
|
228
|
+
const key = `${name}:${Math.round(d * 1e4)}`;
|
|
229
|
+
let f = _polysBySize.get(key);
|
|
220
230
|
if (!f) {
|
|
221
|
-
if (
|
|
222
|
-
|
|
231
|
+
if (_polysBySize.size > 64) _polysBySize.clear();
|
|
232
|
+
_polysBySize.set(key, (f = cursorShapePolys(name, d)));
|
|
223
233
|
}
|
|
224
234
|
return f;
|
|
225
235
|
}
|
|
226
236
|
|
|
227
237
|
/**
|
|
228
|
-
* Draw the
|
|
229
|
-
* the pointer, the right eye's as far left (crossed = in front). The shared
|
|
230
|
-
*
|
|
231
|
-
* own half, so
|
|
232
|
-
*
|
|
238
|
+
* Draw the cursor into both halves of a woven buffer: the left eye's copy `disparityPx / 2` right of
|
|
239
|
+
* the pointer, the right eye's as far left (crossed = in front). The shared sprite (the arrow by
|
|
240
|
+
* default, or `shape`): its dark outline, then the white fill inset by a px; the hotspot on the
|
|
241
|
+
* pointer. Each copy is clipped to its own half, so a sprite at the edge never bleeds into the
|
|
242
|
+
* other eye.
|
|
243
|
+
* @param {number} sizePx sprite height (one arrow height) in output px
|
|
244
|
+
* @param {string} [shape='arrow'] a CURSOR_SHAPES name
|
|
233
245
|
*/
|
|
234
|
-
export function drawPairArrow(g, outW, outH, u, v, disparityPx, sizePx) {
|
|
246
|
+
export function drawPairArrow(g, outW, outH, u, v, disparityPx, sizePx, shape = 'arrow') {
|
|
235
247
|
const cy = v * outH;
|
|
236
|
-
const
|
|
248
|
+
const polys = shapePolys(shape, sizePx);
|
|
237
249
|
for (const eye of [0, 1]) {
|
|
238
250
|
const cx = eye * outW + u * outW + (eye === 0 ? disparityPx / 2 : -disparityPx / 2);
|
|
239
251
|
g.save();
|
|
240
252
|
g.beginPath();
|
|
241
253
|
g.rect(eye * outW, 0, outW, outH);
|
|
242
254
|
g.clip();
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
255
|
+
drawCursorShape2D(g, shape, cx, cy, sizePx, polys);
|
|
256
|
+
g.restore();
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// A scratch 2D canvas for the softened arrow (drawImage blends and honours the clip; putImageData
|
|
261
|
+
// would do neither). Null where no canvas can be made — then the crisp arrow is drawn.
|
|
262
|
+
let _scratch = null;
|
|
263
|
+
function scratchCanvas(w, h) {
|
|
264
|
+
if (!_scratch || (typeof OffscreenCanvas === 'function' && !(_scratch instanceof OffscreenCanvas))) {
|
|
265
|
+
try {
|
|
266
|
+
if (typeof OffscreenCanvas === 'function') _scratch = new OffscreenCanvas(1, 1);
|
|
267
|
+
else if (globalThis.document && document.createElement) _scratch = document.createElement('canvas');
|
|
268
|
+
} catch {
|
|
269
|
+
_scratch = null;
|
|
249
270
|
}
|
|
271
|
+
}
|
|
272
|
+
if (!_scratch || typeof _scratch.getContext !== 'function') return null;
|
|
273
|
+
if (_scratch.width < w) _scratch.width = w;
|
|
274
|
+
if (_scratch.height < h) _scratch.height = h;
|
|
275
|
+
return _scratch.getContext('2d');
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* The SOFTENED arrow into both halves (ADR-046 Amendment 2 §B.7): as drawPairArrow — same
|
|
280
|
+
* placement, same tip, same clip to each half — but rasterised from the core's 13-tap blur of the
|
|
281
|
+
* arrow with `radius` (arrow heights) and `alpha` (opacity). Returns false (nothing drawn) when no
|
|
282
|
+
* scratch canvas is available, so the caller can fall back to the crisp arrow.
|
|
283
|
+
*/
|
|
284
|
+
export function drawPairArrowSoft(g, outW, outH, u, v, disparityPx, sizePx, radius, alpha, shape = 'arrow') {
|
|
285
|
+
const cy = v * outH;
|
|
286
|
+
const polys = shapePolys(shape, sizePx);
|
|
287
|
+
const rasters = [0, 1].map((eye) => {
|
|
288
|
+
const cx = eye * outW + u * outW + (eye === 0 ? disparityPx / 2 : -disparityPx / 2);
|
|
289
|
+
return cursorShapeRaster(shape, cx, cy, sizePx, radius, alpha, [1, 1, 1, 1], [0, 0, 0, 1], polys);
|
|
290
|
+
});
|
|
291
|
+
const sg = scratchCanvas(Math.max(rasters[0].width, rasters[1].width), Math.max(rasters[0].height, rasters[1].height));
|
|
292
|
+
if (!sg || typeof sg.putImageData !== 'function' || typeof ImageData !== 'function') return false;
|
|
293
|
+
for (const eye of [0, 1]) {
|
|
294
|
+
const r = rasters[eye];
|
|
295
|
+
sg.putImageData(new ImageData(r.data, r.width, r.height), 0, 0);
|
|
296
|
+
g.save();
|
|
297
|
+
g.beginPath();
|
|
298
|
+
g.rect(eye * outW, 0, outW, outH);
|
|
299
|
+
g.clip();
|
|
300
|
+
g.drawImage(sg.canvas, 0, 0, r.width, r.height, r.x0, r.y0, r.width, r.height);
|
|
301
|
+
g.restore();
|
|
302
|
+
}
|
|
303
|
+
return true;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* A url() image cursor into both halves: at its CSS size (one sprite unit = `sizePx` output px per
|
|
308
|
+
* 19 CSS px), its hotspot on the pointer, softened in the PIXELS (cursorSoftenPixels: `radiusPx`
|
|
309
|
+
* blur, `alpha` opacity) — never a CSS filter or opacity. Returns false when no scratch canvas.
|
|
310
|
+
*/
|
|
311
|
+
export function drawPairImage(g, outW, outH, u, v, disparityPx, sizePx, sprite, radiusPx, alpha) {
|
|
312
|
+
const k = sizePx / 19; // output px per CSS px
|
|
313
|
+
const w = Math.max(1, Math.round(sprite.width * k)), h = Math.max(1, Math.round(sprite.height * k));
|
|
314
|
+
const sg = scratchCanvas(w + 2 * (Math.ceil(radiusPx) + 2), h + 2 * (Math.ceil(radiusPx) + 2));
|
|
315
|
+
if (!sg || typeof sg.getImageData !== 'function' || typeof ImageData !== 'function') return false;
|
|
316
|
+
sg.clearRect(0, 0, sg.canvas.width, sg.canvas.height);
|
|
317
|
+
sg.drawImage(sprite.entry.image, 0, 0, w, h);
|
|
318
|
+
const soft = cursorSoftenPixels(sg.getImageData(0, 0, w, h).data, w, h, radiusPx, alpha);
|
|
319
|
+
sg.clearRect(0, 0, sg.canvas.width, sg.canvas.height);
|
|
320
|
+
sg.putImageData(new ImageData(soft.data, soft.width, soft.height), 0, 0);
|
|
321
|
+
const cy = v * outH - sprite.hotspot[1] * k - soft.pad;
|
|
322
|
+
for (const eye of [0, 1]) {
|
|
323
|
+
const cx = eye * outW + u * outW + (eye === 0 ? disparityPx / 2 : -disparityPx / 2) - sprite.hotspot[0] * k - soft.pad;
|
|
324
|
+
g.save();
|
|
325
|
+
g.beginPath();
|
|
326
|
+
g.rect(eye * outW, 0, outW, outH);
|
|
327
|
+
g.clip();
|
|
328
|
+
g.drawImage(sg.canvas, 0, 0, soft.width, soft.height, Math.round(cx), Math.round(cy), soft.width, soft.height);
|
|
250
329
|
g.restore();
|
|
251
330
|
}
|
|
331
|
+
return true;
|
|
252
332
|
}
|
|
253
333
|
|
|
254
334
|
/**
|
|
@@ -258,8 +338,12 @@ export function drawPairArrow(g, outW, outH, u, v, disparityPx, sizePx) {
|
|
|
258
338
|
*/
|
|
259
339
|
export class PairCursor {
|
|
260
340
|
/** @param {HTMLElement} canvas the tile — the pointer counts over its box */
|
|
261
|
-
constructor(canvas, { height, pointerScope = 'window' } = {}) {
|
|
341
|
+
constructor(canvas, { height, pointerScope = 'window', soften, shape = 'auto', shapeFor = null } = {}) {
|
|
262
342
|
this.pointer = new CursorPointer(canvas, { scope: pointerScope });
|
|
343
|
+
/** Which sprite (CSS mirror / fixed / shapeFor); `.current` = the one drawn last. */
|
|
344
|
+
this.shapes = new CursorShapeSelector(this.pointer, { shape, shapeFor });
|
|
345
|
+
/** Crosstalk softening parameters (resolveCursorSoften). */
|
|
346
|
+
this.soften = resolveCursorSoften(soften, '@displayxr/inline3d pair cursor');
|
|
263
347
|
// A fraction of the tile height when given; by default the OS arrow's size (CSS px → tile).
|
|
264
348
|
this.height = height > 0 ? height : null;
|
|
265
349
|
this.filter = {};
|
|
@@ -271,7 +355,7 @@ export class PairCursor {
|
|
|
271
355
|
this.metersPerCssPx = null;
|
|
272
356
|
/** The eye separation (metres) the baseline units convert with: displayMetrics' (a default today). */
|
|
273
357
|
this.eyeSeparationM = DISPLAY_METRICS_DEFAULTS.eyeSeparationM;
|
|
274
|
-
/** The last frame's placement, for diagnostics: `{ contentPx, targetPx, disparityPx, filteredPx, drawn, pxPerBaseline }` (output px, + = in front) or null. */
|
|
358
|
+
/** The last frame's placement, for diagnostics: `{ contentPx, targetPx, disparityPx, filteredPx, drawn, pxPerBaseline, soften }` (output px, + = in front) or null. */
|
|
275
359
|
this.last = null;
|
|
276
360
|
}
|
|
277
361
|
|
|
@@ -308,9 +392,27 @@ export class PairCursor {
|
|
|
308
392
|
const d = cursorFilterStep(this.filter, CURSOR_DEFAULT_TUNING, target, nowSec);
|
|
309
393
|
const drawn = cursorShouldDraw(d, pxPerBaseline);
|
|
310
394
|
const disparity = -d * pxPerBaseline; // output px, + = in front
|
|
311
|
-
|
|
395
|
+
let soften = null;
|
|
396
|
+
let shape = null;
|
|
397
|
+
if (drawn) {
|
|
398
|
+
// Softening in PANEL px: one output px of an eye spans (tile CSS width × dpr) / outW of them.
|
|
399
|
+
// The ghost offset of a pair IS its disparity (the two halves are the two views).
|
|
400
|
+
const sizePx = hf * geo.outH;
|
|
401
|
+
const dpr = globalThis.devicePixelRatio > 0 ? globalThis.devicePixelRatio : 1;
|
|
402
|
+
const k = r && r.width > 0 ? (r.width * dpr) / geo.outW : 1;
|
|
403
|
+
const spritePx = r && r.height > 0 ? hf * r.height * dpr : sizePx;
|
|
404
|
+
const s = cursorSoften(d, disparity * k, spritePx, this.soften);
|
|
405
|
+
const sprite = this.shapes.pick({ u, v, point: null }, nowSec * 1000);
|
|
406
|
+
shape = this.shapes.current;
|
|
407
|
+
soften = { blurPx: s.blurPx, alpha: s.alpha, ghostPx: Math.abs(disparity * k), spritePx, radius: spritePx > 0 ? s.blurPx / spritePx : 0, soft: s.blurPx > 0 || s.alpha < 1 };
|
|
408
|
+
if (sprite.kind === 'image') {
|
|
409
|
+
drawPairImage(g, geo.outW, geo.outH, u, v, disparity, sizePx, sprite, soften.soft ? soften.radius * sizePx : 0, soften.soft ? s.alpha : 1);
|
|
410
|
+
} else if (sprite.kind === 'shape' && (!soften.soft || !drawPairArrowSoft(g, geo.outW, geo.outH, u, v, disparity, sizePx, soften.radius, s.alpha, shape))) {
|
|
411
|
+
drawPairArrow(g, geo.outW, geo.outH, u, v, disparity, sizePx, shape);
|
|
412
|
+
} // 'none': the page hides its cursor; draw nothing (the OS cursor stays hidden)
|
|
413
|
+
}
|
|
312
414
|
this.pointer.hideCss(drawn); // the OS arrow IS the cursor on the plane
|
|
313
|
-
this.last = { contentPx: this._content, targetPx: -target * pxPerBaseline || 0, disparityPx: drawn ? disparity : 0, filteredPx: disparity || 0, drawn, pxPerBaseline };
|
|
415
|
+
this.last = { contentPx: this._content, targetPx: -target * pxPerBaseline || 0, disparityPx: drawn ? disparity : 0, filteredPx: disparity || 0, drawn, pxPerBaseline, soften, shape };
|
|
314
416
|
return this.last;
|
|
315
417
|
}
|
|
316
418
|
|
|
@@ -345,5 +447,6 @@ export class PairCursor {
|
|
|
345
447
|
|
|
346
448
|
dispose() {
|
|
347
449
|
this.pointer.dispose();
|
|
450
|
+
this.shapes.dispose();
|
|
348
451
|
}
|
|
349
452
|
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// inline3d-cursor-bvh.js — the depth cursor's pick, BVH-accelerated, for three.js scenes.
|
|
2
|
+
//
|
|
3
|
+
// Loaded ONLY when a page opts into `cursor: 'depth'` (./model imports it dynamically on opt-in;
|
|
4
|
+
// ADR-046 §0: zero cost, no download otherwise). It brings the vendored MeshBVH slice of
|
|
5
|
+
// three-mesh-bvh (js/vendor/three-mesh-bvh.js, MIT), which imports the page's own `three`.
|
|
6
|
+
//
|
|
7
|
+
// Same answer as three's Raycaster.intersectObjects(targets, true)[0] — the nearest hit, the same
|
|
8
|
+
// layer test, the same traversal (an object's raycast returning false stops its subtree) — but each
|
|
9
|
+
// plain Mesh is tested against a bounds tree instead of every triangle. Nothing on the page is
|
|
10
|
+
// touched: no prototype patching, no `geometry.boundsTree`, no index reordering (`indirect: true`);
|
|
11
|
+
// the trees live in a WeakMap keyed by geometry.
|
|
12
|
+
//
|
|
13
|
+
// - Built lazily, the first time a ray reaches a mesh's bounding sphere, within a small time
|
|
14
|
+
// budget a frame (one big mesh may exceed it once; it cannot be split). A mesh not built yet is
|
|
15
|
+
// raycast the plain way meanwhile, so the answer never waits for a build.
|
|
16
|
+
// - CPU-animated vertices (the position attribute's version moves): the tree is refitted.
|
|
17
|
+
// A replaced attribute, a changed vertex count or index: rebuilt.
|
|
18
|
+
// - Skinned, morphing, instanced or batched meshes, a partial drawRange, and anything that is not
|
|
19
|
+
// a Mesh (points, lines, sprites, a splat's own raycast): three's own raycast, as before —
|
|
20
|
+
// its answer follows the deformation; a static tree would not.
|
|
21
|
+
|
|
22
|
+
import { MeshBVH } from './vendor/three-mesh-bvh.js';
|
|
23
|
+
|
|
24
|
+
/** Build time allowed per ~frame (ms) before further builds wait for the next one. */
|
|
25
|
+
const BUILD_BUDGET_MS = 4;
|
|
26
|
+
const FRAME_MS = 16;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A hit test for ./three's DepthCursor over `targets` (searched recursively): the nearest hit along
|
|
30
|
+
* a WORLD ray, as a world point, or null.
|
|
31
|
+
* @param {object} THREE the page's three.js namespace.
|
|
32
|
+
* @param {object[]} targets Object3Ds that count as content.
|
|
33
|
+
* @returns {((origin: number[], direction: number[]) => number[]|null) & {info: () => object}}
|
|
34
|
+
*/
|
|
35
|
+
export function bvhHitTest(THREE, targets, { now = () => (globalThis.performance ? globalThis.performance.now() : Date.now()) } = {}) {
|
|
36
|
+
const raycaster = new THREE.Raycaster();
|
|
37
|
+
raycaster.firstHitOnly = true; // MeshBVH.raycastObject3D: the nearest hit only
|
|
38
|
+
const o = new THREE.Vector3();
|
|
39
|
+
const d = new THREE.Vector3();
|
|
40
|
+
const sphere = new THREE.Sphere();
|
|
41
|
+
const trees = new WeakMap(); // geometry → { bvh, pos, idx, count, posVersion, idxVersion } | false (cannot)
|
|
42
|
+
const hits = [];
|
|
43
|
+
const info = { mode: 'bvh', built: 0, refits: 0, failed: 0, buildMs: 0, plainCasts: 0 };
|
|
44
|
+
let windowStart = -Infinity;
|
|
45
|
+
let spent = 0;
|
|
46
|
+
const best = [0, 0, 0];
|
|
47
|
+
let bestDist = Infinity;
|
|
48
|
+
|
|
49
|
+
function treeFor(mesh) {
|
|
50
|
+
if (mesh.isSkinnedMesh || mesh.isInstancedMesh || mesh.isBatchedMesh) return null;
|
|
51
|
+
const g = mesh.geometry;
|
|
52
|
+
if (!g || !g.attributes) return null;
|
|
53
|
+
const pos = g.attributes.position;
|
|
54
|
+
if (!pos) return null;
|
|
55
|
+
if (g.morphAttributes && g.morphAttributes.position && g.morphAttributes.position.length) return null;
|
|
56
|
+
const idx = g.index;
|
|
57
|
+
const full = idx ? idx.count : pos.count;
|
|
58
|
+
const dr = g.drawRange;
|
|
59
|
+
if (dr && (dr.start > 0 || (dr.count !== Infinity && dr.count < full))) return null;
|
|
60
|
+
let t = trees.get(g);
|
|
61
|
+
if (t === false) return null;
|
|
62
|
+
if (t) {
|
|
63
|
+
if (t.pos !== pos || t.idx !== idx || t.count !== pos.count || (idx && idx.version !== t.idxVersion)) {
|
|
64
|
+
trees.delete(g); // replaced or resized: rebuild below
|
|
65
|
+
t = undefined;
|
|
66
|
+
} else {
|
|
67
|
+
if (pos.version !== t.posVersion) {
|
|
68
|
+
t.bvh.refit(); // vertices animated on the CPU
|
|
69
|
+
t.posVersion = pos.version;
|
|
70
|
+
info.refits++;
|
|
71
|
+
}
|
|
72
|
+
return t;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const t0 = now();
|
|
76
|
+
if (t0 - windowStart >= FRAME_MS) {
|
|
77
|
+
windowStart = t0;
|
|
78
|
+
spent = 0;
|
|
79
|
+
}
|
|
80
|
+
if (spent >= BUILD_BUDGET_MS) return null; // plain raycast this time; built on a later frame
|
|
81
|
+
try {
|
|
82
|
+
const bvh = new MeshBVH(g, { indirect: true, setBoundingBox: false, verbose: false });
|
|
83
|
+
t = { bvh, pos, idx, count: pos.count, posVersion: pos.version, idxVersion: idx ? idx.version : 0 };
|
|
84
|
+
trees.set(g, t);
|
|
85
|
+
info.built++;
|
|
86
|
+
} catch (err) {
|
|
87
|
+
trees.set(g, false); // e.g. a three.js older than the slice supports: plain raycast for good
|
|
88
|
+
info.failed++;
|
|
89
|
+
t = null;
|
|
90
|
+
}
|
|
91
|
+
const dt = now() - t0;
|
|
92
|
+
spent += dt;
|
|
93
|
+
info.buildMs += dt;
|
|
94
|
+
return t;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function castOne(obj) {
|
|
98
|
+
hits.length = 0;
|
|
99
|
+
let result;
|
|
100
|
+
const t = obj.isMesh ? treeFor(obj) : null;
|
|
101
|
+
if (t) {
|
|
102
|
+
const g = obj.geometry;
|
|
103
|
+
if (g.boundingSphere === null) g.computeBoundingSphere(); // as Mesh.raycast does
|
|
104
|
+
sphere.copy(g.boundingSphere).applyMatrix4(obj.matrixWorld);
|
|
105
|
+
if (!raycaster.ray.intersectsSphere(sphere)) return undefined;
|
|
106
|
+
t.bvh.raycastObject3D(obj, raycaster, hits);
|
|
107
|
+
} else {
|
|
108
|
+
if (obj.isMesh || obj.isPoints || obj.isLine || obj.isSprite) info.plainCasts++; // drawables only, not groups
|
|
109
|
+
result = obj.raycast(raycaster, hits);
|
|
110
|
+
}
|
|
111
|
+
for (let i = 0; i < hits.length; i++) {
|
|
112
|
+
const h = hits[i];
|
|
113
|
+
if (h.distance < bestDist) {
|
|
114
|
+
bestDist = h.distance;
|
|
115
|
+
best[0] = h.point.x;
|
|
116
|
+
best[1] = h.point.y;
|
|
117
|
+
best[2] = h.point.z;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Raycaster.intersectObject's traversal: the layer test, and `false` from raycast stops the subtree.
|
|
124
|
+
function visit(obj) {
|
|
125
|
+
let propagate = true;
|
|
126
|
+
if (obj.layers.test(raycaster.layers)) {
|
|
127
|
+
if (castOne(obj) === false) propagate = false;
|
|
128
|
+
}
|
|
129
|
+
if (propagate) {
|
|
130
|
+
const c = obj.children;
|
|
131
|
+
for (let i = 0; i < c.length; i++) visit(c[i]);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const hitTest = (origin, direction) => {
|
|
136
|
+
o.set(origin[0], origin[1], origin[2]);
|
|
137
|
+
d.set(direction[0], direction[1], direction[2]).normalize();
|
|
138
|
+
raycaster.set(o, d);
|
|
139
|
+
bestDist = Infinity;
|
|
140
|
+
for (let i = 0; i < targets.length; i++) visit(targets[i]);
|
|
141
|
+
hits.length = 0;
|
|
142
|
+
return bestDist < Infinity ? [best[0], best[1], best[2]] : null;
|
|
143
|
+
};
|
|
144
|
+
/** Diagnostics: `{ mode: 'bvh', built, refits, failed, buildMs, plainCasts }` (cumulative). */
|
|
145
|
+
hitTest.info = () => ({ ...info });
|
|
146
|
+
return hitTest;
|
|
147
|
+
}
|