@react-three-dom/core 0.2.0 → 0.3.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/dist/index.cjs +1544 -709
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +493 -46
- package/dist/index.d.ts +493 -46
- package/dist/index.js +1542 -711
- package/dist/index.js.map +1 -1
- package/package.json +9 -6
package/dist/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { Box3,
|
|
1
|
+
import { Box3, Object3D, Vector3, Matrix4, Frustum, Raycaster, Vector2, BufferGeometry, Material, Color, MeshBasicMaterial, DoubleSide, Mesh, BoxGeometry, EdgesGeometry, LineBasicMaterial, LineSegments, InstancedMesh, PerspectiveCamera, OrthographicCamera } from 'three';
|
|
2
2
|
import { useRef, useEffect } from 'react';
|
|
3
3
|
import { useThree, useFrame } from '@react-three/fiber';
|
|
4
|
+
import { computeBoundsTree, disposeBoundsTree, acceleratedRaycast } from 'three-mesh-bvh';
|
|
4
5
|
|
|
5
6
|
// src/version.ts
|
|
6
|
-
var version = "0.
|
|
7
|
+
var version = "0.3.0";
|
|
7
8
|
|
|
8
9
|
// src/debug.ts
|
|
9
10
|
var _enabled = false;
|
|
@@ -37,6 +38,10 @@ function extractMetadata(obj) {
|
|
|
37
38
|
childrenUuids: obj.children.map((c) => c.uuid),
|
|
38
39
|
boundsDirty: true
|
|
39
40
|
};
|
|
41
|
+
extractStaticFields(obj, meta);
|
|
42
|
+
return meta;
|
|
43
|
+
}
|
|
44
|
+
function extractStaticFields(obj, meta) {
|
|
40
45
|
try {
|
|
41
46
|
if ("geometry" in obj) {
|
|
42
47
|
const geom = obj.geometry;
|
|
@@ -74,13 +79,79 @@ function extractMetadata(obj) {
|
|
|
74
79
|
}
|
|
75
80
|
} catch {
|
|
76
81
|
}
|
|
77
|
-
|
|
82
|
+
try {
|
|
83
|
+
if (obj instanceof PerspectiveCamera) {
|
|
84
|
+
meta.fov = obj.fov;
|
|
85
|
+
meta.near = obj.near;
|
|
86
|
+
meta.far = obj.far;
|
|
87
|
+
meta.zoom = obj.zoom;
|
|
88
|
+
} else if (obj instanceof OrthographicCamera) {
|
|
89
|
+
meta.near = obj.near;
|
|
90
|
+
meta.far = obj.far;
|
|
91
|
+
meta.zoom = obj.zoom;
|
|
92
|
+
}
|
|
93
|
+
} catch {
|
|
94
|
+
}
|
|
78
95
|
}
|
|
79
|
-
function
|
|
80
|
-
|
|
96
|
+
function updateDynamicFields(obj, meta) {
|
|
97
|
+
let changed = false;
|
|
98
|
+
if (meta.visible !== obj.visible) {
|
|
99
|
+
meta.visible = obj.visible;
|
|
100
|
+
changed = true;
|
|
101
|
+
}
|
|
102
|
+
if (meta.name !== obj.name) {
|
|
103
|
+
meta.name = obj.name;
|
|
104
|
+
changed = true;
|
|
105
|
+
}
|
|
106
|
+
const testId = obj.userData?.testId;
|
|
107
|
+
if (meta.testId !== testId) {
|
|
108
|
+
meta.testId = testId;
|
|
109
|
+
changed = true;
|
|
110
|
+
}
|
|
111
|
+
const p = obj.position;
|
|
112
|
+
if (meta.position[0] !== p.x || meta.position[1] !== p.y || meta.position[2] !== p.z) {
|
|
113
|
+
meta.position[0] = p.x;
|
|
114
|
+
meta.position[1] = p.y;
|
|
115
|
+
meta.position[2] = p.z;
|
|
116
|
+
changed = true;
|
|
117
|
+
}
|
|
118
|
+
const r = obj.rotation;
|
|
119
|
+
if (meta.rotation[0] !== r.x || meta.rotation[1] !== r.y || meta.rotation[2] !== r.z) {
|
|
120
|
+
meta.rotation[0] = r.x;
|
|
121
|
+
meta.rotation[1] = r.y;
|
|
122
|
+
meta.rotation[2] = r.z;
|
|
123
|
+
changed = true;
|
|
124
|
+
}
|
|
125
|
+
const s = obj.scale;
|
|
126
|
+
if (meta.scale[0] !== s.x || meta.scale[1] !== s.y || meta.scale[2] !== s.z) {
|
|
127
|
+
meta.scale[0] = s.x;
|
|
128
|
+
meta.scale[1] = s.y;
|
|
129
|
+
meta.scale[2] = s.z;
|
|
130
|
+
changed = true;
|
|
131
|
+
}
|
|
132
|
+
const parentUuid = obj.parent?.uuid ?? null;
|
|
133
|
+
if (meta.parentUuid !== parentUuid) {
|
|
134
|
+
meta.parentUuid = parentUuid;
|
|
135
|
+
changed = true;
|
|
136
|
+
}
|
|
137
|
+
const children = obj.children;
|
|
138
|
+
const cached = meta.childrenUuids;
|
|
139
|
+
if (cached.length !== children.length) {
|
|
140
|
+
meta.childrenUuids = children.map((c) => c.uuid);
|
|
141
|
+
changed = true;
|
|
142
|
+
} else {
|
|
143
|
+
for (let i = 0; i < cached.length; i++) {
|
|
144
|
+
if (cached[i] !== children[i].uuid) {
|
|
145
|
+
meta.childrenUuids = children.map((c) => c.uuid);
|
|
146
|
+
changed = true;
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return changed;
|
|
81
152
|
}
|
|
82
153
|
var _box3 = new Box3();
|
|
83
|
-
function inspectObject(obj, metadata) {
|
|
154
|
+
function inspectObject(obj, metadata, options) {
|
|
84
155
|
let worldMatrix = Array(16).fill(0);
|
|
85
156
|
let boundsMin = [0, 0, 0];
|
|
86
157
|
let boundsMax = [0, 0, 0];
|
|
@@ -107,12 +178,13 @@ function inspectObject(obj, metadata) {
|
|
|
107
178
|
try {
|
|
108
179
|
if ("geometry" in obj) {
|
|
109
180
|
const geom = obj.geometry;
|
|
110
|
-
if (geom instanceof BufferGeometry) {
|
|
181
|
+
if (geom instanceof BufferGeometry && geom.attributes) {
|
|
111
182
|
const geoInspection = {
|
|
112
183
|
type: geom.type,
|
|
113
184
|
attributes: {}
|
|
114
185
|
};
|
|
115
186
|
for (const [name, attr] of Object.entries(geom.attributes)) {
|
|
187
|
+
if (!attr) continue;
|
|
116
188
|
geoInspection.attributes[name] = {
|
|
117
189
|
itemSize: attr.itemSize,
|
|
118
190
|
count: attr.count
|
|
@@ -121,6 +193,15 @@ function inspectObject(obj, metadata) {
|
|
|
121
193
|
if (geom.index) {
|
|
122
194
|
geoInspection.index = { count: geom.index.count };
|
|
123
195
|
}
|
|
196
|
+
if (options?.includeGeometryData) {
|
|
197
|
+
const posAttr = geom.getAttribute("position");
|
|
198
|
+
if (posAttr?.array) {
|
|
199
|
+
geoInspection.positionData = Array.from(posAttr.array);
|
|
200
|
+
}
|
|
201
|
+
if (geom.index?.array) {
|
|
202
|
+
geoInspection.indexData = Array.from(geom.index.array);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
124
205
|
geom.computeBoundingSphere();
|
|
125
206
|
const sphere = geom.boundingSphere;
|
|
126
207
|
if (sphere) {
|
|
@@ -138,6 +219,7 @@ function inspectObject(obj, metadata) {
|
|
|
138
219
|
try {
|
|
139
220
|
if ("material" in obj) {
|
|
140
221
|
const rawMat = obj.material;
|
|
222
|
+
if (!rawMat) throw new Error("disposed");
|
|
141
223
|
const mat = Array.isArray(rawMat) ? rawMat[0] : rawMat;
|
|
142
224
|
if (mat instanceof Material) {
|
|
143
225
|
const matInspection = {
|
|
@@ -195,6 +277,10 @@ var ObjectStore = class {
|
|
|
195
277
|
this._listeners = [];
|
|
196
278
|
// Track the root scene(s) for scoping
|
|
197
279
|
this._trackedRoots = /* @__PURE__ */ new WeakSet();
|
|
280
|
+
// ---- Async registration state ----
|
|
281
|
+
this._asyncRegQueue = [];
|
|
282
|
+
this._asyncRegHandle = null;
|
|
283
|
+
this._asyncRegBatchSize = 1e3;
|
|
198
284
|
}
|
|
199
285
|
// -------------------------------------------------------------------------
|
|
200
286
|
// Registration
|
|
@@ -202,6 +288,7 @@ var ObjectStore = class {
|
|
|
202
288
|
/**
|
|
203
289
|
* Register a single object into the store.
|
|
204
290
|
* Populates Tier 1 metadata and all indexes.
|
|
291
|
+
* Tags the object with `__r3fdom_tracked = true` for O(1) scene membership checks.
|
|
205
292
|
*/
|
|
206
293
|
register(obj) {
|
|
207
294
|
if (obj.userData?.__r3fdom_internal) {
|
|
@@ -213,6 +300,7 @@ var ObjectStore = class {
|
|
|
213
300
|
this._metaByObject.set(obj, meta);
|
|
214
301
|
this._objectByUuid.set(meta.uuid, obj);
|
|
215
302
|
this._flatListDirty = true;
|
|
303
|
+
obj.userData.__r3fdom_tracked = true;
|
|
216
304
|
if (meta.testId) {
|
|
217
305
|
this._objectsByTestId.set(meta.testId, obj);
|
|
218
306
|
}
|
|
@@ -231,10 +319,8 @@ var ObjectStore = class {
|
|
|
231
319
|
return meta;
|
|
232
320
|
}
|
|
233
321
|
/**
|
|
234
|
-
* Register an entire subtree (object + all descendants).
|
|
235
|
-
*
|
|
236
|
-
* is enabled) so that one bad object doesn't prevent the rest from being
|
|
237
|
-
* tracked.
|
|
322
|
+
* Register an entire subtree (object + all descendants) synchronously.
|
|
323
|
+
* Prefer `registerTreeAsync` for large scenes (100k+) to avoid blocking.
|
|
238
324
|
*/
|
|
239
325
|
registerTree(root) {
|
|
240
326
|
this._trackedRoots.add(root);
|
|
@@ -246,8 +332,64 @@ var ObjectStore = class {
|
|
|
246
332
|
}
|
|
247
333
|
});
|
|
248
334
|
}
|
|
335
|
+
/**
|
|
336
|
+
* Register an entire subtree asynchronously using requestIdleCallback.
|
|
337
|
+
* Processes ~1000 objects per idle slice to avoid blocking the main thread.
|
|
338
|
+
*
|
|
339
|
+
* IMPORTANT: install patchObject3D BEFORE calling this so that objects
|
|
340
|
+
* added to the scene during async registration are caught by the patch.
|
|
341
|
+
*
|
|
342
|
+
* Returns a cancel function. Also cancelled automatically by dispose().
|
|
343
|
+
*/
|
|
344
|
+
registerTreeAsync(root) {
|
|
345
|
+
this._trackedRoots.add(root);
|
|
346
|
+
const queue = [];
|
|
347
|
+
root.traverse((obj) => queue.push(obj));
|
|
348
|
+
this._asyncRegQueue = queue;
|
|
349
|
+
r3fLog("store", `registerTreeAsync: ${queue.length} objects queued`);
|
|
350
|
+
this._scheduleRegChunk();
|
|
351
|
+
return () => this._cancelAsyncRegistration();
|
|
352
|
+
}
|
|
353
|
+
_scheduleRegChunk() {
|
|
354
|
+
if (this._asyncRegQueue.length === 0) {
|
|
355
|
+
this._asyncRegHandle = null;
|
|
356
|
+
r3fLog("store", `registerTreeAsync complete: ${this.getCount()} objects registered`);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
const callback = (deadline) => {
|
|
360
|
+
const hasTime = deadline ? () => deadline.timeRemaining() > 1 : () => true;
|
|
361
|
+
let processed = 0;
|
|
362
|
+
while (this._asyncRegQueue.length > 0 && processed < this._asyncRegBatchSize && hasTime()) {
|
|
363
|
+
const obj = this._asyncRegQueue.shift();
|
|
364
|
+
try {
|
|
365
|
+
this.register(obj);
|
|
366
|
+
} catch (err) {
|
|
367
|
+
r3fLog("store", `registerTreeAsync: failed to register "${obj.name || obj.uuid}"`, err);
|
|
368
|
+
}
|
|
369
|
+
processed++;
|
|
370
|
+
}
|
|
371
|
+
this._scheduleRegChunk();
|
|
372
|
+
};
|
|
373
|
+
if (typeof requestIdleCallback === "function") {
|
|
374
|
+
this._asyncRegHandle = requestIdleCallback(callback, { timeout: 50 });
|
|
375
|
+
} else {
|
|
376
|
+
this._asyncRegHandle = setTimeout(callback, 4);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
_cancelAsyncRegistration() {
|
|
380
|
+
if (this._asyncRegHandle !== null) {
|
|
381
|
+
if (typeof cancelIdleCallback === "function") {
|
|
382
|
+
cancelIdleCallback(this._asyncRegHandle);
|
|
383
|
+
} else {
|
|
384
|
+
clearTimeout(this._asyncRegHandle);
|
|
385
|
+
}
|
|
386
|
+
this._asyncRegHandle = null;
|
|
387
|
+
}
|
|
388
|
+
this._asyncRegQueue = [];
|
|
389
|
+
}
|
|
249
390
|
/**
|
|
250
391
|
* Unregister a single object from the store.
|
|
392
|
+
* Clears the `__r3fdom_tracked` flag.
|
|
251
393
|
*/
|
|
252
394
|
unregister(obj) {
|
|
253
395
|
const meta = this._metaByObject.get(obj);
|
|
@@ -256,6 +398,7 @@ var ObjectStore = class {
|
|
|
256
398
|
this._objectByUuid.delete(meta.uuid);
|
|
257
399
|
this._dirtyQueue.delete(obj);
|
|
258
400
|
this._flatListDirty = true;
|
|
401
|
+
delete obj.userData.__r3fdom_tracked;
|
|
259
402
|
if (meta.testId) {
|
|
260
403
|
this._objectsByTestId.delete(meta.testId);
|
|
261
404
|
}
|
|
@@ -286,49 +429,50 @@ var ObjectStore = class {
|
|
|
286
429
|
// Tier 1: Update (compare-and-set, returns true if changed)
|
|
287
430
|
// -------------------------------------------------------------------------
|
|
288
431
|
/**
|
|
289
|
-
* Refresh Tier 1
|
|
432
|
+
* Refresh dynamic Tier 1 fields from the live Three.js object.
|
|
433
|
+
* Only reads transform, visibility, children count, and parent —
|
|
434
|
+
* skips static fields (geometry, material) that don't change per-frame.
|
|
435
|
+
* Mutates metadata in-place to avoid allocation.
|
|
290
436
|
* Returns true if any values changed.
|
|
291
|
-
* Returns false (no change) if extracting metadata throws so that the
|
|
292
|
-
* previous metadata is preserved.
|
|
293
437
|
*/
|
|
294
438
|
update(obj) {
|
|
295
|
-
const
|
|
296
|
-
if (!
|
|
297
|
-
let
|
|
439
|
+
const meta = this._metaByObject.get(obj);
|
|
440
|
+
if (!meta) return false;
|
|
441
|
+
let changed;
|
|
298
442
|
try {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
if (prev.testId) this._objectsByTestId.delete(prev.testId);
|
|
308
|
-
if (curr.testId) this._objectsByTestId.set(curr.testId, obj);
|
|
309
|
-
}
|
|
310
|
-
if (prev.name !== curr.name) {
|
|
311
|
-
if (prev.name) {
|
|
312
|
-
const nameSet = this._objectsByName.get(prev.name);
|
|
313
|
-
if (nameSet) {
|
|
314
|
-
nameSet.delete(obj);
|
|
315
|
-
if (nameSet.size === 0) this._objectsByName.delete(prev.name);
|
|
316
|
-
}
|
|
443
|
+
const prevTestId = meta.testId;
|
|
444
|
+
const prevName = meta.name;
|
|
445
|
+
changed = updateDynamicFields(obj, meta);
|
|
446
|
+
if (changed) {
|
|
447
|
+
if (prevTestId !== meta.testId) {
|
|
448
|
+
r3fLog("store", `testId changed: "${prevTestId}" \u2192 "${meta.testId}" (${meta.type})`);
|
|
449
|
+
if (prevTestId) this._objectsByTestId.delete(prevTestId);
|
|
450
|
+
if (meta.testId) this._objectsByTestId.set(meta.testId, obj);
|
|
317
451
|
}
|
|
318
|
-
if (
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
452
|
+
if (prevName !== meta.name) {
|
|
453
|
+
if (prevName) {
|
|
454
|
+
const nameSet = this._objectsByName.get(prevName);
|
|
455
|
+
if (nameSet) {
|
|
456
|
+
nameSet.delete(obj);
|
|
457
|
+
if (nameSet.size === 0) this._objectsByName.delete(prevName);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
if (meta.name) {
|
|
461
|
+
let nameSet = this._objectsByName.get(meta.name);
|
|
462
|
+
if (!nameSet) {
|
|
463
|
+
nameSet = /* @__PURE__ */ new Set();
|
|
464
|
+
this._objectsByName.set(meta.name, nameSet);
|
|
465
|
+
}
|
|
466
|
+
nameSet.add(obj);
|
|
323
467
|
}
|
|
324
|
-
nameSet.add(obj);
|
|
325
468
|
}
|
|
469
|
+
this._emit({ type: "update", object: obj, metadata: meta });
|
|
326
470
|
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
return
|
|
471
|
+
} catch (err) {
|
|
472
|
+
r3fLog("store", `update: updateDynamicFields failed for "${obj.name || obj.uuid}"`, err);
|
|
473
|
+
return false;
|
|
330
474
|
}
|
|
331
|
-
return
|
|
475
|
+
return changed;
|
|
332
476
|
}
|
|
333
477
|
// -------------------------------------------------------------------------
|
|
334
478
|
// Tier 2: On-demand inspection (never cached)
|
|
@@ -337,13 +481,14 @@ var ObjectStore = class {
|
|
|
337
481
|
* Compute full inspection data from a live Three.js object.
|
|
338
482
|
* This reads geometry buffers, material properties, world bounds, etc.
|
|
339
483
|
* Cost: 0.1–2ms depending on geometry complexity.
|
|
484
|
+
* Pass { includeGeometryData: true } to include vertex positions and triangle indices (higher cost for large meshes).
|
|
340
485
|
*/
|
|
341
|
-
inspect(idOrUuid) {
|
|
486
|
+
inspect(idOrUuid, options) {
|
|
342
487
|
const obj = this.getObject3D(idOrUuid);
|
|
343
488
|
if (!obj) return null;
|
|
344
489
|
const meta = this._metaByObject.get(obj);
|
|
345
490
|
if (!meta) return null;
|
|
346
|
-
return inspectObject(obj, meta);
|
|
491
|
+
return inspectObject(obj, meta, options);
|
|
347
492
|
}
|
|
348
493
|
// -------------------------------------------------------------------------
|
|
349
494
|
// Lookups (O(1))
|
|
@@ -371,6 +516,23 @@ var ObjectStore = class {
|
|
|
371
516
|
}
|
|
372
517
|
return results;
|
|
373
518
|
}
|
|
519
|
+
/** Get direct children of an object by testId or uuid. Returns empty array if not found. */
|
|
520
|
+
getChildren(idOrUuid) {
|
|
521
|
+
const meta = this.getByTestId(idOrUuid) ?? this.getByUuid(idOrUuid);
|
|
522
|
+
if (!meta) return [];
|
|
523
|
+
const results = [];
|
|
524
|
+
for (const childUuid of meta.childrenUuids) {
|
|
525
|
+
const childMeta = this.getByUuid(childUuid);
|
|
526
|
+
if (childMeta) results.push(childMeta);
|
|
527
|
+
}
|
|
528
|
+
return results;
|
|
529
|
+
}
|
|
530
|
+
/** Get parent of an object by testId or uuid. Returns null if not found or if root. */
|
|
531
|
+
getParent(idOrUuid) {
|
|
532
|
+
const meta = this.getByTestId(idOrUuid) ?? this.getByUuid(idOrUuid);
|
|
533
|
+
if (!meta || meta.parentUuid === null) return null;
|
|
534
|
+
return this.getByUuid(meta.parentUuid);
|
|
535
|
+
}
|
|
374
536
|
/**
|
|
375
537
|
* Batch lookup: get metadata for multiple objects by testId or uuid.
|
|
376
538
|
* Returns a Map from the requested id to its metadata (or null if not found).
|
|
@@ -398,6 +560,30 @@ var ObjectStore = class {
|
|
|
398
560
|
}
|
|
399
561
|
return results;
|
|
400
562
|
}
|
|
563
|
+
/**
|
|
564
|
+
* Get all objects with a given geometry type (e.g. "BoxGeometry", "BufferGeometry").
|
|
565
|
+
* Linear scan — O(n). Only meshes/points/lines have geometryType.
|
|
566
|
+
*/
|
|
567
|
+
getByGeometryType(type) {
|
|
568
|
+
const results = [];
|
|
569
|
+
for (const obj of this.getFlatList()) {
|
|
570
|
+
const meta = this._metaByObject.get(obj);
|
|
571
|
+
if (meta && meta.geometryType === type) results.push(meta);
|
|
572
|
+
}
|
|
573
|
+
return results;
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Get all objects with a given material type (e.g. "MeshStandardMaterial").
|
|
577
|
+
* Linear scan — O(n). Only meshes/points/lines have materialType.
|
|
578
|
+
*/
|
|
579
|
+
getByMaterialType(type) {
|
|
580
|
+
const results = [];
|
|
581
|
+
for (const obj of this.getFlatList()) {
|
|
582
|
+
const meta = this._metaByObject.get(obj);
|
|
583
|
+
if (meta && meta.materialType === type) results.push(meta);
|
|
584
|
+
}
|
|
585
|
+
return results;
|
|
586
|
+
}
|
|
401
587
|
/**
|
|
402
588
|
* Get all objects that have a specific userData key.
|
|
403
589
|
* If `value` is provided, only returns objects where `userData[key]` matches.
|
|
@@ -450,13 +636,16 @@ var ObjectStore = class {
|
|
|
450
636
|
return this._trackedRoots.has(obj);
|
|
451
637
|
}
|
|
452
638
|
/**
|
|
453
|
-
*
|
|
454
|
-
*
|
|
455
|
-
*
|
|
639
|
+
* Check if an object belongs to a tracked scene.
|
|
640
|
+
* Fast path: checks the `__r3fdom_tracked` flag set during register (O(1)).
|
|
641
|
+
* Fallback: walks up the parent chain to find a tracked root.
|
|
642
|
+
* The fallback is needed for newly added objects that aren't registered yet.
|
|
456
643
|
*/
|
|
457
644
|
isInTrackedScene(obj) {
|
|
458
|
-
|
|
645
|
+
if (obj.userData?.__r3fdom_tracked) return true;
|
|
646
|
+
let current = obj.parent;
|
|
459
647
|
while (current) {
|
|
648
|
+
if (current.userData?.__r3fdom_tracked) return true;
|
|
460
649
|
if (this._trackedRoots.has(current)) return true;
|
|
461
650
|
current = current.parent;
|
|
462
651
|
}
|
|
@@ -516,10 +705,39 @@ var ObjectStore = class {
|
|
|
516
705
|
}
|
|
517
706
|
}
|
|
518
707
|
// -------------------------------------------------------------------------
|
|
708
|
+
// GC: sweep orphaned objects
|
|
709
|
+
// -------------------------------------------------------------------------
|
|
710
|
+
/**
|
|
711
|
+
* Sweep objects in `_objectByUuid` that are no longer in any tracked scene.
|
|
712
|
+
* This catches objects that were removed from the scene graph without
|
|
713
|
+
* triggering the patched Object3D.remove (e.g. direct `.children` splice,
|
|
714
|
+
* or the remove hook failing silently).
|
|
715
|
+
*
|
|
716
|
+
* At BIM scale, call this periodically (e.g. every 30s or after a floor
|
|
717
|
+
* load/unload) to prevent memory leaks from retained Object3D references.
|
|
718
|
+
*
|
|
719
|
+
* Returns the number of orphans cleaned up.
|
|
720
|
+
*/
|
|
721
|
+
sweepOrphans() {
|
|
722
|
+
let swept = 0;
|
|
723
|
+
for (const [uuid, obj] of this._objectByUuid) {
|
|
724
|
+
if (!obj.parent && !this._trackedRoots.has(obj)) {
|
|
725
|
+
this.unregister(obj);
|
|
726
|
+
swept++;
|
|
727
|
+
r3fLog("store", `sweepOrphans: removed orphan "${obj.name || uuid}"`);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
return swept;
|
|
731
|
+
}
|
|
732
|
+
// -------------------------------------------------------------------------
|
|
519
733
|
// Cleanup
|
|
520
734
|
// -------------------------------------------------------------------------
|
|
521
735
|
/** Remove all tracked objects and reset state. */
|
|
522
736
|
dispose() {
|
|
737
|
+
this._cancelAsyncRegistration();
|
|
738
|
+
for (const obj of this._objectByUuid.values()) {
|
|
739
|
+
if (obj.userData) delete obj.userData.__r3fdom_tracked;
|
|
740
|
+
}
|
|
523
741
|
this._objectByUuid.clear();
|
|
524
742
|
this._objectsByTestId.clear();
|
|
525
743
|
this._objectsByName.clear();
|
|
@@ -736,7 +954,11 @@ var ATTRIBUTE_MAP = {
|
|
|
736
954
|
"data-scale": (m) => serializeTuple(m.scale),
|
|
737
955
|
"data-vertex-count": (m) => m.vertexCount !== void 0 ? String(m.vertexCount) : void 0,
|
|
738
956
|
"data-triangle-count": (m) => m.triangleCount !== void 0 ? String(m.triangleCount) : void 0,
|
|
739
|
-
"data-instance-count": (m) => m.instanceCount !== void 0 ? String(m.instanceCount) : void 0
|
|
957
|
+
"data-instance-count": (m) => m.instanceCount !== void 0 ? String(m.instanceCount) : void 0,
|
|
958
|
+
"data-fov": (m) => m.fov !== void 0 ? String(m.fov) : void 0,
|
|
959
|
+
"data-near": (m) => m.near !== void 0 ? String(m.near) : void 0,
|
|
960
|
+
"data-far": (m) => m.far !== void 0 ? String(m.far) : void 0,
|
|
961
|
+
"data-zoom": (m) => m.zoom !== void 0 ? String(m.zoom) : void 0
|
|
740
962
|
};
|
|
741
963
|
var MANAGED_ATTRIBUTES = Object.keys(ATTRIBUTE_MAP);
|
|
742
964
|
function serializeTuple(tuple) {
|
|
@@ -789,6 +1011,12 @@ var DomMirror = class {
|
|
|
789
1011
|
this._lruSize = 0;
|
|
790
1012
|
// UUID → parent UUID mapping for DOM tree structure
|
|
791
1013
|
this._parentMap = /* @__PURE__ */ new Map();
|
|
1014
|
+
/** When true, mirror elements use pointer-events: auto so DevTools element picker can select them. */
|
|
1015
|
+
this._inspectMode = false;
|
|
1016
|
+
/** Async materialization state for inspect mode */
|
|
1017
|
+
this._asyncQueue = [];
|
|
1018
|
+
this._asyncIdleHandle = null;
|
|
1019
|
+
this._asyncBatchSize = 200;
|
|
792
1020
|
this._store = store;
|
|
793
1021
|
this._maxNodes = maxNodes;
|
|
794
1022
|
}
|
|
@@ -802,6 +1030,29 @@ var DomMirror = class {
|
|
|
802
1030
|
setRoot(rootElement) {
|
|
803
1031
|
this._rootElement = rootElement;
|
|
804
1032
|
}
|
|
1033
|
+
/**
|
|
1034
|
+
* Enable or disable "inspect mode". When turning on, kicks off async
|
|
1035
|
+
* chunked materialization so the full tree becomes browsable in the
|
|
1036
|
+
* Elements tab without blocking the main thread.
|
|
1037
|
+
*
|
|
1038
|
+
* At BIM scale (100k-200k objects) the old synchronous loop would freeze
|
|
1039
|
+
* the page for 2-10s. The new approach uses requestIdleCallback to
|
|
1040
|
+
* spread work across idle frames (~200 nodes per idle slice, ~5ms each).
|
|
1041
|
+
*/
|
|
1042
|
+
setInspectMode(on) {
|
|
1043
|
+
if (this._inspectMode === on) return;
|
|
1044
|
+
this._inspectMode = on;
|
|
1045
|
+
if (on) {
|
|
1046
|
+
this._startAsyncMaterialization();
|
|
1047
|
+
} else {
|
|
1048
|
+
this._cancelAsyncMaterialization();
|
|
1049
|
+
}
|
|
1050
|
+
r3fLog("inspect", "setInspectMode", { on, nodeCount: this._nodes.size });
|
|
1051
|
+
}
|
|
1052
|
+
/** Whether inspect mode is currently enabled. */
|
|
1053
|
+
getInspectMode() {
|
|
1054
|
+
return this._inspectMode;
|
|
1055
|
+
}
|
|
805
1056
|
/**
|
|
806
1057
|
* Build the initial DOM tree from the scene.
|
|
807
1058
|
* Materializes the top 2 levels of the scene hierarchy.
|
|
@@ -830,7 +1081,7 @@ var DomMirror = class {
|
|
|
830
1081
|
}
|
|
831
1082
|
const tag = getTagForType(meta.type);
|
|
832
1083
|
const element = document.createElement(tag);
|
|
833
|
-
element.style.cssText = "display:
|
|
1084
|
+
element.style.cssText = "display:contents;";
|
|
834
1085
|
const prevAttrs = /* @__PURE__ */ new Map();
|
|
835
1086
|
applyAttributes(element, meta, prevAttrs);
|
|
836
1087
|
const lruNode = { uuid, prev: null, next: null };
|
|
@@ -857,15 +1108,39 @@ var DomMirror = class {
|
|
|
857
1108
|
/**
|
|
858
1109
|
* Remove a DOM node but keep JS metadata in the ObjectStore.
|
|
859
1110
|
* Called by LRU eviction or when an object is removed from the scene.
|
|
1111
|
+
* Also dematerializes any materialized descendants so they don't become
|
|
1112
|
+
* orphaned entries in the LRU / _nodes maps.
|
|
860
1113
|
*/
|
|
861
1114
|
dematerialize(uuid) {
|
|
862
1115
|
const node = this._nodes.get(uuid);
|
|
863
1116
|
if (!node) return;
|
|
1117
|
+
const descendants = this._collectMaterializedDescendants(uuid);
|
|
1118
|
+
for (const descUuid of descendants) {
|
|
1119
|
+
const descNode = this._nodes.get(descUuid);
|
|
1120
|
+
if (descNode) {
|
|
1121
|
+
this._lruRemove(descNode.lruNode);
|
|
1122
|
+
this._nodes.delete(descUuid);
|
|
1123
|
+
this._parentMap.delete(descUuid);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
864
1126
|
node.element.remove();
|
|
865
1127
|
this._lruRemove(node.lruNode);
|
|
866
1128
|
this._nodes.delete(uuid);
|
|
867
1129
|
this._parentMap.delete(uuid);
|
|
868
1130
|
}
|
|
1131
|
+
/**
|
|
1132
|
+
* Collect all materialized descendants of a uuid by walking _parentMap.
|
|
1133
|
+
*/
|
|
1134
|
+
_collectMaterializedDescendants(parentUuid) {
|
|
1135
|
+
const result = [];
|
|
1136
|
+
for (const [childUuid, pUuid] of this._parentMap) {
|
|
1137
|
+
if (pUuid === parentUuid) {
|
|
1138
|
+
result.push(childUuid);
|
|
1139
|
+
result.push(...this._collectMaterializedDescendants(childUuid));
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
return result;
|
|
1143
|
+
}
|
|
869
1144
|
// -------------------------------------------------------------------------
|
|
870
1145
|
// Structural updates (called by Object3D.add/remove patch)
|
|
871
1146
|
// -------------------------------------------------------------------------
|
|
@@ -941,6 +1216,22 @@ var DomMirror = class {
|
|
|
941
1216
|
}
|
|
942
1217
|
return null;
|
|
943
1218
|
}
|
|
1219
|
+
/**
|
|
1220
|
+
* Get or lazily materialize a DOM element for an object.
|
|
1221
|
+
* Also materializes the ancestor chain so the element is correctly
|
|
1222
|
+
* nested in the DOM tree. Used by InspectController so that
|
|
1223
|
+
* hover/click always produces a valid mirror element regardless
|
|
1224
|
+
* of whether async materialization has reached it yet.
|
|
1225
|
+
*/
|
|
1226
|
+
getOrMaterialize(uuid) {
|
|
1227
|
+
const existing = this._nodes.get(uuid);
|
|
1228
|
+
if (existing) {
|
|
1229
|
+
this._lruTouch(existing.lruNode);
|
|
1230
|
+
return existing.element;
|
|
1231
|
+
}
|
|
1232
|
+
this._materializeAncestorChain(uuid);
|
|
1233
|
+
return this.materialize(uuid);
|
|
1234
|
+
}
|
|
944
1235
|
/**
|
|
945
1236
|
* Check if an object has a materialized DOM node.
|
|
946
1237
|
*/
|
|
@@ -1003,6 +1294,7 @@ var DomMirror = class {
|
|
|
1003
1294
|
* Remove all materialized DOM nodes and reset state.
|
|
1004
1295
|
*/
|
|
1005
1296
|
dispose() {
|
|
1297
|
+
this._cancelAsyncMaterialization();
|
|
1006
1298
|
for (const [, node] of this._nodes) {
|
|
1007
1299
|
node.element.remove();
|
|
1008
1300
|
}
|
|
@@ -1035,6 +1327,78 @@ var DomMirror = class {
|
|
|
1035
1327
|
}
|
|
1036
1328
|
}
|
|
1037
1329
|
// -------------------------------------------------------------------------
|
|
1330
|
+
// Private: Ancestor chain materialization
|
|
1331
|
+
// -------------------------------------------------------------------------
|
|
1332
|
+
/**
|
|
1333
|
+
* Materialize all ancestors of a uuid from root down, so the target
|
|
1334
|
+
* element will be correctly nested when materialized.
|
|
1335
|
+
*/
|
|
1336
|
+
_materializeAncestorChain(uuid) {
|
|
1337
|
+
const chain = [];
|
|
1338
|
+
let currentUuid = uuid;
|
|
1339
|
+
while (currentUuid) {
|
|
1340
|
+
if (this._nodes.has(currentUuid)) break;
|
|
1341
|
+
const meta = this._store.getByUuid(currentUuid);
|
|
1342
|
+
if (!meta) break;
|
|
1343
|
+
chain.push(currentUuid);
|
|
1344
|
+
currentUuid = meta.parentUuid;
|
|
1345
|
+
}
|
|
1346
|
+
for (let i = chain.length - 1; i > 0; i--) {
|
|
1347
|
+
this.materialize(chain[i]);
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
// -------------------------------------------------------------------------
|
|
1351
|
+
// Private: Async chunked materialization for inspect mode
|
|
1352
|
+
// -------------------------------------------------------------------------
|
|
1353
|
+
_startAsyncMaterialization() {
|
|
1354
|
+
this._cancelAsyncMaterialization();
|
|
1355
|
+
const flatList = this._store.getFlatList();
|
|
1356
|
+
this._asyncQueue = [];
|
|
1357
|
+
for (const obj of flatList) {
|
|
1358
|
+
if (obj.userData?.__r3fdom_internal) continue;
|
|
1359
|
+
if (this._nodes.has(obj.uuid)) continue;
|
|
1360
|
+
this._asyncQueue.push(obj.uuid);
|
|
1361
|
+
}
|
|
1362
|
+
if (this._asyncQueue.length === 0) return;
|
|
1363
|
+
r3fLog("inspect", `Async materialization started: ${this._asyncQueue.length} objects queued`);
|
|
1364
|
+
this._scheduleAsyncChunk();
|
|
1365
|
+
}
|
|
1366
|
+
_cancelAsyncMaterialization() {
|
|
1367
|
+
if (this._asyncIdleHandle !== null) {
|
|
1368
|
+
if (typeof cancelIdleCallback === "function") {
|
|
1369
|
+
cancelIdleCallback(this._asyncIdleHandle);
|
|
1370
|
+
} else {
|
|
1371
|
+
clearTimeout(this._asyncIdleHandle);
|
|
1372
|
+
}
|
|
1373
|
+
this._asyncIdleHandle = null;
|
|
1374
|
+
}
|
|
1375
|
+
this._asyncQueue = [];
|
|
1376
|
+
}
|
|
1377
|
+
_scheduleAsyncChunk() {
|
|
1378
|
+
if (this._asyncQueue.length === 0) {
|
|
1379
|
+
this._asyncIdleHandle = null;
|
|
1380
|
+
r3fLog("inspect", `Async materialization complete: ${this._nodes.size} nodes materialized`);
|
|
1381
|
+
return;
|
|
1382
|
+
}
|
|
1383
|
+
const callback = (deadline) => {
|
|
1384
|
+
const hasTimeRemaining = deadline ? () => deadline.timeRemaining() > 2 : () => true;
|
|
1385
|
+
let processed = 0;
|
|
1386
|
+
while (this._asyncQueue.length > 0 && processed < this._asyncBatchSize && hasTimeRemaining()) {
|
|
1387
|
+
const uuid = this._asyncQueue.shift();
|
|
1388
|
+
if (!this._nodes.has(uuid)) {
|
|
1389
|
+
this.materialize(uuid);
|
|
1390
|
+
}
|
|
1391
|
+
processed++;
|
|
1392
|
+
}
|
|
1393
|
+
this._scheduleAsyncChunk();
|
|
1394
|
+
};
|
|
1395
|
+
if (typeof requestIdleCallback === "function") {
|
|
1396
|
+
this._asyncIdleHandle = requestIdleCallback(callback, { timeout: 100 });
|
|
1397
|
+
} else {
|
|
1398
|
+
this._asyncIdleHandle = setTimeout(callback, 16);
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
// -------------------------------------------------------------------------
|
|
1038
1402
|
// Private: Selector → UUID resolution
|
|
1039
1403
|
// -------------------------------------------------------------------------
|
|
1040
1404
|
/**
|
|
@@ -2288,677 +2652,324 @@ var SelectionManager = class {
|
|
|
2288
2652
|
this._listeners = [];
|
|
2289
2653
|
}
|
|
2290
2654
|
};
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
var
|
|
2294
|
-
var
|
|
2295
|
-
var
|
|
2296
|
-
var
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
}
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
sxMin = Math.min(sxMin, sx);
|
|
2338
|
-
syMin = Math.min(syMin, sy);
|
|
2339
|
-
sxMax = Math.max(sxMax, sx);
|
|
2340
|
-
syMax = Math.max(syMax, sy);
|
|
2341
|
-
}
|
|
2342
|
-
if (!anyInFront) return null;
|
|
2343
|
-
if (anyBehind) {
|
|
2344
|
-
sxMin = Math.min(sxMin, 0);
|
|
2345
|
-
syMin = Math.min(syMin, 0);
|
|
2346
|
-
sxMax = Math.max(sxMax, canvasRect.width);
|
|
2347
|
-
syMax = Math.max(syMax, canvasRect.height);
|
|
2348
|
-
}
|
|
2349
|
-
sxMin = Math.max(0, sxMin);
|
|
2350
|
-
syMin = Math.max(0, syMin);
|
|
2351
|
-
sxMax = Math.min(canvasRect.width, sxMax);
|
|
2352
|
-
syMax = Math.min(canvasRect.height, syMax);
|
|
2353
|
-
const w = sxMax - sxMin;
|
|
2354
|
-
const h = syMax - syMin;
|
|
2355
|
-
if (w < 1 || h < 1) return null;
|
|
2356
|
-
return { left: sxMin, top: syMin, width: w, height: h };
|
|
2357
|
-
}
|
|
2358
|
-
function exposeGlobalAPI(store) {
|
|
2359
|
-
const api = {
|
|
2360
|
-
_ready: true,
|
|
2361
|
-
getByTestId: (id) => store.getByTestId(id),
|
|
2362
|
-
getByUuid: (uuid) => store.getByUuid(uuid),
|
|
2363
|
-
getByName: (name) => store.getByName(name),
|
|
2364
|
-
getCount: () => store.getCount(),
|
|
2365
|
-
getByType: (type) => store.getByType(type),
|
|
2366
|
-
getByUserData: (key, value) => store.getByUserData(key, value),
|
|
2367
|
-
getCountByType: (type) => store.getCountByType(type),
|
|
2368
|
-
getObjects: (ids) => {
|
|
2369
|
-
const map = store.getObjects(ids);
|
|
2370
|
-
const result = {};
|
|
2371
|
-
for (const [id, meta] of map) {
|
|
2372
|
-
result[id] = meta;
|
|
2373
|
-
}
|
|
2374
|
-
return result;
|
|
2375
|
-
},
|
|
2376
|
-
snapshot: () => createSnapshot(store),
|
|
2377
|
-
inspect: (idOrUuid) => store.inspect(idOrUuid),
|
|
2378
|
-
click: (idOrUuid) => {
|
|
2379
|
-
click3D(idOrUuid);
|
|
2380
|
-
},
|
|
2381
|
-
doubleClick: (idOrUuid) => {
|
|
2382
|
-
doubleClick3D(idOrUuid);
|
|
2383
|
-
},
|
|
2384
|
-
contextMenu: (idOrUuid) => {
|
|
2385
|
-
contextMenu3D(idOrUuid);
|
|
2386
|
-
},
|
|
2387
|
-
hover: (idOrUuid) => {
|
|
2388
|
-
hover3D(idOrUuid);
|
|
2389
|
-
},
|
|
2390
|
-
drag: async (idOrUuid, delta) => {
|
|
2391
|
-
await drag3D(idOrUuid, delta);
|
|
2392
|
-
},
|
|
2393
|
-
wheel: (idOrUuid, options) => {
|
|
2394
|
-
wheel3D(idOrUuid, options);
|
|
2395
|
-
},
|
|
2396
|
-
pointerMiss: () => {
|
|
2397
|
-
pointerMiss3D();
|
|
2398
|
-
},
|
|
2399
|
-
drawPath: async (points, options) => {
|
|
2400
|
-
const result = await drawPath(points, options);
|
|
2401
|
-
return { eventCount: result.eventCount, pointCount: result.pointCount };
|
|
2402
|
-
},
|
|
2403
|
-
select: (idOrUuid) => {
|
|
2404
|
-
const obj = store.getObject3D(idOrUuid);
|
|
2405
|
-
if (obj && _selectionManager) _selectionManager.select(obj);
|
|
2406
|
-
},
|
|
2407
|
-
clearSelection: () => {
|
|
2408
|
-
_selectionManager?.clearSelection();
|
|
2409
|
-
},
|
|
2410
|
-
getSelection: () => _selectionManager ? _selectionManager.getSelected().map((o) => o.uuid) : [],
|
|
2411
|
-
getObject3D: (idOrUuid) => store.getObject3D(idOrUuid),
|
|
2412
|
-
version
|
|
2655
|
+
var HOVER_FILL_COLOR = 7317724;
|
|
2656
|
+
var HOVER_FILL_OPACITY = 0.66;
|
|
2657
|
+
var SELECTION_FILL_COLOR = 7317724;
|
|
2658
|
+
var SELECTION_FILL_OPACITY = 0.75;
|
|
2659
|
+
var SELECTION_BBOX_COLOR = 7317724;
|
|
2660
|
+
var SELECTION_BBOX_OPACITY = 0.3;
|
|
2661
|
+
var _box33 = /* @__PURE__ */ new Box3();
|
|
2662
|
+
function hasRenderableGeometry(obj) {
|
|
2663
|
+
return obj.isMesh === true || obj.isLine === true || obj.isPoints === true;
|
|
2664
|
+
}
|
|
2665
|
+
function collectHighlightTargets(obj) {
|
|
2666
|
+
if (hasRenderableGeometry(obj)) return [obj];
|
|
2667
|
+
const targets = [];
|
|
2668
|
+
obj.traverse((child) => {
|
|
2669
|
+
if (child === obj) return;
|
|
2670
|
+
if (child.userData?.__r3fdom_internal) return;
|
|
2671
|
+
if (hasRenderableGeometry(child)) targets.push(child);
|
|
2672
|
+
});
|
|
2673
|
+
return targets;
|
|
2674
|
+
}
|
|
2675
|
+
function markInternal(obj) {
|
|
2676
|
+
obj.userData.__r3fdom_internal = true;
|
|
2677
|
+
obj.raycast = () => {
|
|
2678
|
+
};
|
|
2679
|
+
obj.traverse((child) => {
|
|
2680
|
+
child.userData.__r3fdom_internal = true;
|
|
2681
|
+
child.raycast = () => {
|
|
2682
|
+
};
|
|
2683
|
+
});
|
|
2684
|
+
}
|
|
2685
|
+
function _syncGroupTransform(source, highlightRoot) {
|
|
2686
|
+
source.updateWorldMatrix(true, false);
|
|
2687
|
+
source.matrixWorld.decompose(
|
|
2688
|
+
highlightRoot.position,
|
|
2689
|
+
highlightRoot.quaternion,
|
|
2690
|
+
highlightRoot.scale
|
|
2691
|
+
);
|
|
2692
|
+
}
|
|
2693
|
+
function _attachRenderSync(source, highlightRoot) {
|
|
2694
|
+
highlightRoot.matrixAutoUpdate = false;
|
|
2695
|
+
highlightRoot.updateMatrixWorld = (force) => {
|
|
2696
|
+
source.updateWorldMatrix(true, false);
|
|
2697
|
+
highlightRoot.matrixWorld.copy(source.matrixWorld);
|
|
2698
|
+
for (const child of highlightRoot.children) {
|
|
2699
|
+
child.updateMatrixWorld(force);
|
|
2700
|
+
}
|
|
2413
2701
|
};
|
|
2414
|
-
window.__R3F_DOM__ = api;
|
|
2415
2702
|
}
|
|
2416
|
-
function
|
|
2417
|
-
|
|
2418
|
-
if (
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2703
|
+
function createHighlightMesh(source, fillColor, fillOpacity) {
|
|
2704
|
+
const geom = source.geometry;
|
|
2705
|
+
if (!geom) return null;
|
|
2706
|
+
const group = new Object3D();
|
|
2707
|
+
const disposables = [];
|
|
2708
|
+
const fillMat = new MeshBasicMaterial({
|
|
2709
|
+
color: fillColor,
|
|
2710
|
+
transparent: true,
|
|
2711
|
+
opacity: fillOpacity,
|
|
2712
|
+
depthTest: false,
|
|
2713
|
+
depthWrite: false,
|
|
2714
|
+
side: DoubleSide
|
|
2715
|
+
});
|
|
2716
|
+
disposables.push(fillMat);
|
|
2717
|
+
const fillMesh = new Mesh(geom, fillMat);
|
|
2718
|
+
fillMesh.scale.setScalar(1.005);
|
|
2719
|
+
fillMesh.raycast = () => {
|
|
2720
|
+
};
|
|
2721
|
+
group.add(fillMesh);
|
|
2722
|
+
source.updateWorldMatrix(true, false);
|
|
2723
|
+
source.matrixWorld.decompose(group.position, group.quaternion, group.scale);
|
|
2724
|
+
markInternal(group);
|
|
2725
|
+
_attachRenderSync(source, group);
|
|
2726
|
+
return { root: group, disposables };
|
|
2727
|
+
}
|
|
2728
|
+
function createBoundingBoxHighlight(obj) {
|
|
2729
|
+
_box33.makeEmpty();
|
|
2730
|
+
const targets = collectHighlightTargets(obj);
|
|
2731
|
+
if (targets.length === 0) return null;
|
|
2732
|
+
for (const target of targets) {
|
|
2733
|
+
target.updateWorldMatrix(true, false);
|
|
2734
|
+
const geom = target.geometry;
|
|
2735
|
+
if (geom) {
|
|
2736
|
+
if (!geom.boundingBox) geom.computeBoundingBox();
|
|
2737
|
+
if (geom.boundingBox) {
|
|
2738
|
+
const childBox = geom.boundingBox.clone();
|
|
2739
|
+
childBox.applyMatrix4(target.matrixWorld);
|
|
2740
|
+
_box33.union(childBox);
|
|
2426
2741
|
}
|
|
2427
|
-
}
|
|
2428
|
-
} else {
|
|
2429
|
-
delete window.__R3F_DOM__;
|
|
2430
|
-
r3fLog("bridge", "Global API removed (immediate)");
|
|
2742
|
+
}
|
|
2431
2743
|
}
|
|
2744
|
+
if (_box33.isEmpty()) return null;
|
|
2745
|
+
const size = _box33.getSize(new Vector3());
|
|
2746
|
+
const center = _box33.getCenter(new Vector3());
|
|
2747
|
+
const disposables = [];
|
|
2748
|
+
const boxGeom = new BoxGeometry(size.x, size.y, size.z);
|
|
2749
|
+
disposables.push(boxGeom);
|
|
2750
|
+
const fillMat = new MeshBasicMaterial({
|
|
2751
|
+
color: SELECTION_BBOX_COLOR,
|
|
2752
|
+
transparent: true,
|
|
2753
|
+
opacity: SELECTION_BBOX_OPACITY,
|
|
2754
|
+
depthTest: false,
|
|
2755
|
+
depthWrite: false,
|
|
2756
|
+
side: DoubleSide
|
|
2757
|
+
});
|
|
2758
|
+
disposables.push(fillMat);
|
|
2759
|
+
const fillMesh = new Mesh(boxGeom, fillMat);
|
|
2760
|
+
fillMesh.raycast = () => {
|
|
2761
|
+
};
|
|
2762
|
+
const edgesGeom = new EdgesGeometry(boxGeom);
|
|
2763
|
+
disposables.push(edgesGeom);
|
|
2764
|
+
const edgeMat = new LineBasicMaterial({
|
|
2765
|
+
color: SELECTION_BBOX_COLOR,
|
|
2766
|
+
transparent: true,
|
|
2767
|
+
opacity: 0.5,
|
|
2768
|
+
depthTest: false,
|
|
2769
|
+
depthWrite: false
|
|
2770
|
+
});
|
|
2771
|
+
disposables.push(edgeMat);
|
|
2772
|
+
const edgeMesh = new LineSegments(edgesGeom, edgeMat);
|
|
2773
|
+
edgeMesh.raycast = () => {
|
|
2774
|
+
};
|
|
2775
|
+
const group = new Object3D();
|
|
2776
|
+
group.add(fillMesh);
|
|
2777
|
+
group.add(edgeMesh);
|
|
2778
|
+
group.position.copy(center);
|
|
2779
|
+
group.renderOrder = 998;
|
|
2780
|
+
markInternal(group);
|
|
2781
|
+
group.matrixAutoUpdate = false;
|
|
2782
|
+
group.updateMatrixWorld = (force) => {
|
|
2783
|
+
_box33.makeEmpty();
|
|
2784
|
+
for (const target of targets) {
|
|
2785
|
+
target.updateWorldMatrix(true, false);
|
|
2786
|
+
const g = target.geometry;
|
|
2787
|
+
if (g) {
|
|
2788
|
+
if (!g.boundingBox) g.computeBoundingBox();
|
|
2789
|
+
if (g.boundingBox) {
|
|
2790
|
+
const childBox = g.boundingBox.clone();
|
|
2791
|
+
childBox.applyMatrix4(target.matrixWorld);
|
|
2792
|
+
_box33.union(childBox);
|
|
2793
|
+
}
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
if (!_box33.isEmpty()) {
|
|
2797
|
+
const s = _box33.getSize(new Vector3());
|
|
2798
|
+
const c = _box33.getCenter(new Vector3());
|
|
2799
|
+
group.position.copy(c);
|
|
2800
|
+
group.scale.set(
|
|
2801
|
+
s.x / size.x || 1,
|
|
2802
|
+
s.y / size.y || 1,
|
|
2803
|
+
s.z / size.z || 1
|
|
2804
|
+
);
|
|
2805
|
+
}
|
|
2806
|
+
group.updateMatrix();
|
|
2807
|
+
group.matrixWorld.copy(group.matrix);
|
|
2808
|
+
for (const child of group.children) {
|
|
2809
|
+
child.updateMatrixWorld(force);
|
|
2810
|
+
}
|
|
2811
|
+
};
|
|
2812
|
+
return { root: group, disposables };
|
|
2432
2813
|
}
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
d._h = String(h);
|
|
2814
|
+
var Highlighter = class {
|
|
2815
|
+
constructor(_options = {}) {
|
|
2816
|
+
this._scene = null;
|
|
2817
|
+
this._unsubscribe = null;
|
|
2818
|
+
this._hoverEntries = [];
|
|
2819
|
+
this._hoverTarget = null;
|
|
2820
|
+
this._selectedEntries = /* @__PURE__ */ new Map();
|
|
2821
|
+
this._hoverPollId = null;
|
|
2822
|
+
this._lastHoveredUuid = null;
|
|
2823
|
+
this._store = null;
|
|
2444
2824
|
}
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
r3fLog("setup", "ThreeDom effect started", { enabled, debug, root, maxDomNodes });
|
|
2465
|
-
const canvas = gl.domElement;
|
|
2466
|
-
const canvasParent = canvas.parentElement;
|
|
2467
|
-
let rootElement = null;
|
|
2468
|
-
let createdRoot = false;
|
|
2469
|
-
if (typeof root === "string") {
|
|
2470
|
-
rootElement = document.querySelector(root);
|
|
2471
|
-
} else {
|
|
2472
|
-
rootElement = root;
|
|
2473
|
-
}
|
|
2474
|
-
if (!rootElement) {
|
|
2475
|
-
rootElement = document.createElement("div");
|
|
2476
|
-
rootElement.id = typeof root === "string" ? root.replace(/^#/, "") : "three-dom-root";
|
|
2477
|
-
createdRoot = true;
|
|
2478
|
-
}
|
|
2479
|
-
canvasParent.style.position = canvasParent.style.position || "relative";
|
|
2480
|
-
canvasParent.appendChild(rootElement);
|
|
2481
|
-
rootElement.style.cssText = [
|
|
2482
|
-
"position: absolute",
|
|
2483
|
-
"top: 0",
|
|
2484
|
-
"left: 0",
|
|
2485
|
-
"width: 100%",
|
|
2486
|
-
"height: 100%",
|
|
2487
|
-
"pointer-events: none",
|
|
2488
|
-
"overflow: hidden",
|
|
2489
|
-
"z-index: 10"
|
|
2490
|
-
].join(";");
|
|
2491
|
-
let store = null;
|
|
2492
|
-
let mirror = null;
|
|
2493
|
-
let unpatch = null;
|
|
2494
|
-
let selectionManager = null;
|
|
2495
|
-
let currentApi;
|
|
2496
|
-
try {
|
|
2497
|
-
store = new ObjectStore();
|
|
2498
|
-
mirror = new DomMirror(store, maxDomNodes);
|
|
2499
|
-
mirror.setRoot(rootElement);
|
|
2500
|
-
r3fLog("setup", "Store and mirror created");
|
|
2501
|
-
ensureCustomElements(store);
|
|
2502
|
-
store.registerTree(scene);
|
|
2503
|
-
r3fLog("setup", `Registered scene tree: ${store.getCount()} objects`);
|
|
2504
|
-
mirror.materializeSubtree(scene.uuid, initialDepth);
|
|
2505
|
-
unpatch = patchObject3D(store, mirror);
|
|
2506
|
-
setInteractionState(store, camera, gl, size);
|
|
2507
|
-
r3fLog("setup", "Object3D patched, interaction state set");
|
|
2508
|
-
selectionManager = new SelectionManager();
|
|
2509
|
-
_selectionManager = selectionManager;
|
|
2510
|
-
_highlighter = null;
|
|
2511
|
-
exposeGlobalAPI(store);
|
|
2512
|
-
r3fLog("bridge", "exposeGlobalAPI called \u2014 bridge is live, _ready=true");
|
|
2513
|
-
currentApi = window.__R3F_DOM__;
|
|
2514
|
-
_store3 = store;
|
|
2515
|
-
_mirror = mirror;
|
|
2516
|
-
const initialCanvasRect = canvas.getBoundingClientRect();
|
|
2517
|
-
const allObjects = store.getFlatList();
|
|
2518
|
-
for (const obj of allObjects) {
|
|
2519
|
-
if (obj.userData?.__r3fdom_internal) continue;
|
|
2520
|
-
const el = mirror.getElement(obj.uuid);
|
|
2521
|
-
if (!el) continue;
|
|
2522
|
-
if (obj.type === "Scene") {
|
|
2523
|
-
setElementRect(el, 0, 0, Math.round(initialCanvasRect.width), Math.round(initialCanvasRect.height));
|
|
2524
|
-
continue;
|
|
2525
|
-
}
|
|
2526
|
-
const rect = projectToScreenRect(obj, camera, initialCanvasRect);
|
|
2527
|
-
if (rect) {
|
|
2528
|
-
let parentLeft = 0;
|
|
2529
|
-
let parentTop = 0;
|
|
2530
|
-
if (obj.parent && obj.parent.type !== "Scene") {
|
|
2531
|
-
const parentRect = projectToScreenRect(obj.parent, camera, initialCanvasRect);
|
|
2532
|
-
if (parentRect) {
|
|
2533
|
-
parentLeft = Math.round(parentRect.left);
|
|
2534
|
-
parentTop = Math.round(parentRect.top);
|
|
2535
|
-
}
|
|
2536
|
-
}
|
|
2537
|
-
setElementRect(
|
|
2538
|
-
el,
|
|
2539
|
-
Math.round(rect.left) - parentLeft,
|
|
2540
|
-
Math.round(rect.top) - parentTop,
|
|
2541
|
-
Math.round(rect.width),
|
|
2542
|
-
Math.round(rect.height)
|
|
2543
|
-
);
|
|
2544
|
-
}
|
|
2545
|
-
}
|
|
2546
|
-
} catch (err) {
|
|
2547
|
-
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
2548
|
-
r3fLog("setup", "ThreeDom setup failed", err);
|
|
2549
|
-
console.error("[react-three-dom] Setup failed:", err);
|
|
2550
|
-
window.__R3F_DOM__ = {
|
|
2551
|
-
_ready: false,
|
|
2552
|
-
_error: errorMsg,
|
|
2553
|
-
getByTestId: () => null,
|
|
2554
|
-
getByUuid: () => null,
|
|
2555
|
-
getByName: () => [],
|
|
2556
|
-
getCount: () => 0,
|
|
2557
|
-
getByType: () => [],
|
|
2558
|
-
getByUserData: () => [],
|
|
2559
|
-
getCountByType: () => 0,
|
|
2560
|
-
getObjects: (ids) => {
|
|
2561
|
-
const result = {};
|
|
2562
|
-
for (const id of ids) result[id] = null;
|
|
2563
|
-
return result;
|
|
2564
|
-
},
|
|
2565
|
-
snapshot: () => ({ timestamp: 0, objectCount: 0, tree: { uuid: "", name: "", type: "Scene", visible: true, position: [0, 0, 0], rotation: [0, 0, 0], scale: [1, 1, 1], children: [] } }),
|
|
2566
|
-
inspect: () => null,
|
|
2567
|
-
click: () => {
|
|
2568
|
-
},
|
|
2569
|
-
doubleClick: () => {
|
|
2570
|
-
},
|
|
2571
|
-
contextMenu: () => {
|
|
2572
|
-
},
|
|
2573
|
-
hover: () => {
|
|
2574
|
-
},
|
|
2575
|
-
drag: async () => {
|
|
2576
|
-
},
|
|
2577
|
-
wheel: () => {
|
|
2578
|
-
},
|
|
2579
|
-
pointerMiss: () => {
|
|
2580
|
-
},
|
|
2581
|
-
drawPath: async () => ({ eventCount: 0, pointCount: 0 }),
|
|
2582
|
-
select: () => {
|
|
2583
|
-
},
|
|
2584
|
-
clearSelection: () => {
|
|
2585
|
-
},
|
|
2586
|
-
getSelection: () => [],
|
|
2587
|
-
getObject3D: () => null,
|
|
2588
|
-
version
|
|
2589
|
-
};
|
|
2590
|
-
currentApi = window.__R3F_DOM__;
|
|
2591
|
-
}
|
|
2592
|
-
return () => {
|
|
2593
|
-
r3fLog("setup", "ThreeDom cleanup started");
|
|
2594
|
-
if (unpatch) unpatch();
|
|
2595
|
-
removeGlobalAPI(currentApi);
|
|
2596
|
-
clearInteractionState();
|
|
2597
|
-
if (selectionManager) selectionManager.dispose();
|
|
2598
|
-
if (mirror) mirror.dispose();
|
|
2599
|
-
if (store) store.dispose();
|
|
2600
|
-
if (createdRoot && rootElement?.parentNode) {
|
|
2601
|
-
rootElement.parentNode.removeChild(rootElement);
|
|
2602
|
-
}
|
|
2603
|
-
_store3 = null;
|
|
2604
|
-
_mirror = null;
|
|
2605
|
-
_selectionManager = null;
|
|
2606
|
-
_highlighter = null;
|
|
2607
|
-
if (debug) enableDebug(false);
|
|
2608
|
-
r3fLog("setup", "ThreeDom cleanup complete");
|
|
2609
|
-
};
|
|
2610
|
-
}, [scene, camera, gl, size, enabled, root, maxDomNodes, initialDepth, debug]);
|
|
2611
|
-
useFrame(() => {
|
|
2612
|
-
if (!enabled || !_store3 || !_mirror) return;
|
|
2613
|
-
try {
|
|
2614
|
-
setInteractionState(_store3, camera, gl, size);
|
|
2615
|
-
const store = _store3;
|
|
2616
|
-
const mirror = _mirror;
|
|
2617
|
-
const canvas = gl.domElement;
|
|
2618
|
-
const canvasRect = canvas.getBoundingClientRect();
|
|
2619
|
-
const start = performance.now();
|
|
2620
|
-
const dirtyObjects = store.drainDirtyQueue();
|
|
2621
|
-
for (const obj of dirtyObjects) {
|
|
2622
|
-
store.update(obj);
|
|
2623
|
-
mirror.syncAttributes(obj);
|
|
2624
|
-
}
|
|
2625
|
-
const budgetRemaining = timeBudgetMs - (performance.now() - start);
|
|
2626
|
-
if (budgetRemaining > 0.1) {
|
|
2627
|
-
const objects2 = store.getFlatList();
|
|
2628
|
-
if (objects2.length > 0) {
|
|
2629
|
-
const end = Math.min(cursorRef.current + batchSize, objects2.length);
|
|
2630
|
-
for (let i = cursorRef.current; i < end; i++) {
|
|
2631
|
-
if (performance.now() - start > timeBudgetMs) break;
|
|
2632
|
-
const obj = objects2[i];
|
|
2633
|
-
const changed = store.update(obj);
|
|
2634
|
-
if (changed) mirror.syncAttributes(obj);
|
|
2635
|
-
}
|
|
2636
|
-
cursorRef.current = end >= objects2.length ? 0 : end;
|
|
2637
|
-
}
|
|
2638
|
-
}
|
|
2639
|
-
const objects = store.getFlatList();
|
|
2640
|
-
if (objects.length > 0) {
|
|
2641
|
-
const posEnd = Math.min(positionCursorRef.current + 50, objects.length);
|
|
2642
|
-
for (let i = positionCursorRef.current; i < posEnd; i++) {
|
|
2643
|
-
const obj = objects[i];
|
|
2644
|
-
if (obj.userData?.__r3fdom_internal) continue;
|
|
2645
|
-
const el = mirror.getElement(obj.uuid);
|
|
2646
|
-
if (!el) continue;
|
|
2647
|
-
if (obj.type === "Scene") {
|
|
2648
|
-
setElementRect(el, 0, 0, Math.round(canvasRect.width), Math.round(canvasRect.height));
|
|
2649
|
-
continue;
|
|
2650
|
-
}
|
|
2651
|
-
const rect = projectToScreenRect(obj, camera, canvasRect);
|
|
2652
|
-
if (rect) {
|
|
2653
|
-
let parentLeft = 0;
|
|
2654
|
-
let parentTop = 0;
|
|
2655
|
-
if (obj.parent && obj.parent.type !== "Scene") {
|
|
2656
|
-
const parentRect = projectToScreenRect(obj.parent, camera, canvasRect);
|
|
2657
|
-
if (parentRect) {
|
|
2658
|
-
parentLeft = Math.round(parentRect.left);
|
|
2659
|
-
parentTop = Math.round(parentRect.top);
|
|
2660
|
-
}
|
|
2661
|
-
}
|
|
2662
|
-
const l = Math.round(rect.left) - parentLeft;
|
|
2663
|
-
const t = Math.round(rect.top) - parentTop;
|
|
2664
|
-
const w = Math.round(rect.width);
|
|
2665
|
-
const h = Math.round(rect.height);
|
|
2666
|
-
setElementRect(el, l, t, w, h);
|
|
2667
|
-
if (el.style.display === "none") el.style.display = "block";
|
|
2668
|
-
} else {
|
|
2669
|
-
if (el.style.display !== "none") el.style.display = "none";
|
|
2670
|
-
}
|
|
2671
|
-
}
|
|
2672
|
-
positionCursorRef.current = posEnd >= objects.length ? 0 : posEnd;
|
|
2673
|
-
}
|
|
2674
|
-
} catch (err) {
|
|
2675
|
-
r3fLog("sync", "Per-frame sync error", err);
|
|
2676
|
-
}
|
|
2677
|
-
});
|
|
2678
|
-
return null;
|
|
2679
|
-
}
|
|
2680
|
-
var COLORS = {
|
|
2681
|
-
/** Content area — same blue as Chrome DevTools element highlight */
|
|
2682
|
-
content: "rgba(111, 168, 220, 0.66)",
|
|
2683
|
-
/** Slightly dimmer for children of a selected parent */
|
|
2684
|
-
contentChild: "rgba(111, 168, 220, 0.33)",
|
|
2685
|
-
/** Hover highlight — lighter blue */
|
|
2686
|
-
hover: "rgba(111, 168, 220, 0.4)",
|
|
2687
|
-
/** Tooltip background */
|
|
2688
|
-
tooltipBg: "rgba(36, 36, 36, 0.9)",
|
|
2689
|
-
/** Tooltip text */
|
|
2690
|
-
tooltipText: "#fff",
|
|
2691
|
-
/** Tooltip tag color */
|
|
2692
|
-
tooltipTag: "#e776e0",
|
|
2693
|
-
/** Tooltip dimensions color */
|
|
2694
|
-
tooltipDim: "#c5c5c5",
|
|
2695
|
-
/** Border for selected elements */
|
|
2696
|
-
border: "rgba(111, 168, 220, 0.9)"
|
|
2697
|
-
};
|
|
2698
|
-
var _box2 = /* @__PURE__ */ new Box3();
|
|
2699
|
-
var _v2 = /* @__PURE__ */ new Vector3();
|
|
2700
|
-
var _corners2 = Array.from({ length: 8 }, () => new Vector3());
|
|
2701
|
-
function projectBoundsToScreen(obj, camera, canvas) {
|
|
2702
|
-
_box2.setFromObject(obj);
|
|
2703
|
-
if (_box2.isEmpty()) return null;
|
|
2704
|
-
const { min, max } = _box2;
|
|
2705
|
-
_corners2[0].set(min.x, min.y, min.z);
|
|
2706
|
-
_corners2[1].set(min.x, min.y, max.z);
|
|
2707
|
-
_corners2[2].set(min.x, max.y, min.z);
|
|
2708
|
-
_corners2[3].set(min.x, max.y, max.z);
|
|
2709
|
-
_corners2[4].set(max.x, min.y, min.z);
|
|
2710
|
-
_corners2[5].set(max.x, min.y, max.z);
|
|
2711
|
-
_corners2[6].set(max.x, max.y, min.z);
|
|
2712
|
-
_corners2[7].set(max.x, max.y, max.z);
|
|
2713
|
-
const rect = canvas.getBoundingClientRect();
|
|
2714
|
-
let screenMinX = Infinity;
|
|
2715
|
-
let screenMinY = Infinity;
|
|
2716
|
-
let screenMaxX = -Infinity;
|
|
2717
|
-
let screenMaxY = -Infinity;
|
|
2718
|
-
let allBehind = true;
|
|
2719
|
-
for (const corner of _corners2) {
|
|
2720
|
-
_v2.copy(corner).project(camera);
|
|
2721
|
-
if (_v2.z < 1) allBehind = false;
|
|
2722
|
-
const sx = (_v2.x + 1) / 2 * rect.width;
|
|
2723
|
-
const sy = (1 - _v2.y) / 2 * rect.height;
|
|
2724
|
-
screenMinX = Math.min(screenMinX, sx);
|
|
2725
|
-
screenMinY = Math.min(screenMinY, sy);
|
|
2726
|
-
screenMaxX = Math.max(screenMaxX, sx);
|
|
2727
|
-
screenMaxY = Math.max(screenMaxY, sy);
|
|
2728
|
-
}
|
|
2729
|
-
if (allBehind) return null;
|
|
2730
|
-
screenMinX = Math.max(0, screenMinX);
|
|
2731
|
-
screenMinY = Math.max(0, screenMinY);
|
|
2732
|
-
screenMaxX = Math.min(rect.width, screenMaxX);
|
|
2733
|
-
screenMaxY = Math.min(rect.height, screenMaxY);
|
|
2734
|
-
const width = screenMaxX - screenMinX;
|
|
2735
|
-
const height = screenMaxY - screenMinY;
|
|
2736
|
-
if (width < 1 || height < 1) return null;
|
|
2737
|
-
return {
|
|
2738
|
-
left: rect.left + screenMinX,
|
|
2739
|
-
top: rect.top + screenMinY,
|
|
2740
|
-
width,
|
|
2741
|
-
height
|
|
2742
|
-
};
|
|
2743
|
-
}
|
|
2744
|
-
function getObjectLabel2(obj) {
|
|
2745
|
-
const tag = `three-${obj.type.toLowerCase()}`;
|
|
2746
|
-
const parts = [tag];
|
|
2747
|
-
if (obj.name) {
|
|
2748
|
-
parts.push(`.${obj.name}`);
|
|
2749
|
-
}
|
|
2750
|
-
const testId = obj.userData?.testId;
|
|
2751
|
-
if (testId) {
|
|
2752
|
-
parts.push(`[testId="${testId}"]`);
|
|
2753
|
-
}
|
|
2754
|
-
return parts.join("");
|
|
2755
|
-
}
|
|
2756
|
-
function getObjectDimensions(obj) {
|
|
2757
|
-
_box2.setFromObject(obj);
|
|
2758
|
-
if (_box2.isEmpty()) return "";
|
|
2759
|
-
const size = _box2.getSize(new Vector3());
|
|
2760
|
-
return `${size.x.toFixed(1)} \xD7 ${size.y.toFixed(1)} \xD7 ${size.z.toFixed(1)}`;
|
|
2761
|
-
}
|
|
2762
|
-
function createOverlayElement(color, showBorder) {
|
|
2763
|
-
const el = document.createElement("div");
|
|
2764
|
-
el.style.cssText = `
|
|
2765
|
-
position: fixed;
|
|
2766
|
-
pointer-events: none;
|
|
2767
|
-
z-index: 99998;
|
|
2768
|
-
background: ${color};
|
|
2769
|
-
${showBorder ? `border: 1px solid ${COLORS.border};` : ""}
|
|
2770
|
-
transition: all 0.05s ease-out;
|
|
2771
|
-
box-sizing: border-box;
|
|
2772
|
-
`;
|
|
2773
|
-
return el;
|
|
2774
|
-
}
|
|
2775
|
-
function createTooltipElement() {
|
|
2776
|
-
const el = document.createElement("div");
|
|
2777
|
-
el.style.cssText = `
|
|
2778
|
-
position: fixed;
|
|
2779
|
-
pointer-events: none;
|
|
2780
|
-
z-index: 99999;
|
|
2781
|
-
background: ${COLORS.tooltipBg};
|
|
2782
|
-
color: ${COLORS.tooltipText};
|
|
2783
|
-
font-family: 'SF Mono', Monaco, monospace;
|
|
2784
|
-
font-size: 11px;
|
|
2785
|
-
padding: 4px 8px;
|
|
2786
|
-
border-radius: 3px;
|
|
2787
|
-
white-space: nowrap;
|
|
2788
|
-
line-height: 1.4;
|
|
2789
|
-
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
|
2790
|
-
`;
|
|
2791
|
-
return el;
|
|
2792
|
-
}
|
|
2793
|
-
function positionOverlay(entry, rect) {
|
|
2794
|
-
const { overlayEl, tooltipEl } = entry;
|
|
2795
|
-
overlayEl.style.left = `${rect.left}px`;
|
|
2796
|
-
overlayEl.style.top = `${rect.top}px`;
|
|
2797
|
-
overlayEl.style.width = `${rect.width}px`;
|
|
2798
|
-
overlayEl.style.height = `${rect.height}px`;
|
|
2799
|
-
overlayEl.style.display = "block";
|
|
2800
|
-
tooltipEl.style.left = `${rect.left}px`;
|
|
2801
|
-
tooltipEl.style.top = `${Math.max(0, rect.top - 28)}px`;
|
|
2802
|
-
tooltipEl.style.display = "block";
|
|
2803
|
-
}
|
|
2804
|
-
function hideOverlay(entry) {
|
|
2805
|
-
entry.overlayEl.style.display = "none";
|
|
2806
|
-
entry.tooltipEl.style.display = "none";
|
|
2807
|
-
}
|
|
2808
|
-
var Highlighter = class {
|
|
2809
|
-
constructor(options = {}) {
|
|
2810
|
-
/** Selected object overlays (persistent until deselected) */
|
|
2811
|
-
this._selectedEntries = /* @__PURE__ */ new Map();
|
|
2812
|
-
/** Hover overlay (temporary, single object at a time) */
|
|
2813
|
-
this._hoverEntries = /* @__PURE__ */ new Map();
|
|
2814
|
-
this._camera = null;
|
|
2815
|
-
this._renderer = null;
|
|
2816
|
-
this._unsubscribe = null;
|
|
2817
|
-
/** DevTools hover polling interval */
|
|
2818
|
-
this._hoverPollId = null;
|
|
2819
|
-
this._lastHoveredElement = null;
|
|
2820
|
-
/** Store reference for resolving objects */
|
|
2821
|
-
this._store = null;
|
|
2822
|
-
this._showTooltip = options.showTooltip ?? true;
|
|
2823
|
-
}
|
|
2824
|
-
// -----------------------------------------------------------------------
|
|
2825
|
-
// Lifecycle
|
|
2826
|
-
// -----------------------------------------------------------------------
|
|
2827
|
-
attach(_scene, selectionManager, camera, renderer, store) {
|
|
2828
|
-
this.detach();
|
|
2829
|
-
this._camera = camera;
|
|
2830
|
-
this._renderer = renderer;
|
|
2831
|
-
this._store = store;
|
|
2832
|
-
this._unsubscribe = selectionManager.subscribe((selected) => {
|
|
2833
|
-
this._syncSelectedHighlights(selected);
|
|
2834
|
-
});
|
|
2835
|
-
this._syncSelectedHighlights([...selectionManager.getSelected()]);
|
|
2836
|
-
this._startHoverPolling();
|
|
2837
|
-
}
|
|
2838
|
-
detach() {
|
|
2839
|
-
if (this._unsubscribe) {
|
|
2840
|
-
this._unsubscribe();
|
|
2841
|
-
this._unsubscribe = null;
|
|
2825
|
+
// -----------------------------------------------------------------------
|
|
2826
|
+
// Lifecycle
|
|
2827
|
+
// -----------------------------------------------------------------------
|
|
2828
|
+
/** Bind to a scene and selection manager, start hover polling. */
|
|
2829
|
+
attach(scene, selectionManager, _camera2, _renderer, store) {
|
|
2830
|
+
this.detach();
|
|
2831
|
+
this._scene = scene;
|
|
2832
|
+
this._store = store;
|
|
2833
|
+
this._unsubscribe = selectionManager.subscribe((selected) => {
|
|
2834
|
+
this._syncSelectionHighlights(selected);
|
|
2835
|
+
});
|
|
2836
|
+
this._syncSelectionHighlights([...selectionManager.getSelected()]);
|
|
2837
|
+
this._startHoverPolling();
|
|
2838
|
+
}
|
|
2839
|
+
/** Unbind from the scene, stop polling, and remove all highlights. */
|
|
2840
|
+
detach() {
|
|
2841
|
+
if (this._unsubscribe) {
|
|
2842
|
+
this._unsubscribe();
|
|
2843
|
+
this._unsubscribe = null;
|
|
2842
2844
|
}
|
|
2843
2845
|
this._stopHoverPolling();
|
|
2844
|
-
this.
|
|
2845
|
-
this.
|
|
2846
|
-
this.
|
|
2847
|
-
this._renderer = null;
|
|
2846
|
+
this.clearHoverHighlight();
|
|
2847
|
+
this._clearAllSelectionHighlights();
|
|
2848
|
+
this._scene = null;
|
|
2848
2849
|
this._store = null;
|
|
2849
2850
|
}
|
|
2850
2851
|
// -----------------------------------------------------------------------
|
|
2851
|
-
// Per-frame update
|
|
2852
|
+
// Per-frame update
|
|
2852
2853
|
// -----------------------------------------------------------------------
|
|
2854
|
+
/** Sync highlight group transforms to their source objects. Call each frame. */
|
|
2853
2855
|
update() {
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
}
|
|
2864
|
-
for (const entry of this._hoverEntries.values()) {
|
|
2865
|
-
const rect = projectBoundsToScreen(entry.target, this._camera, canvas);
|
|
2866
|
-
if (rect) {
|
|
2867
|
-
positionOverlay(entry, rect);
|
|
2868
|
-
} else {
|
|
2869
|
-
hideOverlay(entry);
|
|
2856
|
+
for (const entry of this._hoverEntries) {
|
|
2857
|
+
_syncGroupTransform(entry.source, entry.group.root);
|
|
2858
|
+
}
|
|
2859
|
+
for (const selEntry of this._selectedEntries.values()) {
|
|
2860
|
+
for (const mg of selEntry.meshGroups) {
|
|
2861
|
+
const src = mg.root.userData.__r3fdom_source;
|
|
2862
|
+
if (src) {
|
|
2863
|
+
_syncGroupTransform(src, mg.root);
|
|
2864
|
+
}
|
|
2870
2865
|
}
|
|
2871
2866
|
}
|
|
2872
2867
|
}
|
|
2873
2868
|
// -----------------------------------------------------------------------
|
|
2874
|
-
// Public API
|
|
2869
|
+
// Public API: hover highlight
|
|
2875
2870
|
// -----------------------------------------------------------------------
|
|
2876
|
-
highlight(
|
|
2877
|
-
|
|
2871
|
+
/** Show a hover highlight on the given object (replaces any previous hover). */
|
|
2872
|
+
showHoverHighlight(obj) {
|
|
2873
|
+
if (obj === this._hoverTarget) return;
|
|
2874
|
+
this._clearHoverVisuals();
|
|
2875
|
+
if (!this._scene) return;
|
|
2876
|
+
this._hoverTarget = obj;
|
|
2877
|
+
const targets = collectHighlightTargets(obj);
|
|
2878
|
+
for (const target of targets) {
|
|
2879
|
+
const hg = createHighlightMesh(target, HOVER_FILL_COLOR, HOVER_FILL_OPACITY);
|
|
2880
|
+
if (hg) {
|
|
2881
|
+
hg.root.renderOrder = 997;
|
|
2882
|
+
this._scene.add(hg.root);
|
|
2883
|
+
this._hoverEntries.push({ source: target, group: hg });
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
2878
2886
|
}
|
|
2879
|
-
|
|
2880
|
-
|
|
2887
|
+
/** Remove the current hover highlight. */
|
|
2888
|
+
clearHoverHighlight() {
|
|
2889
|
+
this._clearHoverVisuals();
|
|
2890
|
+
this._lastHoveredUuid = null;
|
|
2881
2891
|
}
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2892
|
+
_clearHoverVisuals() {
|
|
2893
|
+
for (const entry of this._hoverEntries) {
|
|
2894
|
+
this._disposeHighlightGroup(entry.group);
|
|
2895
|
+
}
|
|
2896
|
+
this._hoverEntries = [];
|
|
2897
|
+
this._hoverTarget = null;
|
|
2885
2898
|
}
|
|
2899
|
+
// -----------------------------------------------------------------------
|
|
2900
|
+
// Public API: queries
|
|
2901
|
+
// -----------------------------------------------------------------------
|
|
2902
|
+
/** Check if an object currently has a selection highlight. */
|
|
2886
2903
|
isHighlighted(obj) {
|
|
2887
2904
|
return this._selectedEntries.has(obj);
|
|
2888
2905
|
}
|
|
2889
|
-
/**
|
|
2890
|
-
|
|
2891
|
-
this.
|
|
2892
|
-
this.
|
|
2893
|
-
}
|
|
2894
|
-
/** Clear the hover highlight */
|
|
2895
|
-
clearHoverHighlight() {
|
|
2896
|
-
this._clearAllOverlays(this._hoverEntries);
|
|
2897
|
-
this._lastHoveredElement = null;
|
|
2906
|
+
/** Remove all hover and selection highlights. */
|
|
2907
|
+
clearAll() {
|
|
2908
|
+
this.clearHoverHighlight();
|
|
2909
|
+
this._clearAllSelectionHighlights();
|
|
2898
2910
|
}
|
|
2899
2911
|
// -----------------------------------------------------------------------
|
|
2900
2912
|
// Internal: selection highlights
|
|
2901
2913
|
// -----------------------------------------------------------------------
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
const
|
|
2905
|
-
for (const obj of selected) {
|
|
2906
|
-
targetSet.add(obj);
|
|
2907
|
-
obj.traverse((child) => {
|
|
2908
|
-
targetSet.add(child);
|
|
2909
|
-
});
|
|
2910
|
-
}
|
|
2914
|
+
_syncSelectionHighlights(selected) {
|
|
2915
|
+
if (!this._scene) return;
|
|
2916
|
+
const selectedSet = new Set(selected);
|
|
2911
2917
|
for (const [obj] of this._selectedEntries) {
|
|
2912
|
-
if (!
|
|
2913
|
-
this.
|
|
2918
|
+
if (!selectedSet.has(obj)) {
|
|
2919
|
+
this._removeSelectionHighlight(obj);
|
|
2914
2920
|
}
|
|
2915
2921
|
}
|
|
2916
|
-
for (const obj of
|
|
2917
|
-
if (obj.userData?.__r3fdom_internal) continue;
|
|
2922
|
+
for (const obj of selected) {
|
|
2918
2923
|
if (!this._selectedEntries.has(obj)) {
|
|
2919
|
-
|
|
2920
|
-
this._addSelectedHighlight(obj, isChild);
|
|
2924
|
+
this._addSelectionHighlight(obj);
|
|
2921
2925
|
}
|
|
2922
2926
|
}
|
|
2923
2927
|
}
|
|
2924
|
-
|
|
2925
|
-
if (this.
|
|
2926
|
-
const
|
|
2927
|
-
const
|
|
2928
|
-
const
|
|
2929
|
-
|
|
2930
|
-
|
|
2928
|
+
_addSelectionHighlight(obj) {
|
|
2929
|
+
if (!this._scene) return;
|
|
2930
|
+
const targets = collectHighlightTargets(obj);
|
|
2931
|
+
const meshGroups = [];
|
|
2932
|
+
for (const target of targets) {
|
|
2933
|
+
const hg = createHighlightMesh(target, SELECTION_FILL_COLOR, SELECTION_FILL_OPACITY);
|
|
2934
|
+
if (hg) {
|
|
2935
|
+
hg.root.userData.__r3fdom_source = target;
|
|
2936
|
+
hg.root.renderOrder = 999;
|
|
2937
|
+
this._scene.add(hg.root);
|
|
2938
|
+
meshGroups.push(hg);
|
|
2939
|
+
}
|
|
2940
|
+
}
|
|
2941
|
+
let bboxGroup = null;
|
|
2942
|
+
if (targets.length > 1 && obj.type !== "Group") {
|
|
2943
|
+
bboxGroup = createBoundingBoxHighlight(obj);
|
|
2944
|
+
if (bboxGroup) {
|
|
2945
|
+
this._scene.add(bboxGroup.root);
|
|
2946
|
+
}
|
|
2947
|
+
}
|
|
2948
|
+
if (meshGroups.length > 0 || bboxGroup) {
|
|
2949
|
+
this._selectedEntries.set(obj, { source: obj, meshGroups, bboxGroup });
|
|
2931
2950
|
}
|
|
2932
|
-
const label = getObjectLabel2(obj);
|
|
2933
|
-
const dims = getObjectDimensions(obj);
|
|
2934
|
-
tooltipEl.innerHTML = `<span style="color:${COLORS.tooltipTag}">${label}</span>` + (dims ? ` <span style="color:${COLORS.tooltipDim}">${dims}</span>` : "");
|
|
2935
|
-
document.body.appendChild(overlayEl);
|
|
2936
|
-
document.body.appendChild(tooltipEl);
|
|
2937
|
-
const entry = { overlayEl, tooltipEl, target: obj, isChild };
|
|
2938
|
-
this._selectedEntries.set(obj, entry);
|
|
2939
2951
|
}
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
if (!child.userData?.__r3fdom_internal) {
|
|
2959
|
-
this._addHoverHighlightRecursive(child);
|
|
2952
|
+
_removeSelectionHighlight(obj) {
|
|
2953
|
+
const entry = this._selectedEntries.get(obj);
|
|
2954
|
+
if (!entry) return;
|
|
2955
|
+
for (const mg of entry.meshGroups) {
|
|
2956
|
+
this._disposeHighlightGroup(mg);
|
|
2957
|
+
}
|
|
2958
|
+
if (entry.bboxGroup) {
|
|
2959
|
+
this._disposeHighlightGroup(entry.bboxGroup);
|
|
2960
|
+
}
|
|
2961
|
+
this._selectedEntries.delete(obj);
|
|
2962
|
+
}
|
|
2963
|
+
_clearAllSelectionHighlights() {
|
|
2964
|
+
for (const entry of this._selectedEntries.values()) {
|
|
2965
|
+
for (const mg of entry.meshGroups) {
|
|
2966
|
+
this._disposeHighlightGroup(mg);
|
|
2967
|
+
}
|
|
2968
|
+
if (entry.bboxGroup) {
|
|
2969
|
+
this._disposeHighlightGroup(entry.bboxGroup);
|
|
2960
2970
|
}
|
|
2961
2971
|
}
|
|
2972
|
+
this._selectedEntries.clear();
|
|
2962
2973
|
}
|
|
2963
2974
|
// -----------------------------------------------------------------------
|
|
2964
2975
|
// Internal: DevTools hover polling
|
|
@@ -2977,52 +2988,872 @@ var Highlighter = class {
|
|
|
2977
2988
|
_pollDevToolsHover() {
|
|
2978
2989
|
if (!this._store) return;
|
|
2979
2990
|
try {
|
|
2980
|
-
const hoveredEl =
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
this._clearAllOverlays(this._hoverEntries);
|
|
2985
|
-
return;
|
|
2986
|
-
}
|
|
2987
|
-
const uuid = hoveredEl.getAttribute?.("data-uuid");
|
|
2991
|
+
const hoveredEl = window.__r3fdom_hovered__;
|
|
2992
|
+
const uuid = hoveredEl?.getAttribute?.("data-uuid") ?? null;
|
|
2993
|
+
if (uuid === this._lastHoveredUuid) return;
|
|
2994
|
+
this._lastHoveredUuid = uuid;
|
|
2988
2995
|
if (!uuid) {
|
|
2989
|
-
this.
|
|
2996
|
+
this._clearHoverVisuals();
|
|
2990
2997
|
return;
|
|
2991
2998
|
}
|
|
2992
2999
|
const obj = this._store.getObject3D(uuid);
|
|
2993
3000
|
if (obj) {
|
|
2994
3001
|
this.showHoverHighlight(obj);
|
|
2995
3002
|
} else {
|
|
2996
|
-
this.
|
|
3003
|
+
this._clearHoverVisuals();
|
|
2997
3004
|
}
|
|
2998
3005
|
} catch {
|
|
2999
3006
|
}
|
|
3000
3007
|
}
|
|
3001
3008
|
// -----------------------------------------------------------------------
|
|
3002
|
-
// Internal:
|
|
3009
|
+
// Internal: cleanup
|
|
3003
3010
|
// -----------------------------------------------------------------------
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
entry.tooltipEl.remove();
|
|
3009
|
-
map.delete(obj);
|
|
3010
|
-
}
|
|
3011
|
-
_clearAllOverlays(map) {
|
|
3012
|
-
for (const entry of map.values()) {
|
|
3013
|
-
entry.overlayEl.remove();
|
|
3014
|
-
entry.tooltipEl.remove();
|
|
3011
|
+
_disposeHighlightGroup(hg) {
|
|
3012
|
+
hg.root.removeFromParent();
|
|
3013
|
+
for (const d of hg.disposables) {
|
|
3014
|
+
d.dispose();
|
|
3015
3015
|
}
|
|
3016
|
-
|
|
3016
|
+
}
|
|
3017
|
+
/** Detach and release all resources. */
|
|
3018
|
+
dispose() {
|
|
3019
|
+
this.detach();
|
|
3020
|
+
}
|
|
3021
|
+
};
|
|
3022
|
+
|
|
3023
|
+
// src/highlight/selectionDisplayTarget.ts
|
|
3024
|
+
function isFirstMeshInGroup(obj) {
|
|
3025
|
+
const parent = obj.parent;
|
|
3026
|
+
if (!parent || parent.type !== "Group") return false;
|
|
3027
|
+
const firstMesh = parent.children.find((c) => getTagForType(c.type) === "three-mesh");
|
|
3028
|
+
return firstMesh === obj;
|
|
3029
|
+
}
|
|
3030
|
+
function resolveSelectionDisplayTarget(getObject3D, uuid) {
|
|
3031
|
+
const obj = getObject3D(uuid);
|
|
3032
|
+
if (!obj) return null;
|
|
3033
|
+
if (getTagForType(obj.type) !== "three-mesh") return uuid;
|
|
3034
|
+
if (isFirstMeshInGroup(obj) && obj.parent) return obj.parent.uuid;
|
|
3035
|
+
return uuid;
|
|
3036
|
+
}
|
|
3037
|
+
|
|
3038
|
+
// src/highlight/InspectController.ts
|
|
3039
|
+
var RAYCAST_THROTTLE_MS = 50;
|
|
3040
|
+
var HOVER_REVEAL_DEBOUNCE_MS = 300;
|
|
3041
|
+
var InspectController = class {
|
|
3042
|
+
constructor(opts) {
|
|
3043
|
+
this._active = false;
|
|
3044
|
+
this._lastRaycastTime = 0;
|
|
3045
|
+
this._hoveredObject = null;
|
|
3046
|
+
this._hoverRevealTimer = null;
|
|
3047
|
+
this._overlay = null;
|
|
3048
|
+
this._boundPointerMove = null;
|
|
3049
|
+
this._boundPointerDown = null;
|
|
3050
|
+
this._boundContextMenu = null;
|
|
3051
|
+
this._camera = opts.camera;
|
|
3052
|
+
this._renderer = opts.renderer;
|
|
3053
|
+
this._selectionManager = opts.selectionManager;
|
|
3054
|
+
this._highlighter = opts.highlighter;
|
|
3055
|
+
this._raycastAccelerator = opts.raycastAccelerator;
|
|
3056
|
+
this._mirror = opts.mirror;
|
|
3057
|
+
this._store = opts.store;
|
|
3058
|
+
}
|
|
3059
|
+
get active() {
|
|
3060
|
+
return this._active;
|
|
3061
|
+
}
|
|
3062
|
+
/** Update the camera reference (e.g. after a camera switch). */
|
|
3063
|
+
updateCamera(camera) {
|
|
3064
|
+
this._camera = camera;
|
|
3017
3065
|
}
|
|
3018
3066
|
// -----------------------------------------------------------------------
|
|
3019
|
-
//
|
|
3067
|
+
// Enable / disable
|
|
3020
3068
|
// -----------------------------------------------------------------------
|
|
3069
|
+
/** Activate inspect mode — creates overlay on top of canvas. */
|
|
3070
|
+
enable() {
|
|
3071
|
+
if (this._active) return;
|
|
3072
|
+
this._active = true;
|
|
3073
|
+
const canvas = this._renderer.domElement;
|
|
3074
|
+
const parent = canvas.parentElement;
|
|
3075
|
+
if (!parent) return;
|
|
3076
|
+
const overlay = document.createElement("div");
|
|
3077
|
+
overlay.dataset.r3fdomInspect = "true";
|
|
3078
|
+
overlay.style.cssText = [
|
|
3079
|
+
"position:absolute",
|
|
3080
|
+
"inset:0",
|
|
3081
|
+
"z-index:999999",
|
|
3082
|
+
"cursor:crosshair",
|
|
3083
|
+
"background:transparent"
|
|
3084
|
+
].join(";");
|
|
3085
|
+
const parentPos = getComputedStyle(parent).position;
|
|
3086
|
+
if (parentPos === "static") {
|
|
3087
|
+
parent.style.position = "relative";
|
|
3088
|
+
}
|
|
3089
|
+
this._boundPointerMove = this._onPointerMove.bind(this);
|
|
3090
|
+
this._boundPointerDown = this._onPointerDown.bind(this);
|
|
3091
|
+
this._boundContextMenu = (e) => e.preventDefault();
|
|
3092
|
+
overlay.addEventListener("pointermove", this._boundPointerMove);
|
|
3093
|
+
overlay.addEventListener("pointerdown", this._boundPointerDown);
|
|
3094
|
+
overlay.addEventListener("contextmenu", this._boundContextMenu);
|
|
3095
|
+
parent.appendChild(overlay);
|
|
3096
|
+
this._overlay = overlay;
|
|
3097
|
+
r3fLog("inspect", "Inspect mode enabled \u2014 hover to highlight, click to select");
|
|
3098
|
+
}
|
|
3099
|
+
/** Deactivate inspect mode — removes overlay and clears hover state. */
|
|
3100
|
+
disable() {
|
|
3101
|
+
if (!this._active) return;
|
|
3102
|
+
this._active = false;
|
|
3103
|
+
if (this._overlay) {
|
|
3104
|
+
if (this._boundPointerMove) this._overlay.removeEventListener("pointermove", this._boundPointerMove);
|
|
3105
|
+
if (this._boundPointerDown) this._overlay.removeEventListener("pointerdown", this._boundPointerDown);
|
|
3106
|
+
if (this._boundContextMenu) this._overlay.removeEventListener("contextmenu", this._boundContextMenu);
|
|
3107
|
+
this._overlay.remove();
|
|
3108
|
+
this._overlay = null;
|
|
3109
|
+
}
|
|
3110
|
+
this._boundPointerMove = null;
|
|
3111
|
+
this._boundPointerDown = null;
|
|
3112
|
+
this._boundContextMenu = null;
|
|
3113
|
+
this._hoveredObject = null;
|
|
3114
|
+
this._cancelHoverReveal();
|
|
3115
|
+
this._highlighter.clearHoverHighlight();
|
|
3116
|
+
window.__r3fdom_selected_element__ = null;
|
|
3117
|
+
r3fLog("inspect", "InspectController disabled");
|
|
3118
|
+
}
|
|
3119
|
+
/** Disable and release all resources. */
|
|
3021
3120
|
dispose() {
|
|
3022
|
-
this.
|
|
3121
|
+
this.disable();
|
|
3122
|
+
}
|
|
3123
|
+
// -----------------------------------------------------------------------
|
|
3124
|
+
// Raycasting (delegated to RaycastAccelerator)
|
|
3125
|
+
// -----------------------------------------------------------------------
|
|
3126
|
+
_raycastFromEvent(e) {
|
|
3127
|
+
return this._raycastAccelerator.raycastAtMouse(
|
|
3128
|
+
e,
|
|
3129
|
+
this._camera,
|
|
3130
|
+
this._renderer.domElement
|
|
3131
|
+
);
|
|
3132
|
+
}
|
|
3133
|
+
/**
|
|
3134
|
+
* Resolve a raw raycast hit to the logical selection target.
|
|
3135
|
+
* Walks up to find the best Group parent if applicable.
|
|
3136
|
+
*/
|
|
3137
|
+
_resolveTarget(hit) {
|
|
3138
|
+
const displayUuid = resolveSelectionDisplayTarget(
|
|
3139
|
+
(id) => this._store.getObject3D(id),
|
|
3140
|
+
hit.uuid
|
|
3141
|
+
);
|
|
3142
|
+
if (!displayUuid) return hit;
|
|
3143
|
+
return this._store.getObject3D(displayUuid) ?? hit;
|
|
3144
|
+
}
|
|
3145
|
+
// -----------------------------------------------------------------------
|
|
3146
|
+
// Event handlers
|
|
3147
|
+
// -----------------------------------------------------------------------
|
|
3148
|
+
_onPointerMove(e) {
|
|
3149
|
+
e.stopPropagation();
|
|
3150
|
+
e.preventDefault();
|
|
3151
|
+
const now = performance.now();
|
|
3152
|
+
if (now - this._lastRaycastTime < RAYCAST_THROTTLE_MS) return;
|
|
3153
|
+
this._lastRaycastTime = now;
|
|
3154
|
+
const hit = this._raycastFromEvent(e);
|
|
3155
|
+
if (!hit) {
|
|
3156
|
+
if (this._hoveredObject) {
|
|
3157
|
+
this._hoveredObject = null;
|
|
3158
|
+
this._highlighter.clearHoverHighlight();
|
|
3159
|
+
this._cancelHoverReveal();
|
|
3160
|
+
}
|
|
3161
|
+
return;
|
|
3162
|
+
}
|
|
3163
|
+
if (hit === this._hoveredObject) return;
|
|
3164
|
+
this._hoveredObject = hit;
|
|
3165
|
+
this._highlighter.showHoverHighlight(hit);
|
|
3166
|
+
this._scheduleHoverReveal(hit);
|
|
3167
|
+
}
|
|
3168
|
+
/**
|
|
3169
|
+
* After hovering an object for HOVER_REVEAL_DEBOUNCE_MS, auto-reveal its
|
|
3170
|
+
* mirror element in the Elements tab.
|
|
3171
|
+
*/
|
|
3172
|
+
_scheduleHoverReveal(target) {
|
|
3173
|
+
this._cancelHoverReveal();
|
|
3174
|
+
this._hoverRevealTimer = setTimeout(() => {
|
|
3175
|
+
const mirrorEl = this._mirror.getOrMaterialize(target.uuid);
|
|
3176
|
+
if (mirrorEl) {
|
|
3177
|
+
window.__r3fdom_selected_element__ = mirrorEl;
|
|
3178
|
+
}
|
|
3179
|
+
}, HOVER_REVEAL_DEBOUNCE_MS);
|
|
3180
|
+
}
|
|
3181
|
+
_cancelHoverReveal() {
|
|
3182
|
+
if (this._hoverRevealTimer) {
|
|
3183
|
+
clearTimeout(this._hoverRevealTimer);
|
|
3184
|
+
this._hoverRevealTimer = null;
|
|
3185
|
+
}
|
|
3186
|
+
}
|
|
3187
|
+
_onPointerDown(e) {
|
|
3188
|
+
e.stopPropagation();
|
|
3189
|
+
e.preventDefault();
|
|
3190
|
+
const hit = this._raycastFromEvent(e);
|
|
3191
|
+
if (!hit) {
|
|
3192
|
+
this._selectionManager.clearSelection();
|
|
3193
|
+
return;
|
|
3194
|
+
}
|
|
3195
|
+
const target = this._resolveTarget(hit);
|
|
3196
|
+
if (!target) return;
|
|
3197
|
+
this._selectionManager.select(target);
|
|
3198
|
+
const mirrorEl = this._mirror.getOrMaterialize(target.uuid);
|
|
3199
|
+
if (mirrorEl) {
|
|
3200
|
+
window.__r3fdom_selected_element__ = mirrorEl;
|
|
3201
|
+
}
|
|
3202
|
+
r3fLog("inspect", "Object selected via canvas click", {
|
|
3203
|
+
uuid: target.uuid.slice(0, 8),
|
|
3204
|
+
name: target.name || "(unnamed)",
|
|
3205
|
+
type: target.type
|
|
3206
|
+
});
|
|
3023
3207
|
}
|
|
3024
3208
|
};
|
|
3209
|
+
var _bvhPatched = false;
|
|
3210
|
+
function ensureBVHPatched() {
|
|
3211
|
+
if (_bvhPatched) return;
|
|
3212
|
+
_bvhPatched = true;
|
|
3213
|
+
BufferGeometry.prototype.computeBoundsTree = computeBoundsTree;
|
|
3214
|
+
BufferGeometry.prototype.disposeBoundsTree = disposeBoundsTree;
|
|
3215
|
+
Mesh.prototype.raycast = acceleratedRaycast;
|
|
3216
|
+
r3fLog("raycast", "three-mesh-bvh patched into Three.js");
|
|
3217
|
+
}
|
|
3218
|
+
var _raycaster2 = /* @__PURE__ */ new Raycaster();
|
|
3219
|
+
var _mouse = /* @__PURE__ */ new Vector2();
|
|
3220
|
+
function isRaycastable(obj) {
|
|
3221
|
+
if (obj.userData?.__r3fdom_internal) return false;
|
|
3222
|
+
if (!obj.visible) return false;
|
|
3223
|
+
const isMeshLike = obj.isMesh === true || obj.isLine === true || obj.isPoints === true;
|
|
3224
|
+
if (!isMeshLike) return false;
|
|
3225
|
+
const geom = obj.geometry;
|
|
3226
|
+
if (geom) {
|
|
3227
|
+
const posAttr = geom.getAttribute("position");
|
|
3228
|
+
if (posAttr && !posAttr.array) return false;
|
|
3229
|
+
}
|
|
3230
|
+
return true;
|
|
3231
|
+
}
|
|
3232
|
+
var RaycastAccelerator = class {
|
|
3233
|
+
constructor(store) {
|
|
3234
|
+
this._targets = [];
|
|
3235
|
+
this._dirty = true;
|
|
3236
|
+
this._unsubscribe = null;
|
|
3237
|
+
this._bvhBuiltFor = /* @__PURE__ */ new WeakSet();
|
|
3238
|
+
this._store = store;
|
|
3239
|
+
ensureBVHPatched();
|
|
3240
|
+
this._unsubscribe = store.subscribe(() => {
|
|
3241
|
+
this._dirty = true;
|
|
3242
|
+
});
|
|
3243
|
+
}
|
|
3244
|
+
/** Force a target list rebuild on the next raycast. */
|
|
3245
|
+
markDirty() {
|
|
3246
|
+
this._dirty = true;
|
|
3247
|
+
}
|
|
3248
|
+
_rebuild() {
|
|
3249
|
+
this._dirty = false;
|
|
3250
|
+
const flatList = this._store.getFlatList();
|
|
3251
|
+
const targets = [];
|
|
3252
|
+
for (let i = 0; i < flatList.length; i++) {
|
|
3253
|
+
const obj = flatList[i];
|
|
3254
|
+
if (isRaycastable(obj)) {
|
|
3255
|
+
targets.push(obj);
|
|
3256
|
+
}
|
|
3257
|
+
}
|
|
3258
|
+
this._targets = targets;
|
|
3259
|
+
let bvhBudget = 50;
|
|
3260
|
+
for (let i = 0; i < targets.length && bvhBudget > 0; i++) {
|
|
3261
|
+
const obj = targets[i];
|
|
3262
|
+
if (obj.isMesh) {
|
|
3263
|
+
const geom = obj.geometry;
|
|
3264
|
+
if (geom && !this._bvhBuiltFor.has(geom) && !geom.boundsTree) {
|
|
3265
|
+
this._ensureBVH(obj);
|
|
3266
|
+
bvhBudget--;
|
|
3267
|
+
}
|
|
3268
|
+
}
|
|
3269
|
+
}
|
|
3270
|
+
if (bvhBudget === 0) {
|
|
3271
|
+
this._dirty = true;
|
|
3272
|
+
}
|
|
3273
|
+
}
|
|
3274
|
+
/**
|
|
3275
|
+
* Build a BVH for a mesh's geometry if it doesn't have one yet.
|
|
3276
|
+
* Uses indirect mode to avoid modifying the index buffer.
|
|
3277
|
+
* Skips disposed geometries and does NOT mark failed builds so they
|
|
3278
|
+
* can be retried (e.g. after geometry is re-uploaded).
|
|
3279
|
+
*/
|
|
3280
|
+
_ensureBVH(obj) {
|
|
3281
|
+
if (!obj.isMesh) return;
|
|
3282
|
+
const geom = obj.geometry;
|
|
3283
|
+
if (!geom || this._bvhBuiltFor.has(geom)) return;
|
|
3284
|
+
const posAttr = geom.getAttribute("position");
|
|
3285
|
+
if (!posAttr || !posAttr.array) return;
|
|
3286
|
+
if (geom.boundsTree) {
|
|
3287
|
+
this._bvhBuiltFor.add(geom);
|
|
3288
|
+
return;
|
|
3289
|
+
}
|
|
3290
|
+
try {
|
|
3291
|
+
geom.computeBoundsTree({ indirect: true });
|
|
3292
|
+
this._bvhBuiltFor.add(geom);
|
|
3293
|
+
} catch {
|
|
3294
|
+
r3fLog("raycast", `BVH build failed for geometry, will retry next rebuild`);
|
|
3295
|
+
}
|
|
3296
|
+
}
|
|
3297
|
+
/**
|
|
3298
|
+
* Raycast from mouse position against only raycastable meshes.
|
|
3299
|
+
* Returns the closest non-internal hit, or null.
|
|
3300
|
+
*/
|
|
3301
|
+
raycastAtMouse(e, camera, canvas) {
|
|
3302
|
+
if (this._dirty) this._rebuild();
|
|
3303
|
+
const rect = canvas.getBoundingClientRect();
|
|
3304
|
+
_mouse.x = (e.clientX - rect.left) / rect.width * 2 - 1;
|
|
3305
|
+
_mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
|
|
3306
|
+
_raycaster2.setFromCamera(_mouse, camera);
|
|
3307
|
+
_raycaster2.firstHitOnly = true;
|
|
3308
|
+
const intersections = _raycaster2.intersectObjects(this._targets, false);
|
|
3309
|
+
_raycaster2.firstHitOnly = false;
|
|
3310
|
+
for (const intersection of intersections) {
|
|
3311
|
+
if (intersection.object.userData?.__r3fdom_internal) continue;
|
|
3312
|
+
return intersection.object;
|
|
3313
|
+
}
|
|
3314
|
+
return null;
|
|
3315
|
+
}
|
|
3316
|
+
/**
|
|
3317
|
+
* Raycast from NDC coordinates. Used by raycastVerify.
|
|
3318
|
+
*/
|
|
3319
|
+
raycastAtNdc(ndcX, ndcY, camera) {
|
|
3320
|
+
if (this._dirty) this._rebuild();
|
|
3321
|
+
_mouse.set(ndcX, ndcY);
|
|
3322
|
+
_raycaster2.setFromCamera(_mouse, camera);
|
|
3323
|
+
return _raycaster2.intersectObjects(this._targets, false);
|
|
3324
|
+
}
|
|
3325
|
+
/** Current number of raycastable targets. */
|
|
3326
|
+
get targetCount() {
|
|
3327
|
+
if (this._dirty) this._rebuild();
|
|
3328
|
+
return this._targets.length;
|
|
3329
|
+
}
|
|
3330
|
+
/** Unsubscribe from the store and release the target list. */
|
|
3331
|
+
dispose() {
|
|
3332
|
+
if (this._unsubscribe) {
|
|
3333
|
+
this._unsubscribe();
|
|
3334
|
+
this._unsubscribe = null;
|
|
3335
|
+
}
|
|
3336
|
+
this._targets = [];
|
|
3337
|
+
}
|
|
3338
|
+
};
|
|
3339
|
+
|
|
3340
|
+
// src/bridge/ThreeDom.tsx
|
|
3341
|
+
var _stores = /* @__PURE__ */ new Map();
|
|
3342
|
+
var _mirrors = /* @__PURE__ */ new Map();
|
|
3343
|
+
var _selectionManagers = /* @__PURE__ */ new Map();
|
|
3344
|
+
var _highlighters = /* @__PURE__ */ new Map();
|
|
3345
|
+
var _inspectControllers = /* @__PURE__ */ new Map();
|
|
3346
|
+
function getStore2(canvasId = "") {
|
|
3347
|
+
return _stores.get(canvasId) ?? null;
|
|
3348
|
+
}
|
|
3349
|
+
function getMirror(canvasId = "") {
|
|
3350
|
+
return _mirrors.get(canvasId) ?? null;
|
|
3351
|
+
}
|
|
3352
|
+
function getSelectionManager(canvasId = "") {
|
|
3353
|
+
return _selectionManagers.get(canvasId) ?? null;
|
|
3354
|
+
}
|
|
3355
|
+
function getHighlighter(canvasId = "") {
|
|
3356
|
+
return _highlighters.get(canvasId) ?? null;
|
|
3357
|
+
}
|
|
3358
|
+
function getInspectController(canvasId = "") {
|
|
3359
|
+
return _inspectControllers.get(canvasId) ?? null;
|
|
3360
|
+
}
|
|
3361
|
+
function getCanvasIds() {
|
|
3362
|
+
return Array.from(_stores.keys());
|
|
3363
|
+
}
|
|
3364
|
+
function exposeGlobalAPI(store, gl, cameraRef, selMgr, inspCtrl, mirror, canvasId, isPrimary = true) {
|
|
3365
|
+
const api = {
|
|
3366
|
+
_ready: true,
|
|
3367
|
+
canvasId,
|
|
3368
|
+
getByTestId: (id) => store.getByTestId(id),
|
|
3369
|
+
getByUuid: (uuid) => store.getByUuid(uuid),
|
|
3370
|
+
getByName: (name) => store.getByName(name),
|
|
3371
|
+
getChildren: (idOrUuid) => store.getChildren(idOrUuid),
|
|
3372
|
+
getParent: (idOrUuid) => store.getParent(idOrUuid),
|
|
3373
|
+
getCount: () => store.getCount(),
|
|
3374
|
+
getByType: (type) => store.getByType(type),
|
|
3375
|
+
getByGeometryType: (type) => store.getByGeometryType(type),
|
|
3376
|
+
getByMaterialType: (type) => store.getByMaterialType(type),
|
|
3377
|
+
getByUserData: (key, value) => store.getByUserData(key, value),
|
|
3378
|
+
getCountByType: (type) => store.getCountByType(type),
|
|
3379
|
+
getObjects: (ids) => {
|
|
3380
|
+
const map = store.getObjects(ids);
|
|
3381
|
+
const result = {};
|
|
3382
|
+
for (const [id, meta] of map) {
|
|
3383
|
+
result[id] = meta;
|
|
3384
|
+
}
|
|
3385
|
+
return result;
|
|
3386
|
+
},
|
|
3387
|
+
snapshot: () => createSnapshot(store),
|
|
3388
|
+
inspect: (idOrUuid, options) => store.inspect(idOrUuid, options),
|
|
3389
|
+
click: (idOrUuid) => {
|
|
3390
|
+
click3D(idOrUuid);
|
|
3391
|
+
},
|
|
3392
|
+
doubleClick: (idOrUuid) => {
|
|
3393
|
+
doubleClick3D(idOrUuid);
|
|
3394
|
+
},
|
|
3395
|
+
contextMenu: (idOrUuid) => {
|
|
3396
|
+
contextMenu3D(idOrUuid);
|
|
3397
|
+
},
|
|
3398
|
+
hover: (idOrUuid) => {
|
|
3399
|
+
hover3D(idOrUuid);
|
|
3400
|
+
},
|
|
3401
|
+
unhover: () => {
|
|
3402
|
+
unhover3D();
|
|
3403
|
+
},
|
|
3404
|
+
drag: async (idOrUuid, delta) => {
|
|
3405
|
+
await drag3D(idOrUuid, delta);
|
|
3406
|
+
},
|
|
3407
|
+
wheel: (idOrUuid, options) => {
|
|
3408
|
+
wheel3D(idOrUuid, options);
|
|
3409
|
+
},
|
|
3410
|
+
pointerMiss: () => {
|
|
3411
|
+
pointerMiss3D();
|
|
3412
|
+
},
|
|
3413
|
+
drawPath: async (points, options) => {
|
|
3414
|
+
const result = await drawPath(points, options);
|
|
3415
|
+
return { eventCount: result.eventCount, pointCount: result.pointCount };
|
|
3416
|
+
},
|
|
3417
|
+
select: (idOrUuid) => {
|
|
3418
|
+
const obj = store.getObject3D(idOrUuid);
|
|
3419
|
+
if (obj && selMgr) selMgr.select(obj);
|
|
3420
|
+
},
|
|
3421
|
+
clearSelection: () => {
|
|
3422
|
+
selMgr?.clearSelection();
|
|
3423
|
+
},
|
|
3424
|
+
getSelection: () => selMgr ? selMgr.getSelected().map((o) => o.uuid) : [],
|
|
3425
|
+
getObject3D: (idOrUuid) => store.getObject3D(idOrUuid),
|
|
3426
|
+
getSelectionDisplayTarget: (uuid) => resolveSelectionDisplayTarget((id) => store.getObject3D(id), uuid) ?? uuid,
|
|
3427
|
+
setInspectMode: (on) => {
|
|
3428
|
+
r3fLog("inspect", "Global API setInspectMode called", { on });
|
|
3429
|
+
const ctrl = inspCtrl ?? _inspectControllers.get(canvasId ?? "");
|
|
3430
|
+
if (on) {
|
|
3431
|
+
ctrl?.enable();
|
|
3432
|
+
mirror?.setInspectMode(true);
|
|
3433
|
+
} else {
|
|
3434
|
+
ctrl?.disable();
|
|
3435
|
+
mirror?.setInspectMode(false);
|
|
3436
|
+
}
|
|
3437
|
+
},
|
|
3438
|
+
getInspectMode: () => {
|
|
3439
|
+
const ctrl = inspCtrl ?? _inspectControllers.get(canvasId ?? "");
|
|
3440
|
+
return ctrl?.active ?? false;
|
|
3441
|
+
},
|
|
3442
|
+
sweepOrphans: () => store.sweepOrphans(),
|
|
3443
|
+
getDiagnostics: () => ({
|
|
3444
|
+
version,
|
|
3445
|
+
ready: true,
|
|
3446
|
+
objectCount: store.getCount(),
|
|
3447
|
+
meshCount: store.getCountByType("Mesh"),
|
|
3448
|
+
groupCount: store.getCountByType("Group"),
|
|
3449
|
+
lightCount: store.getCountByType("DirectionalLight") + store.getCountByType("PointLight") + store.getCountByType("SpotLight") + store.getCountByType("AmbientLight") + store.getCountByType("HemisphereLight"),
|
|
3450
|
+
cameraCount: store.getCountByType("PerspectiveCamera") + store.getCountByType("OrthographicCamera"),
|
|
3451
|
+
materializedDomNodes: mirror?.getMaterializedCount() ?? 0,
|
|
3452
|
+
maxDomNodes: mirror?.getMaxNodes() ?? 0,
|
|
3453
|
+
canvasWidth: gl.domElement.width,
|
|
3454
|
+
canvasHeight: gl.domElement.height,
|
|
3455
|
+
webglRenderer: (() => {
|
|
3456
|
+
try {
|
|
3457
|
+
const ctx = gl.getContext();
|
|
3458
|
+
const dbg = ctx.getExtension("WEBGL_debug_renderer_info");
|
|
3459
|
+
return dbg ? ctx.getParameter(dbg.UNMASKED_RENDERER_WEBGL) : "unknown";
|
|
3460
|
+
} catch {
|
|
3461
|
+
return "unknown";
|
|
3462
|
+
}
|
|
3463
|
+
})(),
|
|
3464
|
+
dirtyQueueSize: store.getDirtyCount()
|
|
3465
|
+
}),
|
|
3466
|
+
getCameraState: () => {
|
|
3467
|
+
const cam = cameraRef.current;
|
|
3468
|
+
const dir = new Vector3(0, 0, -1).applyQuaternion(cam.quaternion);
|
|
3469
|
+
const target = [
|
|
3470
|
+
cam.position.x + dir.x * 100,
|
|
3471
|
+
cam.position.y + dir.y * 100,
|
|
3472
|
+
cam.position.z + dir.z * 100
|
|
3473
|
+
];
|
|
3474
|
+
const state = {
|
|
3475
|
+
type: cam.type,
|
|
3476
|
+
position: [cam.position.x, cam.position.y, cam.position.z],
|
|
3477
|
+
rotation: [cam.rotation.x, cam.rotation.y, cam.rotation.z],
|
|
3478
|
+
target,
|
|
3479
|
+
near: cam.near,
|
|
3480
|
+
far: cam.far,
|
|
3481
|
+
zoom: cam.zoom
|
|
3482
|
+
};
|
|
3483
|
+
if (cam.type === "PerspectiveCamera") {
|
|
3484
|
+
const pc = cam;
|
|
3485
|
+
state.fov = pc.fov;
|
|
3486
|
+
state.aspect = pc.aspect;
|
|
3487
|
+
} else if (cam.type === "OrthographicCamera") {
|
|
3488
|
+
const oc = cam;
|
|
3489
|
+
state.left = oc.left;
|
|
3490
|
+
state.right = oc.right;
|
|
3491
|
+
state.top = oc.top;
|
|
3492
|
+
state.bottom = oc.bottom;
|
|
3493
|
+
}
|
|
3494
|
+
return state;
|
|
3495
|
+
},
|
|
3496
|
+
fuzzyFind: (query, limit = 5) => {
|
|
3497
|
+
const q = query.toLowerCase();
|
|
3498
|
+
const results = [];
|
|
3499
|
+
for (const obj of store.getFlatList()) {
|
|
3500
|
+
if (results.length >= limit) break;
|
|
3501
|
+
const meta = store.getMetadata(obj);
|
|
3502
|
+
if (!meta) continue;
|
|
3503
|
+
const testId = meta.testId?.toLowerCase() ?? "";
|
|
3504
|
+
const name = meta.name?.toLowerCase() ?? "";
|
|
3505
|
+
if (testId.includes(q) || name.includes(q) || meta.uuid.startsWith(q)) {
|
|
3506
|
+
results.push(meta);
|
|
3507
|
+
}
|
|
3508
|
+
}
|
|
3509
|
+
return results;
|
|
3510
|
+
},
|
|
3511
|
+
version
|
|
3512
|
+
};
|
|
3513
|
+
if (isPrimary) {
|
|
3514
|
+
window.__R3F_DOM__ = api;
|
|
3515
|
+
}
|
|
3516
|
+
if (canvasId) {
|
|
3517
|
+
if (!window.__R3F_DOM_INSTANCES__) window.__R3F_DOM_INSTANCES__ = {};
|
|
3518
|
+
if (window.__R3F_DOM_INSTANCES__[canvasId]) {
|
|
3519
|
+
console.warn(
|
|
3520
|
+
`[react-three-dom] Duplicate canvasId "${canvasId}" \u2014 the previous bridge instance will be overwritten. Each <ThreeDom> must have a unique canvasId.`
|
|
3521
|
+
);
|
|
3522
|
+
}
|
|
3523
|
+
window.__R3F_DOM_INSTANCES__[canvasId] = api;
|
|
3524
|
+
}
|
|
3525
|
+
}
|
|
3526
|
+
function removeGlobalAPI(onlyIfEquals, canvasId) {
|
|
3527
|
+
r3fLog("bridge", "removeGlobalAPI called (deferred)");
|
|
3528
|
+
const removeFromRegistry = (ref) => {
|
|
3529
|
+
if (canvasId && window.__R3F_DOM_INSTANCES__) {
|
|
3530
|
+
if (!ref || window.__R3F_DOM_INSTANCES__[canvasId] === ref) {
|
|
3531
|
+
delete window.__R3F_DOM_INSTANCES__[canvasId];
|
|
3532
|
+
if (Object.keys(window.__R3F_DOM_INSTANCES__).length === 0) {
|
|
3533
|
+
delete window.__R3F_DOM_INSTANCES__;
|
|
3534
|
+
}
|
|
3535
|
+
}
|
|
3536
|
+
}
|
|
3537
|
+
};
|
|
3538
|
+
if (onlyIfEquals !== void 0) {
|
|
3539
|
+
const ref = onlyIfEquals;
|
|
3540
|
+
queueMicrotask(() => {
|
|
3541
|
+
if (window.__R3F_DOM__ === ref) {
|
|
3542
|
+
delete window.__R3F_DOM__;
|
|
3543
|
+
r3fLog("bridge", "Global API removed");
|
|
3544
|
+
} else {
|
|
3545
|
+
r3fLog("bridge", "Global API not removed \u2014 replaced by new instance (Strict Mode remount)");
|
|
3546
|
+
}
|
|
3547
|
+
removeFromRegistry(ref);
|
|
3548
|
+
});
|
|
3549
|
+
} else {
|
|
3550
|
+
delete window.__R3F_DOM__;
|
|
3551
|
+
removeFromRegistry();
|
|
3552
|
+
r3fLog("bridge", "Global API removed (immediate)");
|
|
3553
|
+
}
|
|
3554
|
+
}
|
|
3555
|
+
function createStubBridge(error, canvasId) {
|
|
3556
|
+
return {
|
|
3557
|
+
_ready: false,
|
|
3558
|
+
_error: error,
|
|
3559
|
+
canvasId,
|
|
3560
|
+
getByTestId: () => null,
|
|
3561
|
+
getByUuid: () => null,
|
|
3562
|
+
getByName: () => [],
|
|
3563
|
+
getChildren: () => [],
|
|
3564
|
+
getParent: () => null,
|
|
3565
|
+
getCount: () => 0,
|
|
3566
|
+
getByType: () => [],
|
|
3567
|
+
getByGeometryType: () => [],
|
|
3568
|
+
getByMaterialType: () => [],
|
|
3569
|
+
getByUserData: () => [],
|
|
3570
|
+
getCountByType: () => 0,
|
|
3571
|
+
getObjects: (ids) => {
|
|
3572
|
+
const result = {};
|
|
3573
|
+
for (const id of ids) result[id] = null;
|
|
3574
|
+
return result;
|
|
3575
|
+
},
|
|
3576
|
+
snapshot: () => ({
|
|
3577
|
+
timestamp: 0,
|
|
3578
|
+
objectCount: 0,
|
|
3579
|
+
tree: {
|
|
3580
|
+
uuid: "",
|
|
3581
|
+
name: "",
|
|
3582
|
+
type: "Scene",
|
|
3583
|
+
visible: true,
|
|
3584
|
+
position: [0, 0, 0],
|
|
3585
|
+
rotation: [0, 0, 0],
|
|
3586
|
+
scale: [1, 1, 1],
|
|
3587
|
+
children: []
|
|
3588
|
+
}
|
|
3589
|
+
}),
|
|
3590
|
+
inspect: () => null,
|
|
3591
|
+
click: () => {
|
|
3592
|
+
},
|
|
3593
|
+
doubleClick: () => {
|
|
3594
|
+
},
|
|
3595
|
+
contextMenu: () => {
|
|
3596
|
+
},
|
|
3597
|
+
hover: () => {
|
|
3598
|
+
},
|
|
3599
|
+
unhover: () => {
|
|
3600
|
+
},
|
|
3601
|
+
drag: async () => {
|
|
3602
|
+
},
|
|
3603
|
+
wheel: () => {
|
|
3604
|
+
},
|
|
3605
|
+
pointerMiss: () => {
|
|
3606
|
+
},
|
|
3607
|
+
drawPath: async () => ({ eventCount: 0, pointCount: 0 }),
|
|
3608
|
+
select: () => {
|
|
3609
|
+
},
|
|
3610
|
+
clearSelection: () => {
|
|
3611
|
+
},
|
|
3612
|
+
getSelection: () => [],
|
|
3613
|
+
getObject3D: () => null,
|
|
3614
|
+
getSelectionDisplayTarget: (uuid) => uuid,
|
|
3615
|
+
setInspectMode: () => {
|
|
3616
|
+
},
|
|
3617
|
+
getInspectMode: () => false,
|
|
3618
|
+
sweepOrphans: () => 0,
|
|
3619
|
+
getDiagnostics: () => ({
|
|
3620
|
+
version,
|
|
3621
|
+
ready: false,
|
|
3622
|
+
error: error ?? void 0,
|
|
3623
|
+
objectCount: 0,
|
|
3624
|
+
meshCount: 0,
|
|
3625
|
+
groupCount: 0,
|
|
3626
|
+
lightCount: 0,
|
|
3627
|
+
cameraCount: 0,
|
|
3628
|
+
materializedDomNodes: 0,
|
|
3629
|
+
maxDomNodes: 0,
|
|
3630
|
+
canvasWidth: 0,
|
|
3631
|
+
canvasHeight: 0,
|
|
3632
|
+
webglRenderer: "unavailable",
|
|
3633
|
+
dirtyQueueSize: 0
|
|
3634
|
+
}),
|
|
3635
|
+
getCameraState: () => ({
|
|
3636
|
+
type: "unknown",
|
|
3637
|
+
position: [0, 0, 0],
|
|
3638
|
+
rotation: [0, 0, 0],
|
|
3639
|
+
target: [0, 0, -100],
|
|
3640
|
+
near: 0.1,
|
|
3641
|
+
far: 1e3,
|
|
3642
|
+
zoom: 1
|
|
3643
|
+
}),
|
|
3644
|
+
fuzzyFind: () => [],
|
|
3645
|
+
version
|
|
3646
|
+
};
|
|
3647
|
+
}
|
|
3648
|
+
function ThreeDom({
|
|
3649
|
+
canvasId,
|
|
3650
|
+
primary,
|
|
3651
|
+
root = "#three-dom-root",
|
|
3652
|
+
batchSize = 500,
|
|
3653
|
+
syncBudgetMs = 0.5,
|
|
3654
|
+
maxDomNodes = 2e3,
|
|
3655
|
+
initialDepth = 3,
|
|
3656
|
+
enabled = true,
|
|
3657
|
+
debug = false,
|
|
3658
|
+
inspect: inspectProp = false
|
|
3659
|
+
} = {}) {
|
|
3660
|
+
const isPrimary = primary ?? canvasId === void 0;
|
|
3661
|
+
const instanceKey = canvasId ?? "";
|
|
3662
|
+
const scene = useThree((s) => s.scene);
|
|
3663
|
+
const camera = useThree((s) => s.camera);
|
|
3664
|
+
const gl = useThree((s) => s.gl);
|
|
3665
|
+
const size = useThree((s) => s.size);
|
|
3666
|
+
const cursorRef = useRef(0);
|
|
3667
|
+
const lastSweepRef = useRef(0);
|
|
3668
|
+
const cameraRef = useRef(camera);
|
|
3669
|
+
cameraRef.current = camera;
|
|
3670
|
+
useEffect(() => {
|
|
3671
|
+
if (!enabled) return;
|
|
3672
|
+
if (debug) enableDebug(true);
|
|
3673
|
+
r3fLog("setup", "ThreeDom effect started", { enabled, debug, root, maxDomNodes });
|
|
3674
|
+
const canvas = gl.domElement;
|
|
3675
|
+
canvas.setAttribute("data-r3f-canvas", canvasId ?? "true");
|
|
3676
|
+
const canvasParent = canvas.parentElement;
|
|
3677
|
+
let rootElement = null;
|
|
3678
|
+
let createdRoot = false;
|
|
3679
|
+
if (typeof root === "string") {
|
|
3680
|
+
rootElement = document.querySelector(root);
|
|
3681
|
+
} else {
|
|
3682
|
+
rootElement = root;
|
|
3683
|
+
}
|
|
3684
|
+
if (!rootElement) {
|
|
3685
|
+
rootElement = document.createElement("div");
|
|
3686
|
+
rootElement.id = typeof root === "string" ? root.replace(/^#/, "") : "three-dom-root";
|
|
3687
|
+
createdRoot = true;
|
|
3688
|
+
}
|
|
3689
|
+
canvasParent.style.position = canvasParent.style.position || "relative";
|
|
3690
|
+
canvasParent.appendChild(rootElement);
|
|
3691
|
+
rootElement.style.cssText = "width:0;height:0;overflow:hidden;pointer-events:none;opacity:0;";
|
|
3692
|
+
let store = null;
|
|
3693
|
+
let mirror = null;
|
|
3694
|
+
let unpatch = null;
|
|
3695
|
+
let cancelAsyncReg = null;
|
|
3696
|
+
let selectionManager = null;
|
|
3697
|
+
let highlighter = null;
|
|
3698
|
+
let raycastAccelerator = null;
|
|
3699
|
+
let inspectController = null;
|
|
3700
|
+
let currentApi;
|
|
3701
|
+
try {
|
|
3702
|
+
const webglContext = gl.getContext();
|
|
3703
|
+
if (!webglContext || webglContext.isContextLost?.()) {
|
|
3704
|
+
const msg = "WebGL context not available. For headless Chromium, add --enable-webgl and optionally --use-gl=angle --use-angle=swiftshader-webgl to launch args.";
|
|
3705
|
+
const stubApi = createStubBridge(msg, canvasId);
|
|
3706
|
+
if (isPrimary) window.__R3F_DOM__ = stubApi;
|
|
3707
|
+
if (canvasId) {
|
|
3708
|
+
if (!window.__R3F_DOM_INSTANCES__) window.__R3F_DOM_INSTANCES__ = {};
|
|
3709
|
+
window.__R3F_DOM_INSTANCES__[canvasId] = stubApi;
|
|
3710
|
+
}
|
|
3711
|
+
r3fLog("setup", msg);
|
|
3712
|
+
return () => {
|
|
3713
|
+
removeGlobalAPI(stubApi, canvasId);
|
|
3714
|
+
canvas.removeAttribute("data-r3f-canvas");
|
|
3715
|
+
if (createdRoot && rootElement?.parentNode) {
|
|
3716
|
+
rootElement.parentNode.removeChild(rootElement);
|
|
3717
|
+
}
|
|
3718
|
+
if (debug) enableDebug(false);
|
|
3719
|
+
};
|
|
3720
|
+
}
|
|
3721
|
+
store = new ObjectStore();
|
|
3722
|
+
mirror = new DomMirror(store, maxDomNodes);
|
|
3723
|
+
mirror.setRoot(rootElement);
|
|
3724
|
+
r3fLog("setup", "Store and mirror created");
|
|
3725
|
+
ensureCustomElements(store);
|
|
3726
|
+
unpatch = patchObject3D(store, mirror);
|
|
3727
|
+
setInteractionState(store, camera, gl, size);
|
|
3728
|
+
r3fLog("setup", "Object3D patched, interaction state set");
|
|
3729
|
+
store.registerTree(scene);
|
|
3730
|
+
if (!store.has(camera)) {
|
|
3731
|
+
const camMeta = store.register(camera);
|
|
3732
|
+
camMeta.parentUuid = scene.uuid;
|
|
3733
|
+
mirror.materialize(camera.uuid);
|
|
3734
|
+
}
|
|
3735
|
+
mirror.materializeSubtree(scene.uuid, initialDepth);
|
|
3736
|
+
cancelAsyncReg = store.registerTreeAsync(scene);
|
|
3737
|
+
r3fLog("setup", `Scene registered: ${store.getCount()} objects, async watcher started`);
|
|
3738
|
+
selectionManager = new SelectionManager();
|
|
3739
|
+
highlighter = new Highlighter();
|
|
3740
|
+
highlighter.attach(scene, selectionManager, camera, gl, store);
|
|
3741
|
+
raycastAccelerator = new RaycastAccelerator(store);
|
|
3742
|
+
inspectController = new InspectController({
|
|
3743
|
+
camera,
|
|
3744
|
+
renderer: gl,
|
|
3745
|
+
selectionManager,
|
|
3746
|
+
highlighter,
|
|
3747
|
+
raycastAccelerator,
|
|
3748
|
+
mirror,
|
|
3749
|
+
store
|
|
3750
|
+
});
|
|
3751
|
+
_selectionManagers.set(instanceKey, selectionManager);
|
|
3752
|
+
_highlighters.set(instanceKey, highlighter);
|
|
3753
|
+
_inspectControllers.set(instanceKey, inspectController);
|
|
3754
|
+
exposeGlobalAPI(store, gl, cameraRef, selectionManager, inspectController, mirror, canvasId, isPrimary);
|
|
3755
|
+
r3fLog("bridge", `exposeGlobalAPI called \u2014 bridge is live, _ready=true${canvasId ? `, canvasId="${canvasId}"` : ""}`);
|
|
3756
|
+
currentApi = canvasId ? window.__R3F_DOM_INSTANCES__?.[canvasId] : window.__R3F_DOM__;
|
|
3757
|
+
_stores.set(instanceKey, store);
|
|
3758
|
+
_mirrors.set(instanceKey, mirror);
|
|
3759
|
+
if (inspectProp) {
|
|
3760
|
+
inspectController.enable();
|
|
3761
|
+
}
|
|
3762
|
+
if (debug) {
|
|
3763
|
+
const inspectStatus = inspectProp ? "on" : "off";
|
|
3764
|
+
console.log(
|
|
3765
|
+
"%c[react-three-dom]%c v" + version + " \u2014 " + store.getCount() + " objects \xB7 inspect: " + inspectStatus + "\n %c\u2192%c Toggle inspect: %c__R3F_DOM__.setInspectMode(true|false)%c\n %c\u2192%c Hover 3D objects to highlight + auto-reveal in Elements tab",
|
|
3766
|
+
"background:#1a73e8;color:#fff;padding:2px 6px;border-radius:3px;font-weight:bold",
|
|
3767
|
+
"color:inherit",
|
|
3768
|
+
"color:#1a73e8",
|
|
3769
|
+
"color:inherit",
|
|
3770
|
+
"color:#e8a317;font-family:monospace",
|
|
3771
|
+
"color:inherit",
|
|
3772
|
+
"color:#1a73e8",
|
|
3773
|
+
"color:inherit"
|
|
3774
|
+
);
|
|
3775
|
+
}
|
|
3776
|
+
} catch (err) {
|
|
3777
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
3778
|
+
r3fLog("setup", "ThreeDom setup failed", err);
|
|
3779
|
+
console.error("[react-three-dom] Setup failed:", err);
|
|
3780
|
+
const stubApi = createStubBridge(errorMsg, canvasId);
|
|
3781
|
+
if (isPrimary) window.__R3F_DOM__ = stubApi;
|
|
3782
|
+
if (canvasId) {
|
|
3783
|
+
if (!window.__R3F_DOM_INSTANCES__) window.__R3F_DOM_INSTANCES__ = {};
|
|
3784
|
+
window.__R3F_DOM_INSTANCES__[canvasId] = stubApi;
|
|
3785
|
+
}
|
|
3786
|
+
currentApi = stubApi;
|
|
3787
|
+
}
|
|
3788
|
+
return () => {
|
|
3789
|
+
r3fLog("setup", "ThreeDom cleanup started");
|
|
3790
|
+
if (cancelAsyncReg) cancelAsyncReg();
|
|
3791
|
+
if (inspectController) inspectController.dispose();
|
|
3792
|
+
if (raycastAccelerator) raycastAccelerator.dispose();
|
|
3793
|
+
if (highlighter) highlighter.dispose();
|
|
3794
|
+
if (unpatch) unpatch();
|
|
3795
|
+
removeGlobalAPI(currentApi, canvasId);
|
|
3796
|
+
clearInteractionState();
|
|
3797
|
+
if (selectionManager) selectionManager.dispose();
|
|
3798
|
+
if (mirror) mirror.dispose();
|
|
3799
|
+
if (store) store.dispose();
|
|
3800
|
+
canvas.removeAttribute("data-r3f-canvas");
|
|
3801
|
+
if (createdRoot && rootElement?.parentNode) {
|
|
3802
|
+
rootElement.parentNode.removeChild(rootElement);
|
|
3803
|
+
}
|
|
3804
|
+
_stores.delete(instanceKey);
|
|
3805
|
+
_mirrors.delete(instanceKey);
|
|
3806
|
+
_selectionManagers.delete(instanceKey);
|
|
3807
|
+
_highlighters.delete(instanceKey);
|
|
3808
|
+
_inspectControllers.delete(instanceKey);
|
|
3809
|
+
if (debug) enableDebug(false);
|
|
3810
|
+
r3fLog("setup", "ThreeDom cleanup complete");
|
|
3811
|
+
};
|
|
3812
|
+
}, [scene, camera, gl, enabled, root, maxDomNodes, initialDepth, debug, inspectProp, canvasId, isPrimary, instanceKey]);
|
|
3813
|
+
useFrame(() => {
|
|
3814
|
+
const _store3 = _stores.get(instanceKey);
|
|
3815
|
+
const _mirror = _mirrors.get(instanceKey);
|
|
3816
|
+
const _highlighter = _highlighters.get(instanceKey);
|
|
3817
|
+
const _inspectController = _inspectControllers.get(instanceKey);
|
|
3818
|
+
if (!enabled || !_store3 || !_mirror) return;
|
|
3819
|
+
try {
|
|
3820
|
+
setInteractionState(_store3, camera, gl, size);
|
|
3821
|
+
if (_inspectController) _inspectController.updateCamera(camera);
|
|
3822
|
+
const store = _store3;
|
|
3823
|
+
const mirror = _mirror;
|
|
3824
|
+
const start = performance.now();
|
|
3825
|
+
const dirtyObjects = store.drainDirtyQueue();
|
|
3826
|
+
for (const obj of dirtyObjects) {
|
|
3827
|
+
store.update(obj);
|
|
3828
|
+
mirror.syncAttributes(obj);
|
|
3829
|
+
}
|
|
3830
|
+
const budgetRemaining = syncBudgetMs - (performance.now() - start);
|
|
3831
|
+
if (budgetRemaining > 0.1) {
|
|
3832
|
+
const objects = store.getFlatList();
|
|
3833
|
+
if (objects.length > 0) {
|
|
3834
|
+
const end = Math.min(cursorRef.current + batchSize, objects.length);
|
|
3835
|
+
for (let i = cursorRef.current; i < end; i++) {
|
|
3836
|
+
if (performance.now() - start > syncBudgetMs) break;
|
|
3837
|
+
const obj = objects[i];
|
|
3838
|
+
const changed = store.update(obj);
|
|
3839
|
+
if (changed) mirror.syncAttributes(obj);
|
|
3840
|
+
}
|
|
3841
|
+
cursorRef.current = end >= objects.length ? 0 : end;
|
|
3842
|
+
}
|
|
3843
|
+
}
|
|
3844
|
+
if (_highlighter) _highlighter.update();
|
|
3845
|
+
const now = performance.now();
|
|
3846
|
+
if (now - lastSweepRef.current > 3e4) {
|
|
3847
|
+
lastSweepRef.current = now;
|
|
3848
|
+
store.sweepOrphans();
|
|
3849
|
+
}
|
|
3850
|
+
} catch (err) {
|
|
3851
|
+
r3fLog("sync", "Per-frame sync error", err);
|
|
3852
|
+
}
|
|
3853
|
+
});
|
|
3854
|
+
return null;
|
|
3855
|
+
}
|
|
3025
3856
|
|
|
3026
|
-
export { DomMirror, Highlighter, MANAGED_ATTRIBUTES, ObjectStore, SelectionManager, TAG_MAP, ThreeDom, ThreeElement, applyAttributes, circlePath, click3D, computeAttributes, contextMenu3D, createFlatSnapshot, createSnapshot, curvePath, dispatchClick, dispatchContextMenu, dispatchDoubleClick, dispatchDrag, dispatchHover, dispatchPointerMiss, dispatchUnhover, dispatchWheel, doubleClick3D, drag3D, drawPath, enableDebug, ensureCustomElements, getHighlighter, getMirror, getSelectionManager, getStore2 as getStore, getTagForType, hover3D, isDebugEnabled, isInFrustum, isPatched, linePath, patchObject3D, pointerMiss3D, previewDragWorldDelta, projectAllSamplePoints, projectToScreen, r3fLog, rectPath, resolveObject, restoreObject3D, screenDeltaToWorld, unhover3D, verifyRaycastHit, verifyRaycastHitMultiPoint, version, wheel3D };
|
|
3857
|
+
export { DomMirror, Highlighter, InspectController, MANAGED_ATTRIBUTES, ObjectStore, RaycastAccelerator, SelectionManager, TAG_MAP, ThreeDom, ThreeElement, applyAttributes, circlePath, click3D, computeAttributes, contextMenu3D, createFlatSnapshot, createSnapshot, curvePath, dispatchClick, dispatchContextMenu, dispatchDoubleClick, dispatchDrag, dispatchHover, dispatchPointerMiss, dispatchUnhover, dispatchWheel, doubleClick3D, drag3D, drawPath, enableDebug, ensureCustomElements, getCanvasIds, getHighlighter, getInspectController, getMirror, getSelectionManager, getStore2 as getStore, getTagForType, hover3D, isDebugEnabled, isInFrustum, isPatched, linePath, patchObject3D, pointerMiss3D, previewDragWorldDelta, projectAllSamplePoints, projectToScreen, r3fLog, rectPath, resolveObject, restoreObject3D, screenDeltaToWorld, unhover3D, verifyRaycastHit, verifyRaycastHitMultiPoint, version, wheel3D };
|
|
3027
3858
|
//# sourceMappingURL=index.js.map
|
|
3028
3859
|
//# sourceMappingURL=index.js.map
|