@react-three-dom/core 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -6,6 +6,25 @@ var fiber = require('@react-three/fiber');
6
6
 
7
7
  // src/version.ts
8
8
  var version = "0.1.0";
9
+
10
+ // src/debug.ts
11
+ var _enabled = false;
12
+ function enableDebug(on = true) {
13
+ _enabled = on;
14
+ }
15
+ function isDebugEnabled() {
16
+ return _enabled || typeof window !== "undefined" && !!window.__R3F_DOM_DEBUG__;
17
+ }
18
+ function r3fLog(area, msg, data) {
19
+ if (!_enabled && !(typeof window !== "undefined" && window.__R3F_DOM_DEBUG__)) {
20
+ return;
21
+ }
22
+ if (data !== void 0) {
23
+ console.log(`[r3f-dom:${area}]`, msg, data);
24
+ } else {
25
+ console.log(`[r3f-dom:${area}]`, msg);
26
+ }
27
+ }
9
28
  function extractMetadata(obj) {
10
29
  const meta = {
11
30
  uuid: obj.uuid,
@@ -20,31 +39,42 @@ function extractMetadata(obj) {
20
39
  childrenUuids: obj.children.map((c) => c.uuid),
21
40
  boundsDirty: true
22
41
  };
23
- if ("geometry" in obj) {
24
- const geom = obj.geometry;
25
- if (geom instanceof three.BufferGeometry) {
26
- meta.geometryType = geom.type;
27
- const posAttr = geom.getAttribute("position");
28
- if (posAttr) {
29
- meta.vertexCount = posAttr.count;
30
- if (geom.index) {
31
- meta.triangleCount = Math.floor(geom.index.count / 3);
32
- } else {
33
- meta.triangleCount = Math.floor(posAttr.count / 3);
42
+ try {
43
+ if ("geometry" in obj) {
44
+ const geom = obj.geometry;
45
+ if (geom instanceof three.BufferGeometry) {
46
+ meta.geometryType = geom.type;
47
+ const posAttr = geom.getAttribute("position");
48
+ if (posAttr) {
49
+ meta.vertexCount = posAttr.count;
50
+ if (geom.index) {
51
+ meta.triangleCount = Math.floor(geom.index.count / 3);
52
+ } else {
53
+ meta.triangleCount = Math.floor(posAttr.count / 3);
54
+ }
34
55
  }
35
56
  }
36
57
  }
58
+ } catch {
59
+ r3fLog("store", `extractMetadata: geometry access failed for "${obj.name || obj.uuid}"`);
37
60
  }
38
- if ("material" in obj) {
39
- const mat = obj.material;
40
- if (mat instanceof three.Material) {
41
- meta.materialType = mat.type;
42
- } else if (Array.isArray(mat) && mat.length > 0) {
43
- meta.materialType = mat[0].type + (mat.length > 1 ? ` (+${mat.length - 1})` : "");
61
+ try {
62
+ if ("material" in obj) {
63
+ const mat = obj.material;
64
+ if (mat instanceof three.Material) {
65
+ meta.materialType = mat.type;
66
+ } else if (Array.isArray(mat) && mat.length > 0) {
67
+ meta.materialType = mat[0].type + (mat.length > 1 ? ` (+${mat.length - 1})` : "");
68
+ }
44
69
  }
70
+ } catch {
71
+ r3fLog("store", `extractMetadata: material access failed for "${obj.name || obj.uuid}"`);
45
72
  }
46
- if (obj instanceof three.InstancedMesh) {
47
- meta.instanceCount = obj.count;
73
+ try {
74
+ if (obj instanceof three.InstancedMesh) {
75
+ meta.instanceCount = obj.count;
76
+ }
77
+ } catch {
48
78
  }
49
79
  return meta;
50
80
  }
@@ -53,81 +83,101 @@ function hasChanged(prev, curr) {
53
83
  }
54
84
  var _box3 = new three.Box3();
55
85
  function inspectObject(obj, metadata) {
56
- obj.updateWorldMatrix(true, false);
57
- const worldMatrix = Array.from(obj.matrixWorld.elements);
58
- _box3.setFromObject(obj);
59
- const boundsMin = [_box3.min.x, _box3.min.y, _box3.min.z];
60
- const boundsMax = [_box3.max.x, _box3.max.y, _box3.max.z];
86
+ let worldMatrix = Array(16).fill(0);
87
+ let boundsMin = [0, 0, 0];
88
+ let boundsMax = [0, 0, 0];
89
+ try {
90
+ obj.updateWorldMatrix(true, false);
91
+ worldMatrix = Array.from(obj.matrixWorld.elements);
92
+ _box3.setFromObject(obj);
93
+ boundsMin = [_box3.min.x, _box3.min.y, _box3.min.z];
94
+ boundsMax = [_box3.max.x, _box3.max.y, _box3.max.z];
95
+ } catch {
96
+ r3fLog("store", `inspectObject: world matrix / bounds failed for "${obj.name || obj.uuid}"`);
97
+ }
61
98
  const inspection = {
62
99
  metadata,
63
100
  worldMatrix,
64
101
  bounds: { min: boundsMin, max: boundsMax },
65
- userData: { ...obj.userData }
102
+ userData: {}
66
103
  };
67
- if ("geometry" in obj) {
68
- const geom = obj.geometry;
69
- if (geom instanceof three.BufferGeometry) {
70
- const geoInspection = {
71
- type: geom.type,
72
- attributes: {}
73
- };
74
- for (const [name, attr] of Object.entries(geom.attributes)) {
75
- geoInspection.attributes[name] = {
76
- itemSize: attr.itemSize,
77
- count: attr.count
104
+ try {
105
+ inspection.userData = { ...obj.userData };
106
+ } catch {
107
+ r3fLog("store", `inspectObject: userData copy failed for "${obj.name || obj.uuid}"`);
108
+ }
109
+ try {
110
+ if ("geometry" in obj) {
111
+ const geom = obj.geometry;
112
+ if (geom instanceof three.BufferGeometry) {
113
+ const geoInspection = {
114
+ type: geom.type,
115
+ attributes: {}
78
116
  };
117
+ for (const [name, attr] of Object.entries(geom.attributes)) {
118
+ geoInspection.attributes[name] = {
119
+ itemSize: attr.itemSize,
120
+ count: attr.count
121
+ };
122
+ }
123
+ if (geom.index) {
124
+ geoInspection.index = { count: geom.index.count };
125
+ }
126
+ geom.computeBoundingSphere();
127
+ const sphere = geom.boundingSphere;
128
+ if (sphere) {
129
+ geoInspection.boundingSphere = {
130
+ center: [sphere.center.x, sphere.center.y, sphere.center.z],
131
+ radius: sphere.radius
132
+ };
133
+ }
134
+ inspection.geometry = geoInspection;
79
135
  }
80
- if (geom.index) {
81
- geoInspection.index = { count: geom.index.count };
82
- }
83
- geom.computeBoundingSphere();
84
- const sphere = geom.boundingSphere;
85
- if (sphere) {
86
- geoInspection.boundingSphere = {
87
- center: [sphere.center.x, sphere.center.y, sphere.center.z],
88
- radius: sphere.radius
136
+ }
137
+ } catch {
138
+ r3fLog("store", `inspectObject: geometry inspection failed for "${obj.name || obj.uuid}"`);
139
+ }
140
+ try {
141
+ if ("material" in obj) {
142
+ const rawMat = obj.material;
143
+ const mat = Array.isArray(rawMat) ? rawMat[0] : rawMat;
144
+ if (mat instanceof three.Material) {
145
+ const matInspection = {
146
+ type: mat.type,
147
+ transparent: mat.transparent,
148
+ opacity: mat.opacity,
149
+ side: mat.side
89
150
  };
90
- }
91
- inspection.geometry = geoInspection;
92
- }
93
- }
94
- if ("material" in obj) {
95
- const rawMat = obj.material;
96
- const mat = Array.isArray(rawMat) ? rawMat[0] : rawMat;
97
- if (mat instanceof three.Material) {
98
- const matInspection = {
99
- type: mat.type,
100
- transparent: mat.transparent,
101
- opacity: mat.opacity,
102
- side: mat.side
103
- };
104
- if ("color" in mat && mat.color instanceof three.Color) {
105
- matInspection.color = "#" + mat.color.getHexString();
106
- }
107
- if ("map" in mat) {
108
- const map = mat.map;
109
- if (map) {
110
- matInspection.map = map.name || map.uuid || "unnamed";
151
+ if ("color" in mat && mat.color instanceof three.Color) {
152
+ matInspection.color = "#" + mat.color.getHexString();
111
153
  }
112
- }
113
- if ("uniforms" in mat) {
114
- const uniforms = mat.uniforms;
115
- matInspection.uniforms = {};
116
- for (const [key, uniform] of Object.entries(uniforms)) {
117
- const val = uniform.value;
118
- if (val === null || val === void 0) {
119
- matInspection.uniforms[key] = val;
120
- } else if (typeof val === "number" || typeof val === "boolean" || typeof val === "string") {
121
- matInspection.uniforms[key] = val;
122
- } else if (typeof val === "object" && "toArray" in val) {
123
- matInspection.uniforms[key] = val.toArray();
124
- } else {
125
- matInspection.uniforms[key] = `[${typeof val}]`;
154
+ if ("map" in mat) {
155
+ const map = mat.map;
156
+ if (map) {
157
+ matInspection.map = map.name || map.uuid || "unnamed";
158
+ }
159
+ }
160
+ if ("uniforms" in mat) {
161
+ const uniforms = mat.uniforms;
162
+ matInspection.uniforms = {};
163
+ for (const [key, uniform] of Object.entries(uniforms)) {
164
+ const val = uniform.value;
165
+ if (val === null || val === void 0) {
166
+ matInspection.uniforms[key] = val;
167
+ } else if (typeof val === "number" || typeof val === "boolean" || typeof val === "string") {
168
+ matInspection.uniforms[key] = val;
169
+ } else if (typeof val === "object" && "toArray" in val) {
170
+ matInspection.uniforms[key] = val.toArray();
171
+ } else {
172
+ matInspection.uniforms[key] = `[${typeof val}]`;
173
+ }
126
174
  }
127
175
  }
176
+ inspection.material = matInspection;
128
177
  }
129
- inspection.material = matInspection;
130
178
  }
179
+ } catch {
180
+ r3fLog("store", `inspectObject: material inspection failed for "${obj.name || obj.uuid}"`);
131
181
  }
132
182
  return inspection;
133
183
  }
@@ -177,15 +227,25 @@ var ObjectStore = class {
177
227
  nameSet.add(obj);
178
228
  }
179
229
  this._emit({ type: "add", object: obj, metadata: meta });
230
+ if (meta.testId) {
231
+ r3fLog("store", `Registered "${meta.testId}" (${meta.type})`);
232
+ }
180
233
  return meta;
181
234
  }
182
235
  /**
183
236
  * Register an entire subtree (object + all descendants).
237
+ * Individual objects that fail to register are skipped (logged when debug
238
+ * is enabled) so that one bad object doesn't prevent the rest from being
239
+ * tracked.
184
240
  */
185
241
  registerTree(root) {
186
242
  this._trackedRoots.add(root);
187
243
  root.traverse((obj) => {
188
- this.register(obj);
244
+ try {
245
+ this.register(obj);
246
+ } catch (err) {
247
+ r3fLog("store", `registerTree: failed to register "${obj.name || obj.uuid}"`, err);
248
+ }
189
249
  });
190
250
  }
191
251
  /**
@@ -210,6 +270,9 @@ var ObjectStore = class {
210
270
  }
211
271
  }
212
272
  }
273
+ if (meta.testId) {
274
+ r3fLog("store", `Unregistered "${meta.testId}" (${meta.type})`);
275
+ }
213
276
  this._emit({ type: "remove", object: obj, metadata: meta });
214
277
  }
215
278
  /**
@@ -227,13 +290,22 @@ var ObjectStore = class {
227
290
  /**
228
291
  * Refresh Tier 1 metadata from the live Three.js object.
229
292
  * Returns true if any values changed.
293
+ * Returns false (no change) if extracting metadata throws so that the
294
+ * previous metadata is preserved.
230
295
  */
231
296
  update(obj) {
232
297
  const prev = this._metaByObject.get(obj);
233
298
  if (!prev) return false;
234
- const curr = extractMetadata(obj);
299
+ let curr;
300
+ try {
301
+ curr = extractMetadata(obj);
302
+ } catch (err) {
303
+ r3fLog("store", `update: extractMetadata failed for "${obj.name || obj.uuid}"`, err);
304
+ return false;
305
+ }
235
306
  if (hasChanged(prev, curr)) {
236
307
  if (prev.testId !== curr.testId) {
308
+ r3fLog("store", `testId changed: "${prev.testId}" \u2192 "${curr.testId}" (${curr.type})`);
237
309
  if (prev.testId) this._objectsByTestId.delete(prev.testId);
238
310
  if (curr.testId) this._objectsByTestId.set(curr.testId, obj);
239
311
  }
@@ -301,6 +373,64 @@ var ObjectStore = class {
301
373
  }
302
374
  return results;
303
375
  }
376
+ /**
377
+ * Batch lookup: get metadata for multiple objects by testId or uuid.
378
+ * Returns a Map from the requested id to its metadata (or null if not found).
379
+ * Single round-trip — much more efficient than calling getByTestId/getByUuid
380
+ * in a loop for BIM/CAD scenes with many objects.
381
+ * O(k) where k is the number of requested ids.
382
+ */
383
+ getObjects(ids) {
384
+ const results = /* @__PURE__ */ new Map();
385
+ for (const id of ids) {
386
+ const meta = this.getByTestId(id) ?? this.getByUuid(id);
387
+ results.set(id, meta);
388
+ }
389
+ return results;
390
+ }
391
+ /**
392
+ * Get all objects of a given Three.js type (e.g. "Mesh", "Group", "Line").
393
+ * Linear scan — O(n) where n is total tracked objects.
394
+ */
395
+ getByType(type) {
396
+ const results = [];
397
+ for (const obj of this.getFlatList()) {
398
+ const meta = this._metaByObject.get(obj);
399
+ if (meta && meta.type === type) results.push(meta);
400
+ }
401
+ return results;
402
+ }
403
+ /**
404
+ * Get all objects that have a specific userData key.
405
+ * If `value` is provided, only returns objects where `userData[key]` matches.
406
+ * Uses JSON.stringify for deep equality on complex values.
407
+ * Linear scan — O(n).
408
+ */
409
+ getByUserData(key, value) {
410
+ const results = [];
411
+ const checkValue = value !== void 0;
412
+ const valueJson = checkValue ? JSON.stringify(value) : "";
413
+ for (const obj of this.getFlatList()) {
414
+ if (!(key in obj.userData)) continue;
415
+ if (checkValue && JSON.stringify(obj.userData[key]) !== valueJson) continue;
416
+ const meta = this._metaByObject.get(obj);
417
+ if (meta) results.push(meta);
418
+ }
419
+ return results;
420
+ }
421
+ /**
422
+ * Count objects of a given Three.js type.
423
+ * More efficient than getByType().length — no array allocation.
424
+ * Linear scan — O(n).
425
+ */
426
+ getCountByType(type) {
427
+ let count = 0;
428
+ for (const obj of this.getFlatList()) {
429
+ const meta = this._metaByObject.get(obj);
430
+ if (meta && meta.type === type) count++;
431
+ }
432
+ return count;
433
+ }
304
434
  /** Get the raw Three.js Object3D by testId or uuid. */
305
435
  getObject3D(idOrUuid) {
306
436
  return this._objectsByTestId.get(idOrUuid) ?? this._objectByUuid.get(idOrUuid) ?? null;
@@ -1034,6 +1164,7 @@ function registerSubtree(obj, store, mirror) {
1034
1164
  function patchObject3D(store, mirror) {
1035
1165
  _activePairs.push({ store, mirror });
1036
1166
  if (!_patched) {
1167
+ r3fLog("patch", "Patching Object3D.prototype.add and .remove");
1037
1168
  _originalAdd = three.Object3D.prototype.add;
1038
1169
  _originalRemove = three.Object3D.prototype.remove;
1039
1170
  three.Object3D.prototype.add = function patchedAdd(...objects) {
@@ -1042,8 +1173,15 @@ function patchObject3D(store, mirror) {
1042
1173
  if (pair) {
1043
1174
  for (const obj of objects) {
1044
1175
  if (obj === this) continue;
1045
- registerSubtree(obj, pair.store, pair.mirror);
1176
+ try {
1177
+ r3fLog("patch", `patchedAdd: "${obj.name || obj.type}" added to "${this.name || this.type}"`);
1178
+ registerSubtree(obj, pair.store, pair.mirror);
1179
+ } catch (err) {
1180
+ r3fLog("patch", `patchedAdd: failed to register "${obj.name || obj.type}"`, err);
1181
+ }
1046
1182
  }
1183
+ pair.store.update(this);
1184
+ pair.store.markDirty(this);
1047
1185
  }
1048
1186
  return this;
1049
1187
  };
@@ -1052,11 +1190,18 @@ function patchObject3D(store, mirror) {
1052
1190
  if (pair) {
1053
1191
  for (const obj of objects) {
1054
1192
  if (obj === this) continue;
1055
- pair.mirror.onObjectRemoved(obj);
1056
- obj.traverse((child) => {
1057
- pair.store.unregister(child);
1058
- });
1193
+ try {
1194
+ r3fLog("patch", `patchedRemove: "${obj.name || obj.type}" removed from "${this.name || this.type}"`);
1195
+ pair.mirror.onObjectRemoved(obj);
1196
+ obj.traverse((child) => {
1197
+ pair.store.unregister(child);
1198
+ });
1199
+ } catch (err) {
1200
+ r3fLog("patch", `patchedRemove: failed to unregister "${obj.name || obj.type}"`, err);
1201
+ }
1059
1202
  }
1203
+ pair.store.update(this);
1204
+ pair.store.markDirty(this);
1060
1205
  }
1061
1206
  _originalRemove.call(this, ...objects);
1062
1207
  return this;
@@ -1077,6 +1222,7 @@ function patchObject3D(store, mirror) {
1077
1222
  }
1078
1223
  function restoreObject3D() {
1079
1224
  if (!_patched) return;
1225
+ r3fLog("patch", "Restoring original Object3D.prototype.add and .remove");
1080
1226
  if (_originalAdd) {
1081
1227
  three.Object3D.prototype.add = _originalAdd;
1082
1228
  _originalAdd = null;
@@ -1100,7 +1246,7 @@ function buildNodeTree(store, meta) {
1100
1246
  children.push(buildNodeTree(store, childMeta));
1101
1247
  }
1102
1248
  }
1103
- return {
1249
+ const node = {
1104
1250
  uuid: meta.uuid,
1105
1251
  name: meta.name,
1106
1252
  type: meta.type,
@@ -1111,6 +1257,12 @@ function buildNodeTree(store, meta) {
1111
1257
  scale: [...meta.scale],
1112
1258
  children
1113
1259
  };
1260
+ if (meta.geometryType) node.geometryType = meta.geometryType;
1261
+ if (meta.materialType) node.materialType = meta.materialType;
1262
+ if (meta.vertexCount != null) node.vertexCount = meta.vertexCount;
1263
+ if (meta.triangleCount != null) node.triangleCount = meta.triangleCount;
1264
+ if (meta.instanceCount != null) node.instanceCount = meta.instanceCount;
1265
+ return node;
1114
1266
  }
1115
1267
  function findRoot(store) {
1116
1268
  const allObjects = store.getFlatList();
@@ -1699,6 +1851,7 @@ function click3D(idOrUuid, options = {}) {
1699
1851
  const camera = getCamera();
1700
1852
  const gl = getRenderer();
1701
1853
  const size = getCanvasSize();
1854
+ r3fLog("click", `click3D("${idOrUuid}") \u2014 resolving projection`);
1702
1855
  const projection = projectToScreen(obj, camera, size);
1703
1856
  if (!projection) {
1704
1857
  throw new Error(
@@ -1791,6 +1944,7 @@ function hover3D(idOrUuid, options = {}) {
1791
1944
  const camera = getCamera();
1792
1945
  const gl = getRenderer();
1793
1946
  const size = getCanvasSize();
1947
+ r3fLog("hover", `hover3D("${idOrUuid}") \u2014 resolving projection`);
1794
1948
  const projection = projectToScreen(obj, camera, size);
1795
1949
  if (!projection) {
1796
1950
  throw new Error(
@@ -1825,6 +1979,7 @@ async function drag3D(idOrUuid, delta, options = {}) {
1825
1979
  const camera = getCamera();
1826
1980
  const gl = getRenderer();
1827
1981
  const size = getCanvasSize();
1982
+ r3fLog("drag", `drag3D("${idOrUuid}") mode=${mode}`, delta);
1828
1983
  const projection = projectToScreen(obj, camera, size);
1829
1984
  if (!projection) {
1830
1985
  throw new Error(
@@ -1894,6 +2049,162 @@ function pointerMiss3D(options = {}) {
1894
2049
  dispatchPointerMiss(canvas, options.point);
1895
2050
  }
1896
2051
 
2052
+ // src/interactions/drawPath.ts
2053
+ var _nextDrawPointerId = 5e3;
2054
+ function makeDrawPointerInit(canvas, point, pointerId, pointerType, overrides) {
2055
+ const rect = canvas.getBoundingClientRect();
2056
+ const clientX = rect.left + point.x;
2057
+ const clientY = rect.top + point.y;
2058
+ return {
2059
+ bubbles: true,
2060
+ cancelable: true,
2061
+ composed: true,
2062
+ clientX,
2063
+ clientY,
2064
+ screenX: clientX,
2065
+ screenY: clientY,
2066
+ pointerId,
2067
+ pointerType,
2068
+ isPrimary: true,
2069
+ button: 0,
2070
+ buttons: 1,
2071
+ width: 1,
2072
+ height: 1,
2073
+ pressure: point.pressure ?? 0.5,
2074
+ ...overrides
2075
+ };
2076
+ }
2077
+ async function drawPath(points, options = {}) {
2078
+ if (points.length < 2) {
2079
+ throw new Error(
2080
+ `[react-three-dom] drawPath requires at least 2 points, got ${points.length}.`
2081
+ );
2082
+ }
2083
+ const {
2084
+ stepDelayMs = 0,
2085
+ pointerType = "mouse",
2086
+ clickAtEnd = false,
2087
+ canvas: explicitCanvas
2088
+ } = options;
2089
+ const canvas = explicitCanvas ?? getRenderer().domElement;
2090
+ const pointerId = _nextDrawPointerId++;
2091
+ let eventCount = 0;
2092
+ r3fLog("draw", `drawPath \u2014 ${points.length} points, delay=${stepDelayMs}ms, pointerType=${pointerType}`);
2093
+ const first = points[0];
2094
+ canvas.dispatchEvent(
2095
+ new PointerEvent("pointerdown", makeDrawPointerInit(canvas, first, pointerId, pointerType))
2096
+ );
2097
+ eventCount++;
2098
+ for (let i = 1; i < points.length; i++) {
2099
+ if (stepDelayMs > 0) {
2100
+ await sleep2(stepDelayMs);
2101
+ }
2102
+ canvas.dispatchEvent(
2103
+ new PointerEvent(
2104
+ "pointermove",
2105
+ makeDrawPointerInit(canvas, points[i], pointerId, pointerType)
2106
+ )
2107
+ );
2108
+ eventCount++;
2109
+ }
2110
+ const last = points[points.length - 1];
2111
+ canvas.dispatchEvent(
2112
+ new PointerEvent(
2113
+ "pointerup",
2114
+ makeDrawPointerInit(canvas, last, pointerId, pointerType, {
2115
+ buttons: 0,
2116
+ pressure: 0
2117
+ })
2118
+ )
2119
+ );
2120
+ eventCount++;
2121
+ if (clickAtEnd) {
2122
+ const rect = canvas.getBoundingClientRect();
2123
+ canvas.dispatchEvent(
2124
+ new MouseEvent("click", {
2125
+ bubbles: true,
2126
+ cancelable: true,
2127
+ clientX: rect.left + last.x,
2128
+ clientY: rect.top + last.y,
2129
+ button: 0
2130
+ })
2131
+ );
2132
+ eventCount++;
2133
+ }
2134
+ r3fLog("draw", `drawPath complete \u2014 ${eventCount} events dispatched`);
2135
+ return {
2136
+ eventCount,
2137
+ pointCount: points.length,
2138
+ startPoint: first,
2139
+ endPoint: last
2140
+ };
2141
+ }
2142
+ function linePath(start, end, steps = 10, pressure = 0.5) {
2143
+ const points = [];
2144
+ const totalSteps = steps + 1;
2145
+ for (let i = 0; i <= totalSteps; i++) {
2146
+ const t = i / totalSteps;
2147
+ points.push({
2148
+ x: start.x + (end.x - start.x) * t,
2149
+ y: start.y + (end.y - start.y) * t,
2150
+ pressure
2151
+ });
2152
+ }
2153
+ return points;
2154
+ }
2155
+ function curvePath(start, control, end, steps = 20, pressure = 0.5) {
2156
+ const points = [];
2157
+ for (let i = 0; i <= steps; i++) {
2158
+ const t = i / steps;
2159
+ const invT = 1 - t;
2160
+ points.push({
2161
+ x: invT * invT * start.x + 2 * invT * t * control.x + t * t * end.x,
2162
+ y: invT * invT * start.y + 2 * invT * t * control.y + t * t * end.y,
2163
+ pressure
2164
+ });
2165
+ }
2166
+ return points;
2167
+ }
2168
+ function rectPath(topLeft, bottomRight, pointsPerSide = 5, pressure = 0.5) {
2169
+ const topRight = { x: bottomRight.x, y: topLeft.y };
2170
+ const bottomLeft = { x: topLeft.x, y: bottomRight.y };
2171
+ const sides = [
2172
+ [topLeft, topRight],
2173
+ [topRight, bottomRight],
2174
+ [bottomRight, bottomLeft],
2175
+ [bottomLeft, topLeft]
2176
+ ];
2177
+ const points = [];
2178
+ for (const [from, to] of sides) {
2179
+ for (let i = 0; i < pointsPerSide; i++) {
2180
+ const t = i / pointsPerSide;
2181
+ points.push({
2182
+ x: from.x + (to.x - from.x) * t,
2183
+ y: from.y + (to.y - from.y) * t,
2184
+ pressure
2185
+ });
2186
+ }
2187
+ }
2188
+ points.push({ x: topLeft.x, y: topLeft.y, pressure });
2189
+ return points;
2190
+ }
2191
+ function circlePath(center, radiusX, radiusY, steps = 36, pressure = 0.5) {
2192
+ const ry = radiusY ?? radiusX;
2193
+ const points = [];
2194
+ for (let i = 0; i <= steps; i++) {
2195
+ const angle = i / steps * Math.PI * 2;
2196
+ points.push({
2197
+ x: center.x + Math.cos(angle) * radiusX,
2198
+ y: center.y + Math.sin(angle) * ry,
2199
+ pressure
2200
+ });
2201
+ }
2202
+ return points;
2203
+ }
2204
+ function sleep2(ms) {
2205
+ return new Promise((resolve) => setTimeout(resolve, ms));
2206
+ }
2207
+
1897
2208
  // src/highlight/SelectionManager.ts
1898
2209
  var SelectionManager = class {
1899
2210
  constructor() {
@@ -2048,10 +2359,22 @@ function projectToScreenRect(obj, camera, canvasRect) {
2048
2359
  }
2049
2360
  function exposeGlobalAPI(store) {
2050
2361
  const api = {
2362
+ _ready: true,
2051
2363
  getByTestId: (id) => store.getByTestId(id),
2052
2364
  getByUuid: (uuid) => store.getByUuid(uuid),
2053
2365
  getByName: (name) => store.getByName(name),
2054
2366
  getCount: () => store.getCount(),
2367
+ getByType: (type) => store.getByType(type),
2368
+ getByUserData: (key, value) => store.getByUserData(key, value),
2369
+ getCountByType: (type) => store.getCountByType(type),
2370
+ getObjects: (ids) => {
2371
+ const map = store.getObjects(ids);
2372
+ const result = {};
2373
+ for (const [id, meta] of map) {
2374
+ result[id] = meta;
2375
+ }
2376
+ return result;
2377
+ },
2055
2378
  snapshot: () => createSnapshot(store),
2056
2379
  inspect: (idOrUuid) => store.inspect(idOrUuid),
2057
2380
  click: (idOrUuid) => {
@@ -2066,8 +2389,8 @@ function exposeGlobalAPI(store) {
2066
2389
  hover: (idOrUuid) => {
2067
2390
  hover3D(idOrUuid);
2068
2391
  },
2069
- drag: (idOrUuid, delta) => {
2070
- void drag3D(idOrUuid, delta);
2392
+ drag: async (idOrUuid, delta) => {
2393
+ await drag3D(idOrUuid, delta);
2071
2394
  },
2072
2395
  wheel: (idOrUuid, options) => {
2073
2396
  wheel3D(idOrUuid, options);
@@ -2075,6 +2398,10 @@ function exposeGlobalAPI(store) {
2075
2398
  pointerMiss: () => {
2076
2399
  pointerMiss3D();
2077
2400
  },
2401
+ drawPath: async (points, options) => {
2402
+ const result = await drawPath(points, options);
2403
+ return { eventCount: result.eventCount, pointCount: result.pointCount };
2404
+ },
2078
2405
  select: (idOrUuid) => {
2079
2406
  const obj = store.getObject3D(idOrUuid);
2080
2407
  if (obj && _selectionManager) _selectionManager.select(obj);
@@ -2082,13 +2409,28 @@ function exposeGlobalAPI(store) {
2082
2409
  clearSelection: () => {
2083
2410
  _selectionManager?.clearSelection();
2084
2411
  },
2412
+ getSelection: () => _selectionManager ? _selectionManager.getSelected().map((o) => o.uuid) : [],
2085
2413
  getObject3D: (idOrUuid) => store.getObject3D(idOrUuid),
2086
2414
  version
2087
2415
  };
2088
2416
  window.__R3F_DOM__ = api;
2089
2417
  }
2090
- function removeGlobalAPI() {
2091
- delete window.__R3F_DOM__;
2418
+ function removeGlobalAPI(onlyIfEquals) {
2419
+ r3fLog("bridge", "removeGlobalAPI called (deferred)");
2420
+ if (onlyIfEquals !== void 0) {
2421
+ const ref = onlyIfEquals;
2422
+ queueMicrotask(() => {
2423
+ if (window.__R3F_DOM__ === ref) {
2424
+ delete window.__R3F_DOM__;
2425
+ r3fLog("bridge", "Global API removed");
2426
+ } else {
2427
+ r3fLog("bridge", "Global API not removed \u2014 replaced by new instance (Strict Mode remount)");
2428
+ }
2429
+ });
2430
+ } else {
2431
+ delete window.__R3F_DOM__;
2432
+ r3fLog("bridge", "Global API removed (immediate)");
2433
+ }
2092
2434
  }
2093
2435
  function setElementRect(el, l, t, w, h) {
2094
2436
  const d = el.dataset;
@@ -2109,7 +2451,8 @@ function ThreeDom({
2109
2451
  timeBudgetMs = 0.5,
2110
2452
  maxDomNodes = 2e3,
2111
2453
  initialDepth = 3,
2112
- enabled = true
2454
+ enabled = true,
2455
+ debug = false
2113
2456
  } = {}) {
2114
2457
  const scene = fiber.useThree((s) => s.scene);
2115
2458
  const camera = fiber.useThree((s) => s.camera);
@@ -2119,6 +2462,8 @@ function ThreeDom({
2119
2462
  const positionCursorRef = react.useRef(0);
2120
2463
  react.useEffect(() => {
2121
2464
  if (!enabled) return;
2465
+ if (debug) enableDebug(true);
2466
+ r3fLog("setup", "ThreeDom effect started", { enabled, debug, root, maxDomNodes });
2122
2467
  const canvas = gl.domElement;
2123
2468
  const canvasParent = canvas.parentElement;
2124
2469
  let rootElement = null;
@@ -2145,57 +2490,115 @@ function ThreeDom({
2145
2490
  "overflow: hidden",
2146
2491
  "z-index: 10"
2147
2492
  ].join(";");
2148
- const store = new ObjectStore();
2149
- const mirror = new DomMirror(store, maxDomNodes);
2150
- mirror.setRoot(rootElement);
2151
- ensureCustomElements(store);
2152
- store.registerTree(scene);
2153
- mirror.materializeSubtree(scene.uuid, initialDepth);
2154
- const unpatch = patchObject3D(store, mirror);
2155
- setInteractionState(store, camera, gl, size);
2156
- const selectionManager = new SelectionManager();
2157
- _selectionManager = selectionManager;
2158
- _highlighter = null;
2159
- exposeGlobalAPI(store);
2160
- _store3 = store;
2161
- _mirror = mirror;
2162
- const initialCanvasRect = canvas.getBoundingClientRect();
2163
- const allObjects = store.getFlatList();
2164
- for (const obj of allObjects) {
2165
- if (obj.userData?.__r3fdom_internal) continue;
2166
- const el = mirror.getElement(obj.uuid);
2167
- if (!el) continue;
2168
- if (obj.type === "Scene") {
2169
- setElementRect(el, 0, 0, Math.round(initialCanvasRect.width), Math.round(initialCanvasRect.height));
2170
- continue;
2171
- }
2172
- const rect = projectToScreenRect(obj, camera, initialCanvasRect);
2173
- if (rect) {
2174
- let parentLeft = 0;
2175
- let parentTop = 0;
2176
- if (obj.parent && obj.parent.type !== "Scene") {
2177
- const parentRect = projectToScreenRect(obj.parent, camera, initialCanvasRect);
2178
- if (parentRect) {
2179
- parentLeft = Math.round(parentRect.left);
2180
- parentTop = Math.round(parentRect.top);
2493
+ let store = null;
2494
+ let mirror = null;
2495
+ let unpatch = null;
2496
+ let selectionManager = null;
2497
+ let currentApi;
2498
+ try {
2499
+ store = new ObjectStore();
2500
+ mirror = new DomMirror(store, maxDomNodes);
2501
+ mirror.setRoot(rootElement);
2502
+ r3fLog("setup", "Store and mirror created");
2503
+ ensureCustomElements(store);
2504
+ store.registerTree(scene);
2505
+ r3fLog("setup", `Registered scene tree: ${store.getCount()} objects`);
2506
+ mirror.materializeSubtree(scene.uuid, initialDepth);
2507
+ unpatch = patchObject3D(store, mirror);
2508
+ setInteractionState(store, camera, gl, size);
2509
+ r3fLog("setup", "Object3D patched, interaction state set");
2510
+ selectionManager = new SelectionManager();
2511
+ _selectionManager = selectionManager;
2512
+ _highlighter = null;
2513
+ exposeGlobalAPI(store);
2514
+ r3fLog("bridge", "exposeGlobalAPI called \u2014 bridge is live, _ready=true");
2515
+ currentApi = window.__R3F_DOM__;
2516
+ _store3 = store;
2517
+ _mirror = mirror;
2518
+ const initialCanvasRect = canvas.getBoundingClientRect();
2519
+ const allObjects = store.getFlatList();
2520
+ for (const obj of allObjects) {
2521
+ if (obj.userData?.__r3fdom_internal) continue;
2522
+ const el = mirror.getElement(obj.uuid);
2523
+ if (!el) continue;
2524
+ if (obj.type === "Scene") {
2525
+ setElementRect(el, 0, 0, Math.round(initialCanvasRect.width), Math.round(initialCanvasRect.height));
2526
+ continue;
2527
+ }
2528
+ const rect = projectToScreenRect(obj, camera, initialCanvasRect);
2529
+ if (rect) {
2530
+ let parentLeft = 0;
2531
+ let parentTop = 0;
2532
+ if (obj.parent && obj.parent.type !== "Scene") {
2533
+ const parentRect = projectToScreenRect(obj.parent, camera, initialCanvasRect);
2534
+ if (parentRect) {
2535
+ parentLeft = Math.round(parentRect.left);
2536
+ parentTop = Math.round(parentRect.top);
2537
+ }
2181
2538
  }
2539
+ setElementRect(
2540
+ el,
2541
+ Math.round(rect.left) - parentLeft,
2542
+ Math.round(rect.top) - parentTop,
2543
+ Math.round(rect.width),
2544
+ Math.round(rect.height)
2545
+ );
2182
2546
  }
2183
- setElementRect(
2184
- el,
2185
- Math.round(rect.left) - parentLeft,
2186
- Math.round(rect.top) - parentTop,
2187
- Math.round(rect.width),
2188
- Math.round(rect.height)
2189
- );
2190
2547
  }
2548
+ } catch (err) {
2549
+ const errorMsg = err instanceof Error ? err.message : String(err);
2550
+ r3fLog("setup", "ThreeDom setup failed", err);
2551
+ console.error("[react-three-dom] Setup failed:", err);
2552
+ window.__R3F_DOM__ = {
2553
+ _ready: false,
2554
+ _error: errorMsg,
2555
+ getByTestId: () => null,
2556
+ getByUuid: () => null,
2557
+ getByName: () => [],
2558
+ getCount: () => 0,
2559
+ getByType: () => [],
2560
+ getByUserData: () => [],
2561
+ getCountByType: () => 0,
2562
+ getObjects: (ids) => {
2563
+ const result = {};
2564
+ for (const id of ids) result[id] = null;
2565
+ return result;
2566
+ },
2567
+ snapshot: () => ({ timestamp: 0, objectCount: 0, tree: { uuid: "", name: "", type: "Scene", visible: true, position: [0, 0, 0], rotation: [0, 0, 0], scale: [1, 1, 1], children: [] } }),
2568
+ inspect: () => null,
2569
+ click: () => {
2570
+ },
2571
+ doubleClick: () => {
2572
+ },
2573
+ contextMenu: () => {
2574
+ },
2575
+ hover: () => {
2576
+ },
2577
+ drag: async () => {
2578
+ },
2579
+ wheel: () => {
2580
+ },
2581
+ pointerMiss: () => {
2582
+ },
2583
+ drawPath: async () => ({ eventCount: 0, pointCount: 0 }),
2584
+ select: () => {
2585
+ },
2586
+ clearSelection: () => {
2587
+ },
2588
+ getSelection: () => [],
2589
+ getObject3D: () => null,
2590
+ version
2591
+ };
2592
+ currentApi = window.__R3F_DOM__;
2191
2593
  }
2192
2594
  return () => {
2193
- unpatch();
2194
- removeGlobalAPI();
2595
+ r3fLog("setup", "ThreeDom cleanup started");
2596
+ if (unpatch) unpatch();
2597
+ removeGlobalAPI(currentApi);
2195
2598
  clearInteractionState();
2196
- selectionManager.dispose();
2197
- mirror.dispose();
2198
- store.dispose();
2599
+ if (selectionManager) selectionManager.dispose();
2600
+ if (mirror) mirror.dispose();
2601
+ if (store) store.dispose();
2199
2602
  if (createdRoot && rootElement?.parentNode) {
2200
2603
  rootElement.parentNode.removeChild(rootElement);
2201
2604
  }
@@ -2203,69 +2606,75 @@ function ThreeDom({
2203
2606
  _mirror = null;
2204
2607
  _selectionManager = null;
2205
2608
  _highlighter = null;
2609
+ if (debug) enableDebug(false);
2610
+ r3fLog("setup", "ThreeDom cleanup complete");
2206
2611
  };
2207
- }, [scene, camera, gl, size, enabled, root, maxDomNodes, initialDepth]);
2612
+ }, [scene, camera, gl, size, enabled, root, maxDomNodes, initialDepth, debug]);
2208
2613
  fiber.useFrame(() => {
2209
2614
  if (!enabled || !_store3 || !_mirror) return;
2210
- setInteractionState(_store3, camera, gl, size);
2211
- const store = _store3;
2212
- const mirror = _mirror;
2213
- const canvas = gl.domElement;
2214
- const canvasRect = canvas.getBoundingClientRect();
2215
- const start = performance.now();
2216
- const dirtyObjects = store.drainDirtyQueue();
2217
- for (const obj of dirtyObjects) {
2218
- store.update(obj);
2219
- mirror.syncAttributes(obj);
2220
- }
2221
- const budgetRemaining = timeBudgetMs - (performance.now() - start);
2222
- if (budgetRemaining > 0.1) {
2223
- const objects2 = store.getFlatList();
2224
- if (objects2.length > 0) {
2225
- const end = Math.min(cursorRef.current + batchSize, objects2.length);
2226
- for (let i = cursorRef.current; i < end; i++) {
2227
- if (performance.now() - start > timeBudgetMs) break;
2228
- const obj = objects2[i];
2229
- const changed = store.update(obj);
2230
- if (changed) mirror.syncAttributes(obj);
2231
- }
2232
- cursorRef.current = end >= objects2.length ? 0 : end;
2615
+ try {
2616
+ setInteractionState(_store3, camera, gl, size);
2617
+ const store = _store3;
2618
+ const mirror = _mirror;
2619
+ const canvas = gl.domElement;
2620
+ const canvasRect = canvas.getBoundingClientRect();
2621
+ const start = performance.now();
2622
+ const dirtyObjects = store.drainDirtyQueue();
2623
+ for (const obj of dirtyObjects) {
2624
+ store.update(obj);
2625
+ mirror.syncAttributes(obj);
2233
2626
  }
2234
- }
2235
- const objects = store.getFlatList();
2236
- if (objects.length > 0) {
2237
- const posEnd = Math.min(positionCursorRef.current + 50, objects.length);
2238
- for (let i = positionCursorRef.current; i < posEnd; i++) {
2239
- const obj = objects[i];
2240
- if (obj.userData?.__r3fdom_internal) continue;
2241
- const el = mirror.getElement(obj.uuid);
2242
- if (!el) continue;
2243
- if (obj.type === "Scene") {
2244
- setElementRect(el, 0, 0, Math.round(canvasRect.width), Math.round(canvasRect.height));
2245
- continue;
2627
+ const budgetRemaining = timeBudgetMs - (performance.now() - start);
2628
+ if (budgetRemaining > 0.1) {
2629
+ const objects2 = store.getFlatList();
2630
+ if (objects2.length > 0) {
2631
+ const end = Math.min(cursorRef.current + batchSize, objects2.length);
2632
+ for (let i = cursorRef.current; i < end; i++) {
2633
+ if (performance.now() - start > timeBudgetMs) break;
2634
+ const obj = objects2[i];
2635
+ const changed = store.update(obj);
2636
+ if (changed) mirror.syncAttributes(obj);
2637
+ }
2638
+ cursorRef.current = end >= objects2.length ? 0 : end;
2246
2639
  }
2247
- const rect = projectToScreenRect(obj, camera, canvasRect);
2248
- if (rect) {
2249
- let parentLeft = 0;
2250
- let parentTop = 0;
2251
- if (obj.parent && obj.parent.type !== "Scene") {
2252
- const parentRect = projectToScreenRect(obj.parent, camera, canvasRect);
2253
- if (parentRect) {
2254
- parentLeft = Math.round(parentRect.left);
2255
- parentTop = Math.round(parentRect.top);
2640
+ }
2641
+ const objects = store.getFlatList();
2642
+ if (objects.length > 0) {
2643
+ const posEnd = Math.min(positionCursorRef.current + 50, objects.length);
2644
+ for (let i = positionCursorRef.current; i < posEnd; i++) {
2645
+ const obj = objects[i];
2646
+ if (obj.userData?.__r3fdom_internal) continue;
2647
+ const el = mirror.getElement(obj.uuid);
2648
+ if (!el) continue;
2649
+ if (obj.type === "Scene") {
2650
+ setElementRect(el, 0, 0, Math.round(canvasRect.width), Math.round(canvasRect.height));
2651
+ continue;
2652
+ }
2653
+ const rect = projectToScreenRect(obj, camera, canvasRect);
2654
+ if (rect) {
2655
+ let parentLeft = 0;
2656
+ let parentTop = 0;
2657
+ if (obj.parent && obj.parent.type !== "Scene") {
2658
+ const parentRect = projectToScreenRect(obj.parent, camera, canvasRect);
2659
+ if (parentRect) {
2660
+ parentLeft = Math.round(parentRect.left);
2661
+ parentTop = Math.round(parentRect.top);
2662
+ }
2256
2663
  }
2664
+ const l = Math.round(rect.left) - parentLeft;
2665
+ const t = Math.round(rect.top) - parentTop;
2666
+ const w = Math.round(rect.width);
2667
+ const h = Math.round(rect.height);
2668
+ setElementRect(el, l, t, w, h);
2669
+ if (el.style.display === "none") el.style.display = "block";
2670
+ } else {
2671
+ if (el.style.display !== "none") el.style.display = "none";
2257
2672
  }
2258
- const l = Math.round(rect.left) - parentLeft;
2259
- const t = Math.round(rect.top) - parentTop;
2260
- const w = Math.round(rect.width);
2261
- const h = Math.round(rect.height);
2262
- setElementRect(el, l, t, w, h);
2263
- if (el.style.display === "none") el.style.display = "block";
2264
- } else {
2265
- if (el.style.display !== "none") el.style.display = "none";
2266
2673
  }
2674
+ positionCursorRef.current = posEnd >= objects.length ? 0 : posEnd;
2267
2675
  }
2268
- positionCursorRef.current = posEnd >= objects.length ? 0 : posEnd;
2676
+ } catch (err) {
2677
+ r3fLog("sync", "Per-frame sync error", err);
2269
2678
  }
2270
2679
  });
2271
2680
  return null;
@@ -2625,11 +3034,13 @@ exports.TAG_MAP = TAG_MAP;
2625
3034
  exports.ThreeDom = ThreeDom;
2626
3035
  exports.ThreeElement = ThreeElement;
2627
3036
  exports.applyAttributes = applyAttributes;
3037
+ exports.circlePath = circlePath;
2628
3038
  exports.click3D = click3D;
2629
3039
  exports.computeAttributes = computeAttributes;
2630
3040
  exports.contextMenu3D = contextMenu3D;
2631
3041
  exports.createFlatSnapshot = createFlatSnapshot;
2632
3042
  exports.createSnapshot = createSnapshot;
3043
+ exports.curvePath = curvePath;
2633
3044
  exports.dispatchClick = dispatchClick;
2634
3045
  exports.dispatchContextMenu = dispatchContextMenu;
2635
3046
  exports.dispatchDoubleClick = dispatchDoubleClick;
@@ -2640,6 +3051,8 @@ exports.dispatchUnhover = dispatchUnhover;
2640
3051
  exports.dispatchWheel = dispatchWheel;
2641
3052
  exports.doubleClick3D = doubleClick3D;
2642
3053
  exports.drag3D = drag3D;
3054
+ exports.drawPath = drawPath;
3055
+ exports.enableDebug = enableDebug;
2643
3056
  exports.ensureCustomElements = ensureCustomElements;
2644
3057
  exports.getHighlighter = getHighlighter;
2645
3058
  exports.getMirror = getMirror;
@@ -2647,13 +3060,17 @@ exports.getSelectionManager = getSelectionManager;
2647
3060
  exports.getStore = getStore2;
2648
3061
  exports.getTagForType = getTagForType;
2649
3062
  exports.hover3D = hover3D;
3063
+ exports.isDebugEnabled = isDebugEnabled;
2650
3064
  exports.isInFrustum = isInFrustum;
2651
3065
  exports.isPatched = isPatched;
3066
+ exports.linePath = linePath;
2652
3067
  exports.patchObject3D = patchObject3D;
2653
3068
  exports.pointerMiss3D = pointerMiss3D;
2654
3069
  exports.previewDragWorldDelta = previewDragWorldDelta;
2655
3070
  exports.projectAllSamplePoints = projectAllSamplePoints;
2656
3071
  exports.projectToScreen = projectToScreen;
3072
+ exports.r3fLog = r3fLog;
3073
+ exports.rectPath = rectPath;
2657
3074
  exports.resolveObject = resolveObject;
2658
3075
  exports.restoreObject3D = restoreObject3D;
2659
3076
  exports.screenDeltaToWorld = screenDeltaToWorld;