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