@react-three-dom/core 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs 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.1.0";
9
+ var version = "0.3.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
- return meta;
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 hasChanged(prev, curr) {
82
- return prev.visible !== curr.visible || prev.name !== curr.name || prev.testId !== curr.testId || prev.position[0] !== curr.position[0] || prev.position[1] !== curr.position[1] || prev.position[2] !== curr.position[2] || prev.rotation[0] !== curr.rotation[0] || prev.rotation[1] !== curr.rotation[1] || prev.rotation[2] !== curr.rotation[2] || prev.scale[0] !== curr.scale[0] || prev.scale[1] !== curr.scale[1] || prev.scale[2] !== curr.scale[2] || prev.parentUuid !== curr.parentUuid || prev.childrenUuids.length !== curr.childrenUuids.length || prev.instanceCount !== curr.instanceCount;
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
- * Individual objects that fail to register are skipped (logged when debug
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,7 @@ 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;
261
404
  if (meta.testId) {
262
405
  this._objectsByTestId.delete(meta.testId);
263
406
  }
@@ -288,49 +431,50 @@ var ObjectStore = class {
288
431
  // Tier 1: Update (compare-and-set, returns true if changed)
289
432
  // -------------------------------------------------------------------------
290
433
  /**
291
- * Refresh Tier 1 metadata from the live Three.js object.
434
+ * Refresh dynamic Tier 1 fields from the live Three.js object.
435
+ * Only reads transform, visibility, children count, and parent —
436
+ * skips static fields (geometry, material) that don't change per-frame.
437
+ * Mutates metadata in-place to avoid allocation.
292
438
  * Returns true if any values changed.
293
- * Returns false (no change) if extracting metadata throws so that the
294
- * previous metadata is preserved.
295
439
  */
296
440
  update(obj) {
297
- const prev = this._metaByObject.get(obj);
298
- if (!prev) return false;
299
- let curr;
441
+ const meta = this._metaByObject.get(obj);
442
+ if (!meta) return false;
443
+ let changed;
300
444
  try {
301
- curr = extractMetadata(obj);
302
- } catch (err) {
303
- r3fLog("store", `update: extractMetadata failed for "${obj.name || obj.uuid}"`, err);
304
- return false;
305
- }
306
- if (hasChanged(prev, curr)) {
307
- if (prev.testId !== curr.testId) {
308
- r3fLog("store", `testId changed: "${prev.testId}" \u2192 "${curr.testId}" (${curr.type})`);
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
- }
445
+ const prevTestId = meta.testId;
446
+ const prevName = meta.name;
447
+ changed = updateDynamicFields(obj, meta);
448
+ if (changed) {
449
+ if (prevTestId !== meta.testId) {
450
+ r3fLog("store", `testId changed: "${prevTestId}" \u2192 "${meta.testId}" (${meta.type})`);
451
+ if (prevTestId) this._objectsByTestId.delete(prevTestId);
452
+ if (meta.testId) this._objectsByTestId.set(meta.testId, obj);
319
453
  }
320
- if (curr.name) {
321
- let nameSet = this._objectsByName.get(curr.name);
322
- if (!nameSet) {
323
- nameSet = /* @__PURE__ */ new Set();
324
- this._objectsByName.set(curr.name, nameSet);
454
+ if (prevName !== meta.name) {
455
+ if (prevName) {
456
+ const nameSet = this._objectsByName.get(prevName);
457
+ if (nameSet) {
458
+ nameSet.delete(obj);
459
+ if (nameSet.size === 0) this._objectsByName.delete(prevName);
460
+ }
461
+ }
462
+ if (meta.name) {
463
+ let nameSet = this._objectsByName.get(meta.name);
464
+ if (!nameSet) {
465
+ nameSet = /* @__PURE__ */ new Set();
466
+ this._objectsByName.set(meta.name, nameSet);
467
+ }
468
+ nameSet.add(obj);
325
469
  }
326
- nameSet.add(obj);
327
470
  }
471
+ this._emit({ type: "update", object: obj, metadata: meta });
328
472
  }
329
- this._metaByObject.set(obj, curr);
330
- this._emit({ type: "update", object: obj, metadata: curr });
331
- return true;
473
+ } catch (err) {
474
+ r3fLog("store", `update: updateDynamicFields failed for "${obj.name || obj.uuid}"`, err);
475
+ return false;
332
476
  }
333
- return false;
477
+ return changed;
334
478
  }
335
479
  // -------------------------------------------------------------------------
336
480
  // Tier 2: On-demand inspection (never cached)
@@ -339,13 +483,14 @@ var ObjectStore = class {
339
483
  * Compute full inspection data from a live Three.js object.
340
484
  * This reads geometry buffers, material properties, world bounds, etc.
341
485
  * Cost: 0.1–2ms depending on geometry complexity.
486
+ * Pass { includeGeometryData: true } to include vertex positions and triangle indices (higher cost for large meshes).
342
487
  */
343
- inspect(idOrUuid) {
488
+ inspect(idOrUuid, options) {
344
489
  const obj = this.getObject3D(idOrUuid);
345
490
  if (!obj) return null;
346
491
  const meta = this._metaByObject.get(obj);
347
492
  if (!meta) return null;
348
- return inspectObject(obj, meta);
493
+ return inspectObject(obj, meta, options);
349
494
  }
350
495
  // -------------------------------------------------------------------------
351
496
  // Lookups (O(1))
@@ -373,6 +518,23 @@ var ObjectStore = class {
373
518
  }
374
519
  return results;
375
520
  }
521
+ /** Get direct children of an object by testId or uuid. Returns empty array if not found. */
522
+ getChildren(idOrUuid) {
523
+ const meta = this.getByTestId(idOrUuid) ?? this.getByUuid(idOrUuid);
524
+ if (!meta) return [];
525
+ const results = [];
526
+ for (const childUuid of meta.childrenUuids) {
527
+ const childMeta = this.getByUuid(childUuid);
528
+ if (childMeta) results.push(childMeta);
529
+ }
530
+ return results;
531
+ }
532
+ /** Get parent of an object by testId or uuid. Returns null if not found or if root. */
533
+ getParent(idOrUuid) {
534
+ const meta = this.getByTestId(idOrUuid) ?? this.getByUuid(idOrUuid);
535
+ if (!meta || meta.parentUuid === null) return null;
536
+ return this.getByUuid(meta.parentUuid);
537
+ }
376
538
  /**
377
539
  * Batch lookup: get metadata for multiple objects by testId or uuid.
378
540
  * Returns a Map from the requested id to its metadata (or null if not found).
@@ -400,6 +562,30 @@ var ObjectStore = class {
400
562
  }
401
563
  return results;
402
564
  }
565
+ /**
566
+ * Get all objects with a given geometry type (e.g. "BoxGeometry", "BufferGeometry").
567
+ * Linear scan — O(n). Only meshes/points/lines have geometryType.
568
+ */
569
+ getByGeometryType(type) {
570
+ const results = [];
571
+ for (const obj of this.getFlatList()) {
572
+ const meta = this._metaByObject.get(obj);
573
+ if (meta && meta.geometryType === type) results.push(meta);
574
+ }
575
+ return results;
576
+ }
577
+ /**
578
+ * Get all objects with a given material type (e.g. "MeshStandardMaterial").
579
+ * Linear scan — O(n). Only meshes/points/lines have materialType.
580
+ */
581
+ getByMaterialType(type) {
582
+ const results = [];
583
+ for (const obj of this.getFlatList()) {
584
+ const meta = this._metaByObject.get(obj);
585
+ if (meta && meta.materialType === type) results.push(meta);
586
+ }
587
+ return results;
588
+ }
403
589
  /**
404
590
  * Get all objects that have a specific userData key.
405
591
  * If `value` is provided, only returns objects where `userData[key]` matches.
@@ -452,13 +638,16 @@ var ObjectStore = class {
452
638
  return this._trackedRoots.has(obj);
453
639
  }
454
640
  /**
455
- * Walk up from `obj` to see if any ancestor is a tracked root.
456
- * Used by Object3D.add/remove patch to determine if an object
457
- * belongs to a monitored scene.
641
+ * Check if an object belongs to a tracked scene.
642
+ * Fast path: checks the `__r3fdom_tracked` flag set during register (O(1)).
643
+ * Fallback: walks up the parent chain to find a tracked root.
644
+ * The fallback is needed for newly added objects that aren't registered yet.
458
645
  */
459
646
  isInTrackedScene(obj) {
460
- let current = obj;
647
+ if (obj.userData?.__r3fdom_tracked) return true;
648
+ let current = obj.parent;
461
649
  while (current) {
650
+ if (current.userData?.__r3fdom_tracked) return true;
462
651
  if (this._trackedRoots.has(current)) return true;
463
652
  current = current.parent;
464
653
  }
@@ -518,10 +707,39 @@ var ObjectStore = class {
518
707
  }
519
708
  }
520
709
  // -------------------------------------------------------------------------
710
+ // GC: sweep orphaned objects
711
+ // -------------------------------------------------------------------------
712
+ /**
713
+ * Sweep objects in `_objectByUuid` that are no longer in any tracked scene.
714
+ * This catches objects that were removed from the scene graph without
715
+ * triggering the patched Object3D.remove (e.g. direct `.children` splice,
716
+ * or the remove hook failing silently).
717
+ *
718
+ * At BIM scale, call this periodically (e.g. every 30s or after a floor
719
+ * load/unload) to prevent memory leaks from retained Object3D references.
720
+ *
721
+ * Returns the number of orphans cleaned up.
722
+ */
723
+ sweepOrphans() {
724
+ let swept = 0;
725
+ for (const [uuid, obj] of this._objectByUuid) {
726
+ if (!obj.parent && !this._trackedRoots.has(obj)) {
727
+ this.unregister(obj);
728
+ swept++;
729
+ r3fLog("store", `sweepOrphans: removed orphan "${obj.name || uuid}"`);
730
+ }
731
+ }
732
+ return swept;
733
+ }
734
+ // -------------------------------------------------------------------------
521
735
  // Cleanup
522
736
  // -------------------------------------------------------------------------
523
737
  /** Remove all tracked objects and reset state. */
524
738
  dispose() {
739
+ this._cancelAsyncRegistration();
740
+ for (const obj of this._objectByUuid.values()) {
741
+ if (obj.userData) delete obj.userData.__r3fdom_tracked;
742
+ }
525
743
  this._objectByUuid.clear();
526
744
  this._objectsByTestId.clear();
527
745
  this._objectsByName.clear();
@@ -738,7 +956,11 @@ var ATTRIBUTE_MAP = {
738
956
  "data-scale": (m) => serializeTuple(m.scale),
739
957
  "data-vertex-count": (m) => m.vertexCount !== void 0 ? String(m.vertexCount) : void 0,
740
958
  "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
959
+ "data-instance-count": (m) => m.instanceCount !== void 0 ? String(m.instanceCount) : void 0,
960
+ "data-fov": (m) => m.fov !== void 0 ? String(m.fov) : void 0,
961
+ "data-near": (m) => m.near !== void 0 ? String(m.near) : void 0,
962
+ "data-far": (m) => m.far !== void 0 ? String(m.far) : void 0,
963
+ "data-zoom": (m) => m.zoom !== void 0 ? String(m.zoom) : void 0
742
964
  };
743
965
  var MANAGED_ATTRIBUTES = Object.keys(ATTRIBUTE_MAP);
744
966
  function serializeTuple(tuple) {
@@ -791,6 +1013,12 @@ var DomMirror = class {
791
1013
  this._lruSize = 0;
792
1014
  // UUID → parent UUID mapping for DOM tree structure
793
1015
  this._parentMap = /* @__PURE__ */ new Map();
1016
+ /** When true, mirror elements use pointer-events: auto so DevTools element picker can select them. */
1017
+ this._inspectMode = false;
1018
+ /** Async materialization state for inspect mode */
1019
+ this._asyncQueue = [];
1020
+ this._asyncIdleHandle = null;
1021
+ this._asyncBatchSize = 200;
794
1022
  this._store = store;
795
1023
  this._maxNodes = maxNodes;
796
1024
  }
@@ -804,6 +1032,29 @@ var DomMirror = class {
804
1032
  setRoot(rootElement) {
805
1033
  this._rootElement = rootElement;
806
1034
  }
1035
+ /**
1036
+ * Enable or disable "inspect mode". When turning on, kicks off async
1037
+ * chunked materialization so the full tree becomes browsable in the
1038
+ * Elements tab without blocking the main thread.
1039
+ *
1040
+ * At BIM scale (100k-200k objects) the old synchronous loop would freeze
1041
+ * the page for 2-10s. The new approach uses requestIdleCallback to
1042
+ * spread work across idle frames (~200 nodes per idle slice, ~5ms each).
1043
+ */
1044
+ setInspectMode(on) {
1045
+ if (this._inspectMode === on) return;
1046
+ this._inspectMode = on;
1047
+ if (on) {
1048
+ this._startAsyncMaterialization();
1049
+ } else {
1050
+ this._cancelAsyncMaterialization();
1051
+ }
1052
+ r3fLog("inspect", "setInspectMode", { on, nodeCount: this._nodes.size });
1053
+ }
1054
+ /** Whether inspect mode is currently enabled. */
1055
+ getInspectMode() {
1056
+ return this._inspectMode;
1057
+ }
807
1058
  /**
808
1059
  * Build the initial DOM tree from the scene.
809
1060
  * Materializes the top 2 levels of the scene hierarchy.
@@ -832,7 +1083,7 @@ var DomMirror = class {
832
1083
  }
833
1084
  const tag = getTagForType(meta.type);
834
1085
  const element = document.createElement(tag);
835
- element.style.cssText = "display:block;position:absolute;pointer-events:none;box-sizing:border-box;";
1086
+ element.style.cssText = "display:contents;";
836
1087
  const prevAttrs = /* @__PURE__ */ new Map();
837
1088
  applyAttributes(element, meta, prevAttrs);
838
1089
  const lruNode = { uuid, prev: null, next: null };
@@ -859,15 +1110,39 @@ var DomMirror = class {
859
1110
  /**
860
1111
  * Remove a DOM node but keep JS metadata in the ObjectStore.
861
1112
  * Called by LRU eviction or when an object is removed from the scene.
1113
+ * Also dematerializes any materialized descendants so they don't become
1114
+ * orphaned entries in the LRU / _nodes maps.
862
1115
  */
863
1116
  dematerialize(uuid) {
864
1117
  const node = this._nodes.get(uuid);
865
1118
  if (!node) return;
1119
+ const descendants = this._collectMaterializedDescendants(uuid);
1120
+ for (const descUuid of descendants) {
1121
+ const descNode = this._nodes.get(descUuid);
1122
+ if (descNode) {
1123
+ this._lruRemove(descNode.lruNode);
1124
+ this._nodes.delete(descUuid);
1125
+ this._parentMap.delete(descUuid);
1126
+ }
1127
+ }
866
1128
  node.element.remove();
867
1129
  this._lruRemove(node.lruNode);
868
1130
  this._nodes.delete(uuid);
869
1131
  this._parentMap.delete(uuid);
870
1132
  }
1133
+ /**
1134
+ * Collect all materialized descendants of a uuid by walking _parentMap.
1135
+ */
1136
+ _collectMaterializedDescendants(parentUuid) {
1137
+ const result = [];
1138
+ for (const [childUuid, pUuid] of this._parentMap) {
1139
+ if (pUuid === parentUuid) {
1140
+ result.push(childUuid);
1141
+ result.push(...this._collectMaterializedDescendants(childUuid));
1142
+ }
1143
+ }
1144
+ return result;
1145
+ }
871
1146
  // -------------------------------------------------------------------------
872
1147
  // Structural updates (called by Object3D.add/remove patch)
873
1148
  // -------------------------------------------------------------------------
@@ -943,6 +1218,22 @@ var DomMirror = class {
943
1218
  }
944
1219
  return null;
945
1220
  }
1221
+ /**
1222
+ * Get or lazily materialize a DOM element for an object.
1223
+ * Also materializes the ancestor chain so the element is correctly
1224
+ * nested in the DOM tree. Used by InspectController so that
1225
+ * hover/click always produces a valid mirror element regardless
1226
+ * of whether async materialization has reached it yet.
1227
+ */
1228
+ getOrMaterialize(uuid) {
1229
+ const existing = this._nodes.get(uuid);
1230
+ if (existing) {
1231
+ this._lruTouch(existing.lruNode);
1232
+ return existing.element;
1233
+ }
1234
+ this._materializeAncestorChain(uuid);
1235
+ return this.materialize(uuid);
1236
+ }
946
1237
  /**
947
1238
  * Check if an object has a materialized DOM node.
948
1239
  */
@@ -1005,6 +1296,7 @@ var DomMirror = class {
1005
1296
  * Remove all materialized DOM nodes and reset state.
1006
1297
  */
1007
1298
  dispose() {
1299
+ this._cancelAsyncMaterialization();
1008
1300
  for (const [, node] of this._nodes) {
1009
1301
  node.element.remove();
1010
1302
  }
@@ -1037,6 +1329,78 @@ var DomMirror = class {
1037
1329
  }
1038
1330
  }
1039
1331
  // -------------------------------------------------------------------------
1332
+ // Private: Ancestor chain materialization
1333
+ // -------------------------------------------------------------------------
1334
+ /**
1335
+ * Materialize all ancestors of a uuid from root down, so the target
1336
+ * element will be correctly nested when materialized.
1337
+ */
1338
+ _materializeAncestorChain(uuid) {
1339
+ const chain = [];
1340
+ let currentUuid = uuid;
1341
+ while (currentUuid) {
1342
+ if (this._nodes.has(currentUuid)) break;
1343
+ const meta = this._store.getByUuid(currentUuid);
1344
+ if (!meta) break;
1345
+ chain.push(currentUuid);
1346
+ currentUuid = meta.parentUuid;
1347
+ }
1348
+ for (let i = chain.length - 1; i > 0; i--) {
1349
+ this.materialize(chain[i]);
1350
+ }
1351
+ }
1352
+ // -------------------------------------------------------------------------
1353
+ // Private: Async chunked materialization for inspect mode
1354
+ // -------------------------------------------------------------------------
1355
+ _startAsyncMaterialization() {
1356
+ this._cancelAsyncMaterialization();
1357
+ const flatList = this._store.getFlatList();
1358
+ this._asyncQueue = [];
1359
+ for (const obj of flatList) {
1360
+ if (obj.userData?.__r3fdom_internal) continue;
1361
+ if (this._nodes.has(obj.uuid)) continue;
1362
+ this._asyncQueue.push(obj.uuid);
1363
+ }
1364
+ if (this._asyncQueue.length === 0) return;
1365
+ r3fLog("inspect", `Async materialization started: ${this._asyncQueue.length} objects queued`);
1366
+ this._scheduleAsyncChunk();
1367
+ }
1368
+ _cancelAsyncMaterialization() {
1369
+ if (this._asyncIdleHandle !== null) {
1370
+ if (typeof cancelIdleCallback === "function") {
1371
+ cancelIdleCallback(this._asyncIdleHandle);
1372
+ } else {
1373
+ clearTimeout(this._asyncIdleHandle);
1374
+ }
1375
+ this._asyncIdleHandle = null;
1376
+ }
1377
+ this._asyncQueue = [];
1378
+ }
1379
+ _scheduleAsyncChunk() {
1380
+ if (this._asyncQueue.length === 0) {
1381
+ this._asyncIdleHandle = null;
1382
+ r3fLog("inspect", `Async materialization complete: ${this._nodes.size} nodes materialized`);
1383
+ return;
1384
+ }
1385
+ const callback = (deadline) => {
1386
+ const hasTimeRemaining = deadline ? () => deadline.timeRemaining() > 2 : () => true;
1387
+ let processed = 0;
1388
+ while (this._asyncQueue.length > 0 && processed < this._asyncBatchSize && hasTimeRemaining()) {
1389
+ const uuid = this._asyncQueue.shift();
1390
+ if (!this._nodes.has(uuid)) {
1391
+ this.materialize(uuid);
1392
+ }
1393
+ processed++;
1394
+ }
1395
+ this._scheduleAsyncChunk();
1396
+ };
1397
+ if (typeof requestIdleCallback === "function") {
1398
+ this._asyncIdleHandle = requestIdleCallback(callback, { timeout: 100 });
1399
+ } else {
1400
+ this._asyncIdleHandle = setTimeout(callback, 16);
1401
+ }
1402
+ }
1403
+ // -------------------------------------------------------------------------
1040
1404
  // Private: Selector → UUID resolution
1041
1405
  // -------------------------------------------------------------------------
1042
1406
  /**
@@ -2290,677 +2654,324 @@ var SelectionManager = class {
2290
2654
  this._listeners = [];
2291
2655
  }
2292
2656
  };
2293
-
2294
- // src/bridge/ThreeDom.tsx
2295
- var _store3 = null;
2296
- var _mirror = null;
2297
- var _selectionManager = null;
2298
- var _highlighter = null;
2299
- function getStore2() {
2300
- return _store3;
2301
- }
2302
- function getMirror() {
2303
- return _mirror;
2304
- }
2305
- function getSelectionManager() {
2306
- return _selectionManager;
2307
- }
2308
- function getHighlighter() {
2309
- return _highlighter;
2310
- }
2311
- var _box = /* @__PURE__ */ new three.Box3();
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
2657
+ var HOVER_FILL_COLOR = 7317724;
2658
+ var HOVER_FILL_OPACITY = 0.66;
2659
+ var SELECTION_FILL_COLOR = 7317724;
2660
+ var SELECTION_FILL_OPACITY = 0.75;
2661
+ var SELECTION_BBOX_COLOR = 7317724;
2662
+ var SELECTION_BBOX_OPACITY = 0.3;
2663
+ var _box33 = /* @__PURE__ */ new three.Box3();
2664
+ function hasRenderableGeometry(obj) {
2665
+ return obj.isMesh === true || obj.isLine === true || obj.isPoints === true;
2666
+ }
2667
+ function collectHighlightTargets(obj) {
2668
+ if (hasRenderableGeometry(obj)) return [obj];
2669
+ const targets = [];
2670
+ obj.traverse((child) => {
2671
+ if (child === obj) return;
2672
+ if (child.userData?.__r3fdom_internal) return;
2673
+ if (hasRenderableGeometry(child)) targets.push(child);
2674
+ });
2675
+ return targets;
2676
+ }
2677
+ function markInternal(obj) {
2678
+ obj.userData.__r3fdom_internal = true;
2679
+ obj.raycast = () => {
2680
+ };
2681
+ obj.traverse((child) => {
2682
+ child.userData.__r3fdom_internal = true;
2683
+ child.raycast = () => {
2684
+ };
2685
+ });
2686
+ }
2687
+ function _syncGroupTransform(source, highlightRoot) {
2688
+ source.updateWorldMatrix(true, false);
2689
+ source.matrixWorld.decompose(
2690
+ highlightRoot.position,
2691
+ highlightRoot.quaternion,
2692
+ highlightRoot.scale
2693
+ );
2694
+ }
2695
+ function _attachRenderSync(source, highlightRoot) {
2696
+ highlightRoot.matrixAutoUpdate = false;
2697
+ highlightRoot.updateMatrixWorld = (force) => {
2698
+ source.updateWorldMatrix(true, false);
2699
+ highlightRoot.matrixWorld.copy(source.matrixWorld);
2700
+ for (const child of highlightRoot.children) {
2701
+ child.updateMatrixWorld(force);
2702
+ }
2415
2703
  };
2416
- window.__R3F_DOM__ = api;
2417
2704
  }
2418
- function removeGlobalAPI(onlyIfEquals) {
2419
- r3fLog("bridge", "removeGlobalAPI called (deferred)");
2420
- if (onlyIfEquals !== void 0) {
2421
- const ref = onlyIfEquals;
2422
- queueMicrotask(() => {
2423
- if (window.__R3F_DOM__ === ref) {
2424
- delete window.__R3F_DOM__;
2425
- r3fLog("bridge", "Global API removed");
2426
- } else {
2427
- r3fLog("bridge", "Global API not removed \u2014 replaced by new instance (Strict Mode remount)");
2705
+ function createHighlightMesh(source, fillColor, fillOpacity) {
2706
+ const geom = source.geometry;
2707
+ if (!geom) return null;
2708
+ const group = new three.Object3D();
2709
+ const disposables = [];
2710
+ const fillMat = new three.MeshBasicMaterial({
2711
+ color: fillColor,
2712
+ transparent: true,
2713
+ opacity: fillOpacity,
2714
+ depthTest: false,
2715
+ depthWrite: false,
2716
+ side: three.DoubleSide
2717
+ });
2718
+ disposables.push(fillMat);
2719
+ const fillMesh = new three.Mesh(geom, fillMat);
2720
+ fillMesh.scale.setScalar(1.005);
2721
+ fillMesh.raycast = () => {
2722
+ };
2723
+ group.add(fillMesh);
2724
+ source.updateWorldMatrix(true, false);
2725
+ source.matrixWorld.decompose(group.position, group.quaternion, group.scale);
2726
+ markInternal(group);
2727
+ _attachRenderSync(source, group);
2728
+ return { root: group, disposables };
2729
+ }
2730
+ function createBoundingBoxHighlight(obj) {
2731
+ _box33.makeEmpty();
2732
+ const targets = collectHighlightTargets(obj);
2733
+ if (targets.length === 0) return null;
2734
+ for (const target of targets) {
2735
+ target.updateWorldMatrix(true, false);
2736
+ const geom = target.geometry;
2737
+ if (geom) {
2738
+ if (!geom.boundingBox) geom.computeBoundingBox();
2739
+ if (geom.boundingBox) {
2740
+ const childBox = geom.boundingBox.clone();
2741
+ childBox.applyMatrix4(target.matrixWorld);
2742
+ _box33.union(childBox);
2428
2743
  }
2429
- });
2430
- } else {
2431
- delete window.__R3F_DOM__;
2432
- r3fLog("bridge", "Global API removed (immediate)");
2744
+ }
2433
2745
  }
2746
+ if (_box33.isEmpty()) return null;
2747
+ const size = _box33.getSize(new three.Vector3());
2748
+ const center = _box33.getCenter(new three.Vector3());
2749
+ const disposables = [];
2750
+ const boxGeom = new three.BoxGeometry(size.x, size.y, size.z);
2751
+ disposables.push(boxGeom);
2752
+ const fillMat = new three.MeshBasicMaterial({
2753
+ color: SELECTION_BBOX_COLOR,
2754
+ transparent: true,
2755
+ opacity: SELECTION_BBOX_OPACITY,
2756
+ depthTest: false,
2757
+ depthWrite: false,
2758
+ side: three.DoubleSide
2759
+ });
2760
+ disposables.push(fillMat);
2761
+ const fillMesh = new three.Mesh(boxGeom, fillMat);
2762
+ fillMesh.raycast = () => {
2763
+ };
2764
+ const edgesGeom = new three.EdgesGeometry(boxGeom);
2765
+ disposables.push(edgesGeom);
2766
+ const edgeMat = new three.LineBasicMaterial({
2767
+ color: SELECTION_BBOX_COLOR,
2768
+ transparent: true,
2769
+ opacity: 0.5,
2770
+ depthTest: false,
2771
+ depthWrite: false
2772
+ });
2773
+ disposables.push(edgeMat);
2774
+ const edgeMesh = new three.LineSegments(edgesGeom, edgeMat);
2775
+ edgeMesh.raycast = () => {
2776
+ };
2777
+ const group = new three.Object3D();
2778
+ group.add(fillMesh);
2779
+ group.add(edgeMesh);
2780
+ group.position.copy(center);
2781
+ group.renderOrder = 998;
2782
+ markInternal(group);
2783
+ group.matrixAutoUpdate = false;
2784
+ group.updateMatrixWorld = (force) => {
2785
+ _box33.makeEmpty();
2786
+ for (const target of targets) {
2787
+ target.updateWorldMatrix(true, false);
2788
+ const g = target.geometry;
2789
+ if (g) {
2790
+ if (!g.boundingBox) g.computeBoundingBox();
2791
+ if (g.boundingBox) {
2792
+ const childBox = g.boundingBox.clone();
2793
+ childBox.applyMatrix4(target.matrixWorld);
2794
+ _box33.union(childBox);
2795
+ }
2796
+ }
2797
+ }
2798
+ if (!_box33.isEmpty()) {
2799
+ const s = _box33.getSize(new three.Vector3());
2800
+ const c = _box33.getCenter(new three.Vector3());
2801
+ group.position.copy(c);
2802
+ group.scale.set(
2803
+ s.x / size.x || 1,
2804
+ s.y / size.y || 1,
2805
+ s.z / size.z || 1
2806
+ );
2807
+ }
2808
+ group.updateMatrix();
2809
+ group.matrixWorld.copy(group.matrix);
2810
+ for (const child of group.children) {
2811
+ child.updateMatrixWorld(force);
2812
+ }
2813
+ };
2814
+ return { root: group, disposables };
2434
2815
  }
2435
- function setElementRect(el, l, t, w, h) {
2436
- const d = el.dataset;
2437
- if (d._l !== String(l) || d._t !== String(t) || d._w !== String(w) || d._h !== String(h)) {
2438
- el.style.left = `${l}px`;
2439
- el.style.top = `${t}px`;
2440
- el.style.width = `${w}px`;
2441
- el.style.height = `${h}px`;
2442
- d._l = String(l);
2443
- d._t = String(t);
2444
- d._w = String(w);
2445
- d._h = String(h);
2816
+ var Highlighter = class {
2817
+ constructor(_options = {}) {
2818
+ this._scene = null;
2819
+ this._unsubscribe = null;
2820
+ this._hoverEntries = [];
2821
+ this._hoverTarget = null;
2822
+ this._selectedEntries = /* @__PURE__ */ new Map();
2823
+ this._hoverPollId = null;
2824
+ this._lastHoveredUuid = null;
2825
+ this._store = null;
2446
2826
  }
2447
- }
2448
- function ThreeDom({
2449
- root = "#three-dom-root",
2450
- batchSize = 500,
2451
- timeBudgetMs = 0.5,
2452
- maxDomNodes = 2e3,
2453
- initialDepth = 3,
2454
- enabled = true,
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;
2475
- }
2476
- if (!rootElement) {
2477
- rootElement = document.createElement("div");
2478
- rootElement.id = typeof root === "string" ? root.replace(/^#/, "") : "three-dom-root";
2479
- createdRoot = true;
2480
- }
2481
- canvasParent.style.position = canvasParent.style.position || "relative";
2482
- canvasParent.appendChild(rootElement);
2483
- rootElement.style.cssText = [
2484
- "position: absolute",
2485
- "top: 0",
2486
- "left: 0",
2487
- "width: 100%",
2488
- "height: 100%",
2489
- "pointer-events: none",
2490
- "overflow: hidden",
2491
- "z-index: 10"
2492
- ].join(";");
2493
- let store = null;
2494
- let mirror = null;
2495
- let unpatch = null;
2496
- let selectionManager = null;
2497
- let currentApi;
2498
- try {
2499
- store = new ObjectStore();
2500
- mirror = new DomMirror(store, maxDomNodes);
2501
- mirror.setRoot(rootElement);
2502
- r3fLog("setup", "Store and mirror created");
2503
- ensureCustomElements(store);
2504
- store.registerTree(scene);
2505
- r3fLog("setup", `Registered scene tree: ${store.getCount()} objects`);
2506
- mirror.materializeSubtree(scene.uuid, initialDepth);
2507
- unpatch = patchObject3D(store, mirror);
2508
- setInteractionState(store, camera, gl, size);
2509
- r3fLog("setup", "Object3D patched, interaction state set");
2510
- selectionManager = new SelectionManager();
2511
- _selectionManager = selectionManager;
2512
- _highlighter = null;
2513
- exposeGlobalAPI(store);
2514
- r3fLog("bridge", "exposeGlobalAPI called \u2014 bridge is live, _ready=true");
2515
- currentApi = window.__R3F_DOM__;
2516
- _store3 = store;
2517
- _mirror = mirror;
2518
- const initialCanvasRect = canvas.getBoundingClientRect();
2519
- const allObjects = store.getFlatList();
2520
- for (const obj of allObjects) {
2521
- if (obj.userData?.__r3fdom_internal) continue;
2522
- const el = mirror.getElement(obj.uuid);
2523
- if (!el) continue;
2524
- if (obj.type === "Scene") {
2525
- setElementRect(el, 0, 0, Math.round(initialCanvasRect.width), Math.round(initialCanvasRect.height));
2526
- continue;
2527
- }
2528
- const rect = projectToScreenRect(obj, camera, initialCanvasRect);
2529
- if (rect) {
2530
- let parentLeft = 0;
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
- );
2546
- }
2547
- }
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
- }
2594
- return () => {
2595
- r3fLog("setup", "ThreeDom cleanup started");
2596
- if (unpatch) unpatch();
2597
- removeGlobalAPI(currentApi);
2598
- clearInteractionState();
2599
- if (selectionManager) selectionManager.dispose();
2600
- if (mirror) mirror.dispose();
2601
- if (store) store.dispose();
2602
- if (createdRoot && rootElement?.parentNode) {
2603
- rootElement.parentNode.removeChild(rootElement);
2604
- }
2605
- _store3 = null;
2606
- _mirror = null;
2607
- _selectionManager = null;
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);
2678
- }
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
- };
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";
2809
- }
2810
- var Highlighter = class {
2811
- constructor(options = {}) {
2812
- /** Selected object overlays (persistent until deselected) */
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;
2818
- this._unsubscribe = null;
2819
- /** DevTools hover polling interval */
2820
- this._hoverPollId = null;
2821
- this._lastHoveredElement = null;
2822
- /** Store reference for resolving objects */
2823
- this._store = null;
2824
- this._showTooltip = options.showTooltip ?? true;
2825
- }
2826
- // -----------------------------------------------------------------------
2827
- // Lifecycle
2828
- // -----------------------------------------------------------------------
2829
- attach(_scene, selectionManager, camera, renderer, store) {
2830
- this.detach();
2831
- this._camera = camera;
2832
- this._renderer = renderer;
2833
- this._store = store;
2834
- this._unsubscribe = selectionManager.subscribe((selected) => {
2835
- this._syncSelectedHighlights(selected);
2836
- });
2837
- this._syncSelectedHighlights([...selectionManager.getSelected()]);
2838
- this._startHoverPolling();
2839
- }
2840
- detach() {
2841
- if (this._unsubscribe) {
2842
- this._unsubscribe();
2843
- this._unsubscribe = null;
2827
+ // -----------------------------------------------------------------------
2828
+ // Lifecycle
2829
+ // -----------------------------------------------------------------------
2830
+ /** Bind to a scene and selection manager, start hover polling. */
2831
+ attach(scene, selectionManager, _camera2, _renderer, store) {
2832
+ this.detach();
2833
+ this._scene = scene;
2834
+ this._store = store;
2835
+ this._unsubscribe = selectionManager.subscribe((selected) => {
2836
+ this._syncSelectionHighlights(selected);
2837
+ });
2838
+ this._syncSelectionHighlights([...selectionManager.getSelected()]);
2839
+ this._startHoverPolling();
2840
+ }
2841
+ /** Unbind from the scene, stop polling, and remove all highlights. */
2842
+ detach() {
2843
+ if (this._unsubscribe) {
2844
+ this._unsubscribe();
2845
+ this._unsubscribe = null;
2844
2846
  }
2845
2847
  this._stopHoverPolling();
2846
- this._clearAllOverlays(this._selectedEntries);
2847
- this._clearAllOverlays(this._hoverEntries);
2848
- this._camera = null;
2849
- this._renderer = null;
2848
+ this.clearHoverHighlight();
2849
+ this._clearAllSelectionHighlights();
2850
+ this._scene = null;
2850
2851
  this._store = null;
2851
2852
  }
2852
2853
  // -----------------------------------------------------------------------
2853
- // Per-frame update — reposition all overlays to follow camera/objects
2854
+ // Per-frame update
2854
2855
  // -----------------------------------------------------------------------
2856
+ /** Sync highlight group transforms to their source objects. Call each frame. */
2855
2857
  update() {
2856
- if (!this._camera || !this._renderer) return;
2857
- const canvas = this._renderer.domElement;
2858
- for (const entry of this._selectedEntries.values()) {
2859
- const rect = projectBoundsToScreen(entry.target, this._camera, canvas);
2860
- if (rect) {
2861
- positionOverlay(entry, rect);
2862
- } else {
2863
- hideOverlay(entry);
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);
2858
+ for (const entry of this._hoverEntries) {
2859
+ _syncGroupTransform(entry.source, entry.group.root);
2860
+ }
2861
+ for (const selEntry of this._selectedEntries.values()) {
2862
+ for (const mg of selEntry.meshGroups) {
2863
+ const src = mg.root.userData.__r3fdom_source;
2864
+ if (src) {
2865
+ _syncGroupTransform(src, mg.root);
2866
+ }
2872
2867
  }
2873
2868
  }
2874
2869
  }
2875
2870
  // -----------------------------------------------------------------------
2876
- // Public API
2871
+ // Public API: hover highlight
2877
2872
  // -----------------------------------------------------------------------
2878
- highlight(obj) {
2879
- this._addSelectedHighlight(obj, false);
2873
+ /** Show a hover highlight on the given object (replaces any previous hover). */
2874
+ showHoverHighlight(obj) {
2875
+ if (obj === this._hoverTarget) return;
2876
+ this._clearHoverVisuals();
2877
+ if (!this._scene) return;
2878
+ this._hoverTarget = obj;
2879
+ const targets = collectHighlightTargets(obj);
2880
+ for (const target of targets) {
2881
+ const hg = createHighlightMesh(target, HOVER_FILL_COLOR, HOVER_FILL_OPACITY);
2882
+ if (hg) {
2883
+ hg.root.renderOrder = 997;
2884
+ this._scene.add(hg.root);
2885
+ this._hoverEntries.push({ source: target, group: hg });
2886
+ }
2887
+ }
2880
2888
  }
2881
- unhighlight(obj) {
2882
- this._removeOverlay(obj, this._selectedEntries);
2889
+ /** Remove the current hover highlight. */
2890
+ clearHoverHighlight() {
2891
+ this._clearHoverVisuals();
2892
+ this._lastHoveredUuid = null;
2883
2893
  }
2884
- clearAll() {
2885
- this._clearAllOverlays(this._selectedEntries);
2886
- this._clearAllOverlays(this._hoverEntries);
2894
+ _clearHoverVisuals() {
2895
+ for (const entry of this._hoverEntries) {
2896
+ this._disposeHighlightGroup(entry.group);
2897
+ }
2898
+ this._hoverEntries = [];
2899
+ this._hoverTarget = null;
2887
2900
  }
2901
+ // -----------------------------------------------------------------------
2902
+ // Public API: queries
2903
+ // -----------------------------------------------------------------------
2904
+ /** Check if an object currently has a selection highlight. */
2888
2905
  isHighlighted(obj) {
2889
2906
  return this._selectedEntries.has(obj);
2890
2907
  }
2891
- /** Show a temporary hover highlight for an object and its children */
2892
- showHoverHighlight(obj) {
2893
- this._clearAllOverlays(this._hoverEntries);
2894
- this._addHoverHighlightRecursive(obj);
2895
- }
2896
- /** Clear the hover highlight */
2897
- clearHoverHighlight() {
2898
- this._clearAllOverlays(this._hoverEntries);
2899
- this._lastHoveredElement = null;
2908
+ /** Remove all hover and selection highlights. */
2909
+ clearAll() {
2910
+ this.clearHoverHighlight();
2911
+ this._clearAllSelectionHighlights();
2900
2912
  }
2901
2913
  // -----------------------------------------------------------------------
2902
2914
  // Internal: selection highlights
2903
2915
  // -----------------------------------------------------------------------
2904
- _syncSelectedHighlights(selected) {
2905
- const targetSet = /* @__PURE__ */ new Set();
2906
- const primarySet = new Set(selected);
2907
- for (const obj of selected) {
2908
- targetSet.add(obj);
2909
- obj.traverse((child) => {
2910
- targetSet.add(child);
2911
- });
2912
- }
2916
+ _syncSelectionHighlights(selected) {
2917
+ if (!this._scene) return;
2918
+ const selectedSet = new Set(selected);
2913
2919
  for (const [obj] of this._selectedEntries) {
2914
- if (!targetSet.has(obj)) {
2915
- this._removeOverlay(obj, this._selectedEntries);
2920
+ if (!selectedSet.has(obj)) {
2921
+ this._removeSelectionHighlight(obj);
2916
2922
  }
2917
2923
  }
2918
- for (const obj of targetSet) {
2919
- if (obj.userData?.__r3fdom_internal) continue;
2924
+ for (const obj of selected) {
2920
2925
  if (!this._selectedEntries.has(obj)) {
2921
- const isChild = !primarySet.has(obj);
2922
- this._addSelectedHighlight(obj, isChild);
2926
+ this._addSelectionHighlight(obj);
2923
2927
  }
2924
2928
  }
2925
2929
  }
2926
- _addSelectedHighlight(obj, isChild) {
2927
- if (this._selectedEntries.has(obj)) return;
2928
- const color = isChild ? COLORS.contentChild : COLORS.content;
2929
- const overlayEl = createOverlayElement(color, !isChild);
2930
- const tooltipEl = createTooltipElement();
2931
- if (isChild || !this._showTooltip) {
2932
- tooltipEl.style.display = "none";
2930
+ _addSelectionHighlight(obj) {
2931
+ if (!this._scene) return;
2932
+ const targets = collectHighlightTargets(obj);
2933
+ const meshGroups = [];
2934
+ for (const target of targets) {
2935
+ const hg = createHighlightMesh(target, SELECTION_FILL_COLOR, SELECTION_FILL_OPACITY);
2936
+ if (hg) {
2937
+ hg.root.userData.__r3fdom_source = target;
2938
+ hg.root.renderOrder = 999;
2939
+ this._scene.add(hg.root);
2940
+ meshGroups.push(hg);
2941
+ }
2942
+ }
2943
+ let bboxGroup = null;
2944
+ if (targets.length > 1 && obj.type !== "Group") {
2945
+ bboxGroup = createBoundingBoxHighlight(obj);
2946
+ if (bboxGroup) {
2947
+ this._scene.add(bboxGroup.root);
2948
+ }
2949
+ }
2950
+ if (meshGroups.length > 0 || bboxGroup) {
2951
+ this._selectedEntries.set(obj, { source: obj, meshGroups, bboxGroup });
2933
2952
  }
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
2953
  }
2942
- // -----------------------------------------------------------------------
2943
- // Internal: hover highlights
2944
- // -----------------------------------------------------------------------
2945
- _addHoverHighlightRecursive(obj) {
2946
- if (obj.userData?.__r3fdom_internal) return;
2947
- const overlayEl = createOverlayElement(COLORS.hover, false);
2948
- const tooltipEl = createTooltipElement();
2949
- if (this._hoverEntries.size === 0 && this._showTooltip) {
2950
- const label = getObjectLabel2(obj);
2951
- const dims = getObjectDimensions(obj);
2952
- tooltipEl.innerHTML = `<span style="color:${COLORS.tooltipTag}">${label}</span>` + (dims ? ` <span style="color:${COLORS.tooltipDim}">${dims}</span>` : "");
2953
- } else {
2954
- tooltipEl.style.display = "none";
2955
- }
2956
- document.body.appendChild(overlayEl);
2957
- document.body.appendChild(tooltipEl);
2958
- this._hoverEntries.set(obj, { overlayEl, tooltipEl, target: obj, isChild: false });
2959
- for (const child of obj.children) {
2960
- if (!child.userData?.__r3fdom_internal) {
2961
- this._addHoverHighlightRecursive(child);
2954
+ _removeSelectionHighlight(obj) {
2955
+ const entry = this._selectedEntries.get(obj);
2956
+ if (!entry) return;
2957
+ for (const mg of entry.meshGroups) {
2958
+ this._disposeHighlightGroup(mg);
2959
+ }
2960
+ if (entry.bboxGroup) {
2961
+ this._disposeHighlightGroup(entry.bboxGroup);
2962
+ }
2963
+ this._selectedEntries.delete(obj);
2964
+ }
2965
+ _clearAllSelectionHighlights() {
2966
+ for (const entry of this._selectedEntries.values()) {
2967
+ for (const mg of entry.meshGroups) {
2968
+ this._disposeHighlightGroup(mg);
2969
+ }
2970
+ if (entry.bboxGroup) {
2971
+ this._disposeHighlightGroup(entry.bboxGroup);
2962
2972
  }
2963
2973
  }
2974
+ this._selectedEntries.clear();
2964
2975
  }
2965
2976
  // -----------------------------------------------------------------------
2966
2977
  // Internal: DevTools hover polling
@@ -2979,56 +2990,878 @@ var Highlighter = class {
2979
2990
  _pollDevToolsHover() {
2980
2991
  if (!this._store) return;
2981
2992
  try {
2982
- const hoveredEl = globalThis.__r3fdom_hovered__;
2983
- if (hoveredEl === this._lastHoveredElement) return;
2984
- this._lastHoveredElement = hoveredEl ?? null;
2985
- if (!hoveredEl) {
2986
- this._clearAllOverlays(this._hoverEntries);
2987
- return;
2988
- }
2989
- const uuid = hoveredEl.getAttribute?.("data-uuid");
2993
+ const hoveredEl = window.__r3fdom_hovered__;
2994
+ const uuid = hoveredEl?.getAttribute?.("data-uuid") ?? null;
2995
+ if (uuid === this._lastHoveredUuid) return;
2996
+ this._lastHoveredUuid = uuid;
2990
2997
  if (!uuid) {
2991
- this._clearAllOverlays(this._hoverEntries);
2998
+ this._clearHoverVisuals();
2992
2999
  return;
2993
3000
  }
2994
3001
  const obj = this._store.getObject3D(uuid);
2995
3002
  if (obj) {
2996
3003
  this.showHoverHighlight(obj);
2997
3004
  } else {
2998
- this._clearAllOverlays(this._hoverEntries);
3005
+ this._clearHoverVisuals();
2999
3006
  }
3000
3007
  } catch {
3001
3008
  }
3002
3009
  }
3003
3010
  // -----------------------------------------------------------------------
3004
- // Internal: overlay cleanup
3011
+ // Internal: cleanup
3005
3012
  // -----------------------------------------------------------------------
3006
- _removeOverlay(obj, map) {
3007
- const entry = map.get(obj);
3008
- if (!entry) return;
3009
- entry.overlayEl.remove();
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();
3013
+ _disposeHighlightGroup(hg) {
3014
+ hg.root.removeFromParent();
3015
+ for (const d of hg.disposables) {
3016
+ d.dispose();
3017
3017
  }
3018
- map.clear();
3018
+ }
3019
+ /** Detach and release all resources. */
3020
+ dispose() {
3021
+ this.detach();
3022
+ }
3023
+ };
3024
+
3025
+ // src/highlight/selectionDisplayTarget.ts
3026
+ function isFirstMeshInGroup(obj) {
3027
+ const parent = obj.parent;
3028
+ if (!parent || parent.type !== "Group") return false;
3029
+ const firstMesh = parent.children.find((c) => getTagForType(c.type) === "three-mesh");
3030
+ return firstMesh === obj;
3031
+ }
3032
+ function resolveSelectionDisplayTarget(getObject3D, uuid) {
3033
+ const obj = getObject3D(uuid);
3034
+ if (!obj) return null;
3035
+ if (getTagForType(obj.type) !== "three-mesh") return uuid;
3036
+ if (isFirstMeshInGroup(obj) && obj.parent) return obj.parent.uuid;
3037
+ return uuid;
3038
+ }
3039
+
3040
+ // src/highlight/InspectController.ts
3041
+ var RAYCAST_THROTTLE_MS = 50;
3042
+ var HOVER_REVEAL_DEBOUNCE_MS = 300;
3043
+ var InspectController = class {
3044
+ constructor(opts) {
3045
+ this._active = false;
3046
+ this._lastRaycastTime = 0;
3047
+ this._hoveredObject = null;
3048
+ this._hoverRevealTimer = null;
3049
+ this._overlay = null;
3050
+ this._boundPointerMove = null;
3051
+ this._boundPointerDown = null;
3052
+ this._boundContextMenu = null;
3053
+ this._camera = opts.camera;
3054
+ this._renderer = opts.renderer;
3055
+ this._selectionManager = opts.selectionManager;
3056
+ this._highlighter = opts.highlighter;
3057
+ this._raycastAccelerator = opts.raycastAccelerator;
3058
+ this._mirror = opts.mirror;
3059
+ this._store = opts.store;
3060
+ }
3061
+ get active() {
3062
+ return this._active;
3063
+ }
3064
+ /** Update the camera reference (e.g. after a camera switch). */
3065
+ updateCamera(camera) {
3066
+ this._camera = camera;
3019
3067
  }
3020
3068
  // -----------------------------------------------------------------------
3021
- // Cleanup
3069
+ // Enable / disable
3022
3070
  // -----------------------------------------------------------------------
3071
+ /** Activate inspect mode — creates overlay on top of canvas. */
3072
+ enable() {
3073
+ if (this._active) return;
3074
+ this._active = true;
3075
+ const canvas = this._renderer.domElement;
3076
+ const parent = canvas.parentElement;
3077
+ if (!parent) return;
3078
+ const overlay = document.createElement("div");
3079
+ overlay.dataset.r3fdomInspect = "true";
3080
+ overlay.style.cssText = [
3081
+ "position:absolute",
3082
+ "inset:0",
3083
+ "z-index:999999",
3084
+ "cursor:crosshair",
3085
+ "background:transparent"
3086
+ ].join(";");
3087
+ const parentPos = getComputedStyle(parent).position;
3088
+ if (parentPos === "static") {
3089
+ parent.style.position = "relative";
3090
+ }
3091
+ this._boundPointerMove = this._onPointerMove.bind(this);
3092
+ this._boundPointerDown = this._onPointerDown.bind(this);
3093
+ this._boundContextMenu = (e) => e.preventDefault();
3094
+ overlay.addEventListener("pointermove", this._boundPointerMove);
3095
+ overlay.addEventListener("pointerdown", this._boundPointerDown);
3096
+ overlay.addEventListener("contextmenu", this._boundContextMenu);
3097
+ parent.appendChild(overlay);
3098
+ this._overlay = overlay;
3099
+ r3fLog("inspect", "Inspect mode enabled \u2014 hover to highlight, click to select");
3100
+ }
3101
+ /** Deactivate inspect mode — removes overlay and clears hover state. */
3102
+ disable() {
3103
+ if (!this._active) return;
3104
+ this._active = false;
3105
+ if (this._overlay) {
3106
+ if (this._boundPointerMove) this._overlay.removeEventListener("pointermove", this._boundPointerMove);
3107
+ if (this._boundPointerDown) this._overlay.removeEventListener("pointerdown", this._boundPointerDown);
3108
+ if (this._boundContextMenu) this._overlay.removeEventListener("contextmenu", this._boundContextMenu);
3109
+ this._overlay.remove();
3110
+ this._overlay = null;
3111
+ }
3112
+ this._boundPointerMove = null;
3113
+ this._boundPointerDown = null;
3114
+ this._boundContextMenu = null;
3115
+ this._hoveredObject = null;
3116
+ this._cancelHoverReveal();
3117
+ this._highlighter.clearHoverHighlight();
3118
+ window.__r3fdom_selected_element__ = null;
3119
+ r3fLog("inspect", "InspectController disabled");
3120
+ }
3121
+ /** Disable and release all resources. */
3023
3122
  dispose() {
3024
- this.detach();
3123
+ this.disable();
3124
+ }
3125
+ // -----------------------------------------------------------------------
3126
+ // Raycasting (delegated to RaycastAccelerator)
3127
+ // -----------------------------------------------------------------------
3128
+ _raycastFromEvent(e) {
3129
+ return this._raycastAccelerator.raycastAtMouse(
3130
+ e,
3131
+ this._camera,
3132
+ this._renderer.domElement
3133
+ );
3134
+ }
3135
+ /**
3136
+ * Resolve a raw raycast hit to the logical selection target.
3137
+ * Walks up to find the best Group parent if applicable.
3138
+ */
3139
+ _resolveTarget(hit) {
3140
+ const displayUuid = resolveSelectionDisplayTarget(
3141
+ (id) => this._store.getObject3D(id),
3142
+ hit.uuid
3143
+ );
3144
+ if (!displayUuid) return hit;
3145
+ return this._store.getObject3D(displayUuid) ?? hit;
3146
+ }
3147
+ // -----------------------------------------------------------------------
3148
+ // Event handlers
3149
+ // -----------------------------------------------------------------------
3150
+ _onPointerMove(e) {
3151
+ e.stopPropagation();
3152
+ e.preventDefault();
3153
+ const now = performance.now();
3154
+ if (now - this._lastRaycastTime < RAYCAST_THROTTLE_MS) return;
3155
+ this._lastRaycastTime = now;
3156
+ const hit = this._raycastFromEvent(e);
3157
+ if (!hit) {
3158
+ if (this._hoveredObject) {
3159
+ this._hoveredObject = null;
3160
+ this._highlighter.clearHoverHighlight();
3161
+ this._cancelHoverReveal();
3162
+ }
3163
+ return;
3164
+ }
3165
+ if (hit === this._hoveredObject) return;
3166
+ this._hoveredObject = hit;
3167
+ this._highlighter.showHoverHighlight(hit);
3168
+ this._scheduleHoverReveal(hit);
3169
+ }
3170
+ /**
3171
+ * After hovering an object for HOVER_REVEAL_DEBOUNCE_MS, auto-reveal its
3172
+ * mirror element in the Elements tab.
3173
+ */
3174
+ _scheduleHoverReveal(target) {
3175
+ this._cancelHoverReveal();
3176
+ this._hoverRevealTimer = setTimeout(() => {
3177
+ const mirrorEl = this._mirror.getOrMaterialize(target.uuid);
3178
+ if (mirrorEl) {
3179
+ window.__r3fdom_selected_element__ = mirrorEl;
3180
+ }
3181
+ }, HOVER_REVEAL_DEBOUNCE_MS);
3182
+ }
3183
+ _cancelHoverReveal() {
3184
+ if (this._hoverRevealTimer) {
3185
+ clearTimeout(this._hoverRevealTimer);
3186
+ this._hoverRevealTimer = null;
3187
+ }
3188
+ }
3189
+ _onPointerDown(e) {
3190
+ e.stopPropagation();
3191
+ e.preventDefault();
3192
+ const hit = this._raycastFromEvent(e);
3193
+ if (!hit) {
3194
+ this._selectionManager.clearSelection();
3195
+ return;
3196
+ }
3197
+ const target = this._resolveTarget(hit);
3198
+ if (!target) return;
3199
+ this._selectionManager.select(target);
3200
+ const mirrorEl = this._mirror.getOrMaterialize(target.uuid);
3201
+ if (mirrorEl) {
3202
+ window.__r3fdom_selected_element__ = mirrorEl;
3203
+ }
3204
+ r3fLog("inspect", "Object selected via canvas click", {
3205
+ uuid: target.uuid.slice(0, 8),
3206
+ name: target.name || "(unnamed)",
3207
+ type: target.type
3208
+ });
3025
3209
  }
3026
3210
  };
3211
+ var _bvhPatched = false;
3212
+ function ensureBVHPatched() {
3213
+ if (_bvhPatched) return;
3214
+ _bvhPatched = true;
3215
+ three.BufferGeometry.prototype.computeBoundsTree = threeMeshBvh.computeBoundsTree;
3216
+ three.BufferGeometry.prototype.disposeBoundsTree = threeMeshBvh.disposeBoundsTree;
3217
+ three.Mesh.prototype.raycast = threeMeshBvh.acceleratedRaycast;
3218
+ r3fLog("raycast", "three-mesh-bvh patched into Three.js");
3219
+ }
3220
+ var _raycaster2 = /* @__PURE__ */ new three.Raycaster();
3221
+ var _mouse = /* @__PURE__ */ new three.Vector2();
3222
+ function isRaycastable(obj) {
3223
+ if (obj.userData?.__r3fdom_internal) return false;
3224
+ if (!obj.visible) return false;
3225
+ const isMeshLike = obj.isMesh === true || obj.isLine === true || obj.isPoints === true;
3226
+ if (!isMeshLike) return false;
3227
+ const geom = obj.geometry;
3228
+ if (geom) {
3229
+ const posAttr = geom.getAttribute("position");
3230
+ if (posAttr && !posAttr.array) return false;
3231
+ }
3232
+ return true;
3233
+ }
3234
+ var RaycastAccelerator = class {
3235
+ constructor(store) {
3236
+ this._targets = [];
3237
+ this._dirty = true;
3238
+ this._unsubscribe = null;
3239
+ this._bvhBuiltFor = /* @__PURE__ */ new WeakSet();
3240
+ this._store = store;
3241
+ ensureBVHPatched();
3242
+ this._unsubscribe = store.subscribe(() => {
3243
+ this._dirty = true;
3244
+ });
3245
+ }
3246
+ /** Force a target list rebuild on the next raycast. */
3247
+ markDirty() {
3248
+ this._dirty = true;
3249
+ }
3250
+ _rebuild() {
3251
+ this._dirty = false;
3252
+ const flatList = this._store.getFlatList();
3253
+ const targets = [];
3254
+ for (let i = 0; i < flatList.length; i++) {
3255
+ const obj = flatList[i];
3256
+ if (isRaycastable(obj)) {
3257
+ targets.push(obj);
3258
+ }
3259
+ }
3260
+ this._targets = targets;
3261
+ let bvhBudget = 50;
3262
+ for (let i = 0; i < targets.length && bvhBudget > 0; i++) {
3263
+ const obj = targets[i];
3264
+ if (obj.isMesh) {
3265
+ const geom = obj.geometry;
3266
+ if (geom && !this._bvhBuiltFor.has(geom) && !geom.boundsTree) {
3267
+ this._ensureBVH(obj);
3268
+ bvhBudget--;
3269
+ }
3270
+ }
3271
+ }
3272
+ if (bvhBudget === 0) {
3273
+ this._dirty = true;
3274
+ }
3275
+ }
3276
+ /**
3277
+ * Build a BVH for a mesh's geometry if it doesn't have one yet.
3278
+ * Uses indirect mode to avoid modifying the index buffer.
3279
+ * Skips disposed geometries and does NOT mark failed builds so they
3280
+ * can be retried (e.g. after geometry is re-uploaded).
3281
+ */
3282
+ _ensureBVH(obj) {
3283
+ if (!obj.isMesh) return;
3284
+ const geom = obj.geometry;
3285
+ if (!geom || this._bvhBuiltFor.has(geom)) return;
3286
+ const posAttr = geom.getAttribute("position");
3287
+ if (!posAttr || !posAttr.array) return;
3288
+ if (geom.boundsTree) {
3289
+ this._bvhBuiltFor.add(geom);
3290
+ return;
3291
+ }
3292
+ try {
3293
+ geom.computeBoundsTree({ indirect: true });
3294
+ this._bvhBuiltFor.add(geom);
3295
+ } catch {
3296
+ r3fLog("raycast", `BVH build failed for geometry, will retry next rebuild`);
3297
+ }
3298
+ }
3299
+ /**
3300
+ * Raycast from mouse position against only raycastable meshes.
3301
+ * Returns the closest non-internal hit, or null.
3302
+ */
3303
+ raycastAtMouse(e, camera, canvas) {
3304
+ if (this._dirty) this._rebuild();
3305
+ const rect = canvas.getBoundingClientRect();
3306
+ _mouse.x = (e.clientX - rect.left) / rect.width * 2 - 1;
3307
+ _mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
3308
+ _raycaster2.setFromCamera(_mouse, camera);
3309
+ _raycaster2.firstHitOnly = true;
3310
+ const intersections = _raycaster2.intersectObjects(this._targets, false);
3311
+ _raycaster2.firstHitOnly = false;
3312
+ for (const intersection of intersections) {
3313
+ if (intersection.object.userData?.__r3fdom_internal) continue;
3314
+ return intersection.object;
3315
+ }
3316
+ return null;
3317
+ }
3318
+ /**
3319
+ * Raycast from NDC coordinates. Used by raycastVerify.
3320
+ */
3321
+ raycastAtNdc(ndcX, ndcY, camera) {
3322
+ if (this._dirty) this._rebuild();
3323
+ _mouse.set(ndcX, ndcY);
3324
+ _raycaster2.setFromCamera(_mouse, camera);
3325
+ return _raycaster2.intersectObjects(this._targets, false);
3326
+ }
3327
+ /** Current number of raycastable targets. */
3328
+ get targetCount() {
3329
+ if (this._dirty) this._rebuild();
3330
+ return this._targets.length;
3331
+ }
3332
+ /** Unsubscribe from the store and release the target list. */
3333
+ dispose() {
3334
+ if (this._unsubscribe) {
3335
+ this._unsubscribe();
3336
+ this._unsubscribe = null;
3337
+ }
3338
+ this._targets = [];
3339
+ }
3340
+ };
3341
+
3342
+ // src/bridge/ThreeDom.tsx
3343
+ var _stores = /* @__PURE__ */ new Map();
3344
+ var _mirrors = /* @__PURE__ */ new Map();
3345
+ var _selectionManagers = /* @__PURE__ */ new Map();
3346
+ var _highlighters = /* @__PURE__ */ new Map();
3347
+ var _inspectControllers = /* @__PURE__ */ new Map();
3348
+ function getStore2(canvasId = "") {
3349
+ return _stores.get(canvasId) ?? null;
3350
+ }
3351
+ function getMirror(canvasId = "") {
3352
+ return _mirrors.get(canvasId) ?? null;
3353
+ }
3354
+ function getSelectionManager(canvasId = "") {
3355
+ return _selectionManagers.get(canvasId) ?? null;
3356
+ }
3357
+ function getHighlighter(canvasId = "") {
3358
+ return _highlighters.get(canvasId) ?? null;
3359
+ }
3360
+ function getInspectController(canvasId = "") {
3361
+ return _inspectControllers.get(canvasId) ?? null;
3362
+ }
3363
+ function getCanvasIds() {
3364
+ return Array.from(_stores.keys());
3365
+ }
3366
+ function exposeGlobalAPI(store, gl, cameraRef, selMgr, inspCtrl, mirror, canvasId, isPrimary = true) {
3367
+ const api = {
3368
+ _ready: true,
3369
+ canvasId,
3370
+ getByTestId: (id) => store.getByTestId(id),
3371
+ getByUuid: (uuid) => store.getByUuid(uuid),
3372
+ getByName: (name) => store.getByName(name),
3373
+ getChildren: (idOrUuid) => store.getChildren(idOrUuid),
3374
+ getParent: (idOrUuid) => store.getParent(idOrUuid),
3375
+ getCount: () => store.getCount(),
3376
+ getByType: (type) => store.getByType(type),
3377
+ getByGeometryType: (type) => store.getByGeometryType(type),
3378
+ getByMaterialType: (type) => store.getByMaterialType(type),
3379
+ getByUserData: (key, value) => store.getByUserData(key, value),
3380
+ getCountByType: (type) => store.getCountByType(type),
3381
+ getObjects: (ids) => {
3382
+ const map = store.getObjects(ids);
3383
+ const result = {};
3384
+ for (const [id, meta] of map) {
3385
+ result[id] = meta;
3386
+ }
3387
+ return result;
3388
+ },
3389
+ snapshot: () => createSnapshot(store),
3390
+ inspect: (idOrUuid, options) => store.inspect(idOrUuid, options),
3391
+ click: (idOrUuid) => {
3392
+ click3D(idOrUuid);
3393
+ },
3394
+ doubleClick: (idOrUuid) => {
3395
+ doubleClick3D(idOrUuid);
3396
+ },
3397
+ contextMenu: (idOrUuid) => {
3398
+ contextMenu3D(idOrUuid);
3399
+ },
3400
+ hover: (idOrUuid) => {
3401
+ hover3D(idOrUuid);
3402
+ },
3403
+ unhover: () => {
3404
+ unhover3D();
3405
+ },
3406
+ drag: async (idOrUuid, delta) => {
3407
+ await drag3D(idOrUuid, delta);
3408
+ },
3409
+ wheel: (idOrUuid, options) => {
3410
+ wheel3D(idOrUuid, options);
3411
+ },
3412
+ pointerMiss: () => {
3413
+ pointerMiss3D();
3414
+ },
3415
+ drawPath: async (points, options) => {
3416
+ const result = await drawPath(points, options);
3417
+ return { eventCount: result.eventCount, pointCount: result.pointCount };
3418
+ },
3419
+ select: (idOrUuid) => {
3420
+ const obj = store.getObject3D(idOrUuid);
3421
+ if (obj && selMgr) selMgr.select(obj);
3422
+ },
3423
+ clearSelection: () => {
3424
+ selMgr?.clearSelection();
3425
+ },
3426
+ getSelection: () => selMgr ? selMgr.getSelected().map((o) => o.uuid) : [],
3427
+ getObject3D: (idOrUuid) => store.getObject3D(idOrUuid),
3428
+ getSelectionDisplayTarget: (uuid) => resolveSelectionDisplayTarget((id) => store.getObject3D(id), uuid) ?? uuid,
3429
+ setInspectMode: (on) => {
3430
+ r3fLog("inspect", "Global API setInspectMode called", { on });
3431
+ const ctrl = inspCtrl ?? _inspectControllers.get(canvasId ?? "");
3432
+ if (on) {
3433
+ ctrl?.enable();
3434
+ mirror?.setInspectMode(true);
3435
+ } else {
3436
+ ctrl?.disable();
3437
+ mirror?.setInspectMode(false);
3438
+ }
3439
+ },
3440
+ getInspectMode: () => {
3441
+ const ctrl = inspCtrl ?? _inspectControllers.get(canvasId ?? "");
3442
+ return ctrl?.active ?? false;
3443
+ },
3444
+ sweepOrphans: () => store.sweepOrphans(),
3445
+ getDiagnostics: () => ({
3446
+ version,
3447
+ ready: true,
3448
+ objectCount: store.getCount(),
3449
+ meshCount: store.getCountByType("Mesh"),
3450
+ groupCount: store.getCountByType("Group"),
3451
+ lightCount: store.getCountByType("DirectionalLight") + store.getCountByType("PointLight") + store.getCountByType("SpotLight") + store.getCountByType("AmbientLight") + store.getCountByType("HemisphereLight"),
3452
+ cameraCount: store.getCountByType("PerspectiveCamera") + store.getCountByType("OrthographicCamera"),
3453
+ materializedDomNodes: mirror?.getMaterializedCount() ?? 0,
3454
+ maxDomNodes: mirror?.getMaxNodes() ?? 0,
3455
+ canvasWidth: gl.domElement.width,
3456
+ canvasHeight: gl.domElement.height,
3457
+ webglRenderer: (() => {
3458
+ try {
3459
+ const ctx = gl.getContext();
3460
+ const dbg = ctx.getExtension("WEBGL_debug_renderer_info");
3461
+ return dbg ? ctx.getParameter(dbg.UNMASKED_RENDERER_WEBGL) : "unknown";
3462
+ } catch {
3463
+ return "unknown";
3464
+ }
3465
+ })(),
3466
+ dirtyQueueSize: store.getDirtyCount()
3467
+ }),
3468
+ getCameraState: () => {
3469
+ const cam = cameraRef.current;
3470
+ const dir = new three.Vector3(0, 0, -1).applyQuaternion(cam.quaternion);
3471
+ const target = [
3472
+ cam.position.x + dir.x * 100,
3473
+ cam.position.y + dir.y * 100,
3474
+ cam.position.z + dir.z * 100
3475
+ ];
3476
+ const state = {
3477
+ type: cam.type,
3478
+ position: [cam.position.x, cam.position.y, cam.position.z],
3479
+ rotation: [cam.rotation.x, cam.rotation.y, cam.rotation.z],
3480
+ target,
3481
+ near: cam.near,
3482
+ far: cam.far,
3483
+ zoom: cam.zoom
3484
+ };
3485
+ if (cam.type === "PerspectiveCamera") {
3486
+ const pc = cam;
3487
+ state.fov = pc.fov;
3488
+ state.aspect = pc.aspect;
3489
+ } else if (cam.type === "OrthographicCamera") {
3490
+ const oc = cam;
3491
+ state.left = oc.left;
3492
+ state.right = oc.right;
3493
+ state.top = oc.top;
3494
+ state.bottom = oc.bottom;
3495
+ }
3496
+ return state;
3497
+ },
3498
+ fuzzyFind: (query, limit = 5) => {
3499
+ const q = query.toLowerCase();
3500
+ const results = [];
3501
+ for (const obj of store.getFlatList()) {
3502
+ if (results.length >= limit) break;
3503
+ const meta = store.getMetadata(obj);
3504
+ if (!meta) continue;
3505
+ const testId = meta.testId?.toLowerCase() ?? "";
3506
+ const name = meta.name?.toLowerCase() ?? "";
3507
+ if (testId.includes(q) || name.includes(q) || meta.uuid.startsWith(q)) {
3508
+ results.push(meta);
3509
+ }
3510
+ }
3511
+ return results;
3512
+ },
3513
+ version
3514
+ };
3515
+ if (isPrimary) {
3516
+ window.__R3F_DOM__ = api;
3517
+ }
3518
+ if (canvasId) {
3519
+ if (!window.__R3F_DOM_INSTANCES__) window.__R3F_DOM_INSTANCES__ = {};
3520
+ if (window.__R3F_DOM_INSTANCES__[canvasId]) {
3521
+ console.warn(
3522
+ `[react-three-dom] Duplicate canvasId "${canvasId}" \u2014 the previous bridge instance will be overwritten. Each <ThreeDom> must have a unique canvasId.`
3523
+ );
3524
+ }
3525
+ window.__R3F_DOM_INSTANCES__[canvasId] = api;
3526
+ }
3527
+ }
3528
+ function removeGlobalAPI(onlyIfEquals, canvasId) {
3529
+ r3fLog("bridge", "removeGlobalAPI called (deferred)");
3530
+ const removeFromRegistry = (ref) => {
3531
+ if (canvasId && window.__R3F_DOM_INSTANCES__) {
3532
+ if (!ref || window.__R3F_DOM_INSTANCES__[canvasId] === ref) {
3533
+ delete window.__R3F_DOM_INSTANCES__[canvasId];
3534
+ if (Object.keys(window.__R3F_DOM_INSTANCES__).length === 0) {
3535
+ delete window.__R3F_DOM_INSTANCES__;
3536
+ }
3537
+ }
3538
+ }
3539
+ };
3540
+ if (onlyIfEquals !== void 0) {
3541
+ const ref = onlyIfEquals;
3542
+ queueMicrotask(() => {
3543
+ if (window.__R3F_DOM__ === ref) {
3544
+ delete window.__R3F_DOM__;
3545
+ r3fLog("bridge", "Global API removed");
3546
+ } else {
3547
+ r3fLog("bridge", "Global API not removed \u2014 replaced by new instance (Strict Mode remount)");
3548
+ }
3549
+ removeFromRegistry(ref);
3550
+ });
3551
+ } else {
3552
+ delete window.__R3F_DOM__;
3553
+ removeFromRegistry();
3554
+ r3fLog("bridge", "Global API removed (immediate)");
3555
+ }
3556
+ }
3557
+ function createStubBridge(error, canvasId) {
3558
+ return {
3559
+ _ready: false,
3560
+ _error: error,
3561
+ canvasId,
3562
+ getByTestId: () => null,
3563
+ getByUuid: () => null,
3564
+ getByName: () => [],
3565
+ getChildren: () => [],
3566
+ getParent: () => null,
3567
+ getCount: () => 0,
3568
+ getByType: () => [],
3569
+ getByGeometryType: () => [],
3570
+ getByMaterialType: () => [],
3571
+ getByUserData: () => [],
3572
+ getCountByType: () => 0,
3573
+ getObjects: (ids) => {
3574
+ const result = {};
3575
+ for (const id of ids) result[id] = null;
3576
+ return result;
3577
+ },
3578
+ snapshot: () => ({
3579
+ timestamp: 0,
3580
+ objectCount: 0,
3581
+ tree: {
3582
+ uuid: "",
3583
+ name: "",
3584
+ type: "Scene",
3585
+ visible: true,
3586
+ position: [0, 0, 0],
3587
+ rotation: [0, 0, 0],
3588
+ scale: [1, 1, 1],
3589
+ children: []
3590
+ }
3591
+ }),
3592
+ inspect: () => null,
3593
+ click: () => {
3594
+ },
3595
+ doubleClick: () => {
3596
+ },
3597
+ contextMenu: () => {
3598
+ },
3599
+ hover: () => {
3600
+ },
3601
+ unhover: () => {
3602
+ },
3603
+ drag: async () => {
3604
+ },
3605
+ wheel: () => {
3606
+ },
3607
+ pointerMiss: () => {
3608
+ },
3609
+ drawPath: async () => ({ eventCount: 0, pointCount: 0 }),
3610
+ select: () => {
3611
+ },
3612
+ clearSelection: () => {
3613
+ },
3614
+ getSelection: () => [],
3615
+ getObject3D: () => null,
3616
+ getSelectionDisplayTarget: (uuid) => uuid,
3617
+ setInspectMode: () => {
3618
+ },
3619
+ getInspectMode: () => false,
3620
+ sweepOrphans: () => 0,
3621
+ getDiagnostics: () => ({
3622
+ version,
3623
+ ready: false,
3624
+ error: error ?? void 0,
3625
+ objectCount: 0,
3626
+ meshCount: 0,
3627
+ groupCount: 0,
3628
+ lightCount: 0,
3629
+ cameraCount: 0,
3630
+ materializedDomNodes: 0,
3631
+ maxDomNodes: 0,
3632
+ canvasWidth: 0,
3633
+ canvasHeight: 0,
3634
+ webglRenderer: "unavailable",
3635
+ dirtyQueueSize: 0
3636
+ }),
3637
+ getCameraState: () => ({
3638
+ type: "unknown",
3639
+ position: [0, 0, 0],
3640
+ rotation: [0, 0, 0],
3641
+ target: [0, 0, -100],
3642
+ near: 0.1,
3643
+ far: 1e3,
3644
+ zoom: 1
3645
+ }),
3646
+ fuzzyFind: () => [],
3647
+ version
3648
+ };
3649
+ }
3650
+ function ThreeDom({
3651
+ canvasId,
3652
+ primary,
3653
+ root = "#three-dom-root",
3654
+ batchSize = 500,
3655
+ syncBudgetMs = 0.5,
3656
+ maxDomNodes = 2e3,
3657
+ initialDepth = 3,
3658
+ enabled = true,
3659
+ debug = false,
3660
+ inspect: inspectProp = false
3661
+ } = {}) {
3662
+ const isPrimary = primary ?? canvasId === void 0;
3663
+ const instanceKey = canvasId ?? "";
3664
+ const scene = fiber.useThree((s) => s.scene);
3665
+ const camera = fiber.useThree((s) => s.camera);
3666
+ const gl = fiber.useThree((s) => s.gl);
3667
+ const size = fiber.useThree((s) => s.size);
3668
+ const cursorRef = react.useRef(0);
3669
+ const lastSweepRef = react.useRef(0);
3670
+ const cameraRef = react.useRef(camera);
3671
+ cameraRef.current = camera;
3672
+ react.useEffect(() => {
3673
+ if (!enabled) return;
3674
+ if (debug) enableDebug(true);
3675
+ r3fLog("setup", "ThreeDom effect started", { enabled, debug, root, maxDomNodes });
3676
+ const canvas = gl.domElement;
3677
+ canvas.setAttribute("data-r3f-canvas", canvasId ?? "true");
3678
+ const canvasParent = canvas.parentElement;
3679
+ let rootElement = null;
3680
+ let createdRoot = false;
3681
+ if (typeof root === "string") {
3682
+ rootElement = document.querySelector(root);
3683
+ } else {
3684
+ rootElement = root;
3685
+ }
3686
+ if (!rootElement) {
3687
+ rootElement = document.createElement("div");
3688
+ rootElement.id = typeof root === "string" ? root.replace(/^#/, "") : "three-dom-root";
3689
+ createdRoot = true;
3690
+ }
3691
+ canvasParent.style.position = canvasParent.style.position || "relative";
3692
+ canvasParent.appendChild(rootElement);
3693
+ rootElement.style.cssText = "width:0;height:0;overflow:hidden;pointer-events:none;opacity:0;";
3694
+ let store = null;
3695
+ let mirror = null;
3696
+ let unpatch = null;
3697
+ let cancelAsyncReg = null;
3698
+ let selectionManager = null;
3699
+ let highlighter = null;
3700
+ let raycastAccelerator = null;
3701
+ let inspectController = null;
3702
+ let currentApi;
3703
+ try {
3704
+ const webglContext = gl.getContext();
3705
+ if (!webglContext || webglContext.isContextLost?.()) {
3706
+ const msg = "WebGL context not available. For headless Chromium, add --enable-webgl and optionally --use-gl=angle --use-angle=swiftshader-webgl to launch args.";
3707
+ const stubApi = createStubBridge(msg, canvasId);
3708
+ if (isPrimary) window.__R3F_DOM__ = stubApi;
3709
+ if (canvasId) {
3710
+ if (!window.__R3F_DOM_INSTANCES__) window.__R3F_DOM_INSTANCES__ = {};
3711
+ window.__R3F_DOM_INSTANCES__[canvasId] = stubApi;
3712
+ }
3713
+ r3fLog("setup", msg);
3714
+ return () => {
3715
+ removeGlobalAPI(stubApi, canvasId);
3716
+ canvas.removeAttribute("data-r3f-canvas");
3717
+ if (createdRoot && rootElement?.parentNode) {
3718
+ rootElement.parentNode.removeChild(rootElement);
3719
+ }
3720
+ if (debug) enableDebug(false);
3721
+ };
3722
+ }
3723
+ store = new ObjectStore();
3724
+ mirror = new DomMirror(store, maxDomNodes);
3725
+ mirror.setRoot(rootElement);
3726
+ r3fLog("setup", "Store and mirror created");
3727
+ ensureCustomElements(store);
3728
+ unpatch = patchObject3D(store, mirror);
3729
+ setInteractionState(store, camera, gl, size);
3730
+ r3fLog("setup", "Object3D patched, interaction state set");
3731
+ store.registerTree(scene);
3732
+ if (!store.has(camera)) {
3733
+ const camMeta = store.register(camera);
3734
+ camMeta.parentUuid = scene.uuid;
3735
+ mirror.materialize(camera.uuid);
3736
+ }
3737
+ mirror.materializeSubtree(scene.uuid, initialDepth);
3738
+ cancelAsyncReg = store.registerTreeAsync(scene);
3739
+ r3fLog("setup", `Scene registered: ${store.getCount()} objects, async watcher started`);
3740
+ selectionManager = new SelectionManager();
3741
+ highlighter = new Highlighter();
3742
+ highlighter.attach(scene, selectionManager, camera, gl, store);
3743
+ raycastAccelerator = new RaycastAccelerator(store);
3744
+ inspectController = new InspectController({
3745
+ camera,
3746
+ renderer: gl,
3747
+ selectionManager,
3748
+ highlighter,
3749
+ raycastAccelerator,
3750
+ mirror,
3751
+ store
3752
+ });
3753
+ _selectionManagers.set(instanceKey, selectionManager);
3754
+ _highlighters.set(instanceKey, highlighter);
3755
+ _inspectControllers.set(instanceKey, inspectController);
3756
+ exposeGlobalAPI(store, gl, cameraRef, selectionManager, inspectController, mirror, canvasId, isPrimary);
3757
+ r3fLog("bridge", `exposeGlobalAPI called \u2014 bridge is live, _ready=true${canvasId ? `, canvasId="${canvasId}"` : ""}`);
3758
+ currentApi = canvasId ? window.__R3F_DOM_INSTANCES__?.[canvasId] : window.__R3F_DOM__;
3759
+ _stores.set(instanceKey, store);
3760
+ _mirrors.set(instanceKey, mirror);
3761
+ if (inspectProp) {
3762
+ inspectController.enable();
3763
+ }
3764
+ if (debug) {
3765
+ const inspectStatus = inspectProp ? "on" : "off";
3766
+ console.log(
3767
+ "%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",
3768
+ "background:#1a73e8;color:#fff;padding:2px 6px;border-radius:3px;font-weight:bold",
3769
+ "color:inherit",
3770
+ "color:#1a73e8",
3771
+ "color:inherit",
3772
+ "color:#e8a317;font-family:monospace",
3773
+ "color:inherit",
3774
+ "color:#1a73e8",
3775
+ "color:inherit"
3776
+ );
3777
+ }
3778
+ } catch (err) {
3779
+ const errorMsg = err instanceof Error ? err.message : String(err);
3780
+ r3fLog("setup", "ThreeDom setup failed", err);
3781
+ console.error("[react-three-dom] Setup failed:", err);
3782
+ const stubApi = createStubBridge(errorMsg, canvasId);
3783
+ if (isPrimary) window.__R3F_DOM__ = stubApi;
3784
+ if (canvasId) {
3785
+ if (!window.__R3F_DOM_INSTANCES__) window.__R3F_DOM_INSTANCES__ = {};
3786
+ window.__R3F_DOM_INSTANCES__[canvasId] = stubApi;
3787
+ }
3788
+ currentApi = stubApi;
3789
+ }
3790
+ return () => {
3791
+ r3fLog("setup", "ThreeDom cleanup started");
3792
+ if (cancelAsyncReg) cancelAsyncReg();
3793
+ if (inspectController) inspectController.dispose();
3794
+ if (raycastAccelerator) raycastAccelerator.dispose();
3795
+ if (highlighter) highlighter.dispose();
3796
+ if (unpatch) unpatch();
3797
+ removeGlobalAPI(currentApi, canvasId);
3798
+ clearInteractionState();
3799
+ if (selectionManager) selectionManager.dispose();
3800
+ if (mirror) mirror.dispose();
3801
+ if (store) store.dispose();
3802
+ canvas.removeAttribute("data-r3f-canvas");
3803
+ if (createdRoot && rootElement?.parentNode) {
3804
+ rootElement.parentNode.removeChild(rootElement);
3805
+ }
3806
+ _stores.delete(instanceKey);
3807
+ _mirrors.delete(instanceKey);
3808
+ _selectionManagers.delete(instanceKey);
3809
+ _highlighters.delete(instanceKey);
3810
+ _inspectControllers.delete(instanceKey);
3811
+ if (debug) enableDebug(false);
3812
+ r3fLog("setup", "ThreeDom cleanup complete");
3813
+ };
3814
+ }, [scene, camera, gl, enabled, root, maxDomNodes, initialDepth, debug, inspectProp, canvasId, isPrimary, instanceKey]);
3815
+ fiber.useFrame(() => {
3816
+ const _store3 = _stores.get(instanceKey);
3817
+ const _mirror = _mirrors.get(instanceKey);
3818
+ const _highlighter = _highlighters.get(instanceKey);
3819
+ const _inspectController = _inspectControllers.get(instanceKey);
3820
+ if (!enabled || !_store3 || !_mirror) return;
3821
+ try {
3822
+ setInteractionState(_store3, camera, gl, size);
3823
+ if (_inspectController) _inspectController.updateCamera(camera);
3824
+ const store = _store3;
3825
+ const mirror = _mirror;
3826
+ const start = performance.now();
3827
+ const dirtyObjects = store.drainDirtyQueue();
3828
+ for (const obj of dirtyObjects) {
3829
+ store.update(obj);
3830
+ mirror.syncAttributes(obj);
3831
+ }
3832
+ const budgetRemaining = syncBudgetMs - (performance.now() - start);
3833
+ if (budgetRemaining > 0.1) {
3834
+ const objects = store.getFlatList();
3835
+ if (objects.length > 0) {
3836
+ const end = Math.min(cursorRef.current + batchSize, objects.length);
3837
+ for (let i = cursorRef.current; i < end; i++) {
3838
+ if (performance.now() - start > syncBudgetMs) break;
3839
+ const obj = objects[i];
3840
+ const changed = store.update(obj);
3841
+ if (changed) mirror.syncAttributes(obj);
3842
+ }
3843
+ cursorRef.current = end >= objects.length ? 0 : end;
3844
+ }
3845
+ }
3846
+ if (_highlighter) _highlighter.update();
3847
+ const now = performance.now();
3848
+ if (now - lastSweepRef.current > 3e4) {
3849
+ lastSweepRef.current = now;
3850
+ store.sweepOrphans();
3851
+ }
3852
+ } catch (err) {
3853
+ r3fLog("sync", "Per-frame sync error", err);
3854
+ }
3855
+ });
3856
+ return null;
3857
+ }
3027
3858
 
3028
3859
  exports.DomMirror = DomMirror;
3029
3860
  exports.Highlighter = Highlighter;
3861
+ exports.InspectController = InspectController;
3030
3862
  exports.MANAGED_ATTRIBUTES = MANAGED_ATTRIBUTES;
3031
3863
  exports.ObjectStore = ObjectStore;
3864
+ exports.RaycastAccelerator = RaycastAccelerator;
3032
3865
  exports.SelectionManager = SelectionManager;
3033
3866
  exports.TAG_MAP = TAG_MAP;
3034
3867
  exports.ThreeDom = ThreeDom;
@@ -3054,7 +3887,9 @@ exports.drag3D = drag3D;
3054
3887
  exports.drawPath = drawPath;
3055
3888
  exports.enableDebug = enableDebug;
3056
3889
  exports.ensureCustomElements = ensureCustomElements;
3890
+ exports.getCanvasIds = getCanvasIds;
3057
3891
  exports.getHighlighter = getHighlighter;
3892
+ exports.getInspectController = getInspectController;
3058
3893
  exports.getMirror = getMirror;
3059
3894
  exports.getSelectionManager = getSelectionManager;
3060
3895
  exports.getStore = getStore2;