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