@react-three-dom/core 0.2.0 → 0.4.0

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