@react-three-dom/core 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,9 +1,29 @@
1
- import { Box3, Vector3, Object3D, Matrix4, Frustum, Raycaster, BufferGeometry, Material, InstancedMesh, Color, Vector2 } from 'three';
1
+ import { Box3, Object3D, Vector3, Matrix4, Frustum, Raycaster, Vector2, BufferGeometry, Material, Color, MeshBasicMaterial, DoubleSide, Mesh, BoxGeometry, EdgesGeometry, LineBasicMaterial, LineSegments, InstancedMesh, PerspectiveCamera, OrthographicCamera } from 'three';
2
2
  import { useRef, useEffect } from 'react';
3
3
  import { useThree, useFrame } from '@react-three/fiber';
4
+ import { computeBoundsTree, disposeBoundsTree, acceleratedRaycast } from 'three-mesh-bvh';
4
5
 
5
6
  // src/version.ts
6
- var version = "0.1.0";
7
+ var version = "0.3.0";
8
+
9
+ // src/debug.ts
10
+ var _enabled = false;
11
+ function enableDebug(on = true) {
12
+ _enabled = on;
13
+ }
14
+ function isDebugEnabled() {
15
+ return _enabled || typeof window !== "undefined" && !!window.__R3F_DOM_DEBUG__;
16
+ }
17
+ function r3fLog(area, msg, data) {
18
+ if (!_enabled && !(typeof window !== "undefined" && window.__R3F_DOM_DEBUG__)) {
19
+ return;
20
+ }
21
+ if (data !== void 0) {
22
+ console.log(`[r3f-dom:${area}]`, msg, data);
23
+ } else {
24
+ console.log(`[r3f-dom:${area}]`, msg);
25
+ }
26
+ }
7
27
  function extractMetadata(obj) {
8
28
  const meta = {
9
29
  uuid: obj.uuid,
@@ -18,114 +38,226 @@ function extractMetadata(obj) {
18
38
  childrenUuids: obj.children.map((c) => c.uuid),
19
39
  boundsDirty: true
20
40
  };
21
- if ("geometry" in obj) {
22
- const geom = obj.geometry;
23
- if (geom instanceof BufferGeometry) {
24
- meta.geometryType = geom.type;
25
- const posAttr = geom.getAttribute("position");
26
- if (posAttr) {
27
- meta.vertexCount = posAttr.count;
28
- if (geom.index) {
29
- meta.triangleCount = Math.floor(geom.index.count / 3);
30
- } else {
31
- meta.triangleCount = Math.floor(posAttr.count / 3);
41
+ extractStaticFields(obj, meta);
42
+ return meta;
43
+ }
44
+ function extractStaticFields(obj, meta) {
45
+ try {
46
+ if ("geometry" in obj) {
47
+ const geom = obj.geometry;
48
+ if (geom instanceof BufferGeometry) {
49
+ meta.geometryType = geom.type;
50
+ const posAttr = geom.getAttribute("position");
51
+ if (posAttr) {
52
+ meta.vertexCount = posAttr.count;
53
+ if (geom.index) {
54
+ meta.triangleCount = Math.floor(geom.index.count / 3);
55
+ } else {
56
+ meta.triangleCount = Math.floor(posAttr.count / 3);
57
+ }
32
58
  }
33
59
  }
34
60
  }
61
+ } catch {
62
+ r3fLog("store", `extractMetadata: geometry access failed for "${obj.name || obj.uuid}"`);
63
+ }
64
+ try {
65
+ if ("material" in obj) {
66
+ const mat = obj.material;
67
+ if (mat instanceof Material) {
68
+ meta.materialType = mat.type;
69
+ } else if (Array.isArray(mat) && mat.length > 0) {
70
+ meta.materialType = mat[0].type + (mat.length > 1 ? ` (+${mat.length - 1})` : "");
71
+ }
72
+ }
73
+ } catch {
74
+ r3fLog("store", `extractMetadata: material access failed for "${obj.name || obj.uuid}"`);
35
75
  }
36
- if ("material" in obj) {
37
- const mat = obj.material;
38
- if (mat instanceof Material) {
39
- meta.materialType = mat.type;
40
- } else if (Array.isArray(mat) && mat.length > 0) {
41
- meta.materialType = mat[0].type + (mat.length > 1 ? ` (+${mat.length - 1})` : "");
76
+ try {
77
+ if (obj instanceof InstancedMesh) {
78
+ meta.instanceCount = obj.count;
42
79
  }
80
+ } catch {
43
81
  }
44
- if (obj instanceof InstancedMesh) {
45
- meta.instanceCount = obj.count;
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 {
46
94
  }
47
- return meta;
48
95
  }
49
- function hasChanged(prev, curr) {
50
- 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;
51
152
  }
52
153
  var _box3 = new Box3();
53
- function inspectObject(obj, metadata) {
54
- obj.updateWorldMatrix(true, false);
55
- const worldMatrix = Array.from(obj.matrixWorld.elements);
56
- _box3.setFromObject(obj);
57
- const boundsMin = [_box3.min.x, _box3.min.y, _box3.min.z];
58
- const boundsMax = [_box3.max.x, _box3.max.y, _box3.max.z];
154
+ function inspectObject(obj, metadata, options) {
155
+ let worldMatrix = Array(16).fill(0);
156
+ let boundsMin = [0, 0, 0];
157
+ let boundsMax = [0, 0, 0];
158
+ try {
159
+ obj.updateWorldMatrix(true, false);
160
+ worldMatrix = Array.from(obj.matrixWorld.elements);
161
+ _box3.setFromObject(obj);
162
+ boundsMin = [_box3.min.x, _box3.min.y, _box3.min.z];
163
+ boundsMax = [_box3.max.x, _box3.max.y, _box3.max.z];
164
+ } catch {
165
+ r3fLog("store", `inspectObject: world matrix / bounds failed for "${obj.name || obj.uuid}"`);
166
+ }
59
167
  const inspection = {
60
168
  metadata,
61
169
  worldMatrix,
62
170
  bounds: { min: boundsMin, max: boundsMax },
63
- userData: { ...obj.userData }
171
+ userData: {}
64
172
  };
65
- if ("geometry" in obj) {
66
- const geom = obj.geometry;
67
- if (geom instanceof BufferGeometry) {
68
- const geoInspection = {
69
- type: geom.type,
70
- attributes: {}
71
- };
72
- for (const [name, attr] of Object.entries(geom.attributes)) {
73
- geoInspection.attributes[name] = {
74
- itemSize: attr.itemSize,
75
- count: attr.count
173
+ try {
174
+ inspection.userData = { ...obj.userData };
175
+ } catch {
176
+ r3fLog("store", `inspectObject: userData copy failed for "${obj.name || obj.uuid}"`);
177
+ }
178
+ try {
179
+ if ("geometry" in obj) {
180
+ const geom = obj.geometry;
181
+ if (geom instanceof BufferGeometry && geom.attributes) {
182
+ const geoInspection = {
183
+ type: geom.type,
184
+ attributes: {}
76
185
  };
186
+ for (const [name, attr] of Object.entries(geom.attributes)) {
187
+ if (!attr) continue;
188
+ geoInspection.attributes[name] = {
189
+ itemSize: attr.itemSize,
190
+ count: attr.count
191
+ };
192
+ }
193
+ if (geom.index) {
194
+ geoInspection.index = { count: geom.index.count };
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
+ }
205
+ geom.computeBoundingSphere();
206
+ const sphere = geom.boundingSphere;
207
+ if (sphere) {
208
+ geoInspection.boundingSphere = {
209
+ center: [sphere.center.x, sphere.center.y, sphere.center.z],
210
+ radius: sphere.radius
211
+ };
212
+ }
213
+ inspection.geometry = geoInspection;
77
214
  }
78
- if (geom.index) {
79
- geoInspection.index = { count: geom.index.count };
80
- }
81
- geom.computeBoundingSphere();
82
- const sphere = geom.boundingSphere;
83
- if (sphere) {
84
- geoInspection.boundingSphere = {
85
- center: [sphere.center.x, sphere.center.y, sphere.center.z],
86
- radius: sphere.radius
215
+ }
216
+ } catch {
217
+ r3fLog("store", `inspectObject: geometry inspection failed for "${obj.name || obj.uuid}"`);
218
+ }
219
+ try {
220
+ if ("material" in obj) {
221
+ const rawMat = obj.material;
222
+ if (!rawMat) throw new Error("disposed");
223
+ const mat = Array.isArray(rawMat) ? rawMat[0] : rawMat;
224
+ if (mat instanceof Material) {
225
+ const matInspection = {
226
+ type: mat.type,
227
+ transparent: mat.transparent,
228
+ opacity: mat.opacity,
229
+ side: mat.side
87
230
  };
88
- }
89
- inspection.geometry = geoInspection;
90
- }
91
- }
92
- if ("material" in obj) {
93
- const rawMat = obj.material;
94
- const mat = Array.isArray(rawMat) ? rawMat[0] : rawMat;
95
- if (mat instanceof Material) {
96
- const matInspection = {
97
- type: mat.type,
98
- transparent: mat.transparent,
99
- opacity: mat.opacity,
100
- side: mat.side
101
- };
102
- if ("color" in mat && mat.color instanceof Color) {
103
- matInspection.color = "#" + mat.color.getHexString();
104
- }
105
- if ("map" in mat) {
106
- const map = mat.map;
107
- if (map) {
108
- matInspection.map = map.name || map.uuid || "unnamed";
231
+ if ("color" in mat && mat.color instanceof Color) {
232
+ matInspection.color = "#" + mat.color.getHexString();
109
233
  }
110
- }
111
- if ("uniforms" in mat) {
112
- const uniforms = mat.uniforms;
113
- matInspection.uniforms = {};
114
- for (const [key, uniform] of Object.entries(uniforms)) {
115
- const val = uniform.value;
116
- if (val === null || val === void 0) {
117
- matInspection.uniforms[key] = val;
118
- } else if (typeof val === "number" || typeof val === "boolean" || typeof val === "string") {
119
- matInspection.uniforms[key] = val;
120
- } else if (typeof val === "object" && "toArray" in val) {
121
- matInspection.uniforms[key] = val.toArray();
122
- } else {
123
- matInspection.uniforms[key] = `[${typeof val}]`;
234
+ if ("map" in mat) {
235
+ const map = mat.map;
236
+ if (map) {
237
+ matInspection.map = map.name || map.uuid || "unnamed";
238
+ }
239
+ }
240
+ if ("uniforms" in mat) {
241
+ const uniforms = mat.uniforms;
242
+ matInspection.uniforms = {};
243
+ for (const [key, uniform] of Object.entries(uniforms)) {
244
+ const val = uniform.value;
245
+ if (val === null || val === void 0) {
246
+ matInspection.uniforms[key] = val;
247
+ } else if (typeof val === "number" || typeof val === "boolean" || typeof val === "string") {
248
+ matInspection.uniforms[key] = val;
249
+ } else if (typeof val === "object" && "toArray" in val) {
250
+ matInspection.uniforms[key] = val.toArray();
251
+ } else {
252
+ matInspection.uniforms[key] = `[${typeof val}]`;
253
+ }
124
254
  }
125
255
  }
256
+ inspection.material = matInspection;
126
257
  }
127
- inspection.material = matInspection;
128
258
  }
259
+ } catch {
260
+ r3fLog("store", `inspectObject: material inspection failed for "${obj.name || obj.uuid}"`);
129
261
  }
130
262
  return inspection;
131
263
  }
@@ -145,6 +277,10 @@ var ObjectStore = class {
145
277
  this._listeners = [];
146
278
  // Track the root scene(s) for scoping
147
279
  this._trackedRoots = /* @__PURE__ */ new WeakSet();
280
+ // ---- Async registration state ----
281
+ this._asyncRegQueue = [];
282
+ this._asyncRegHandle = null;
283
+ this._asyncRegBatchSize = 1e3;
148
284
  }
149
285
  // -------------------------------------------------------------------------
150
286
  // Registration
@@ -152,6 +288,7 @@ var ObjectStore = class {
152
288
  /**
153
289
  * Register a single object into the store.
154
290
  * Populates Tier 1 metadata and all indexes.
291
+ * Tags the object with `__r3fdom_tracked = true` for O(1) scene membership checks.
155
292
  */
156
293
  register(obj) {
157
294
  if (obj.userData?.__r3fdom_internal) {
@@ -163,6 +300,7 @@ var ObjectStore = class {
163
300
  this._metaByObject.set(obj, meta);
164
301
  this._objectByUuid.set(meta.uuid, obj);
165
302
  this._flatListDirty = true;
303
+ obj.userData.__r3fdom_tracked = true;
166
304
  if (meta.testId) {
167
305
  this._objectsByTestId.set(meta.testId, obj);
168
306
  }
@@ -175,19 +313,83 @@ var ObjectStore = class {
175
313
  nameSet.add(obj);
176
314
  }
177
315
  this._emit({ type: "add", object: obj, metadata: meta });
316
+ if (meta.testId) {
317
+ r3fLog("store", `Registered "${meta.testId}" (${meta.type})`);
318
+ }
178
319
  return meta;
179
320
  }
180
321
  /**
181
- * Register an entire subtree (object + all descendants).
322
+ * Register an entire subtree (object + all descendants) synchronously.
323
+ * Prefer `registerTreeAsync` for large scenes (100k+) to avoid blocking.
182
324
  */
183
325
  registerTree(root) {
184
326
  this._trackedRoots.add(root);
185
327
  root.traverse((obj) => {
186
- this.register(obj);
328
+ try {
329
+ this.register(obj);
330
+ } catch (err) {
331
+ r3fLog("store", `registerTree: failed to register "${obj.name || obj.uuid}"`, err);
332
+ }
187
333
  });
188
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
+ }
189
390
  /**
190
391
  * Unregister a single object from the store.
392
+ * Clears the `__r3fdom_tracked` flag.
191
393
  */
192
394
  unregister(obj) {
193
395
  const meta = this._metaByObject.get(obj);
@@ -196,6 +398,7 @@ var ObjectStore = class {
196
398
  this._objectByUuid.delete(meta.uuid);
197
399
  this._dirtyQueue.delete(obj);
198
400
  this._flatListDirty = true;
401
+ delete obj.userData.__r3fdom_tracked;
199
402
  if (meta.testId) {
200
403
  this._objectsByTestId.delete(meta.testId);
201
404
  }
@@ -208,6 +411,9 @@ var ObjectStore = class {
208
411
  }
209
412
  }
210
413
  }
414
+ if (meta.testId) {
415
+ r3fLog("store", `Unregistered "${meta.testId}" (${meta.type})`);
416
+ }
211
417
  this._emit({ type: "remove", object: obj, metadata: meta });
212
418
  }
213
419
  /**
@@ -223,40 +429,50 @@ var ObjectStore = class {
223
429
  // Tier 1: Update (compare-and-set, returns true if changed)
224
430
  // -------------------------------------------------------------------------
225
431
  /**
226
- * Refresh Tier 1 metadata from the live Three.js object.
432
+ * Refresh dynamic Tier 1 fields from the live Three.js object.
433
+ * Only reads transform, visibility, children count, and parent —
434
+ * skips static fields (geometry, material) that don't change per-frame.
435
+ * Mutates metadata in-place to avoid allocation.
227
436
  * Returns true if any values changed.
228
437
  */
229
438
  update(obj) {
230
- const prev = this._metaByObject.get(obj);
231
- if (!prev) return false;
232
- const curr = extractMetadata(obj);
233
- if (hasChanged(prev, curr)) {
234
- if (prev.testId !== curr.testId) {
235
- if (prev.testId) this._objectsByTestId.delete(prev.testId);
236
- if (curr.testId) this._objectsByTestId.set(curr.testId, obj);
237
- }
238
- if (prev.name !== curr.name) {
239
- if (prev.name) {
240
- const nameSet = this._objectsByName.get(prev.name);
241
- if (nameSet) {
242
- nameSet.delete(obj);
243
- if (nameSet.size === 0) this._objectsByName.delete(prev.name);
244
- }
439
+ const meta = this._metaByObject.get(obj);
440
+ if (!meta) return false;
441
+ let changed;
442
+ try {
443
+ const prevTestId = meta.testId;
444
+ const prevName = meta.name;
445
+ changed = updateDynamicFields(obj, meta);
446
+ if (changed) {
447
+ if (prevTestId !== meta.testId) {
448
+ r3fLog("store", `testId changed: "${prevTestId}" \u2192 "${meta.testId}" (${meta.type})`);
449
+ if (prevTestId) this._objectsByTestId.delete(prevTestId);
450
+ if (meta.testId) this._objectsByTestId.set(meta.testId, obj);
245
451
  }
246
- if (curr.name) {
247
- let nameSet = this._objectsByName.get(curr.name);
248
- if (!nameSet) {
249
- nameSet = /* @__PURE__ */ new Set();
250
- this._objectsByName.set(curr.name, nameSet);
452
+ if (prevName !== meta.name) {
453
+ if (prevName) {
454
+ const nameSet = this._objectsByName.get(prevName);
455
+ if (nameSet) {
456
+ nameSet.delete(obj);
457
+ if (nameSet.size === 0) this._objectsByName.delete(prevName);
458
+ }
459
+ }
460
+ if (meta.name) {
461
+ let nameSet = this._objectsByName.get(meta.name);
462
+ if (!nameSet) {
463
+ nameSet = /* @__PURE__ */ new Set();
464
+ this._objectsByName.set(meta.name, nameSet);
465
+ }
466
+ nameSet.add(obj);
251
467
  }
252
- nameSet.add(obj);
253
468
  }
469
+ this._emit({ type: "update", object: obj, metadata: meta });
254
470
  }
255
- this._metaByObject.set(obj, curr);
256
- this._emit({ type: "update", object: obj, metadata: curr });
257
- return true;
471
+ } catch (err) {
472
+ r3fLog("store", `update: updateDynamicFields failed for "${obj.name || obj.uuid}"`, err);
473
+ return false;
258
474
  }
259
- return false;
475
+ return changed;
260
476
  }
261
477
  // -------------------------------------------------------------------------
262
478
  // Tier 2: On-demand inspection (never cached)
@@ -265,13 +481,14 @@ var ObjectStore = class {
265
481
  * Compute full inspection data from a live Three.js object.
266
482
  * This reads geometry buffers, material properties, world bounds, etc.
267
483
  * Cost: 0.1–2ms depending on geometry complexity.
484
+ * Pass { includeGeometryData: true } to include vertex positions and triangle indices (higher cost for large meshes).
268
485
  */
269
- inspect(idOrUuid) {
486
+ inspect(idOrUuid, options) {
270
487
  const obj = this.getObject3D(idOrUuid);
271
488
  if (!obj) return null;
272
489
  const meta = this._metaByObject.get(obj);
273
490
  if (!meta) return null;
274
- return inspectObject(obj, meta);
491
+ return inspectObject(obj, meta, options);
275
492
  }
276
493
  // -------------------------------------------------------------------------
277
494
  // Lookups (O(1))
@@ -299,6 +516,105 @@ var ObjectStore = class {
299
516
  }
300
517
  return results;
301
518
  }
519
+ /** Get direct children of an object by testId or uuid. Returns empty array if not found. */
520
+ getChildren(idOrUuid) {
521
+ const meta = this.getByTestId(idOrUuid) ?? this.getByUuid(idOrUuid);
522
+ if (!meta) return [];
523
+ const results = [];
524
+ for (const childUuid of meta.childrenUuids) {
525
+ const childMeta = this.getByUuid(childUuid);
526
+ if (childMeta) results.push(childMeta);
527
+ }
528
+ return results;
529
+ }
530
+ /** Get parent of an object by testId or uuid. Returns null if not found or if root. */
531
+ getParent(idOrUuid) {
532
+ const meta = this.getByTestId(idOrUuid) ?? this.getByUuid(idOrUuid);
533
+ if (!meta || meta.parentUuid === null) return null;
534
+ return this.getByUuid(meta.parentUuid);
535
+ }
536
+ /**
537
+ * Batch lookup: get metadata for multiple objects by testId or uuid.
538
+ * Returns a Map from the requested id to its metadata (or null if not found).
539
+ * Single round-trip — much more efficient than calling getByTestId/getByUuid
540
+ * in a loop for BIM/CAD scenes with many objects.
541
+ * O(k) where k is the number of requested ids.
542
+ */
543
+ getObjects(ids) {
544
+ const results = /* @__PURE__ */ new Map();
545
+ for (const id of ids) {
546
+ const meta = this.getByTestId(id) ?? this.getByUuid(id);
547
+ results.set(id, meta);
548
+ }
549
+ return results;
550
+ }
551
+ /**
552
+ * Get all objects of a given Three.js type (e.g. "Mesh", "Group", "Line").
553
+ * Linear scan — O(n) where n is total tracked objects.
554
+ */
555
+ getByType(type) {
556
+ const results = [];
557
+ for (const obj of this.getFlatList()) {
558
+ const meta = this._metaByObject.get(obj);
559
+ if (meta && meta.type === type) results.push(meta);
560
+ }
561
+ return results;
562
+ }
563
+ /**
564
+ * Get all objects with a given geometry type (e.g. "BoxGeometry", "BufferGeometry").
565
+ * Linear scan — O(n). Only meshes/points/lines have geometryType.
566
+ */
567
+ getByGeometryType(type) {
568
+ const results = [];
569
+ for (const obj of this.getFlatList()) {
570
+ const meta = this._metaByObject.get(obj);
571
+ if (meta && meta.geometryType === type) results.push(meta);
572
+ }
573
+ return results;
574
+ }
575
+ /**
576
+ * Get all objects with a given material type (e.g. "MeshStandardMaterial").
577
+ * Linear scan — O(n). Only meshes/points/lines have materialType.
578
+ */
579
+ getByMaterialType(type) {
580
+ const results = [];
581
+ for (const obj of this.getFlatList()) {
582
+ const meta = this._metaByObject.get(obj);
583
+ if (meta && meta.materialType === type) results.push(meta);
584
+ }
585
+ return results;
586
+ }
587
+ /**
588
+ * Get all objects that have a specific userData key.
589
+ * If `value` is provided, only returns objects where `userData[key]` matches.
590
+ * Uses JSON.stringify for deep equality on complex values.
591
+ * Linear scan — O(n).
592
+ */
593
+ getByUserData(key, value) {
594
+ const results = [];
595
+ const checkValue = value !== void 0;
596
+ const valueJson = checkValue ? JSON.stringify(value) : "";
597
+ for (const obj of this.getFlatList()) {
598
+ if (!(key in obj.userData)) continue;
599
+ if (checkValue && JSON.stringify(obj.userData[key]) !== valueJson) continue;
600
+ const meta = this._metaByObject.get(obj);
601
+ if (meta) results.push(meta);
602
+ }
603
+ return results;
604
+ }
605
+ /**
606
+ * Count objects of a given Three.js type.
607
+ * More efficient than getByType().length — no array allocation.
608
+ * Linear scan — O(n).
609
+ */
610
+ getCountByType(type) {
611
+ let count = 0;
612
+ for (const obj of this.getFlatList()) {
613
+ const meta = this._metaByObject.get(obj);
614
+ if (meta && meta.type === type) count++;
615
+ }
616
+ return count;
617
+ }
302
618
  /** Get the raw Three.js Object3D by testId or uuid. */
303
619
  getObject3D(idOrUuid) {
304
620
  return this._objectsByTestId.get(idOrUuid) ?? this._objectByUuid.get(idOrUuid) ?? null;
@@ -320,13 +636,16 @@ var ObjectStore = class {
320
636
  return this._trackedRoots.has(obj);
321
637
  }
322
638
  /**
323
- * Walk up from `obj` to see if any ancestor is a tracked root.
324
- * Used by Object3D.add/remove patch to determine if an object
325
- * belongs to a monitored scene.
639
+ * Check if an object belongs to a tracked scene.
640
+ * Fast path: checks the `__r3fdom_tracked` flag set during register (O(1)).
641
+ * Fallback: walks up the parent chain to find a tracked root.
642
+ * The fallback is needed for newly added objects that aren't registered yet.
326
643
  */
327
644
  isInTrackedScene(obj) {
328
- let current = obj;
645
+ if (obj.userData?.__r3fdom_tracked) return true;
646
+ let current = obj.parent;
329
647
  while (current) {
648
+ if (current.userData?.__r3fdom_tracked) return true;
330
649
  if (this._trackedRoots.has(current)) return true;
331
650
  current = current.parent;
332
651
  }
@@ -386,10 +705,39 @@ var ObjectStore = class {
386
705
  }
387
706
  }
388
707
  // -------------------------------------------------------------------------
708
+ // GC: sweep orphaned objects
709
+ // -------------------------------------------------------------------------
710
+ /**
711
+ * Sweep objects in `_objectByUuid` that are no longer in any tracked scene.
712
+ * This catches objects that were removed from the scene graph without
713
+ * triggering the patched Object3D.remove (e.g. direct `.children` splice,
714
+ * or the remove hook failing silently).
715
+ *
716
+ * At BIM scale, call this periodically (e.g. every 30s or after a floor
717
+ * load/unload) to prevent memory leaks from retained Object3D references.
718
+ *
719
+ * Returns the number of orphans cleaned up.
720
+ */
721
+ sweepOrphans() {
722
+ let swept = 0;
723
+ for (const [uuid, obj] of this._objectByUuid) {
724
+ if (!obj.parent && !this._trackedRoots.has(obj)) {
725
+ this.unregister(obj);
726
+ swept++;
727
+ r3fLog("store", `sweepOrphans: removed orphan "${obj.name || uuid}"`);
728
+ }
729
+ }
730
+ return swept;
731
+ }
732
+ // -------------------------------------------------------------------------
389
733
  // Cleanup
390
734
  // -------------------------------------------------------------------------
391
735
  /** Remove all tracked objects and reset state. */
392
736
  dispose() {
737
+ this._cancelAsyncRegistration();
738
+ for (const obj of this._objectByUuid.values()) {
739
+ if (obj.userData) delete obj.userData.__r3fdom_tracked;
740
+ }
393
741
  this._objectByUuid.clear();
394
742
  this._objectsByTestId.clear();
395
743
  this._objectsByName.clear();
@@ -606,7 +954,11 @@ var ATTRIBUTE_MAP = {
606
954
  "data-scale": (m) => serializeTuple(m.scale),
607
955
  "data-vertex-count": (m) => m.vertexCount !== void 0 ? String(m.vertexCount) : void 0,
608
956
  "data-triangle-count": (m) => m.triangleCount !== void 0 ? String(m.triangleCount) : void 0,
609
- "data-instance-count": (m) => m.instanceCount !== void 0 ? String(m.instanceCount) : void 0
957
+ "data-instance-count": (m) => m.instanceCount !== void 0 ? String(m.instanceCount) : void 0,
958
+ "data-fov": (m) => m.fov !== void 0 ? String(m.fov) : void 0,
959
+ "data-near": (m) => m.near !== void 0 ? String(m.near) : void 0,
960
+ "data-far": (m) => m.far !== void 0 ? String(m.far) : void 0,
961
+ "data-zoom": (m) => m.zoom !== void 0 ? String(m.zoom) : void 0
610
962
  };
611
963
  var MANAGED_ATTRIBUTES = Object.keys(ATTRIBUTE_MAP);
612
964
  function serializeTuple(tuple) {
@@ -659,6 +1011,12 @@ var DomMirror = class {
659
1011
  this._lruSize = 0;
660
1012
  // UUID → parent UUID mapping for DOM tree structure
661
1013
  this._parentMap = /* @__PURE__ */ new Map();
1014
+ /** When true, mirror elements use pointer-events: auto so DevTools element picker can select them. */
1015
+ this._inspectMode = false;
1016
+ /** Async materialization state for inspect mode */
1017
+ this._asyncQueue = [];
1018
+ this._asyncIdleHandle = null;
1019
+ this._asyncBatchSize = 200;
662
1020
  this._store = store;
663
1021
  this._maxNodes = maxNodes;
664
1022
  }
@@ -672,6 +1030,29 @@ var DomMirror = class {
672
1030
  setRoot(rootElement) {
673
1031
  this._rootElement = rootElement;
674
1032
  }
1033
+ /**
1034
+ * Enable or disable "inspect mode". When turning on, kicks off async
1035
+ * chunked materialization so the full tree becomes browsable in the
1036
+ * Elements tab without blocking the main thread.
1037
+ *
1038
+ * At BIM scale (100k-200k objects) the old synchronous loop would freeze
1039
+ * the page for 2-10s. The new approach uses requestIdleCallback to
1040
+ * spread work across idle frames (~200 nodes per idle slice, ~5ms each).
1041
+ */
1042
+ setInspectMode(on) {
1043
+ if (this._inspectMode === on) return;
1044
+ this._inspectMode = on;
1045
+ if (on) {
1046
+ this._startAsyncMaterialization();
1047
+ } else {
1048
+ this._cancelAsyncMaterialization();
1049
+ }
1050
+ r3fLog("inspect", "setInspectMode", { on, nodeCount: this._nodes.size });
1051
+ }
1052
+ /** Whether inspect mode is currently enabled. */
1053
+ getInspectMode() {
1054
+ return this._inspectMode;
1055
+ }
675
1056
  /**
676
1057
  * Build the initial DOM tree from the scene.
677
1058
  * Materializes the top 2 levels of the scene hierarchy.
@@ -700,7 +1081,7 @@ var DomMirror = class {
700
1081
  }
701
1082
  const tag = getTagForType(meta.type);
702
1083
  const element = document.createElement(tag);
703
- element.style.cssText = "display:block;position:absolute;pointer-events:none;box-sizing:border-box;";
1084
+ element.style.cssText = "display:contents;";
704
1085
  const prevAttrs = /* @__PURE__ */ new Map();
705
1086
  applyAttributes(element, meta, prevAttrs);
706
1087
  const lruNode = { uuid, prev: null, next: null };
@@ -727,15 +1108,39 @@ var DomMirror = class {
727
1108
  /**
728
1109
  * Remove a DOM node but keep JS metadata in the ObjectStore.
729
1110
  * Called by LRU eviction or when an object is removed from the scene.
1111
+ * Also dematerializes any materialized descendants so they don't become
1112
+ * orphaned entries in the LRU / _nodes maps.
730
1113
  */
731
1114
  dematerialize(uuid) {
732
1115
  const node = this._nodes.get(uuid);
733
1116
  if (!node) return;
1117
+ const descendants = this._collectMaterializedDescendants(uuid);
1118
+ for (const descUuid of descendants) {
1119
+ const descNode = this._nodes.get(descUuid);
1120
+ if (descNode) {
1121
+ this._lruRemove(descNode.lruNode);
1122
+ this._nodes.delete(descUuid);
1123
+ this._parentMap.delete(descUuid);
1124
+ }
1125
+ }
734
1126
  node.element.remove();
735
1127
  this._lruRemove(node.lruNode);
736
1128
  this._nodes.delete(uuid);
737
1129
  this._parentMap.delete(uuid);
738
1130
  }
1131
+ /**
1132
+ * Collect all materialized descendants of a uuid by walking _parentMap.
1133
+ */
1134
+ _collectMaterializedDescendants(parentUuid) {
1135
+ const result = [];
1136
+ for (const [childUuid, pUuid] of this._parentMap) {
1137
+ if (pUuid === parentUuid) {
1138
+ result.push(childUuid);
1139
+ result.push(...this._collectMaterializedDescendants(childUuid));
1140
+ }
1141
+ }
1142
+ return result;
1143
+ }
739
1144
  // -------------------------------------------------------------------------
740
1145
  // Structural updates (called by Object3D.add/remove patch)
741
1146
  // -------------------------------------------------------------------------
@@ -811,6 +1216,22 @@ var DomMirror = class {
811
1216
  }
812
1217
  return null;
813
1218
  }
1219
+ /**
1220
+ * Get or lazily materialize a DOM element for an object.
1221
+ * Also materializes the ancestor chain so the element is correctly
1222
+ * nested in the DOM tree. Used by InspectController so that
1223
+ * hover/click always produces a valid mirror element regardless
1224
+ * of whether async materialization has reached it yet.
1225
+ */
1226
+ getOrMaterialize(uuid) {
1227
+ const existing = this._nodes.get(uuid);
1228
+ if (existing) {
1229
+ this._lruTouch(existing.lruNode);
1230
+ return existing.element;
1231
+ }
1232
+ this._materializeAncestorChain(uuid);
1233
+ return this.materialize(uuid);
1234
+ }
814
1235
  /**
815
1236
  * Check if an object has a materialized DOM node.
816
1237
  */
@@ -873,6 +1294,7 @@ var DomMirror = class {
873
1294
  * Remove all materialized DOM nodes and reset state.
874
1295
  */
875
1296
  dispose() {
1297
+ this._cancelAsyncMaterialization();
876
1298
  for (const [, node] of this._nodes) {
877
1299
  node.element.remove();
878
1300
  }
@@ -905,6 +1327,78 @@ var DomMirror = class {
905
1327
  }
906
1328
  }
907
1329
  // -------------------------------------------------------------------------
1330
+ // Private: Ancestor chain materialization
1331
+ // -------------------------------------------------------------------------
1332
+ /**
1333
+ * Materialize all ancestors of a uuid from root down, so the target
1334
+ * element will be correctly nested when materialized.
1335
+ */
1336
+ _materializeAncestorChain(uuid) {
1337
+ const chain = [];
1338
+ let currentUuid = uuid;
1339
+ while (currentUuid) {
1340
+ if (this._nodes.has(currentUuid)) break;
1341
+ const meta = this._store.getByUuid(currentUuid);
1342
+ if (!meta) break;
1343
+ chain.push(currentUuid);
1344
+ currentUuid = meta.parentUuid;
1345
+ }
1346
+ for (let i = chain.length - 1; i > 0; i--) {
1347
+ this.materialize(chain[i]);
1348
+ }
1349
+ }
1350
+ // -------------------------------------------------------------------------
1351
+ // Private: Async chunked materialization for inspect mode
1352
+ // -------------------------------------------------------------------------
1353
+ _startAsyncMaterialization() {
1354
+ this._cancelAsyncMaterialization();
1355
+ const flatList = this._store.getFlatList();
1356
+ this._asyncQueue = [];
1357
+ for (const obj of flatList) {
1358
+ if (obj.userData?.__r3fdom_internal) continue;
1359
+ if (this._nodes.has(obj.uuid)) continue;
1360
+ this._asyncQueue.push(obj.uuid);
1361
+ }
1362
+ if (this._asyncQueue.length === 0) return;
1363
+ r3fLog("inspect", `Async materialization started: ${this._asyncQueue.length} objects queued`);
1364
+ this._scheduleAsyncChunk();
1365
+ }
1366
+ _cancelAsyncMaterialization() {
1367
+ if (this._asyncIdleHandle !== null) {
1368
+ if (typeof cancelIdleCallback === "function") {
1369
+ cancelIdleCallback(this._asyncIdleHandle);
1370
+ } else {
1371
+ clearTimeout(this._asyncIdleHandle);
1372
+ }
1373
+ this._asyncIdleHandle = null;
1374
+ }
1375
+ this._asyncQueue = [];
1376
+ }
1377
+ _scheduleAsyncChunk() {
1378
+ if (this._asyncQueue.length === 0) {
1379
+ this._asyncIdleHandle = null;
1380
+ r3fLog("inspect", `Async materialization complete: ${this._nodes.size} nodes materialized`);
1381
+ return;
1382
+ }
1383
+ const callback = (deadline) => {
1384
+ const hasTimeRemaining = deadline ? () => deadline.timeRemaining() > 2 : () => true;
1385
+ let processed = 0;
1386
+ while (this._asyncQueue.length > 0 && processed < this._asyncBatchSize && hasTimeRemaining()) {
1387
+ const uuid = this._asyncQueue.shift();
1388
+ if (!this._nodes.has(uuid)) {
1389
+ this.materialize(uuid);
1390
+ }
1391
+ processed++;
1392
+ }
1393
+ this._scheduleAsyncChunk();
1394
+ };
1395
+ if (typeof requestIdleCallback === "function") {
1396
+ this._asyncIdleHandle = requestIdleCallback(callback, { timeout: 100 });
1397
+ } else {
1398
+ this._asyncIdleHandle = setTimeout(callback, 16);
1399
+ }
1400
+ }
1401
+ // -------------------------------------------------------------------------
908
1402
  // Private: Selector → UUID resolution
909
1403
  // -------------------------------------------------------------------------
910
1404
  /**
@@ -1032,6 +1526,7 @@ function registerSubtree(obj, store, mirror) {
1032
1526
  function patchObject3D(store, mirror) {
1033
1527
  _activePairs.push({ store, mirror });
1034
1528
  if (!_patched) {
1529
+ r3fLog("patch", "Patching Object3D.prototype.add and .remove");
1035
1530
  _originalAdd = Object3D.prototype.add;
1036
1531
  _originalRemove = Object3D.prototype.remove;
1037
1532
  Object3D.prototype.add = function patchedAdd(...objects) {
@@ -1040,8 +1535,15 @@ function patchObject3D(store, mirror) {
1040
1535
  if (pair) {
1041
1536
  for (const obj of objects) {
1042
1537
  if (obj === this) continue;
1043
- registerSubtree(obj, pair.store, pair.mirror);
1538
+ try {
1539
+ r3fLog("patch", `patchedAdd: "${obj.name || obj.type}" added to "${this.name || this.type}"`);
1540
+ registerSubtree(obj, pair.store, pair.mirror);
1541
+ } catch (err) {
1542
+ r3fLog("patch", `patchedAdd: failed to register "${obj.name || obj.type}"`, err);
1543
+ }
1044
1544
  }
1545
+ pair.store.update(this);
1546
+ pair.store.markDirty(this);
1045
1547
  }
1046
1548
  return this;
1047
1549
  };
@@ -1050,11 +1552,18 @@ function patchObject3D(store, mirror) {
1050
1552
  if (pair) {
1051
1553
  for (const obj of objects) {
1052
1554
  if (obj === this) continue;
1053
- pair.mirror.onObjectRemoved(obj);
1054
- obj.traverse((child) => {
1055
- pair.store.unregister(child);
1056
- });
1555
+ try {
1556
+ r3fLog("patch", `patchedRemove: "${obj.name || obj.type}" removed from "${this.name || this.type}"`);
1557
+ pair.mirror.onObjectRemoved(obj);
1558
+ obj.traverse((child) => {
1559
+ pair.store.unregister(child);
1560
+ });
1561
+ } catch (err) {
1562
+ r3fLog("patch", `patchedRemove: failed to unregister "${obj.name || obj.type}"`, err);
1563
+ }
1057
1564
  }
1565
+ pair.store.update(this);
1566
+ pair.store.markDirty(this);
1058
1567
  }
1059
1568
  _originalRemove.call(this, ...objects);
1060
1569
  return this;
@@ -1075,6 +1584,7 @@ function patchObject3D(store, mirror) {
1075
1584
  }
1076
1585
  function restoreObject3D() {
1077
1586
  if (!_patched) return;
1587
+ r3fLog("patch", "Restoring original Object3D.prototype.add and .remove");
1078
1588
  if (_originalAdd) {
1079
1589
  Object3D.prototype.add = _originalAdd;
1080
1590
  _originalAdd = null;
@@ -1098,7 +1608,7 @@ function buildNodeTree(store, meta) {
1098
1608
  children.push(buildNodeTree(store, childMeta));
1099
1609
  }
1100
1610
  }
1101
- return {
1611
+ const node = {
1102
1612
  uuid: meta.uuid,
1103
1613
  name: meta.name,
1104
1614
  type: meta.type,
@@ -1109,6 +1619,12 @@ function buildNodeTree(store, meta) {
1109
1619
  scale: [...meta.scale],
1110
1620
  children
1111
1621
  };
1622
+ if (meta.geometryType) node.geometryType = meta.geometryType;
1623
+ if (meta.materialType) node.materialType = meta.materialType;
1624
+ if (meta.vertexCount != null) node.vertexCount = meta.vertexCount;
1625
+ if (meta.triangleCount != null) node.triangleCount = meta.triangleCount;
1626
+ if (meta.instanceCount != null) node.instanceCount = meta.instanceCount;
1627
+ return node;
1112
1628
  }
1113
1629
  function findRoot(store) {
1114
1630
  const allObjects = store.getFlatList();
@@ -1697,6 +2213,7 @@ function click3D(idOrUuid, options = {}) {
1697
2213
  const camera = getCamera();
1698
2214
  const gl = getRenderer();
1699
2215
  const size = getCanvasSize();
2216
+ r3fLog("click", `click3D("${idOrUuid}") \u2014 resolving projection`);
1700
2217
  const projection = projectToScreen(obj, camera, size);
1701
2218
  if (!projection) {
1702
2219
  throw new Error(
@@ -1789,6 +2306,7 @@ function hover3D(idOrUuid, options = {}) {
1789
2306
  const camera = getCamera();
1790
2307
  const gl = getRenderer();
1791
2308
  const size = getCanvasSize();
2309
+ r3fLog("hover", `hover3D("${idOrUuid}") \u2014 resolving projection`);
1792
2310
  const projection = projectToScreen(obj, camera, size);
1793
2311
  if (!projection) {
1794
2312
  throw new Error(
@@ -1823,6 +2341,7 @@ async function drag3D(idOrUuid, delta, options = {}) {
1823
2341
  const camera = getCamera();
1824
2342
  const gl = getRenderer();
1825
2343
  const size = getCanvasSize();
2344
+ r3fLog("drag", `drag3D("${idOrUuid}") mode=${mode}`, delta);
1826
2345
  const projection = projectToScreen(obj, camera, size);
1827
2346
  if (!projection) {
1828
2347
  throw new Error(
@@ -1892,35 +2411,191 @@ function pointerMiss3D(options = {}) {
1892
2411
  dispatchPointerMiss(canvas, options.point);
1893
2412
  }
1894
2413
 
1895
- // src/highlight/SelectionManager.ts
1896
- var SelectionManager = class {
1897
- constructor() {
1898
- /** Currently selected objects (ordered by selection time). */
1899
- this._selected = [];
1900
- /** Listeners notified on selection change. */
1901
- this._listeners = [];
2414
+ // src/interactions/drawPath.ts
2415
+ var _nextDrawPointerId = 5e3;
2416
+ function makeDrawPointerInit(canvas, point, pointerId, pointerType, overrides) {
2417
+ const rect = canvas.getBoundingClientRect();
2418
+ const clientX = rect.left + point.x;
2419
+ const clientY = rect.top + point.y;
2420
+ return {
2421
+ bubbles: true,
2422
+ cancelable: true,
2423
+ composed: true,
2424
+ clientX,
2425
+ clientY,
2426
+ screenX: clientX,
2427
+ screenY: clientY,
2428
+ pointerId,
2429
+ pointerType,
2430
+ isPrimary: true,
2431
+ button: 0,
2432
+ buttons: 1,
2433
+ width: 1,
2434
+ height: 1,
2435
+ pressure: point.pressure ?? 0.5,
2436
+ ...overrides
2437
+ };
2438
+ }
2439
+ async function drawPath(points, options = {}) {
2440
+ if (points.length < 2) {
2441
+ throw new Error(
2442
+ `[react-three-dom] drawPath requires at least 2 points, got ${points.length}.`
2443
+ );
1902
2444
  }
1903
- // -----------------------------------------------------------------------
1904
- // Selection API
1905
- // -----------------------------------------------------------------------
1906
- /** Select a single object (clears previous selection). */
1907
- select(obj) {
1908
- if (this._selected.length === 1 && this._selected[0] === obj) return;
1909
- this._selected = [obj];
1910
- this._notify();
2445
+ const {
2446
+ stepDelayMs = 0,
2447
+ pointerType = "mouse",
2448
+ clickAtEnd = false,
2449
+ canvas: explicitCanvas
2450
+ } = options;
2451
+ const canvas = explicitCanvas ?? getRenderer().domElement;
2452
+ const pointerId = _nextDrawPointerId++;
2453
+ let eventCount = 0;
2454
+ r3fLog("draw", `drawPath \u2014 ${points.length} points, delay=${stepDelayMs}ms, pointerType=${pointerType}`);
2455
+ const first = points[0];
2456
+ canvas.dispatchEvent(
2457
+ new PointerEvent("pointerdown", makeDrawPointerInit(canvas, first, pointerId, pointerType))
2458
+ );
2459
+ eventCount++;
2460
+ for (let i = 1; i < points.length; i++) {
2461
+ if (stepDelayMs > 0) {
2462
+ await sleep2(stepDelayMs);
2463
+ }
2464
+ canvas.dispatchEvent(
2465
+ new PointerEvent(
2466
+ "pointermove",
2467
+ makeDrawPointerInit(canvas, points[i], pointerId, pointerType)
2468
+ )
2469
+ );
2470
+ eventCount++;
1911
2471
  }
1912
- /** Add an object to the current selection (multi-select). */
1913
- addToSelection(obj) {
1914
- if (this._selected.includes(obj)) return;
1915
- this._selected.push(obj);
1916
- this._notify();
2472
+ const last = points[points.length - 1];
2473
+ canvas.dispatchEvent(
2474
+ new PointerEvent(
2475
+ "pointerup",
2476
+ makeDrawPointerInit(canvas, last, pointerId, pointerType, {
2477
+ buttons: 0,
2478
+ pressure: 0
2479
+ })
2480
+ )
2481
+ );
2482
+ eventCount++;
2483
+ if (clickAtEnd) {
2484
+ const rect = canvas.getBoundingClientRect();
2485
+ canvas.dispatchEvent(
2486
+ new MouseEvent("click", {
2487
+ bubbles: true,
2488
+ cancelable: true,
2489
+ clientX: rect.left + last.x,
2490
+ clientY: rect.top + last.y,
2491
+ button: 0
2492
+ })
2493
+ );
2494
+ eventCount++;
1917
2495
  }
1918
- /** Remove an object from the selection. */
1919
- removeFromSelection(obj) {
1920
- const idx = this._selected.indexOf(obj);
1921
- if (idx === -1) return;
1922
- this._selected.splice(idx, 1);
1923
- this._notify();
2496
+ r3fLog("draw", `drawPath complete \u2014 ${eventCount} events dispatched`);
2497
+ return {
2498
+ eventCount,
2499
+ pointCount: points.length,
2500
+ startPoint: first,
2501
+ endPoint: last
2502
+ };
2503
+ }
2504
+ function linePath(start, end, steps = 10, pressure = 0.5) {
2505
+ const points = [];
2506
+ const totalSteps = steps + 1;
2507
+ for (let i = 0; i <= totalSteps; i++) {
2508
+ const t = i / totalSteps;
2509
+ points.push({
2510
+ x: start.x + (end.x - start.x) * t,
2511
+ y: start.y + (end.y - start.y) * t,
2512
+ pressure
2513
+ });
2514
+ }
2515
+ return points;
2516
+ }
2517
+ function curvePath(start, control, end, steps = 20, pressure = 0.5) {
2518
+ const points = [];
2519
+ for (let i = 0; i <= steps; i++) {
2520
+ const t = i / steps;
2521
+ const invT = 1 - t;
2522
+ points.push({
2523
+ x: invT * invT * start.x + 2 * invT * t * control.x + t * t * end.x,
2524
+ y: invT * invT * start.y + 2 * invT * t * control.y + t * t * end.y,
2525
+ pressure
2526
+ });
2527
+ }
2528
+ return points;
2529
+ }
2530
+ function rectPath(topLeft, bottomRight, pointsPerSide = 5, pressure = 0.5) {
2531
+ const topRight = { x: bottomRight.x, y: topLeft.y };
2532
+ const bottomLeft = { x: topLeft.x, y: bottomRight.y };
2533
+ const sides = [
2534
+ [topLeft, topRight],
2535
+ [topRight, bottomRight],
2536
+ [bottomRight, bottomLeft],
2537
+ [bottomLeft, topLeft]
2538
+ ];
2539
+ const points = [];
2540
+ for (const [from, to] of sides) {
2541
+ for (let i = 0; i < pointsPerSide; i++) {
2542
+ const t = i / pointsPerSide;
2543
+ points.push({
2544
+ x: from.x + (to.x - from.x) * t,
2545
+ y: from.y + (to.y - from.y) * t,
2546
+ pressure
2547
+ });
2548
+ }
2549
+ }
2550
+ points.push({ x: topLeft.x, y: topLeft.y, pressure });
2551
+ return points;
2552
+ }
2553
+ function circlePath(center, radiusX, radiusY, steps = 36, pressure = 0.5) {
2554
+ const ry = radiusY ?? radiusX;
2555
+ const points = [];
2556
+ for (let i = 0; i <= steps; i++) {
2557
+ const angle = i / steps * Math.PI * 2;
2558
+ points.push({
2559
+ x: center.x + Math.cos(angle) * radiusX,
2560
+ y: center.y + Math.sin(angle) * ry,
2561
+ pressure
2562
+ });
2563
+ }
2564
+ return points;
2565
+ }
2566
+ function sleep2(ms) {
2567
+ return new Promise((resolve) => setTimeout(resolve, ms));
2568
+ }
2569
+
2570
+ // src/highlight/SelectionManager.ts
2571
+ var SelectionManager = class {
2572
+ constructor() {
2573
+ /** Currently selected objects (ordered by selection time). */
2574
+ this._selected = [];
2575
+ /** Listeners notified on selection change. */
2576
+ this._listeners = [];
2577
+ }
2578
+ // -----------------------------------------------------------------------
2579
+ // Selection API
2580
+ // -----------------------------------------------------------------------
2581
+ /** Select a single object (clears previous selection). */
2582
+ select(obj) {
2583
+ if (this._selected.length === 1 && this._selected[0] === obj) return;
2584
+ this._selected = [obj];
2585
+ this._notify();
2586
+ }
2587
+ /** Add an object to the current selection (multi-select). */
2588
+ addToSelection(obj) {
2589
+ if (this._selected.includes(obj)) return;
2590
+ this._selected.push(obj);
2591
+ this._notify();
2592
+ }
2593
+ /** Remove an object from the selection. */
2594
+ removeFromSelection(obj) {
2595
+ const idx = this._selected.indexOf(obj);
2596
+ if (idx === -1) return;
2597
+ this._selected.splice(idx, 1);
2598
+ this._notify();
1924
2599
  }
1925
2600
  /** Toggle an object in/out of the selection. */
1926
2601
  toggleSelection(obj) {
@@ -1977,579 +2652,324 @@ var SelectionManager = class {
1977
2652
  this._listeners = [];
1978
2653
  }
1979
2654
  };
1980
-
1981
- // src/bridge/ThreeDom.tsx
1982
- var _store3 = null;
1983
- var _mirror = null;
1984
- var _selectionManager = null;
1985
- var _highlighter = null;
1986
- function getStore2() {
1987
- return _store3;
1988
- }
1989
- function getMirror() {
1990
- return _mirror;
1991
- }
1992
- function getSelectionManager() {
1993
- return _selectionManager;
1994
- }
1995
- function getHighlighter() {
1996
- return _highlighter;
1997
- }
1998
- var _box = /* @__PURE__ */ new Box3();
1999
- var _v = /* @__PURE__ */ new Vector3();
2000
- var _corners = Array.from({ length: 8 }, () => new Vector3());
2001
- function projectToScreenRect(obj, camera, canvasRect) {
2002
- _box.setFromObject(obj);
2003
- if (_box.isEmpty()) return null;
2004
- const { min, max } = _box;
2005
- _corners[0].set(min.x, min.y, min.z);
2006
- _corners[1].set(min.x, min.y, max.z);
2007
- _corners[2].set(min.x, max.y, min.z);
2008
- _corners[3].set(min.x, max.y, max.z);
2009
- _corners[4].set(max.x, min.y, min.z);
2010
- _corners[5].set(max.x, min.y, max.z);
2011
- _corners[6].set(max.x, max.y, min.z);
2012
- _corners[7].set(max.x, max.y, max.z);
2013
- let sxMin = Infinity, syMin = Infinity;
2014
- let sxMax = -Infinity, syMax = -Infinity;
2015
- let anyInFront = false;
2016
- let anyBehind = false;
2017
- for (const corner of _corners) {
2018
- _v.copy(corner).project(camera);
2019
- if (_v.z >= 1) {
2020
- anyBehind = true;
2021
- continue;
2022
- }
2023
- anyInFront = true;
2024
- const sx = (_v.x + 1) / 2 * canvasRect.width;
2025
- const sy = (1 - _v.y) / 2 * canvasRect.height;
2026
- sxMin = Math.min(sxMin, sx);
2027
- syMin = Math.min(syMin, sy);
2028
- sxMax = Math.max(sxMax, sx);
2029
- syMax = Math.max(syMax, sy);
2030
- }
2031
- if (!anyInFront) return null;
2032
- if (anyBehind) {
2033
- sxMin = Math.min(sxMin, 0);
2034
- syMin = Math.min(syMin, 0);
2035
- sxMax = Math.max(sxMax, canvasRect.width);
2036
- syMax = Math.max(syMax, canvasRect.height);
2037
- }
2038
- sxMin = Math.max(0, sxMin);
2039
- syMin = Math.max(0, syMin);
2040
- sxMax = Math.min(canvasRect.width, sxMax);
2041
- syMax = Math.min(canvasRect.height, syMax);
2042
- const w = sxMax - sxMin;
2043
- const h = syMax - syMin;
2044
- if (w < 1 || h < 1) return null;
2045
- return { left: sxMin, top: syMin, width: w, height: h };
2046
- }
2047
- function exposeGlobalAPI(store) {
2048
- const api = {
2049
- getByTestId: (id) => store.getByTestId(id),
2050
- getByUuid: (uuid) => store.getByUuid(uuid),
2051
- getByName: (name) => store.getByName(name),
2052
- getCount: () => store.getCount(),
2053
- snapshot: () => createSnapshot(store),
2054
- inspect: (idOrUuid) => store.inspect(idOrUuid),
2055
- click: (idOrUuid) => {
2056
- click3D(idOrUuid);
2057
- },
2058
- doubleClick: (idOrUuid) => {
2059
- doubleClick3D(idOrUuid);
2060
- },
2061
- contextMenu: (idOrUuid) => {
2062
- contextMenu3D(idOrUuid);
2063
- },
2064
- hover: (idOrUuid) => {
2065
- hover3D(idOrUuid);
2066
- },
2067
- drag: (idOrUuid, delta) => {
2068
- void drag3D(idOrUuid, delta);
2069
- },
2070
- wheel: (idOrUuid, options) => {
2071
- wheel3D(idOrUuid, options);
2072
- },
2073
- pointerMiss: () => {
2074
- pointerMiss3D();
2075
- },
2076
- select: (idOrUuid) => {
2077
- const obj = store.getObject3D(idOrUuid);
2078
- if (obj && _selectionManager) _selectionManager.select(obj);
2079
- },
2080
- clearSelection: () => {
2081
- _selectionManager?.clearSelection();
2082
- },
2083
- getObject3D: (idOrUuid) => store.getObject3D(idOrUuid),
2084
- version
2085
- };
2086
- window.__R3F_DOM__ = api;
2655
+ var HOVER_FILL_COLOR = 7317724;
2656
+ var HOVER_FILL_OPACITY = 0.66;
2657
+ var SELECTION_FILL_COLOR = 7317724;
2658
+ var SELECTION_FILL_OPACITY = 0.75;
2659
+ var SELECTION_BBOX_COLOR = 7317724;
2660
+ var SELECTION_BBOX_OPACITY = 0.3;
2661
+ var _box33 = /* @__PURE__ */ new Box3();
2662
+ function hasRenderableGeometry(obj) {
2663
+ return obj.isMesh === true || obj.isLine === true || obj.isPoints === true;
2664
+ }
2665
+ function collectHighlightTargets(obj) {
2666
+ if (hasRenderableGeometry(obj)) return [obj];
2667
+ const targets = [];
2668
+ obj.traverse((child) => {
2669
+ if (child === obj) return;
2670
+ if (child.userData?.__r3fdom_internal) return;
2671
+ if (hasRenderableGeometry(child)) targets.push(child);
2672
+ });
2673
+ return targets;
2087
2674
  }
2088
- function removeGlobalAPI() {
2089
- delete window.__R3F_DOM__;
2675
+ function markInternal(obj) {
2676
+ obj.userData.__r3fdom_internal = true;
2677
+ obj.raycast = () => {
2678
+ };
2679
+ obj.traverse((child) => {
2680
+ child.userData.__r3fdom_internal = true;
2681
+ child.raycast = () => {
2682
+ };
2683
+ });
2090
2684
  }
2091
- function setElementRect(el, l, t, w, h) {
2092
- const d = el.dataset;
2093
- if (d._l !== String(l) || d._t !== String(t) || d._w !== String(w) || d._h !== String(h)) {
2094
- el.style.left = `${l}px`;
2095
- el.style.top = `${t}px`;
2096
- el.style.width = `${w}px`;
2097
- el.style.height = `${h}px`;
2098
- d._l = String(l);
2099
- d._t = String(t);
2100
- d._w = String(w);
2101
- d._h = String(h);
2102
- }
2685
+ function _syncGroupTransform(source, highlightRoot) {
2686
+ source.updateWorldMatrix(true, false);
2687
+ source.matrixWorld.decompose(
2688
+ highlightRoot.position,
2689
+ highlightRoot.quaternion,
2690
+ highlightRoot.scale
2691
+ );
2103
2692
  }
2104
- function ThreeDom({
2105
- root = "#three-dom-root",
2106
- batchSize = 500,
2107
- timeBudgetMs = 0.5,
2108
- maxDomNodes = 2e3,
2109
- initialDepth = 3,
2110
- enabled = true
2111
- } = {}) {
2112
- const scene = useThree((s) => s.scene);
2113
- const camera = useThree((s) => s.camera);
2114
- const gl = useThree((s) => s.gl);
2115
- const size = useThree((s) => s.size);
2116
- const cursorRef = useRef(0);
2117
- const positionCursorRef = useRef(0);
2118
- useEffect(() => {
2119
- if (!enabled) return;
2120
- const canvas = gl.domElement;
2121
- const canvasParent = canvas.parentElement;
2122
- let rootElement = null;
2123
- let createdRoot = false;
2124
- if (typeof root === "string") {
2125
- rootElement = document.querySelector(root);
2126
- } else {
2127
- rootElement = root;
2128
- }
2129
- if (!rootElement) {
2130
- rootElement = document.createElement("div");
2131
- rootElement.id = typeof root === "string" ? root.replace(/^#/, "") : "three-dom-root";
2132
- createdRoot = true;
2693
+ function _attachRenderSync(source, highlightRoot) {
2694
+ highlightRoot.matrixAutoUpdate = false;
2695
+ highlightRoot.updateMatrixWorld = (force) => {
2696
+ source.updateWorldMatrix(true, false);
2697
+ highlightRoot.matrixWorld.copy(source.matrixWorld);
2698
+ for (const child of highlightRoot.children) {
2699
+ child.updateMatrixWorld(force);
2133
2700
  }
2134
- canvasParent.style.position = canvasParent.style.position || "relative";
2135
- canvasParent.appendChild(rootElement);
2136
- rootElement.style.cssText = [
2137
- "position: absolute",
2138
- "top: 0",
2139
- "left: 0",
2140
- "width: 100%",
2141
- "height: 100%",
2142
- "pointer-events: none",
2143
- "overflow: hidden",
2144
- "z-index: 10"
2145
- ].join(";");
2146
- const store = new ObjectStore();
2147
- const mirror = new DomMirror(store, maxDomNodes);
2148
- mirror.setRoot(rootElement);
2149
- ensureCustomElements(store);
2150
- store.registerTree(scene);
2151
- mirror.materializeSubtree(scene.uuid, initialDepth);
2152
- const unpatch = patchObject3D(store, mirror);
2153
- setInteractionState(store, camera, gl, size);
2154
- const selectionManager = new SelectionManager();
2155
- _selectionManager = selectionManager;
2156
- _highlighter = null;
2157
- exposeGlobalAPI(store);
2158
- _store3 = store;
2159
- _mirror = mirror;
2160
- const initialCanvasRect = canvas.getBoundingClientRect();
2161
- const allObjects = store.getFlatList();
2162
- for (const obj of allObjects) {
2163
- if (obj.userData?.__r3fdom_internal) continue;
2164
- const el = mirror.getElement(obj.uuid);
2165
- if (!el) continue;
2166
- if (obj.type === "Scene") {
2167
- setElementRect(el, 0, 0, Math.round(initialCanvasRect.width), Math.round(initialCanvasRect.height));
2168
- continue;
2169
- }
2170
- const rect = projectToScreenRect(obj, camera, initialCanvasRect);
2171
- if (rect) {
2172
- let parentLeft = 0;
2173
- let parentTop = 0;
2174
- if (obj.parent && obj.parent.type !== "Scene") {
2175
- const parentRect = projectToScreenRect(obj.parent, camera, initialCanvasRect);
2176
- if (parentRect) {
2177
- parentLeft = Math.round(parentRect.left);
2178
- parentTop = Math.round(parentRect.top);
2179
- }
2180
- }
2181
- setElementRect(
2182
- el,
2183
- Math.round(rect.left) - parentLeft,
2184
- Math.round(rect.top) - parentTop,
2185
- Math.round(rect.width),
2186
- Math.round(rect.height)
2187
- );
2701
+ };
2702
+ }
2703
+ function createHighlightMesh(source, fillColor, fillOpacity) {
2704
+ const geom = source.geometry;
2705
+ if (!geom) return null;
2706
+ const group = new Object3D();
2707
+ const disposables = [];
2708
+ const fillMat = new MeshBasicMaterial({
2709
+ color: fillColor,
2710
+ transparent: true,
2711
+ opacity: fillOpacity,
2712
+ depthTest: false,
2713
+ depthWrite: false,
2714
+ side: DoubleSide
2715
+ });
2716
+ disposables.push(fillMat);
2717
+ const fillMesh = new Mesh(geom, fillMat);
2718
+ fillMesh.scale.setScalar(1.005);
2719
+ fillMesh.raycast = () => {
2720
+ };
2721
+ group.add(fillMesh);
2722
+ source.updateWorldMatrix(true, false);
2723
+ source.matrixWorld.decompose(group.position, group.quaternion, group.scale);
2724
+ markInternal(group);
2725
+ _attachRenderSync(source, group);
2726
+ return { root: group, disposables };
2727
+ }
2728
+ function createBoundingBoxHighlight(obj) {
2729
+ _box33.makeEmpty();
2730
+ const targets = collectHighlightTargets(obj);
2731
+ if (targets.length === 0) return null;
2732
+ for (const target of targets) {
2733
+ target.updateWorldMatrix(true, false);
2734
+ const geom = target.geometry;
2735
+ if (geom) {
2736
+ if (!geom.boundingBox) geom.computeBoundingBox();
2737
+ if (geom.boundingBox) {
2738
+ const childBox = geom.boundingBox.clone();
2739
+ childBox.applyMatrix4(target.matrixWorld);
2740
+ _box33.union(childBox);
2188
2741
  }
2189
2742
  }
2190
- return () => {
2191
- unpatch();
2192
- removeGlobalAPI();
2193
- clearInteractionState();
2194
- selectionManager.dispose();
2195
- mirror.dispose();
2196
- store.dispose();
2197
- if (createdRoot && rootElement?.parentNode) {
2198
- rootElement.parentNode.removeChild(rootElement);
2199
- }
2200
- _store3 = null;
2201
- _mirror = null;
2202
- _selectionManager = null;
2203
- _highlighter = null;
2204
- };
2205
- }, [scene, camera, gl, size, enabled, root, maxDomNodes, initialDepth]);
2206
- useFrame(() => {
2207
- if (!enabled || !_store3 || !_mirror) return;
2208
- setInteractionState(_store3, camera, gl, size);
2209
- const store = _store3;
2210
- const mirror = _mirror;
2211
- const canvas = gl.domElement;
2212
- const canvasRect = canvas.getBoundingClientRect();
2213
- const start = performance.now();
2214
- const dirtyObjects = store.drainDirtyQueue();
2215
- for (const obj of dirtyObjects) {
2216
- store.update(obj);
2217
- mirror.syncAttributes(obj);
2218
- }
2219
- const budgetRemaining = timeBudgetMs - (performance.now() - start);
2220
- if (budgetRemaining > 0.1) {
2221
- const objects2 = store.getFlatList();
2222
- if (objects2.length > 0) {
2223
- const end = Math.min(cursorRef.current + batchSize, objects2.length);
2224
- for (let i = cursorRef.current; i < end; i++) {
2225
- if (performance.now() - start > timeBudgetMs) break;
2226
- const obj = objects2[i];
2227
- const changed = store.update(obj);
2228
- if (changed) mirror.syncAttributes(obj);
2743
+ }
2744
+ if (_box33.isEmpty()) return null;
2745
+ const size = _box33.getSize(new Vector3());
2746
+ const center = _box33.getCenter(new Vector3());
2747
+ const disposables = [];
2748
+ const boxGeom = new BoxGeometry(size.x, size.y, size.z);
2749
+ disposables.push(boxGeom);
2750
+ const fillMat = new MeshBasicMaterial({
2751
+ color: SELECTION_BBOX_COLOR,
2752
+ transparent: true,
2753
+ opacity: SELECTION_BBOX_OPACITY,
2754
+ depthTest: false,
2755
+ depthWrite: false,
2756
+ side: DoubleSide
2757
+ });
2758
+ disposables.push(fillMat);
2759
+ const fillMesh = new Mesh(boxGeom, fillMat);
2760
+ fillMesh.raycast = () => {
2761
+ };
2762
+ const edgesGeom = new EdgesGeometry(boxGeom);
2763
+ disposables.push(edgesGeom);
2764
+ const edgeMat = new LineBasicMaterial({
2765
+ color: SELECTION_BBOX_COLOR,
2766
+ transparent: true,
2767
+ opacity: 0.5,
2768
+ depthTest: false,
2769
+ depthWrite: false
2770
+ });
2771
+ disposables.push(edgeMat);
2772
+ const edgeMesh = new LineSegments(edgesGeom, edgeMat);
2773
+ edgeMesh.raycast = () => {
2774
+ };
2775
+ const group = new Object3D();
2776
+ group.add(fillMesh);
2777
+ group.add(edgeMesh);
2778
+ group.position.copy(center);
2779
+ group.renderOrder = 998;
2780
+ markInternal(group);
2781
+ group.matrixAutoUpdate = false;
2782
+ group.updateMatrixWorld = (force) => {
2783
+ _box33.makeEmpty();
2784
+ for (const target of targets) {
2785
+ target.updateWorldMatrix(true, false);
2786
+ const g = target.geometry;
2787
+ if (g) {
2788
+ if (!g.boundingBox) g.computeBoundingBox();
2789
+ if (g.boundingBox) {
2790
+ const childBox = g.boundingBox.clone();
2791
+ childBox.applyMatrix4(target.matrixWorld);
2792
+ _box33.union(childBox);
2229
2793
  }
2230
- cursorRef.current = end >= objects2.length ? 0 : end;
2231
2794
  }
2232
2795
  }
2233
- const objects = store.getFlatList();
2234
- if (objects.length > 0) {
2235
- const posEnd = Math.min(positionCursorRef.current + 50, objects.length);
2236
- for (let i = positionCursorRef.current; i < posEnd; i++) {
2237
- const obj = objects[i];
2238
- if (obj.userData?.__r3fdom_internal) continue;
2239
- const el = mirror.getElement(obj.uuid);
2240
- if (!el) continue;
2241
- if (obj.type === "Scene") {
2242
- setElementRect(el, 0, 0, Math.round(canvasRect.width), Math.round(canvasRect.height));
2243
- continue;
2244
- }
2245
- const rect = projectToScreenRect(obj, camera, canvasRect);
2246
- if (rect) {
2247
- let parentLeft = 0;
2248
- let parentTop = 0;
2249
- if (obj.parent && obj.parent.type !== "Scene") {
2250
- const parentRect = projectToScreenRect(obj.parent, camera, canvasRect);
2251
- if (parentRect) {
2252
- parentLeft = Math.round(parentRect.left);
2253
- parentTop = Math.round(parentRect.top);
2254
- }
2255
- }
2256
- const l = Math.round(rect.left) - parentLeft;
2257
- const t = Math.round(rect.top) - parentTop;
2258
- const w = Math.round(rect.width);
2259
- const h = Math.round(rect.height);
2260
- setElementRect(el, l, t, w, h);
2261
- if (el.style.display === "none") el.style.display = "block";
2262
- } else {
2263
- if (el.style.display !== "none") el.style.display = "none";
2264
- }
2265
- }
2266
- positionCursorRef.current = posEnd >= objects.length ? 0 : posEnd;
2796
+ if (!_box33.isEmpty()) {
2797
+ const s = _box33.getSize(new Vector3());
2798
+ const c = _box33.getCenter(new Vector3());
2799
+ group.position.copy(c);
2800
+ group.scale.set(
2801
+ s.x / size.x || 1,
2802
+ s.y / size.y || 1,
2803
+ s.z / size.z || 1
2804
+ );
2805
+ }
2806
+ group.updateMatrix();
2807
+ group.matrixWorld.copy(group.matrix);
2808
+ for (const child of group.children) {
2809
+ child.updateMatrixWorld(force);
2267
2810
  }
2268
- });
2269
- return null;
2270
- }
2271
- var COLORS = {
2272
- /** Content area — same blue as Chrome DevTools element highlight */
2273
- content: "rgba(111, 168, 220, 0.66)",
2274
- /** Slightly dimmer for children of a selected parent */
2275
- contentChild: "rgba(111, 168, 220, 0.33)",
2276
- /** Hover highlight — lighter blue */
2277
- hover: "rgba(111, 168, 220, 0.4)",
2278
- /** Tooltip background */
2279
- tooltipBg: "rgba(36, 36, 36, 0.9)",
2280
- /** Tooltip text */
2281
- tooltipText: "#fff",
2282
- /** Tooltip tag color */
2283
- tooltipTag: "#e776e0",
2284
- /** Tooltip dimensions color */
2285
- tooltipDim: "#c5c5c5",
2286
- /** Border for selected elements */
2287
- border: "rgba(111, 168, 220, 0.9)"
2288
- };
2289
- var _box2 = /* @__PURE__ */ new Box3();
2290
- var _v2 = /* @__PURE__ */ new Vector3();
2291
- var _corners2 = Array.from({ length: 8 }, () => new Vector3());
2292
- function projectBoundsToScreen(obj, camera, canvas) {
2293
- _box2.setFromObject(obj);
2294
- if (_box2.isEmpty()) return null;
2295
- const { min, max } = _box2;
2296
- _corners2[0].set(min.x, min.y, min.z);
2297
- _corners2[1].set(min.x, min.y, max.z);
2298
- _corners2[2].set(min.x, max.y, min.z);
2299
- _corners2[3].set(min.x, max.y, max.z);
2300
- _corners2[4].set(max.x, min.y, min.z);
2301
- _corners2[5].set(max.x, min.y, max.z);
2302
- _corners2[6].set(max.x, max.y, min.z);
2303
- _corners2[7].set(max.x, max.y, max.z);
2304
- const rect = canvas.getBoundingClientRect();
2305
- let screenMinX = Infinity;
2306
- let screenMinY = Infinity;
2307
- let screenMaxX = -Infinity;
2308
- let screenMaxY = -Infinity;
2309
- let allBehind = true;
2310
- for (const corner of _corners2) {
2311
- _v2.copy(corner).project(camera);
2312
- if (_v2.z < 1) allBehind = false;
2313
- const sx = (_v2.x + 1) / 2 * rect.width;
2314
- const sy = (1 - _v2.y) / 2 * rect.height;
2315
- screenMinX = Math.min(screenMinX, sx);
2316
- screenMinY = Math.min(screenMinY, sy);
2317
- screenMaxX = Math.max(screenMaxX, sx);
2318
- screenMaxY = Math.max(screenMaxY, sy);
2319
- }
2320
- if (allBehind) return null;
2321
- screenMinX = Math.max(0, screenMinX);
2322
- screenMinY = Math.max(0, screenMinY);
2323
- screenMaxX = Math.min(rect.width, screenMaxX);
2324
- screenMaxY = Math.min(rect.height, screenMaxY);
2325
- const width = screenMaxX - screenMinX;
2326
- const height = screenMaxY - screenMinY;
2327
- if (width < 1 || height < 1) return null;
2328
- return {
2329
- left: rect.left + screenMinX,
2330
- top: rect.top + screenMinY,
2331
- width,
2332
- height
2333
2811
  };
2334
- }
2335
- function getObjectLabel2(obj) {
2336
- const tag = `three-${obj.type.toLowerCase()}`;
2337
- const parts = [tag];
2338
- if (obj.name) {
2339
- parts.push(`.${obj.name}`);
2340
- }
2341
- const testId = obj.userData?.testId;
2342
- if (testId) {
2343
- parts.push(`[testId="${testId}"]`);
2344
- }
2345
- return parts.join("");
2346
- }
2347
- function getObjectDimensions(obj) {
2348
- _box2.setFromObject(obj);
2349
- if (_box2.isEmpty()) return "";
2350
- const size = _box2.getSize(new Vector3());
2351
- return `${size.x.toFixed(1)} \xD7 ${size.y.toFixed(1)} \xD7 ${size.z.toFixed(1)}`;
2352
- }
2353
- function createOverlayElement(color, showBorder) {
2354
- const el = document.createElement("div");
2355
- el.style.cssText = `
2356
- position: fixed;
2357
- pointer-events: none;
2358
- z-index: 99998;
2359
- background: ${color};
2360
- ${showBorder ? `border: 1px solid ${COLORS.border};` : ""}
2361
- transition: all 0.05s ease-out;
2362
- box-sizing: border-box;
2363
- `;
2364
- return el;
2365
- }
2366
- function createTooltipElement() {
2367
- const el = document.createElement("div");
2368
- el.style.cssText = `
2369
- position: fixed;
2370
- pointer-events: none;
2371
- z-index: 99999;
2372
- background: ${COLORS.tooltipBg};
2373
- color: ${COLORS.tooltipText};
2374
- font-family: 'SF Mono', Monaco, monospace;
2375
- font-size: 11px;
2376
- padding: 4px 8px;
2377
- border-radius: 3px;
2378
- white-space: nowrap;
2379
- line-height: 1.4;
2380
- box-shadow: 0 2px 8px rgba(0,0,0,0.3);
2381
- `;
2382
- return el;
2383
- }
2384
- function positionOverlay(entry, rect) {
2385
- const { overlayEl, tooltipEl } = entry;
2386
- overlayEl.style.left = `${rect.left}px`;
2387
- overlayEl.style.top = `${rect.top}px`;
2388
- overlayEl.style.width = `${rect.width}px`;
2389
- overlayEl.style.height = `${rect.height}px`;
2390
- overlayEl.style.display = "block";
2391
- tooltipEl.style.left = `${rect.left}px`;
2392
- tooltipEl.style.top = `${Math.max(0, rect.top - 28)}px`;
2393
- tooltipEl.style.display = "block";
2394
- }
2395
- function hideOverlay(entry) {
2396
- entry.overlayEl.style.display = "none";
2397
- entry.tooltipEl.style.display = "none";
2812
+ return { root: group, disposables };
2398
2813
  }
2399
2814
  var Highlighter = class {
2400
- constructor(options = {}) {
2401
- /** Selected object overlays (persistent until deselected) */
2402
- this._selectedEntries = /* @__PURE__ */ new Map();
2403
- /** Hover overlay (temporary, single object at a time) */
2404
- this._hoverEntries = /* @__PURE__ */ new Map();
2405
- this._camera = null;
2406
- this._renderer = null;
2815
+ constructor(_options = {}) {
2816
+ this._scene = null;
2407
2817
  this._unsubscribe = null;
2408
- /** DevTools hover polling interval */
2818
+ this._hoverEntries = [];
2819
+ this._hoverTarget = null;
2820
+ this._selectedEntries = /* @__PURE__ */ new Map();
2409
2821
  this._hoverPollId = null;
2410
- this._lastHoveredElement = null;
2411
- /** Store reference for resolving objects */
2822
+ this._lastHoveredUuid = null;
2412
2823
  this._store = null;
2413
- this._showTooltip = options.showTooltip ?? true;
2414
2824
  }
2415
2825
  // -----------------------------------------------------------------------
2416
2826
  // Lifecycle
2417
2827
  // -----------------------------------------------------------------------
2418
- attach(_scene, selectionManager, camera, renderer, store) {
2828
+ /** Bind to a scene and selection manager, start hover polling. */
2829
+ attach(scene, selectionManager, _camera2, _renderer, store) {
2419
2830
  this.detach();
2420
- this._camera = camera;
2421
- this._renderer = renderer;
2831
+ this._scene = scene;
2422
2832
  this._store = store;
2423
2833
  this._unsubscribe = selectionManager.subscribe((selected) => {
2424
- this._syncSelectedHighlights(selected);
2834
+ this._syncSelectionHighlights(selected);
2425
2835
  });
2426
- this._syncSelectedHighlights([...selectionManager.getSelected()]);
2836
+ this._syncSelectionHighlights([...selectionManager.getSelected()]);
2427
2837
  this._startHoverPolling();
2428
2838
  }
2839
+ /** Unbind from the scene, stop polling, and remove all highlights. */
2429
2840
  detach() {
2430
2841
  if (this._unsubscribe) {
2431
2842
  this._unsubscribe();
2432
2843
  this._unsubscribe = null;
2433
2844
  }
2434
2845
  this._stopHoverPolling();
2435
- this._clearAllOverlays(this._selectedEntries);
2436
- this._clearAllOverlays(this._hoverEntries);
2437
- this._camera = null;
2438
- this._renderer = null;
2846
+ this.clearHoverHighlight();
2847
+ this._clearAllSelectionHighlights();
2848
+ this._scene = null;
2439
2849
  this._store = null;
2440
2850
  }
2441
2851
  // -----------------------------------------------------------------------
2442
- // Per-frame update — reposition all overlays to follow camera/objects
2852
+ // Per-frame update
2443
2853
  // -----------------------------------------------------------------------
2854
+ /** Sync highlight group transforms to their source objects. Call each frame. */
2444
2855
  update() {
2445
- if (!this._camera || !this._renderer) return;
2446
- const canvas = this._renderer.domElement;
2447
- for (const entry of this._selectedEntries.values()) {
2448
- const rect = projectBoundsToScreen(entry.target, this._camera, canvas);
2449
- if (rect) {
2450
- positionOverlay(entry, rect);
2451
- } else {
2452
- hideOverlay(entry);
2453
- }
2454
- }
2455
- for (const entry of this._hoverEntries.values()) {
2456
- const rect = projectBoundsToScreen(entry.target, this._camera, canvas);
2457
- if (rect) {
2458
- positionOverlay(entry, rect);
2459
- } else {
2460
- hideOverlay(entry);
2856
+ for (const entry of this._hoverEntries) {
2857
+ _syncGroupTransform(entry.source, entry.group.root);
2858
+ }
2859
+ for (const selEntry of this._selectedEntries.values()) {
2860
+ for (const mg of selEntry.meshGroups) {
2861
+ const src = mg.root.userData.__r3fdom_source;
2862
+ if (src) {
2863
+ _syncGroupTransform(src, mg.root);
2864
+ }
2461
2865
  }
2462
2866
  }
2463
2867
  }
2464
2868
  // -----------------------------------------------------------------------
2465
- // Public API
2869
+ // Public API: hover highlight
2466
2870
  // -----------------------------------------------------------------------
2467
- highlight(obj) {
2468
- this._addSelectedHighlight(obj, false);
2871
+ /** Show a hover highlight on the given object (replaces any previous hover). */
2872
+ showHoverHighlight(obj) {
2873
+ if (obj === this._hoverTarget) return;
2874
+ this._clearHoverVisuals();
2875
+ if (!this._scene) return;
2876
+ this._hoverTarget = obj;
2877
+ const targets = collectHighlightTargets(obj);
2878
+ for (const target of targets) {
2879
+ const hg = createHighlightMesh(target, HOVER_FILL_COLOR, HOVER_FILL_OPACITY);
2880
+ if (hg) {
2881
+ hg.root.renderOrder = 997;
2882
+ this._scene.add(hg.root);
2883
+ this._hoverEntries.push({ source: target, group: hg });
2884
+ }
2885
+ }
2469
2886
  }
2470
- unhighlight(obj) {
2471
- this._removeOverlay(obj, this._selectedEntries);
2887
+ /** Remove the current hover highlight. */
2888
+ clearHoverHighlight() {
2889
+ this._clearHoverVisuals();
2890
+ this._lastHoveredUuid = null;
2472
2891
  }
2473
- clearAll() {
2474
- this._clearAllOverlays(this._selectedEntries);
2475
- this._clearAllOverlays(this._hoverEntries);
2892
+ _clearHoverVisuals() {
2893
+ for (const entry of this._hoverEntries) {
2894
+ this._disposeHighlightGroup(entry.group);
2895
+ }
2896
+ this._hoverEntries = [];
2897
+ this._hoverTarget = null;
2476
2898
  }
2899
+ // -----------------------------------------------------------------------
2900
+ // Public API: queries
2901
+ // -----------------------------------------------------------------------
2902
+ /** Check if an object currently has a selection highlight. */
2477
2903
  isHighlighted(obj) {
2478
2904
  return this._selectedEntries.has(obj);
2479
2905
  }
2480
- /** Show a temporary hover highlight for an object and its children */
2481
- showHoverHighlight(obj) {
2482
- this._clearAllOverlays(this._hoverEntries);
2483
- this._addHoverHighlightRecursive(obj);
2484
- }
2485
- /** Clear the hover highlight */
2486
- clearHoverHighlight() {
2487
- this._clearAllOverlays(this._hoverEntries);
2488
- this._lastHoveredElement = null;
2906
+ /** Remove all hover and selection highlights. */
2907
+ clearAll() {
2908
+ this.clearHoverHighlight();
2909
+ this._clearAllSelectionHighlights();
2489
2910
  }
2490
2911
  // -----------------------------------------------------------------------
2491
2912
  // Internal: selection highlights
2492
2913
  // -----------------------------------------------------------------------
2493
- _syncSelectedHighlights(selected) {
2494
- const targetSet = /* @__PURE__ */ new Set();
2495
- const primarySet = new Set(selected);
2496
- for (const obj of selected) {
2497
- targetSet.add(obj);
2498
- obj.traverse((child) => {
2499
- targetSet.add(child);
2500
- });
2501
- }
2914
+ _syncSelectionHighlights(selected) {
2915
+ if (!this._scene) return;
2916
+ const selectedSet = new Set(selected);
2502
2917
  for (const [obj] of this._selectedEntries) {
2503
- if (!targetSet.has(obj)) {
2504
- this._removeOverlay(obj, this._selectedEntries);
2918
+ if (!selectedSet.has(obj)) {
2919
+ this._removeSelectionHighlight(obj);
2505
2920
  }
2506
2921
  }
2507
- for (const obj of targetSet) {
2508
- if (obj.userData?.__r3fdom_internal) continue;
2922
+ for (const obj of selected) {
2509
2923
  if (!this._selectedEntries.has(obj)) {
2510
- const isChild = !primarySet.has(obj);
2511
- this._addSelectedHighlight(obj, isChild);
2924
+ this._addSelectionHighlight(obj);
2512
2925
  }
2513
2926
  }
2514
2927
  }
2515
- _addSelectedHighlight(obj, isChild) {
2516
- if (this._selectedEntries.has(obj)) return;
2517
- const color = isChild ? COLORS.contentChild : COLORS.content;
2518
- const overlayEl = createOverlayElement(color, !isChild);
2519
- const tooltipEl = createTooltipElement();
2520
- if (isChild || !this._showTooltip) {
2521
- tooltipEl.style.display = "none";
2928
+ _addSelectionHighlight(obj) {
2929
+ if (!this._scene) return;
2930
+ const targets = collectHighlightTargets(obj);
2931
+ const meshGroups = [];
2932
+ for (const target of targets) {
2933
+ const hg = createHighlightMesh(target, SELECTION_FILL_COLOR, SELECTION_FILL_OPACITY);
2934
+ if (hg) {
2935
+ hg.root.userData.__r3fdom_source = target;
2936
+ hg.root.renderOrder = 999;
2937
+ this._scene.add(hg.root);
2938
+ meshGroups.push(hg);
2939
+ }
2940
+ }
2941
+ let bboxGroup = null;
2942
+ if (targets.length > 1 && obj.type !== "Group") {
2943
+ bboxGroup = createBoundingBoxHighlight(obj);
2944
+ if (bboxGroup) {
2945
+ this._scene.add(bboxGroup.root);
2946
+ }
2947
+ }
2948
+ if (meshGroups.length > 0 || bboxGroup) {
2949
+ this._selectedEntries.set(obj, { source: obj, meshGroups, bboxGroup });
2522
2950
  }
2523
- const label = getObjectLabel2(obj);
2524
- const dims = getObjectDimensions(obj);
2525
- tooltipEl.innerHTML = `<span style="color:${COLORS.tooltipTag}">${label}</span>` + (dims ? ` <span style="color:${COLORS.tooltipDim}">${dims}</span>` : "");
2526
- document.body.appendChild(overlayEl);
2527
- document.body.appendChild(tooltipEl);
2528
- const entry = { overlayEl, tooltipEl, target: obj, isChild };
2529
- this._selectedEntries.set(obj, entry);
2530
2951
  }
2531
- // -----------------------------------------------------------------------
2532
- // Internal: hover highlights
2533
- // -----------------------------------------------------------------------
2534
- _addHoverHighlightRecursive(obj) {
2535
- if (obj.userData?.__r3fdom_internal) return;
2536
- const overlayEl = createOverlayElement(COLORS.hover, false);
2537
- const tooltipEl = createTooltipElement();
2538
- if (this._hoverEntries.size === 0 && this._showTooltip) {
2539
- const label = getObjectLabel2(obj);
2540
- const dims = getObjectDimensions(obj);
2541
- tooltipEl.innerHTML = `<span style="color:${COLORS.tooltipTag}">${label}</span>` + (dims ? ` <span style="color:${COLORS.tooltipDim}">${dims}</span>` : "");
2542
- } else {
2543
- tooltipEl.style.display = "none";
2544
- }
2545
- document.body.appendChild(overlayEl);
2546
- document.body.appendChild(tooltipEl);
2547
- this._hoverEntries.set(obj, { overlayEl, tooltipEl, target: obj, isChild: false });
2548
- for (const child of obj.children) {
2549
- if (!child.userData?.__r3fdom_internal) {
2550
- this._addHoverHighlightRecursive(child);
2952
+ _removeSelectionHighlight(obj) {
2953
+ const entry = this._selectedEntries.get(obj);
2954
+ if (!entry) return;
2955
+ for (const mg of entry.meshGroups) {
2956
+ this._disposeHighlightGroup(mg);
2957
+ }
2958
+ if (entry.bboxGroup) {
2959
+ this._disposeHighlightGroup(entry.bboxGroup);
2960
+ }
2961
+ this._selectedEntries.delete(obj);
2962
+ }
2963
+ _clearAllSelectionHighlights() {
2964
+ for (const entry of this._selectedEntries.values()) {
2965
+ for (const mg of entry.meshGroups) {
2966
+ this._disposeHighlightGroup(mg);
2967
+ }
2968
+ if (entry.bboxGroup) {
2969
+ this._disposeHighlightGroup(entry.bboxGroup);
2551
2970
  }
2552
2971
  }
2972
+ this._selectedEntries.clear();
2553
2973
  }
2554
2974
  // -----------------------------------------------------------------------
2555
2975
  // Internal: DevTools hover polling
@@ -2568,52 +2988,872 @@ var Highlighter = class {
2568
2988
  _pollDevToolsHover() {
2569
2989
  if (!this._store) return;
2570
2990
  try {
2571
- const hoveredEl = globalThis.__r3fdom_hovered__;
2572
- if (hoveredEl === this._lastHoveredElement) return;
2573
- this._lastHoveredElement = hoveredEl ?? null;
2574
- if (!hoveredEl) {
2575
- this._clearAllOverlays(this._hoverEntries);
2576
- return;
2577
- }
2578
- const uuid = hoveredEl.getAttribute?.("data-uuid");
2991
+ const hoveredEl = window.__r3fdom_hovered__;
2992
+ const uuid = hoveredEl?.getAttribute?.("data-uuid") ?? null;
2993
+ if (uuid === this._lastHoveredUuid) return;
2994
+ this._lastHoveredUuid = uuid;
2579
2995
  if (!uuid) {
2580
- this._clearAllOverlays(this._hoverEntries);
2996
+ this._clearHoverVisuals();
2581
2997
  return;
2582
2998
  }
2583
2999
  const obj = this._store.getObject3D(uuid);
2584
3000
  if (obj) {
2585
3001
  this.showHoverHighlight(obj);
2586
3002
  } else {
2587
- this._clearAllOverlays(this._hoverEntries);
3003
+ this._clearHoverVisuals();
2588
3004
  }
2589
3005
  } catch {
2590
3006
  }
2591
3007
  }
2592
3008
  // -----------------------------------------------------------------------
2593
- // Internal: overlay cleanup
3009
+ // Internal: cleanup
2594
3010
  // -----------------------------------------------------------------------
2595
- _removeOverlay(obj, map) {
2596
- const entry = map.get(obj);
2597
- if (!entry) return;
2598
- entry.overlayEl.remove();
2599
- entry.tooltipEl.remove();
2600
- map.delete(obj);
2601
- }
2602
- _clearAllOverlays(map) {
2603
- for (const entry of map.values()) {
2604
- entry.overlayEl.remove();
2605
- entry.tooltipEl.remove();
3011
+ _disposeHighlightGroup(hg) {
3012
+ hg.root.removeFromParent();
3013
+ for (const d of hg.disposables) {
3014
+ d.dispose();
2606
3015
  }
2607
- map.clear();
3016
+ }
3017
+ /** Detach and release all resources. */
3018
+ dispose() {
3019
+ this.detach();
3020
+ }
3021
+ };
3022
+
3023
+ // src/highlight/selectionDisplayTarget.ts
3024
+ function isFirstMeshInGroup(obj) {
3025
+ const parent = obj.parent;
3026
+ if (!parent || parent.type !== "Group") return false;
3027
+ const firstMesh = parent.children.find((c) => getTagForType(c.type) === "three-mesh");
3028
+ return firstMesh === obj;
3029
+ }
3030
+ function resolveSelectionDisplayTarget(getObject3D, uuid) {
3031
+ const obj = getObject3D(uuid);
3032
+ if (!obj) return null;
3033
+ if (getTagForType(obj.type) !== "three-mesh") return uuid;
3034
+ if (isFirstMeshInGroup(obj) && obj.parent) return obj.parent.uuid;
3035
+ return uuid;
3036
+ }
3037
+
3038
+ // src/highlight/InspectController.ts
3039
+ var RAYCAST_THROTTLE_MS = 50;
3040
+ var HOVER_REVEAL_DEBOUNCE_MS = 300;
3041
+ var InspectController = class {
3042
+ constructor(opts) {
3043
+ this._active = false;
3044
+ this._lastRaycastTime = 0;
3045
+ this._hoveredObject = null;
3046
+ this._hoverRevealTimer = null;
3047
+ this._overlay = null;
3048
+ this._boundPointerMove = null;
3049
+ this._boundPointerDown = null;
3050
+ this._boundContextMenu = null;
3051
+ this._camera = opts.camera;
3052
+ this._renderer = opts.renderer;
3053
+ this._selectionManager = opts.selectionManager;
3054
+ this._highlighter = opts.highlighter;
3055
+ this._raycastAccelerator = opts.raycastAccelerator;
3056
+ this._mirror = opts.mirror;
3057
+ this._store = opts.store;
3058
+ }
3059
+ get active() {
3060
+ return this._active;
3061
+ }
3062
+ /** Update the camera reference (e.g. after a camera switch). */
3063
+ updateCamera(camera) {
3064
+ this._camera = camera;
2608
3065
  }
2609
3066
  // -----------------------------------------------------------------------
2610
- // Cleanup
3067
+ // Enable / disable
2611
3068
  // -----------------------------------------------------------------------
3069
+ /** Activate inspect mode — creates overlay on top of canvas. */
3070
+ enable() {
3071
+ if (this._active) return;
3072
+ this._active = true;
3073
+ const canvas = this._renderer.domElement;
3074
+ const parent = canvas.parentElement;
3075
+ if (!parent) return;
3076
+ const overlay = document.createElement("div");
3077
+ overlay.dataset.r3fdomInspect = "true";
3078
+ overlay.style.cssText = [
3079
+ "position:absolute",
3080
+ "inset:0",
3081
+ "z-index:999999",
3082
+ "cursor:crosshair",
3083
+ "background:transparent"
3084
+ ].join(";");
3085
+ const parentPos = getComputedStyle(parent).position;
3086
+ if (parentPos === "static") {
3087
+ parent.style.position = "relative";
3088
+ }
3089
+ this._boundPointerMove = this._onPointerMove.bind(this);
3090
+ this._boundPointerDown = this._onPointerDown.bind(this);
3091
+ this._boundContextMenu = (e) => e.preventDefault();
3092
+ overlay.addEventListener("pointermove", this._boundPointerMove);
3093
+ overlay.addEventListener("pointerdown", this._boundPointerDown);
3094
+ overlay.addEventListener("contextmenu", this._boundContextMenu);
3095
+ parent.appendChild(overlay);
3096
+ this._overlay = overlay;
3097
+ r3fLog("inspect", "Inspect mode enabled \u2014 hover to highlight, click to select");
3098
+ }
3099
+ /** Deactivate inspect mode — removes overlay and clears hover state. */
3100
+ disable() {
3101
+ if (!this._active) return;
3102
+ this._active = false;
3103
+ if (this._overlay) {
3104
+ if (this._boundPointerMove) this._overlay.removeEventListener("pointermove", this._boundPointerMove);
3105
+ if (this._boundPointerDown) this._overlay.removeEventListener("pointerdown", this._boundPointerDown);
3106
+ if (this._boundContextMenu) this._overlay.removeEventListener("contextmenu", this._boundContextMenu);
3107
+ this._overlay.remove();
3108
+ this._overlay = null;
3109
+ }
3110
+ this._boundPointerMove = null;
3111
+ this._boundPointerDown = null;
3112
+ this._boundContextMenu = null;
3113
+ this._hoveredObject = null;
3114
+ this._cancelHoverReveal();
3115
+ this._highlighter.clearHoverHighlight();
3116
+ window.__r3fdom_selected_element__ = null;
3117
+ r3fLog("inspect", "InspectController disabled");
3118
+ }
3119
+ /** Disable and release all resources. */
2612
3120
  dispose() {
2613
- this.detach();
3121
+ this.disable();
3122
+ }
3123
+ // -----------------------------------------------------------------------
3124
+ // Raycasting (delegated to RaycastAccelerator)
3125
+ // -----------------------------------------------------------------------
3126
+ _raycastFromEvent(e) {
3127
+ return this._raycastAccelerator.raycastAtMouse(
3128
+ e,
3129
+ this._camera,
3130
+ this._renderer.domElement
3131
+ );
3132
+ }
3133
+ /**
3134
+ * Resolve a raw raycast hit to the logical selection target.
3135
+ * Walks up to find the best Group parent if applicable.
3136
+ */
3137
+ _resolveTarget(hit) {
3138
+ const displayUuid = resolveSelectionDisplayTarget(
3139
+ (id) => this._store.getObject3D(id),
3140
+ hit.uuid
3141
+ );
3142
+ if (!displayUuid) return hit;
3143
+ return this._store.getObject3D(displayUuid) ?? hit;
3144
+ }
3145
+ // -----------------------------------------------------------------------
3146
+ // Event handlers
3147
+ // -----------------------------------------------------------------------
3148
+ _onPointerMove(e) {
3149
+ e.stopPropagation();
3150
+ e.preventDefault();
3151
+ const now = performance.now();
3152
+ if (now - this._lastRaycastTime < RAYCAST_THROTTLE_MS) return;
3153
+ this._lastRaycastTime = now;
3154
+ const hit = this._raycastFromEvent(e);
3155
+ if (!hit) {
3156
+ if (this._hoveredObject) {
3157
+ this._hoveredObject = null;
3158
+ this._highlighter.clearHoverHighlight();
3159
+ this._cancelHoverReveal();
3160
+ }
3161
+ return;
3162
+ }
3163
+ if (hit === this._hoveredObject) return;
3164
+ this._hoveredObject = hit;
3165
+ this._highlighter.showHoverHighlight(hit);
3166
+ this._scheduleHoverReveal(hit);
3167
+ }
3168
+ /**
3169
+ * After hovering an object for HOVER_REVEAL_DEBOUNCE_MS, auto-reveal its
3170
+ * mirror element in the Elements tab.
3171
+ */
3172
+ _scheduleHoverReveal(target) {
3173
+ this._cancelHoverReveal();
3174
+ this._hoverRevealTimer = setTimeout(() => {
3175
+ const mirrorEl = this._mirror.getOrMaterialize(target.uuid);
3176
+ if (mirrorEl) {
3177
+ window.__r3fdom_selected_element__ = mirrorEl;
3178
+ }
3179
+ }, HOVER_REVEAL_DEBOUNCE_MS);
3180
+ }
3181
+ _cancelHoverReveal() {
3182
+ if (this._hoverRevealTimer) {
3183
+ clearTimeout(this._hoverRevealTimer);
3184
+ this._hoverRevealTimer = null;
3185
+ }
3186
+ }
3187
+ _onPointerDown(e) {
3188
+ e.stopPropagation();
3189
+ e.preventDefault();
3190
+ const hit = this._raycastFromEvent(e);
3191
+ if (!hit) {
3192
+ this._selectionManager.clearSelection();
3193
+ return;
3194
+ }
3195
+ const target = this._resolveTarget(hit);
3196
+ if (!target) return;
3197
+ this._selectionManager.select(target);
3198
+ const mirrorEl = this._mirror.getOrMaterialize(target.uuid);
3199
+ if (mirrorEl) {
3200
+ window.__r3fdom_selected_element__ = mirrorEl;
3201
+ }
3202
+ r3fLog("inspect", "Object selected via canvas click", {
3203
+ uuid: target.uuid.slice(0, 8),
3204
+ name: target.name || "(unnamed)",
3205
+ type: target.type
3206
+ });
2614
3207
  }
2615
3208
  };
3209
+ var _bvhPatched = false;
3210
+ function ensureBVHPatched() {
3211
+ if (_bvhPatched) return;
3212
+ _bvhPatched = true;
3213
+ BufferGeometry.prototype.computeBoundsTree = computeBoundsTree;
3214
+ BufferGeometry.prototype.disposeBoundsTree = disposeBoundsTree;
3215
+ Mesh.prototype.raycast = acceleratedRaycast;
3216
+ r3fLog("raycast", "three-mesh-bvh patched into Three.js");
3217
+ }
3218
+ var _raycaster2 = /* @__PURE__ */ new Raycaster();
3219
+ var _mouse = /* @__PURE__ */ new Vector2();
3220
+ function isRaycastable(obj) {
3221
+ if (obj.userData?.__r3fdom_internal) return false;
3222
+ if (!obj.visible) return false;
3223
+ const isMeshLike = obj.isMesh === true || obj.isLine === true || obj.isPoints === true;
3224
+ if (!isMeshLike) return false;
3225
+ const geom = obj.geometry;
3226
+ if (geom) {
3227
+ const posAttr = geom.getAttribute("position");
3228
+ if (posAttr && !posAttr.array) return false;
3229
+ }
3230
+ return true;
3231
+ }
3232
+ var RaycastAccelerator = class {
3233
+ constructor(store) {
3234
+ this._targets = [];
3235
+ this._dirty = true;
3236
+ this._unsubscribe = null;
3237
+ this._bvhBuiltFor = /* @__PURE__ */ new WeakSet();
3238
+ this._store = store;
3239
+ ensureBVHPatched();
3240
+ this._unsubscribe = store.subscribe(() => {
3241
+ this._dirty = true;
3242
+ });
3243
+ }
3244
+ /** Force a target list rebuild on the next raycast. */
3245
+ markDirty() {
3246
+ this._dirty = true;
3247
+ }
3248
+ _rebuild() {
3249
+ this._dirty = false;
3250
+ const flatList = this._store.getFlatList();
3251
+ const targets = [];
3252
+ for (let i = 0; i < flatList.length; i++) {
3253
+ const obj = flatList[i];
3254
+ if (isRaycastable(obj)) {
3255
+ targets.push(obj);
3256
+ }
3257
+ }
3258
+ this._targets = targets;
3259
+ let bvhBudget = 50;
3260
+ for (let i = 0; i < targets.length && bvhBudget > 0; i++) {
3261
+ const obj = targets[i];
3262
+ if (obj.isMesh) {
3263
+ const geom = obj.geometry;
3264
+ if (geom && !this._bvhBuiltFor.has(geom) && !geom.boundsTree) {
3265
+ this._ensureBVH(obj);
3266
+ bvhBudget--;
3267
+ }
3268
+ }
3269
+ }
3270
+ if (bvhBudget === 0) {
3271
+ this._dirty = true;
3272
+ }
3273
+ }
3274
+ /**
3275
+ * Build a BVH for a mesh's geometry if it doesn't have one yet.
3276
+ * Uses indirect mode to avoid modifying the index buffer.
3277
+ * Skips disposed geometries and does NOT mark failed builds so they
3278
+ * can be retried (e.g. after geometry is re-uploaded).
3279
+ */
3280
+ _ensureBVH(obj) {
3281
+ if (!obj.isMesh) return;
3282
+ const geom = obj.geometry;
3283
+ if (!geom || this._bvhBuiltFor.has(geom)) return;
3284
+ const posAttr = geom.getAttribute("position");
3285
+ if (!posAttr || !posAttr.array) return;
3286
+ if (geom.boundsTree) {
3287
+ this._bvhBuiltFor.add(geom);
3288
+ return;
3289
+ }
3290
+ try {
3291
+ geom.computeBoundsTree({ indirect: true });
3292
+ this._bvhBuiltFor.add(geom);
3293
+ } catch {
3294
+ r3fLog("raycast", `BVH build failed for geometry, will retry next rebuild`);
3295
+ }
3296
+ }
3297
+ /**
3298
+ * Raycast from mouse position against only raycastable meshes.
3299
+ * Returns the closest non-internal hit, or null.
3300
+ */
3301
+ raycastAtMouse(e, camera, canvas) {
3302
+ if (this._dirty) this._rebuild();
3303
+ const rect = canvas.getBoundingClientRect();
3304
+ _mouse.x = (e.clientX - rect.left) / rect.width * 2 - 1;
3305
+ _mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
3306
+ _raycaster2.setFromCamera(_mouse, camera);
3307
+ _raycaster2.firstHitOnly = true;
3308
+ const intersections = _raycaster2.intersectObjects(this._targets, false);
3309
+ _raycaster2.firstHitOnly = false;
3310
+ for (const intersection of intersections) {
3311
+ if (intersection.object.userData?.__r3fdom_internal) continue;
3312
+ return intersection.object;
3313
+ }
3314
+ return null;
3315
+ }
3316
+ /**
3317
+ * Raycast from NDC coordinates. Used by raycastVerify.
3318
+ */
3319
+ raycastAtNdc(ndcX, ndcY, camera) {
3320
+ if (this._dirty) this._rebuild();
3321
+ _mouse.set(ndcX, ndcY);
3322
+ _raycaster2.setFromCamera(_mouse, camera);
3323
+ return _raycaster2.intersectObjects(this._targets, false);
3324
+ }
3325
+ /** Current number of raycastable targets. */
3326
+ get targetCount() {
3327
+ if (this._dirty) this._rebuild();
3328
+ return this._targets.length;
3329
+ }
3330
+ /** Unsubscribe from the store and release the target list. */
3331
+ dispose() {
3332
+ if (this._unsubscribe) {
3333
+ this._unsubscribe();
3334
+ this._unsubscribe = null;
3335
+ }
3336
+ this._targets = [];
3337
+ }
3338
+ };
3339
+
3340
+ // src/bridge/ThreeDom.tsx
3341
+ var _stores = /* @__PURE__ */ new Map();
3342
+ var _mirrors = /* @__PURE__ */ new Map();
3343
+ var _selectionManagers = /* @__PURE__ */ new Map();
3344
+ var _highlighters = /* @__PURE__ */ new Map();
3345
+ var _inspectControllers = /* @__PURE__ */ new Map();
3346
+ function getStore2(canvasId = "") {
3347
+ return _stores.get(canvasId) ?? null;
3348
+ }
3349
+ function getMirror(canvasId = "") {
3350
+ return _mirrors.get(canvasId) ?? null;
3351
+ }
3352
+ function getSelectionManager(canvasId = "") {
3353
+ return _selectionManagers.get(canvasId) ?? null;
3354
+ }
3355
+ function getHighlighter(canvasId = "") {
3356
+ return _highlighters.get(canvasId) ?? null;
3357
+ }
3358
+ function getInspectController(canvasId = "") {
3359
+ return _inspectControllers.get(canvasId) ?? null;
3360
+ }
3361
+ function getCanvasIds() {
3362
+ return Array.from(_stores.keys());
3363
+ }
3364
+ function exposeGlobalAPI(store, gl, cameraRef, selMgr, inspCtrl, mirror, canvasId, isPrimary = true) {
3365
+ const api = {
3366
+ _ready: true,
3367
+ canvasId,
3368
+ getByTestId: (id) => store.getByTestId(id),
3369
+ getByUuid: (uuid) => store.getByUuid(uuid),
3370
+ getByName: (name) => store.getByName(name),
3371
+ getChildren: (idOrUuid) => store.getChildren(idOrUuid),
3372
+ getParent: (idOrUuid) => store.getParent(idOrUuid),
3373
+ getCount: () => store.getCount(),
3374
+ getByType: (type) => store.getByType(type),
3375
+ getByGeometryType: (type) => store.getByGeometryType(type),
3376
+ getByMaterialType: (type) => store.getByMaterialType(type),
3377
+ getByUserData: (key, value) => store.getByUserData(key, value),
3378
+ getCountByType: (type) => store.getCountByType(type),
3379
+ getObjects: (ids) => {
3380
+ const map = store.getObjects(ids);
3381
+ const result = {};
3382
+ for (const [id, meta] of map) {
3383
+ result[id] = meta;
3384
+ }
3385
+ return result;
3386
+ },
3387
+ snapshot: () => createSnapshot(store),
3388
+ inspect: (idOrUuid, options) => store.inspect(idOrUuid, options),
3389
+ click: (idOrUuid) => {
3390
+ click3D(idOrUuid);
3391
+ },
3392
+ doubleClick: (idOrUuid) => {
3393
+ doubleClick3D(idOrUuid);
3394
+ },
3395
+ contextMenu: (idOrUuid) => {
3396
+ contextMenu3D(idOrUuid);
3397
+ },
3398
+ hover: (idOrUuid) => {
3399
+ hover3D(idOrUuid);
3400
+ },
3401
+ unhover: () => {
3402
+ unhover3D();
3403
+ },
3404
+ drag: async (idOrUuid, delta) => {
3405
+ await drag3D(idOrUuid, delta);
3406
+ },
3407
+ wheel: (idOrUuid, options) => {
3408
+ wheel3D(idOrUuid, options);
3409
+ },
3410
+ pointerMiss: () => {
3411
+ pointerMiss3D();
3412
+ },
3413
+ drawPath: async (points, options) => {
3414
+ const result = await drawPath(points, options);
3415
+ return { eventCount: result.eventCount, pointCount: result.pointCount };
3416
+ },
3417
+ select: (idOrUuid) => {
3418
+ const obj = store.getObject3D(idOrUuid);
3419
+ if (obj && selMgr) selMgr.select(obj);
3420
+ },
3421
+ clearSelection: () => {
3422
+ selMgr?.clearSelection();
3423
+ },
3424
+ getSelection: () => selMgr ? selMgr.getSelected().map((o) => o.uuid) : [],
3425
+ getObject3D: (idOrUuid) => store.getObject3D(idOrUuid),
3426
+ getSelectionDisplayTarget: (uuid) => resolveSelectionDisplayTarget((id) => store.getObject3D(id), uuid) ?? uuid,
3427
+ setInspectMode: (on) => {
3428
+ r3fLog("inspect", "Global API setInspectMode called", { on });
3429
+ const ctrl = inspCtrl ?? _inspectControllers.get(canvasId ?? "");
3430
+ if (on) {
3431
+ ctrl?.enable();
3432
+ mirror?.setInspectMode(true);
3433
+ } else {
3434
+ ctrl?.disable();
3435
+ mirror?.setInspectMode(false);
3436
+ }
3437
+ },
3438
+ getInspectMode: () => {
3439
+ const ctrl = inspCtrl ?? _inspectControllers.get(canvasId ?? "");
3440
+ return ctrl?.active ?? false;
3441
+ },
3442
+ sweepOrphans: () => store.sweepOrphans(),
3443
+ getDiagnostics: () => ({
3444
+ version,
3445
+ ready: true,
3446
+ objectCount: store.getCount(),
3447
+ meshCount: store.getCountByType("Mesh"),
3448
+ groupCount: store.getCountByType("Group"),
3449
+ lightCount: store.getCountByType("DirectionalLight") + store.getCountByType("PointLight") + store.getCountByType("SpotLight") + store.getCountByType("AmbientLight") + store.getCountByType("HemisphereLight"),
3450
+ cameraCount: store.getCountByType("PerspectiveCamera") + store.getCountByType("OrthographicCamera"),
3451
+ materializedDomNodes: mirror?.getMaterializedCount() ?? 0,
3452
+ maxDomNodes: mirror?.getMaxNodes() ?? 0,
3453
+ canvasWidth: gl.domElement.width,
3454
+ canvasHeight: gl.domElement.height,
3455
+ webglRenderer: (() => {
3456
+ try {
3457
+ const ctx = gl.getContext();
3458
+ const dbg = ctx.getExtension("WEBGL_debug_renderer_info");
3459
+ return dbg ? ctx.getParameter(dbg.UNMASKED_RENDERER_WEBGL) : "unknown";
3460
+ } catch {
3461
+ return "unknown";
3462
+ }
3463
+ })(),
3464
+ dirtyQueueSize: store.getDirtyCount()
3465
+ }),
3466
+ getCameraState: () => {
3467
+ const cam = cameraRef.current;
3468
+ const dir = new Vector3(0, 0, -1).applyQuaternion(cam.quaternion);
3469
+ const target = [
3470
+ cam.position.x + dir.x * 100,
3471
+ cam.position.y + dir.y * 100,
3472
+ cam.position.z + dir.z * 100
3473
+ ];
3474
+ const state = {
3475
+ type: cam.type,
3476
+ position: [cam.position.x, cam.position.y, cam.position.z],
3477
+ rotation: [cam.rotation.x, cam.rotation.y, cam.rotation.z],
3478
+ target,
3479
+ near: cam.near,
3480
+ far: cam.far,
3481
+ zoom: cam.zoom
3482
+ };
3483
+ if (cam.type === "PerspectiveCamera") {
3484
+ const pc = cam;
3485
+ state.fov = pc.fov;
3486
+ state.aspect = pc.aspect;
3487
+ } else if (cam.type === "OrthographicCamera") {
3488
+ const oc = cam;
3489
+ state.left = oc.left;
3490
+ state.right = oc.right;
3491
+ state.top = oc.top;
3492
+ state.bottom = oc.bottom;
3493
+ }
3494
+ return state;
3495
+ },
3496
+ fuzzyFind: (query, limit = 5) => {
3497
+ const q = query.toLowerCase();
3498
+ const results = [];
3499
+ for (const obj of store.getFlatList()) {
3500
+ if (results.length >= limit) break;
3501
+ const meta = store.getMetadata(obj);
3502
+ if (!meta) continue;
3503
+ const testId = meta.testId?.toLowerCase() ?? "";
3504
+ const name = meta.name?.toLowerCase() ?? "";
3505
+ if (testId.includes(q) || name.includes(q) || meta.uuid.startsWith(q)) {
3506
+ results.push(meta);
3507
+ }
3508
+ }
3509
+ return results;
3510
+ },
3511
+ version
3512
+ };
3513
+ if (isPrimary) {
3514
+ window.__R3F_DOM__ = api;
3515
+ }
3516
+ if (canvasId) {
3517
+ if (!window.__R3F_DOM_INSTANCES__) window.__R3F_DOM_INSTANCES__ = {};
3518
+ if (window.__R3F_DOM_INSTANCES__[canvasId]) {
3519
+ console.warn(
3520
+ `[react-three-dom] Duplicate canvasId "${canvasId}" \u2014 the previous bridge instance will be overwritten. Each <ThreeDom> must have a unique canvasId.`
3521
+ );
3522
+ }
3523
+ window.__R3F_DOM_INSTANCES__[canvasId] = api;
3524
+ }
3525
+ }
3526
+ function removeGlobalAPI(onlyIfEquals, canvasId) {
3527
+ r3fLog("bridge", "removeGlobalAPI called (deferred)");
3528
+ const removeFromRegistry = (ref) => {
3529
+ if (canvasId && window.__R3F_DOM_INSTANCES__) {
3530
+ if (!ref || window.__R3F_DOM_INSTANCES__[canvasId] === ref) {
3531
+ delete window.__R3F_DOM_INSTANCES__[canvasId];
3532
+ if (Object.keys(window.__R3F_DOM_INSTANCES__).length === 0) {
3533
+ delete window.__R3F_DOM_INSTANCES__;
3534
+ }
3535
+ }
3536
+ }
3537
+ };
3538
+ if (onlyIfEquals !== void 0) {
3539
+ const ref = onlyIfEquals;
3540
+ queueMicrotask(() => {
3541
+ if (window.__R3F_DOM__ === ref) {
3542
+ delete window.__R3F_DOM__;
3543
+ r3fLog("bridge", "Global API removed");
3544
+ } else {
3545
+ r3fLog("bridge", "Global API not removed \u2014 replaced by new instance (Strict Mode remount)");
3546
+ }
3547
+ removeFromRegistry(ref);
3548
+ });
3549
+ } else {
3550
+ delete window.__R3F_DOM__;
3551
+ removeFromRegistry();
3552
+ r3fLog("bridge", "Global API removed (immediate)");
3553
+ }
3554
+ }
3555
+ function createStubBridge(error, canvasId) {
3556
+ return {
3557
+ _ready: false,
3558
+ _error: error,
3559
+ canvasId,
3560
+ getByTestId: () => null,
3561
+ getByUuid: () => null,
3562
+ getByName: () => [],
3563
+ getChildren: () => [],
3564
+ getParent: () => null,
3565
+ getCount: () => 0,
3566
+ getByType: () => [],
3567
+ getByGeometryType: () => [],
3568
+ getByMaterialType: () => [],
3569
+ getByUserData: () => [],
3570
+ getCountByType: () => 0,
3571
+ getObjects: (ids) => {
3572
+ const result = {};
3573
+ for (const id of ids) result[id] = null;
3574
+ return result;
3575
+ },
3576
+ snapshot: () => ({
3577
+ timestamp: 0,
3578
+ objectCount: 0,
3579
+ tree: {
3580
+ uuid: "",
3581
+ name: "",
3582
+ type: "Scene",
3583
+ visible: true,
3584
+ position: [0, 0, 0],
3585
+ rotation: [0, 0, 0],
3586
+ scale: [1, 1, 1],
3587
+ children: []
3588
+ }
3589
+ }),
3590
+ inspect: () => null,
3591
+ click: () => {
3592
+ },
3593
+ doubleClick: () => {
3594
+ },
3595
+ contextMenu: () => {
3596
+ },
3597
+ hover: () => {
3598
+ },
3599
+ unhover: () => {
3600
+ },
3601
+ drag: async () => {
3602
+ },
3603
+ wheel: () => {
3604
+ },
3605
+ pointerMiss: () => {
3606
+ },
3607
+ drawPath: async () => ({ eventCount: 0, pointCount: 0 }),
3608
+ select: () => {
3609
+ },
3610
+ clearSelection: () => {
3611
+ },
3612
+ getSelection: () => [],
3613
+ getObject3D: () => null,
3614
+ getSelectionDisplayTarget: (uuid) => uuid,
3615
+ setInspectMode: () => {
3616
+ },
3617
+ getInspectMode: () => false,
3618
+ sweepOrphans: () => 0,
3619
+ getDiagnostics: () => ({
3620
+ version,
3621
+ ready: false,
3622
+ error: error ?? void 0,
3623
+ objectCount: 0,
3624
+ meshCount: 0,
3625
+ groupCount: 0,
3626
+ lightCount: 0,
3627
+ cameraCount: 0,
3628
+ materializedDomNodes: 0,
3629
+ maxDomNodes: 0,
3630
+ canvasWidth: 0,
3631
+ canvasHeight: 0,
3632
+ webglRenderer: "unavailable",
3633
+ dirtyQueueSize: 0
3634
+ }),
3635
+ getCameraState: () => ({
3636
+ type: "unknown",
3637
+ position: [0, 0, 0],
3638
+ rotation: [0, 0, 0],
3639
+ target: [0, 0, -100],
3640
+ near: 0.1,
3641
+ far: 1e3,
3642
+ zoom: 1
3643
+ }),
3644
+ fuzzyFind: () => [],
3645
+ version
3646
+ };
3647
+ }
3648
+ function ThreeDom({
3649
+ canvasId,
3650
+ primary,
3651
+ root = "#three-dom-root",
3652
+ batchSize = 500,
3653
+ syncBudgetMs = 0.5,
3654
+ maxDomNodes = 2e3,
3655
+ initialDepth = 3,
3656
+ enabled = true,
3657
+ debug = false,
3658
+ inspect: inspectProp = false
3659
+ } = {}) {
3660
+ const isPrimary = primary ?? canvasId === void 0;
3661
+ const instanceKey = canvasId ?? "";
3662
+ const scene = useThree((s) => s.scene);
3663
+ const camera = useThree((s) => s.camera);
3664
+ const gl = useThree((s) => s.gl);
3665
+ const size = useThree((s) => s.size);
3666
+ const cursorRef = useRef(0);
3667
+ const lastSweepRef = useRef(0);
3668
+ const cameraRef = useRef(camera);
3669
+ cameraRef.current = camera;
3670
+ useEffect(() => {
3671
+ if (!enabled) return;
3672
+ if (debug) enableDebug(true);
3673
+ r3fLog("setup", "ThreeDom effect started", { enabled, debug, root, maxDomNodes });
3674
+ const canvas = gl.domElement;
3675
+ canvas.setAttribute("data-r3f-canvas", canvasId ?? "true");
3676
+ const canvasParent = canvas.parentElement;
3677
+ let rootElement = null;
3678
+ let createdRoot = false;
3679
+ if (typeof root === "string") {
3680
+ rootElement = document.querySelector(root);
3681
+ } else {
3682
+ rootElement = root;
3683
+ }
3684
+ if (!rootElement) {
3685
+ rootElement = document.createElement("div");
3686
+ rootElement.id = typeof root === "string" ? root.replace(/^#/, "") : "three-dom-root";
3687
+ createdRoot = true;
3688
+ }
3689
+ canvasParent.style.position = canvasParent.style.position || "relative";
3690
+ canvasParent.appendChild(rootElement);
3691
+ rootElement.style.cssText = "width:0;height:0;overflow:hidden;pointer-events:none;opacity:0;";
3692
+ let store = null;
3693
+ let mirror = null;
3694
+ let unpatch = null;
3695
+ let cancelAsyncReg = null;
3696
+ let selectionManager = null;
3697
+ let highlighter = null;
3698
+ let raycastAccelerator = null;
3699
+ let inspectController = null;
3700
+ let currentApi;
3701
+ try {
3702
+ const webglContext = gl.getContext();
3703
+ if (!webglContext || webglContext.isContextLost?.()) {
3704
+ const msg = "WebGL context not available. For headless Chromium, add --enable-webgl and optionally --use-gl=angle --use-angle=swiftshader-webgl to launch args.";
3705
+ const stubApi = createStubBridge(msg, canvasId);
3706
+ if (isPrimary) window.__R3F_DOM__ = stubApi;
3707
+ if (canvasId) {
3708
+ if (!window.__R3F_DOM_INSTANCES__) window.__R3F_DOM_INSTANCES__ = {};
3709
+ window.__R3F_DOM_INSTANCES__[canvasId] = stubApi;
3710
+ }
3711
+ r3fLog("setup", msg);
3712
+ return () => {
3713
+ removeGlobalAPI(stubApi, canvasId);
3714
+ canvas.removeAttribute("data-r3f-canvas");
3715
+ if (createdRoot && rootElement?.parentNode) {
3716
+ rootElement.parentNode.removeChild(rootElement);
3717
+ }
3718
+ if (debug) enableDebug(false);
3719
+ };
3720
+ }
3721
+ store = new ObjectStore();
3722
+ mirror = new DomMirror(store, maxDomNodes);
3723
+ mirror.setRoot(rootElement);
3724
+ r3fLog("setup", "Store and mirror created");
3725
+ ensureCustomElements(store);
3726
+ unpatch = patchObject3D(store, mirror);
3727
+ setInteractionState(store, camera, gl, size);
3728
+ r3fLog("setup", "Object3D patched, interaction state set");
3729
+ store.registerTree(scene);
3730
+ if (!store.has(camera)) {
3731
+ const camMeta = store.register(camera);
3732
+ camMeta.parentUuid = scene.uuid;
3733
+ mirror.materialize(camera.uuid);
3734
+ }
3735
+ mirror.materializeSubtree(scene.uuid, initialDepth);
3736
+ cancelAsyncReg = store.registerTreeAsync(scene);
3737
+ r3fLog("setup", `Scene registered: ${store.getCount()} objects, async watcher started`);
3738
+ selectionManager = new SelectionManager();
3739
+ highlighter = new Highlighter();
3740
+ highlighter.attach(scene, selectionManager, camera, gl, store);
3741
+ raycastAccelerator = new RaycastAccelerator(store);
3742
+ inspectController = new InspectController({
3743
+ camera,
3744
+ renderer: gl,
3745
+ selectionManager,
3746
+ highlighter,
3747
+ raycastAccelerator,
3748
+ mirror,
3749
+ store
3750
+ });
3751
+ _selectionManagers.set(instanceKey, selectionManager);
3752
+ _highlighters.set(instanceKey, highlighter);
3753
+ _inspectControllers.set(instanceKey, inspectController);
3754
+ exposeGlobalAPI(store, gl, cameraRef, selectionManager, inspectController, mirror, canvasId, isPrimary);
3755
+ r3fLog("bridge", `exposeGlobalAPI called \u2014 bridge is live, _ready=true${canvasId ? `, canvasId="${canvasId}"` : ""}`);
3756
+ currentApi = canvasId ? window.__R3F_DOM_INSTANCES__?.[canvasId] : window.__R3F_DOM__;
3757
+ _stores.set(instanceKey, store);
3758
+ _mirrors.set(instanceKey, mirror);
3759
+ if (inspectProp) {
3760
+ inspectController.enable();
3761
+ }
3762
+ if (debug) {
3763
+ const inspectStatus = inspectProp ? "on" : "off";
3764
+ console.log(
3765
+ "%c[react-three-dom]%c v" + version + " \u2014 " + store.getCount() + " objects \xB7 inspect: " + inspectStatus + "\n %c\u2192%c Toggle inspect: %c__R3F_DOM__.setInspectMode(true|false)%c\n %c\u2192%c Hover 3D objects to highlight + auto-reveal in Elements tab",
3766
+ "background:#1a73e8;color:#fff;padding:2px 6px;border-radius:3px;font-weight:bold",
3767
+ "color:inherit",
3768
+ "color:#1a73e8",
3769
+ "color:inherit",
3770
+ "color:#e8a317;font-family:monospace",
3771
+ "color:inherit",
3772
+ "color:#1a73e8",
3773
+ "color:inherit"
3774
+ );
3775
+ }
3776
+ } catch (err) {
3777
+ const errorMsg = err instanceof Error ? err.message : String(err);
3778
+ r3fLog("setup", "ThreeDom setup failed", err);
3779
+ console.error("[react-three-dom] Setup failed:", err);
3780
+ const stubApi = createStubBridge(errorMsg, canvasId);
3781
+ if (isPrimary) window.__R3F_DOM__ = stubApi;
3782
+ if (canvasId) {
3783
+ if (!window.__R3F_DOM_INSTANCES__) window.__R3F_DOM_INSTANCES__ = {};
3784
+ window.__R3F_DOM_INSTANCES__[canvasId] = stubApi;
3785
+ }
3786
+ currentApi = stubApi;
3787
+ }
3788
+ return () => {
3789
+ r3fLog("setup", "ThreeDom cleanup started");
3790
+ if (cancelAsyncReg) cancelAsyncReg();
3791
+ if (inspectController) inspectController.dispose();
3792
+ if (raycastAccelerator) raycastAccelerator.dispose();
3793
+ if (highlighter) highlighter.dispose();
3794
+ if (unpatch) unpatch();
3795
+ removeGlobalAPI(currentApi, canvasId);
3796
+ clearInteractionState();
3797
+ if (selectionManager) selectionManager.dispose();
3798
+ if (mirror) mirror.dispose();
3799
+ if (store) store.dispose();
3800
+ canvas.removeAttribute("data-r3f-canvas");
3801
+ if (createdRoot && rootElement?.parentNode) {
3802
+ rootElement.parentNode.removeChild(rootElement);
3803
+ }
3804
+ _stores.delete(instanceKey);
3805
+ _mirrors.delete(instanceKey);
3806
+ _selectionManagers.delete(instanceKey);
3807
+ _highlighters.delete(instanceKey);
3808
+ _inspectControllers.delete(instanceKey);
3809
+ if (debug) enableDebug(false);
3810
+ r3fLog("setup", "ThreeDom cleanup complete");
3811
+ };
3812
+ }, [scene, camera, gl, enabled, root, maxDomNodes, initialDepth, debug, inspectProp, canvasId, isPrimary, instanceKey]);
3813
+ useFrame(() => {
3814
+ const _store3 = _stores.get(instanceKey);
3815
+ const _mirror = _mirrors.get(instanceKey);
3816
+ const _highlighter = _highlighters.get(instanceKey);
3817
+ const _inspectController = _inspectControllers.get(instanceKey);
3818
+ if (!enabled || !_store3 || !_mirror) return;
3819
+ try {
3820
+ setInteractionState(_store3, camera, gl, size);
3821
+ if (_inspectController) _inspectController.updateCamera(camera);
3822
+ const store = _store3;
3823
+ const mirror = _mirror;
3824
+ const start = performance.now();
3825
+ const dirtyObjects = store.drainDirtyQueue();
3826
+ for (const obj of dirtyObjects) {
3827
+ store.update(obj);
3828
+ mirror.syncAttributes(obj);
3829
+ }
3830
+ const budgetRemaining = syncBudgetMs - (performance.now() - start);
3831
+ if (budgetRemaining > 0.1) {
3832
+ const objects = store.getFlatList();
3833
+ if (objects.length > 0) {
3834
+ const end = Math.min(cursorRef.current + batchSize, objects.length);
3835
+ for (let i = cursorRef.current; i < end; i++) {
3836
+ if (performance.now() - start > syncBudgetMs) break;
3837
+ const obj = objects[i];
3838
+ const changed = store.update(obj);
3839
+ if (changed) mirror.syncAttributes(obj);
3840
+ }
3841
+ cursorRef.current = end >= objects.length ? 0 : end;
3842
+ }
3843
+ }
3844
+ if (_highlighter) _highlighter.update();
3845
+ const now = performance.now();
3846
+ if (now - lastSweepRef.current > 3e4) {
3847
+ lastSweepRef.current = now;
3848
+ store.sweepOrphans();
3849
+ }
3850
+ } catch (err) {
3851
+ r3fLog("sync", "Per-frame sync error", err);
3852
+ }
3853
+ });
3854
+ return null;
3855
+ }
2616
3856
 
2617
- export { DomMirror, Highlighter, MANAGED_ATTRIBUTES, ObjectStore, SelectionManager, TAG_MAP, ThreeDom, ThreeElement, applyAttributes, click3D, computeAttributes, contextMenu3D, createFlatSnapshot, createSnapshot, dispatchClick, dispatchContextMenu, dispatchDoubleClick, dispatchDrag, dispatchHover, dispatchPointerMiss, dispatchUnhover, dispatchWheel, doubleClick3D, drag3D, ensureCustomElements, getHighlighter, getMirror, getSelectionManager, getStore2 as getStore, getTagForType, hover3D, isInFrustum, isPatched, patchObject3D, pointerMiss3D, previewDragWorldDelta, projectAllSamplePoints, projectToScreen, resolveObject, restoreObject3D, screenDeltaToWorld, unhover3D, verifyRaycastHit, verifyRaycastHitMultiPoint, version, wheel3D };
3857
+ export { DomMirror, Highlighter, InspectController, MANAGED_ATTRIBUTES, ObjectStore, RaycastAccelerator, SelectionManager, TAG_MAP, ThreeDom, ThreeElement, applyAttributes, circlePath, click3D, computeAttributes, contextMenu3D, createFlatSnapshot, createSnapshot, curvePath, dispatchClick, dispatchContextMenu, dispatchDoubleClick, dispatchDrag, dispatchHover, dispatchPointerMiss, dispatchUnhover, dispatchWheel, doubleClick3D, drag3D, drawPath, enableDebug, ensureCustomElements, getCanvasIds, getHighlighter, getInspectController, getMirror, getSelectionManager, getStore2 as getStore, getTagForType, hover3D, isDebugEnabled, isInFrustum, isPatched, linePath, patchObject3D, pointerMiss3D, previewDragWorldDelta, projectAllSamplePoints, projectToScreen, r3fLog, rectPath, resolveObject, restoreObject3D, screenDeltaToWorld, unhover3D, verifyRaycastHit, verifyRaycastHitMultiPoint, version, wheel3D };
2618
3858
  //# sourceMappingURL=index.js.map
2619
3859
  //# sourceMappingURL=index.js.map