@react-three-dom/core 0.1.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 +2666 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1054 -0
- package/dist/index.d.ts +1054 -0
- package/dist/index.js +2619 -0
- package/dist/index.js.map +1 -0
- package/package.json +43 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2666 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var three = require('three');
|
|
4
|
+
var react = require('react');
|
|
5
|
+
var fiber = require('@react-three/fiber');
|
|
6
|
+
|
|
7
|
+
// src/version.ts
|
|
8
|
+
var version = "0.1.0";
|
|
9
|
+
function extractMetadata(obj) {
|
|
10
|
+
const meta = {
|
|
11
|
+
uuid: obj.uuid,
|
|
12
|
+
name: obj.name,
|
|
13
|
+
type: obj.type,
|
|
14
|
+
visible: obj.visible,
|
|
15
|
+
testId: obj.userData?.testId,
|
|
16
|
+
position: [obj.position.x, obj.position.y, obj.position.z],
|
|
17
|
+
rotation: [obj.rotation.x, obj.rotation.y, obj.rotation.z],
|
|
18
|
+
scale: [obj.scale.x, obj.scale.y, obj.scale.z],
|
|
19
|
+
parentUuid: obj.parent?.uuid ?? null,
|
|
20
|
+
childrenUuids: obj.children.map((c) => c.uuid),
|
|
21
|
+
boundsDirty: true
|
|
22
|
+
};
|
|
23
|
+
if ("geometry" in obj) {
|
|
24
|
+
const geom = obj.geometry;
|
|
25
|
+
if (geom instanceof three.BufferGeometry) {
|
|
26
|
+
meta.geometryType = geom.type;
|
|
27
|
+
const posAttr = geom.getAttribute("position");
|
|
28
|
+
if (posAttr) {
|
|
29
|
+
meta.vertexCount = posAttr.count;
|
|
30
|
+
if (geom.index) {
|
|
31
|
+
meta.triangleCount = Math.floor(geom.index.count / 3);
|
|
32
|
+
} else {
|
|
33
|
+
meta.triangleCount = Math.floor(posAttr.count / 3);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if ("material" in obj) {
|
|
39
|
+
const mat = obj.material;
|
|
40
|
+
if (mat instanceof three.Material) {
|
|
41
|
+
meta.materialType = mat.type;
|
|
42
|
+
} else if (Array.isArray(mat) && mat.length > 0) {
|
|
43
|
+
meta.materialType = mat[0].type + (mat.length > 1 ? ` (+${mat.length - 1})` : "");
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (obj instanceof three.InstancedMesh) {
|
|
47
|
+
meta.instanceCount = obj.count;
|
|
48
|
+
}
|
|
49
|
+
return meta;
|
|
50
|
+
}
|
|
51
|
+
function hasChanged(prev, curr) {
|
|
52
|
+
return prev.visible !== curr.visible || prev.name !== curr.name || prev.testId !== curr.testId || prev.position[0] !== curr.position[0] || prev.position[1] !== curr.position[1] || prev.position[2] !== curr.position[2] || prev.rotation[0] !== curr.rotation[0] || prev.rotation[1] !== curr.rotation[1] || prev.rotation[2] !== curr.rotation[2] || prev.scale[0] !== curr.scale[0] || prev.scale[1] !== curr.scale[1] || prev.scale[2] !== curr.scale[2] || prev.parentUuid !== curr.parentUuid || prev.childrenUuids.length !== curr.childrenUuids.length || prev.instanceCount !== curr.instanceCount;
|
|
53
|
+
}
|
|
54
|
+
var _box3 = new three.Box3();
|
|
55
|
+
function inspectObject(obj, metadata) {
|
|
56
|
+
obj.updateWorldMatrix(true, false);
|
|
57
|
+
const worldMatrix = Array.from(obj.matrixWorld.elements);
|
|
58
|
+
_box3.setFromObject(obj);
|
|
59
|
+
const boundsMin = [_box3.min.x, _box3.min.y, _box3.min.z];
|
|
60
|
+
const boundsMax = [_box3.max.x, _box3.max.y, _box3.max.z];
|
|
61
|
+
const inspection = {
|
|
62
|
+
metadata,
|
|
63
|
+
worldMatrix,
|
|
64
|
+
bounds: { min: boundsMin, max: boundsMax },
|
|
65
|
+
userData: { ...obj.userData }
|
|
66
|
+
};
|
|
67
|
+
if ("geometry" in obj) {
|
|
68
|
+
const geom = obj.geometry;
|
|
69
|
+
if (geom instanceof three.BufferGeometry) {
|
|
70
|
+
const geoInspection = {
|
|
71
|
+
type: geom.type,
|
|
72
|
+
attributes: {}
|
|
73
|
+
};
|
|
74
|
+
for (const [name, attr] of Object.entries(geom.attributes)) {
|
|
75
|
+
geoInspection.attributes[name] = {
|
|
76
|
+
itemSize: attr.itemSize,
|
|
77
|
+
count: attr.count
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
if (geom.index) {
|
|
81
|
+
geoInspection.index = { count: geom.index.count };
|
|
82
|
+
}
|
|
83
|
+
geom.computeBoundingSphere();
|
|
84
|
+
const sphere = geom.boundingSphere;
|
|
85
|
+
if (sphere) {
|
|
86
|
+
geoInspection.boundingSphere = {
|
|
87
|
+
center: [sphere.center.x, sphere.center.y, sphere.center.z],
|
|
88
|
+
radius: sphere.radius
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
inspection.geometry = geoInspection;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if ("material" in obj) {
|
|
95
|
+
const rawMat = obj.material;
|
|
96
|
+
const mat = Array.isArray(rawMat) ? rawMat[0] : rawMat;
|
|
97
|
+
if (mat instanceof three.Material) {
|
|
98
|
+
const matInspection = {
|
|
99
|
+
type: mat.type,
|
|
100
|
+
transparent: mat.transparent,
|
|
101
|
+
opacity: mat.opacity,
|
|
102
|
+
side: mat.side
|
|
103
|
+
};
|
|
104
|
+
if ("color" in mat && mat.color instanceof three.Color) {
|
|
105
|
+
matInspection.color = "#" + mat.color.getHexString();
|
|
106
|
+
}
|
|
107
|
+
if ("map" in mat) {
|
|
108
|
+
const map = mat.map;
|
|
109
|
+
if (map) {
|
|
110
|
+
matInspection.map = map.name || map.uuid || "unnamed";
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if ("uniforms" in mat) {
|
|
114
|
+
const uniforms = mat.uniforms;
|
|
115
|
+
matInspection.uniforms = {};
|
|
116
|
+
for (const [key, uniform] of Object.entries(uniforms)) {
|
|
117
|
+
const val = uniform.value;
|
|
118
|
+
if (val === null || val === void 0) {
|
|
119
|
+
matInspection.uniforms[key] = val;
|
|
120
|
+
} else if (typeof val === "number" || typeof val === "boolean" || typeof val === "string") {
|
|
121
|
+
matInspection.uniforms[key] = val;
|
|
122
|
+
} else if (typeof val === "object" && "toArray" in val) {
|
|
123
|
+
matInspection.uniforms[key] = val.toArray();
|
|
124
|
+
} else {
|
|
125
|
+
matInspection.uniforms[key] = `[${typeof val}]`;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
inspection.material = matInspection;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return inspection;
|
|
133
|
+
}
|
|
134
|
+
var ObjectStore = class {
|
|
135
|
+
constructor() {
|
|
136
|
+
// Tier 1: metadata for every tracked object
|
|
137
|
+
this._metaByObject = /* @__PURE__ */ new WeakMap();
|
|
138
|
+
this._objectByUuid = /* @__PURE__ */ new Map();
|
|
139
|
+
this._objectsByTestId = /* @__PURE__ */ new Map();
|
|
140
|
+
this._objectsByName = /* @__PURE__ */ new Map();
|
|
141
|
+
// Flat list for amortized iteration
|
|
142
|
+
this._flatList = [];
|
|
143
|
+
this._flatListDirty = true;
|
|
144
|
+
// Priority dirty queue: objects that changed and need immediate sync
|
|
145
|
+
this._dirtyQueue = /* @__PURE__ */ new Set();
|
|
146
|
+
// Event listeners
|
|
147
|
+
this._listeners = [];
|
|
148
|
+
// Track the root scene(s) for scoping
|
|
149
|
+
this._trackedRoots = /* @__PURE__ */ new WeakSet();
|
|
150
|
+
}
|
|
151
|
+
// -------------------------------------------------------------------------
|
|
152
|
+
// Registration
|
|
153
|
+
// -------------------------------------------------------------------------
|
|
154
|
+
/**
|
|
155
|
+
* Register a single object into the store.
|
|
156
|
+
* Populates Tier 1 metadata and all indexes.
|
|
157
|
+
*/
|
|
158
|
+
register(obj) {
|
|
159
|
+
if (obj.userData?.__r3fdom_internal) {
|
|
160
|
+
return extractMetadata(obj);
|
|
161
|
+
}
|
|
162
|
+
const existing = this._metaByObject.get(obj);
|
|
163
|
+
if (existing) return existing;
|
|
164
|
+
const meta = extractMetadata(obj);
|
|
165
|
+
this._metaByObject.set(obj, meta);
|
|
166
|
+
this._objectByUuid.set(meta.uuid, obj);
|
|
167
|
+
this._flatListDirty = true;
|
|
168
|
+
if (meta.testId) {
|
|
169
|
+
this._objectsByTestId.set(meta.testId, obj);
|
|
170
|
+
}
|
|
171
|
+
if (meta.name) {
|
|
172
|
+
let nameSet = this._objectsByName.get(meta.name);
|
|
173
|
+
if (!nameSet) {
|
|
174
|
+
nameSet = /* @__PURE__ */ new Set();
|
|
175
|
+
this._objectsByName.set(meta.name, nameSet);
|
|
176
|
+
}
|
|
177
|
+
nameSet.add(obj);
|
|
178
|
+
}
|
|
179
|
+
this._emit({ type: "add", object: obj, metadata: meta });
|
|
180
|
+
return meta;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Register an entire subtree (object + all descendants).
|
|
184
|
+
*/
|
|
185
|
+
registerTree(root) {
|
|
186
|
+
this._trackedRoots.add(root);
|
|
187
|
+
root.traverse((obj) => {
|
|
188
|
+
this.register(obj);
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Unregister a single object from the store.
|
|
193
|
+
*/
|
|
194
|
+
unregister(obj) {
|
|
195
|
+
const meta = this._metaByObject.get(obj);
|
|
196
|
+
if (!meta) return;
|
|
197
|
+
this._metaByObject.delete(obj);
|
|
198
|
+
this._objectByUuid.delete(meta.uuid);
|
|
199
|
+
this._dirtyQueue.delete(obj);
|
|
200
|
+
this._flatListDirty = true;
|
|
201
|
+
if (meta.testId) {
|
|
202
|
+
this._objectsByTestId.delete(meta.testId);
|
|
203
|
+
}
|
|
204
|
+
if (meta.name) {
|
|
205
|
+
const nameSet = this._objectsByName.get(meta.name);
|
|
206
|
+
if (nameSet) {
|
|
207
|
+
nameSet.delete(obj);
|
|
208
|
+
if (nameSet.size === 0) {
|
|
209
|
+
this._objectsByName.delete(meta.name);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
this._emit({ type: "remove", object: obj, metadata: meta });
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Unregister an entire subtree (object + all descendants).
|
|
217
|
+
*/
|
|
218
|
+
unregisterTree(root) {
|
|
219
|
+
root.traverse((obj) => {
|
|
220
|
+
this.unregister(obj);
|
|
221
|
+
});
|
|
222
|
+
this._trackedRoots.delete(root);
|
|
223
|
+
}
|
|
224
|
+
// -------------------------------------------------------------------------
|
|
225
|
+
// Tier 1: Update (compare-and-set, returns true if changed)
|
|
226
|
+
// -------------------------------------------------------------------------
|
|
227
|
+
/**
|
|
228
|
+
* Refresh Tier 1 metadata from the live Three.js object.
|
|
229
|
+
* Returns true if any values changed.
|
|
230
|
+
*/
|
|
231
|
+
update(obj) {
|
|
232
|
+
const prev = this._metaByObject.get(obj);
|
|
233
|
+
if (!prev) return false;
|
|
234
|
+
const curr = extractMetadata(obj);
|
|
235
|
+
if (hasChanged(prev, curr)) {
|
|
236
|
+
if (prev.testId !== curr.testId) {
|
|
237
|
+
if (prev.testId) this._objectsByTestId.delete(prev.testId);
|
|
238
|
+
if (curr.testId) this._objectsByTestId.set(curr.testId, obj);
|
|
239
|
+
}
|
|
240
|
+
if (prev.name !== curr.name) {
|
|
241
|
+
if (prev.name) {
|
|
242
|
+
const nameSet = this._objectsByName.get(prev.name);
|
|
243
|
+
if (nameSet) {
|
|
244
|
+
nameSet.delete(obj);
|
|
245
|
+
if (nameSet.size === 0) this._objectsByName.delete(prev.name);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (curr.name) {
|
|
249
|
+
let nameSet = this._objectsByName.get(curr.name);
|
|
250
|
+
if (!nameSet) {
|
|
251
|
+
nameSet = /* @__PURE__ */ new Set();
|
|
252
|
+
this._objectsByName.set(curr.name, nameSet);
|
|
253
|
+
}
|
|
254
|
+
nameSet.add(obj);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
this._metaByObject.set(obj, curr);
|
|
258
|
+
this._emit({ type: "update", object: obj, metadata: curr });
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
// -------------------------------------------------------------------------
|
|
264
|
+
// Tier 2: On-demand inspection (never cached)
|
|
265
|
+
// -------------------------------------------------------------------------
|
|
266
|
+
/**
|
|
267
|
+
* Compute full inspection data from a live Three.js object.
|
|
268
|
+
* This reads geometry buffers, material properties, world bounds, etc.
|
|
269
|
+
* Cost: 0.1–2ms depending on geometry complexity.
|
|
270
|
+
*/
|
|
271
|
+
inspect(idOrUuid) {
|
|
272
|
+
const obj = this.getObject3D(idOrUuid);
|
|
273
|
+
if (!obj) return null;
|
|
274
|
+
const meta = this._metaByObject.get(obj);
|
|
275
|
+
if (!meta) return null;
|
|
276
|
+
return inspectObject(obj, meta);
|
|
277
|
+
}
|
|
278
|
+
// -------------------------------------------------------------------------
|
|
279
|
+
// Lookups (O(1))
|
|
280
|
+
// -------------------------------------------------------------------------
|
|
281
|
+
/** Get metadata by testId. O(1). */
|
|
282
|
+
getByTestId(testId) {
|
|
283
|
+
const obj = this._objectsByTestId.get(testId);
|
|
284
|
+
if (!obj) return null;
|
|
285
|
+
return this._metaByObject.get(obj) ?? null;
|
|
286
|
+
}
|
|
287
|
+
/** Get metadata by uuid. O(1). */
|
|
288
|
+
getByUuid(uuid) {
|
|
289
|
+
const obj = this._objectByUuid.get(uuid);
|
|
290
|
+
if (!obj) return null;
|
|
291
|
+
return this._metaByObject.get(obj) ?? null;
|
|
292
|
+
}
|
|
293
|
+
/** Get metadata by name (returns array since names aren't unique). O(1). */
|
|
294
|
+
getByName(name) {
|
|
295
|
+
const objs = this._objectsByName.get(name);
|
|
296
|
+
if (!objs) return [];
|
|
297
|
+
const results = [];
|
|
298
|
+
for (const obj of objs) {
|
|
299
|
+
const meta = this._metaByObject.get(obj);
|
|
300
|
+
if (meta) results.push(meta);
|
|
301
|
+
}
|
|
302
|
+
return results;
|
|
303
|
+
}
|
|
304
|
+
/** Get the raw Three.js Object3D by testId or uuid. */
|
|
305
|
+
getObject3D(idOrUuid) {
|
|
306
|
+
return this._objectsByTestId.get(idOrUuid) ?? this._objectByUuid.get(idOrUuid) ?? null;
|
|
307
|
+
}
|
|
308
|
+
/** Get metadata for a known Object3D reference. */
|
|
309
|
+
getMetadata(obj) {
|
|
310
|
+
return this._metaByObject.get(obj) ?? null;
|
|
311
|
+
}
|
|
312
|
+
/** Check if an object is registered. */
|
|
313
|
+
has(obj) {
|
|
314
|
+
return this._metaByObject.has(obj);
|
|
315
|
+
}
|
|
316
|
+
/** Total number of tracked objects. */
|
|
317
|
+
getCount() {
|
|
318
|
+
return this._objectByUuid.size;
|
|
319
|
+
}
|
|
320
|
+
/** Check if a root scene is tracked. */
|
|
321
|
+
isTrackedRoot(obj) {
|
|
322
|
+
return this._trackedRoots.has(obj);
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Walk up from `obj` to see if any ancestor is a tracked root.
|
|
326
|
+
* Used by Object3D.add/remove patch to determine if an object
|
|
327
|
+
* belongs to a monitored scene.
|
|
328
|
+
*/
|
|
329
|
+
isInTrackedScene(obj) {
|
|
330
|
+
let current = obj;
|
|
331
|
+
while (current) {
|
|
332
|
+
if (this._trackedRoots.has(current)) return true;
|
|
333
|
+
current = current.parent;
|
|
334
|
+
}
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
// -------------------------------------------------------------------------
|
|
338
|
+
// Flat list for amortized iteration
|
|
339
|
+
// -------------------------------------------------------------------------
|
|
340
|
+
/**
|
|
341
|
+
* Get a flat array of all tracked objects for amortized batch processing.
|
|
342
|
+
* Rebuilds lazily when the list is dirty (objects added/removed).
|
|
343
|
+
*/
|
|
344
|
+
getFlatList() {
|
|
345
|
+
if (this._flatListDirty) {
|
|
346
|
+
this._flatList = Array.from(this._objectByUuid.values());
|
|
347
|
+
this._flatListDirty = false;
|
|
348
|
+
}
|
|
349
|
+
return this._flatList;
|
|
350
|
+
}
|
|
351
|
+
// -------------------------------------------------------------------------
|
|
352
|
+
// Priority dirty queue
|
|
353
|
+
// -------------------------------------------------------------------------
|
|
354
|
+
/** Mark an object as dirty (needs priority sync next frame). */
|
|
355
|
+
markDirty(obj) {
|
|
356
|
+
if (this._metaByObject.has(obj)) {
|
|
357
|
+
this._dirtyQueue.add(obj);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Drain the dirty queue, returning all objects that need priority sync.
|
|
362
|
+
* Clears the queue after draining.
|
|
363
|
+
*/
|
|
364
|
+
drainDirtyQueue() {
|
|
365
|
+
if (this._dirtyQueue.size === 0) return [];
|
|
366
|
+
const objects = Array.from(this._dirtyQueue);
|
|
367
|
+
this._dirtyQueue.clear();
|
|
368
|
+
return objects;
|
|
369
|
+
}
|
|
370
|
+
/** Number of objects currently in the dirty queue. */
|
|
371
|
+
getDirtyCount() {
|
|
372
|
+
return this._dirtyQueue.size;
|
|
373
|
+
}
|
|
374
|
+
// -------------------------------------------------------------------------
|
|
375
|
+
// Event system
|
|
376
|
+
// -------------------------------------------------------------------------
|
|
377
|
+
/** Subscribe to store events (add, remove, update). */
|
|
378
|
+
subscribe(listener) {
|
|
379
|
+
this._listeners.push(listener);
|
|
380
|
+
return () => {
|
|
381
|
+
const idx = this._listeners.indexOf(listener);
|
|
382
|
+
if (idx !== -1) this._listeners.splice(idx, 1);
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
_emit(event) {
|
|
386
|
+
for (const listener of this._listeners) {
|
|
387
|
+
listener(event);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
// -------------------------------------------------------------------------
|
|
391
|
+
// Cleanup
|
|
392
|
+
// -------------------------------------------------------------------------
|
|
393
|
+
/** Remove all tracked objects and reset state. */
|
|
394
|
+
dispose() {
|
|
395
|
+
this._objectByUuid.clear();
|
|
396
|
+
this._objectsByTestId.clear();
|
|
397
|
+
this._objectsByName.clear();
|
|
398
|
+
this._flatList = [];
|
|
399
|
+
this._flatListDirty = true;
|
|
400
|
+
this._dirtyQueue.clear();
|
|
401
|
+
this._listeners = [];
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
// src/mirror/CustomElements.ts
|
|
406
|
+
var TAG_MAP = {
|
|
407
|
+
// Scenes
|
|
408
|
+
Scene: "three-scene",
|
|
409
|
+
// Groups / containers
|
|
410
|
+
Group: "three-group",
|
|
411
|
+
LOD: "three-group",
|
|
412
|
+
Bone: "three-group",
|
|
413
|
+
// Meshes
|
|
414
|
+
Mesh: "three-mesh",
|
|
415
|
+
SkinnedMesh: "three-mesh",
|
|
416
|
+
InstancedMesh: "three-mesh",
|
|
417
|
+
LineSegments: "three-mesh",
|
|
418
|
+
Line: "three-mesh",
|
|
419
|
+
LineLoop: "three-mesh",
|
|
420
|
+
Points: "three-mesh",
|
|
421
|
+
Sprite: "three-mesh",
|
|
422
|
+
// Lights
|
|
423
|
+
AmbientLight: "three-light",
|
|
424
|
+
DirectionalLight: "three-light",
|
|
425
|
+
HemisphereLight: "three-light",
|
|
426
|
+
PointLight: "three-light",
|
|
427
|
+
RectAreaLight: "three-light",
|
|
428
|
+
SpotLight: "three-light",
|
|
429
|
+
LightProbe: "three-light",
|
|
430
|
+
// Cameras
|
|
431
|
+
PerspectiveCamera: "three-camera",
|
|
432
|
+
OrthographicCamera: "three-camera",
|
|
433
|
+
ArrayCamera: "three-camera",
|
|
434
|
+
CubeCamera: "three-camera",
|
|
435
|
+
// Helpers (map to object as fallback)
|
|
436
|
+
BoxHelper: "three-object",
|
|
437
|
+
ArrowHelper: "three-object",
|
|
438
|
+
AxesHelper: "three-object",
|
|
439
|
+
GridHelper: "three-object",
|
|
440
|
+
SkeletonHelper: "three-object"
|
|
441
|
+
};
|
|
442
|
+
var ALL_TAGS = [
|
|
443
|
+
"three-scene",
|
|
444
|
+
"three-group",
|
|
445
|
+
"three-mesh",
|
|
446
|
+
"three-light",
|
|
447
|
+
"three-camera",
|
|
448
|
+
"three-object"
|
|
449
|
+
];
|
|
450
|
+
var DEFAULT_TAG = "three-object";
|
|
451
|
+
function getTagForType(type) {
|
|
452
|
+
return TAG_MAP[type] ?? DEFAULT_TAG;
|
|
453
|
+
}
|
|
454
|
+
var _store = null;
|
|
455
|
+
var ThreeElement = class extends HTMLElement {
|
|
456
|
+
// -------------------------------------------------------------------------
|
|
457
|
+
// Tier 1: Lightweight cached metadata (always available)
|
|
458
|
+
// -------------------------------------------------------------------------
|
|
459
|
+
/**
|
|
460
|
+
* Returns the Tier 1 cached metadata for this object.
|
|
461
|
+
* Instant, no computation. Returns null if the element is not linked.
|
|
462
|
+
*/
|
|
463
|
+
get metadata() {
|
|
464
|
+
const uuid = this.dataset.uuid;
|
|
465
|
+
if (!uuid || !_store) return null;
|
|
466
|
+
return _store.getByUuid(uuid);
|
|
467
|
+
}
|
|
468
|
+
// -------------------------------------------------------------------------
|
|
469
|
+
// Tier 2: On-demand heavy inspection (reads live Three.js object)
|
|
470
|
+
// -------------------------------------------------------------------------
|
|
471
|
+
/**
|
|
472
|
+
* Performs a full inspection of the linked Three.js object.
|
|
473
|
+
* Reads geometry buffers, material properties, world bounds, etc.
|
|
474
|
+
* Cost: 0.1–2ms depending on geometry complexity.
|
|
475
|
+
*/
|
|
476
|
+
inspect() {
|
|
477
|
+
const uuid = this.dataset.uuid;
|
|
478
|
+
if (!uuid || !_store) return null;
|
|
479
|
+
return _store.inspect(uuid);
|
|
480
|
+
}
|
|
481
|
+
// -------------------------------------------------------------------------
|
|
482
|
+
// Raw Three.js object reference
|
|
483
|
+
// -------------------------------------------------------------------------
|
|
484
|
+
/**
|
|
485
|
+
* Returns the raw Three.js Object3D linked to this DOM element.
|
|
486
|
+
* Allows direct access to any Three.js property or method.
|
|
487
|
+
*/
|
|
488
|
+
get object3D() {
|
|
489
|
+
const uuid = this.dataset.uuid;
|
|
490
|
+
if (!uuid || !_store) return null;
|
|
491
|
+
return _store.getObject3D(uuid);
|
|
492
|
+
}
|
|
493
|
+
// -------------------------------------------------------------------------
|
|
494
|
+
// Convenience shortcuts (read from Tier 1 metadata)
|
|
495
|
+
// -------------------------------------------------------------------------
|
|
496
|
+
/**
|
|
497
|
+
* Local position as [x, y, z].
|
|
498
|
+
*/
|
|
499
|
+
get position() {
|
|
500
|
+
return this.metadata?.position ?? null;
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Local euler rotation as [x, y, z] in radians.
|
|
504
|
+
*/
|
|
505
|
+
get rotation() {
|
|
506
|
+
return this.metadata?.rotation ?? null;
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Local scale as [x, y, z].
|
|
510
|
+
*/
|
|
511
|
+
get scale() {
|
|
512
|
+
return this.metadata?.scale ?? null;
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* Whether the object is visible (does not check parent chain).
|
|
516
|
+
*/
|
|
517
|
+
get visible() {
|
|
518
|
+
return this.metadata?.visible ?? false;
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* The testId from userData.testId, if set.
|
|
522
|
+
*/
|
|
523
|
+
get testId() {
|
|
524
|
+
return this.metadata?.testId;
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* World-space bounding box. Computed on-demand (Tier 2).
|
|
528
|
+
*/
|
|
529
|
+
get bounds() {
|
|
530
|
+
const inspection = this.inspect();
|
|
531
|
+
return inspection?.bounds ?? null;
|
|
532
|
+
}
|
|
533
|
+
// -------------------------------------------------------------------------
|
|
534
|
+
// Interaction methods
|
|
535
|
+
// -------------------------------------------------------------------------
|
|
536
|
+
/**
|
|
537
|
+
* Trigger a deterministic click on the linked 3D object.
|
|
538
|
+
* Projects the object center to screen coordinates and dispatches pointer events.
|
|
539
|
+
*/
|
|
540
|
+
click() {
|
|
541
|
+
const uuid = this.dataset.uuid;
|
|
542
|
+
if (!uuid) {
|
|
543
|
+
console.warn("[react-three-dom] Cannot click: element has no data-uuid");
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
if (typeof window.__R3F_DOM__?.click === "function") {
|
|
547
|
+
window.__R3F_DOM__.click(uuid);
|
|
548
|
+
} else {
|
|
549
|
+
console.warn("[react-three-dom] Cannot click: bridge API not initialized");
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Trigger a deterministic hover on the linked 3D object.
|
|
554
|
+
*/
|
|
555
|
+
hover() {
|
|
556
|
+
const uuid = this.dataset.uuid;
|
|
557
|
+
if (!uuid) {
|
|
558
|
+
console.warn("[react-three-dom] Cannot hover: element has no data-uuid");
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
if (typeof window.__R3F_DOM__?.hover === "function") {
|
|
562
|
+
window.__R3F_DOM__.hover(uuid);
|
|
563
|
+
} else {
|
|
564
|
+
console.warn("[react-three-dom] Cannot hover: bridge API not initialized");
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
// -------------------------------------------------------------------------
|
|
568
|
+
// Console-friendly output
|
|
569
|
+
// -------------------------------------------------------------------------
|
|
570
|
+
/**
|
|
571
|
+
* Returns a readable string representation for console output.
|
|
572
|
+
*/
|
|
573
|
+
toString() {
|
|
574
|
+
const tag = this.tagName.toLowerCase();
|
|
575
|
+
const name = this.dataset.name ? ` name="${this.dataset.name}"` : "";
|
|
576
|
+
const testId = this.dataset.testId ? ` testId="${this.dataset.testId}"` : "";
|
|
577
|
+
const type = this.dataset.type ? ` type="${this.dataset.type}"` : "";
|
|
578
|
+
return `<${tag}${name}${testId}${type}>`;
|
|
579
|
+
}
|
|
580
|
+
};
|
|
581
|
+
var _registered = false;
|
|
582
|
+
function ensureCustomElements(store) {
|
|
583
|
+
if (_registered) return;
|
|
584
|
+
if (typeof customElements === "undefined") return;
|
|
585
|
+
_store = store;
|
|
586
|
+
for (const tag of ALL_TAGS) {
|
|
587
|
+
if (!customElements.get(tag)) {
|
|
588
|
+
const elementClass = class extends ThreeElement {
|
|
589
|
+
};
|
|
590
|
+
Object.defineProperty(elementClass, "name", { value: `ThreeElement_${tag}` });
|
|
591
|
+
customElements.define(tag, elementClass);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
_registered = true;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// src/mirror/attributes.ts
|
|
598
|
+
var ATTRIBUTE_MAP = {
|
|
599
|
+
"data-uuid": (m) => m.uuid,
|
|
600
|
+
"data-name": (m) => m.name || void 0,
|
|
601
|
+
"data-type": (m) => m.type,
|
|
602
|
+
"data-visible": (m) => String(m.visible),
|
|
603
|
+
"data-test-id": (m) => m.testId,
|
|
604
|
+
"data-geometry": (m) => m.geometryType,
|
|
605
|
+
"data-material": (m) => m.materialType,
|
|
606
|
+
"data-position": (m) => serializeTuple(m.position),
|
|
607
|
+
"data-rotation": (m) => serializeTuple(m.rotation),
|
|
608
|
+
"data-scale": (m) => serializeTuple(m.scale),
|
|
609
|
+
"data-vertex-count": (m) => m.vertexCount !== void 0 ? String(m.vertexCount) : void 0,
|
|
610
|
+
"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
|
|
612
|
+
};
|
|
613
|
+
var MANAGED_ATTRIBUTES = Object.keys(ATTRIBUTE_MAP);
|
|
614
|
+
function serializeTuple(tuple) {
|
|
615
|
+
return `${round(tuple[0])},${round(tuple[1])},${round(tuple[2])}`;
|
|
616
|
+
}
|
|
617
|
+
function round(n) {
|
|
618
|
+
const rounded = Math.round(n * 1e4) / 1e4;
|
|
619
|
+
return String(rounded === 0 ? 0 : rounded);
|
|
620
|
+
}
|
|
621
|
+
function computeAttributes(meta) {
|
|
622
|
+
const attrs = /* @__PURE__ */ new Map();
|
|
623
|
+
for (const [key, extractor] of Object.entries(ATTRIBUTE_MAP)) {
|
|
624
|
+
const value = extractor(meta);
|
|
625
|
+
if (value !== void 0) {
|
|
626
|
+
attrs.set(key, value);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
return attrs;
|
|
630
|
+
}
|
|
631
|
+
function applyAttributes(element, meta, prevAttrs) {
|
|
632
|
+
let writeCount = 0;
|
|
633
|
+
const newAttrs = computeAttributes(meta);
|
|
634
|
+
for (const [key, value] of newAttrs) {
|
|
635
|
+
const prev = prevAttrs.get(key);
|
|
636
|
+
if (prev !== value) {
|
|
637
|
+
element.setAttribute(key, value);
|
|
638
|
+
prevAttrs.set(key, value);
|
|
639
|
+
writeCount++;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
for (const key of prevAttrs.keys()) {
|
|
643
|
+
if (!newAttrs.has(key)) {
|
|
644
|
+
element.removeAttribute(key);
|
|
645
|
+
prevAttrs.delete(key);
|
|
646
|
+
writeCount++;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
return writeCount;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// src/mirror/DomMirror.ts
|
|
653
|
+
var DomMirror = class {
|
|
654
|
+
constructor(store, maxNodes = 2e3) {
|
|
655
|
+
this._rootElement = null;
|
|
656
|
+
// Materialized nodes indexed by uuid
|
|
657
|
+
this._nodes = /* @__PURE__ */ new Map();
|
|
658
|
+
// LRU doubly-linked list for eviction (head = most recently used, tail = least)
|
|
659
|
+
this._lruHead = null;
|
|
660
|
+
this._lruTail = null;
|
|
661
|
+
this._lruSize = 0;
|
|
662
|
+
// UUID → parent UUID mapping for DOM tree structure
|
|
663
|
+
this._parentMap = /* @__PURE__ */ new Map();
|
|
664
|
+
this._store = store;
|
|
665
|
+
this._maxNodes = maxNodes;
|
|
666
|
+
}
|
|
667
|
+
// -------------------------------------------------------------------------
|
|
668
|
+
// Initialization
|
|
669
|
+
// -------------------------------------------------------------------------
|
|
670
|
+
/**
|
|
671
|
+
* Set the root DOM element where the mirror tree will be appended.
|
|
672
|
+
* Typically a hidden div: <div id="three-dom-root" style="display:none">
|
|
673
|
+
*/
|
|
674
|
+
setRoot(rootElement) {
|
|
675
|
+
this._rootElement = rootElement;
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* Build the initial DOM tree from the scene.
|
|
679
|
+
* Materializes the top 2 levels of the scene hierarchy.
|
|
680
|
+
*/
|
|
681
|
+
buildInitialTree(scene) {
|
|
682
|
+
if (!this._rootElement) return;
|
|
683
|
+
this.materializeSubtree(scene.uuid, 2);
|
|
684
|
+
}
|
|
685
|
+
// -------------------------------------------------------------------------
|
|
686
|
+
// Materialization
|
|
687
|
+
// -------------------------------------------------------------------------
|
|
688
|
+
/**
|
|
689
|
+
* Create a DOM node for a single object.
|
|
690
|
+
* If already materialized, touches the LRU and returns the existing element.
|
|
691
|
+
*/
|
|
692
|
+
materialize(uuid) {
|
|
693
|
+
const existing = this._nodes.get(uuid);
|
|
694
|
+
if (existing) {
|
|
695
|
+
this._lruTouch(existing.lruNode);
|
|
696
|
+
return existing.element;
|
|
697
|
+
}
|
|
698
|
+
const meta = this._store.getByUuid(uuid);
|
|
699
|
+
if (!meta) return null;
|
|
700
|
+
if (this._lruSize >= this._maxNodes) {
|
|
701
|
+
this._evictLRU();
|
|
702
|
+
}
|
|
703
|
+
const tag = getTagForType(meta.type);
|
|
704
|
+
const element = document.createElement(tag);
|
|
705
|
+
element.style.cssText = "display:block;position:absolute;pointer-events:none;box-sizing:border-box;";
|
|
706
|
+
const prevAttrs = /* @__PURE__ */ new Map();
|
|
707
|
+
applyAttributes(element, meta, prevAttrs);
|
|
708
|
+
const lruNode = { uuid, prev: null, next: null };
|
|
709
|
+
this._lruPush(lruNode);
|
|
710
|
+
const node = { element, prevAttrs, lruNode };
|
|
711
|
+
this._nodes.set(uuid, node);
|
|
712
|
+
this._parentMap.set(uuid, meta.parentUuid);
|
|
713
|
+
this._insertIntoDom(uuid, meta.parentUuid, element);
|
|
714
|
+
return element;
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* Materialize a subtree starting from the given uuid, up to the specified depth.
|
|
718
|
+
* depth=0 materializes just the node, depth=1 includes direct children, etc.
|
|
719
|
+
*/
|
|
720
|
+
materializeSubtree(uuid, depth) {
|
|
721
|
+
this.materialize(uuid);
|
|
722
|
+
if (depth <= 0) return;
|
|
723
|
+
const meta = this._store.getByUuid(uuid);
|
|
724
|
+
if (!meta) return;
|
|
725
|
+
for (const childUuid of meta.childrenUuids) {
|
|
726
|
+
this.materializeSubtree(childUuid, depth - 1);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
/**
|
|
730
|
+
* Remove a DOM node but keep JS metadata in the ObjectStore.
|
|
731
|
+
* Called by LRU eviction or when an object is removed from the scene.
|
|
732
|
+
*/
|
|
733
|
+
dematerialize(uuid) {
|
|
734
|
+
const node = this._nodes.get(uuid);
|
|
735
|
+
if (!node) return;
|
|
736
|
+
node.element.remove();
|
|
737
|
+
this._lruRemove(node.lruNode);
|
|
738
|
+
this._nodes.delete(uuid);
|
|
739
|
+
this._parentMap.delete(uuid);
|
|
740
|
+
}
|
|
741
|
+
// -------------------------------------------------------------------------
|
|
742
|
+
// Structural updates (called by Object3D.add/remove patch)
|
|
743
|
+
// -------------------------------------------------------------------------
|
|
744
|
+
/**
|
|
745
|
+
* Called when a new object is added to the tracked scene.
|
|
746
|
+
* Only materializes if the parent is already materialized (lazy expansion).
|
|
747
|
+
*/
|
|
748
|
+
onObjectAdded(obj) {
|
|
749
|
+
const parentUuid = obj.parent?.uuid;
|
|
750
|
+
if (!parentUuid) return;
|
|
751
|
+
const parentNode = this._nodes.get(parentUuid);
|
|
752
|
+
if (parentNode) {
|
|
753
|
+
this.materialize(obj.uuid);
|
|
754
|
+
}
|
|
755
|
+
if (parentNode) {
|
|
756
|
+
const parentMeta = this._store.getByUuid(parentUuid);
|
|
757
|
+
if (parentMeta) {
|
|
758
|
+
applyAttributes(parentNode.element, parentMeta, parentNode.prevAttrs);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* Called when an object is removed from the tracked scene.
|
|
764
|
+
* Dematerializes the object and all its descendants.
|
|
765
|
+
*/
|
|
766
|
+
onObjectRemoved(obj) {
|
|
767
|
+
obj.traverse((child) => {
|
|
768
|
+
this.dematerialize(child.uuid);
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
// -------------------------------------------------------------------------
|
|
772
|
+
// Attribute sync (called per-frame by ThreeDom)
|
|
773
|
+
// -------------------------------------------------------------------------
|
|
774
|
+
/**
|
|
775
|
+
* Sync Tier 1 attributes for an object if it's materialized.
|
|
776
|
+
* No-op if the object has no materialized DOM node.
|
|
777
|
+
* Returns the number of DOM writes performed.
|
|
778
|
+
*/
|
|
779
|
+
syncAttributes(obj) {
|
|
780
|
+
const node = this._nodes.get(obj.uuid);
|
|
781
|
+
if (!node) return 0;
|
|
782
|
+
const meta = this._store.getMetadata(obj);
|
|
783
|
+
if (!meta) return 0;
|
|
784
|
+
return applyAttributes(node.element, meta, node.prevAttrs);
|
|
785
|
+
}
|
|
786
|
+
/**
|
|
787
|
+
* Sync attributes by uuid (when you don't have the Object3D ref).
|
|
788
|
+
*/
|
|
789
|
+
syncAttributesByUuid(uuid) {
|
|
790
|
+
const node = this._nodes.get(uuid);
|
|
791
|
+
if (!node) return 0;
|
|
792
|
+
const meta = this._store.getByUuid(uuid);
|
|
793
|
+
if (!meta) return 0;
|
|
794
|
+
return applyAttributes(node.element, meta, node.prevAttrs);
|
|
795
|
+
}
|
|
796
|
+
// -------------------------------------------------------------------------
|
|
797
|
+
// Querying
|
|
798
|
+
// -------------------------------------------------------------------------
|
|
799
|
+
/**
|
|
800
|
+
* Get the root DOM element.
|
|
801
|
+
*/
|
|
802
|
+
getRootElement() {
|
|
803
|
+
return this._rootElement;
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* Get the materialized DOM element for an object, if it exists.
|
|
807
|
+
*/
|
|
808
|
+
getElement(uuid) {
|
|
809
|
+
const node = this._nodes.get(uuid);
|
|
810
|
+
if (node) {
|
|
811
|
+
this._lruTouch(node.lruNode);
|
|
812
|
+
return node.element;
|
|
813
|
+
}
|
|
814
|
+
return null;
|
|
815
|
+
}
|
|
816
|
+
/**
|
|
817
|
+
* Check if an object has a materialized DOM node.
|
|
818
|
+
*/
|
|
819
|
+
isMaterialized(uuid) {
|
|
820
|
+
return this._nodes.has(uuid);
|
|
821
|
+
}
|
|
822
|
+
/**
|
|
823
|
+
* Query the mirror DOM using a CSS selector.
|
|
824
|
+
* Falls back to searching the ObjectStore if no materialized nodes match,
|
|
825
|
+
* then materializes the matching objects.
|
|
826
|
+
*/
|
|
827
|
+
querySelector(selector) {
|
|
828
|
+
if (!this._rootElement) return null;
|
|
829
|
+
const existing = this._rootElement.querySelector(selector);
|
|
830
|
+
if (existing) return existing;
|
|
831
|
+
const uuid = this._findUuidBySelector(selector);
|
|
832
|
+
if (uuid) {
|
|
833
|
+
return this.materialize(uuid);
|
|
834
|
+
}
|
|
835
|
+
return null;
|
|
836
|
+
}
|
|
837
|
+
/**
|
|
838
|
+
* Query all matching elements in the mirror DOM.
|
|
839
|
+
*/
|
|
840
|
+
querySelectorAll(selector) {
|
|
841
|
+
if (!this._rootElement) return [];
|
|
842
|
+
const uuids = this._findAllUuidsBySelector(selector);
|
|
843
|
+
const elements = [];
|
|
844
|
+
for (const uuid of uuids) {
|
|
845
|
+
const el = this.materialize(uuid);
|
|
846
|
+
if (el) elements.push(el);
|
|
847
|
+
}
|
|
848
|
+
return elements;
|
|
849
|
+
}
|
|
850
|
+
// -------------------------------------------------------------------------
|
|
851
|
+
// Configuration
|
|
852
|
+
// -------------------------------------------------------------------------
|
|
853
|
+
/**
|
|
854
|
+
* Set the maximum number of materialized DOM nodes.
|
|
855
|
+
* If current count exceeds the new max, excess nodes are evicted immediately.
|
|
856
|
+
*/
|
|
857
|
+
setMaxNodes(max) {
|
|
858
|
+
this._maxNodes = max;
|
|
859
|
+
while (this._lruSize > this._maxNodes) {
|
|
860
|
+
this._evictLRU();
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
/** Current number of materialized DOM nodes. */
|
|
864
|
+
getMaterializedCount() {
|
|
865
|
+
return this._nodes.size;
|
|
866
|
+
}
|
|
867
|
+
/** Maximum allowed materialized DOM nodes. */
|
|
868
|
+
getMaxNodes() {
|
|
869
|
+
return this._maxNodes;
|
|
870
|
+
}
|
|
871
|
+
// -------------------------------------------------------------------------
|
|
872
|
+
// Cleanup
|
|
873
|
+
// -------------------------------------------------------------------------
|
|
874
|
+
/**
|
|
875
|
+
* Remove all materialized DOM nodes and reset state.
|
|
876
|
+
*/
|
|
877
|
+
dispose() {
|
|
878
|
+
for (const [, node] of this._nodes) {
|
|
879
|
+
node.element.remove();
|
|
880
|
+
}
|
|
881
|
+
this._nodes.clear();
|
|
882
|
+
this._parentMap.clear();
|
|
883
|
+
this._lruHead = null;
|
|
884
|
+
this._lruTail = null;
|
|
885
|
+
this._lruSize = 0;
|
|
886
|
+
if (this._rootElement) {
|
|
887
|
+
this._rootElement.innerHTML = "";
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
// -------------------------------------------------------------------------
|
|
891
|
+
// Private: DOM insertion
|
|
892
|
+
// -------------------------------------------------------------------------
|
|
893
|
+
/**
|
|
894
|
+
* Insert a newly created element into the correct position in the DOM tree.
|
|
895
|
+
*/
|
|
896
|
+
_insertIntoDom(_uuid, parentUuid, element) {
|
|
897
|
+
if (!this._rootElement) return;
|
|
898
|
+
if (!parentUuid) {
|
|
899
|
+
this._rootElement.appendChild(element);
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
const parentNode = this._nodes.get(parentUuid);
|
|
903
|
+
if (parentNode) {
|
|
904
|
+
parentNode.element.appendChild(element);
|
|
905
|
+
} else {
|
|
906
|
+
this._rootElement.appendChild(element);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
// -------------------------------------------------------------------------
|
|
910
|
+
// Private: Selector → UUID resolution
|
|
911
|
+
// -------------------------------------------------------------------------
|
|
912
|
+
/**
|
|
913
|
+
* Parse common CSS selector patterns and resolve to a uuid from the store.
|
|
914
|
+
* Supports:
|
|
915
|
+
* [data-test-id="value"]
|
|
916
|
+
* [data-name="value"]
|
|
917
|
+
* [data-uuid="value"]
|
|
918
|
+
* three-mesh, three-light, etc. (by tag/type)
|
|
919
|
+
*/
|
|
920
|
+
_findUuidBySelector(selector) {
|
|
921
|
+
const testIdMatch = selector.match(/\[data-test-id=["']([^"']+)["']\]/);
|
|
922
|
+
if (testIdMatch) {
|
|
923
|
+
const meta = this._store.getByTestId(testIdMatch[1]);
|
|
924
|
+
return meta?.uuid ?? null;
|
|
925
|
+
}
|
|
926
|
+
const uuidMatch = selector.match(/\[data-uuid=["']([^"']+)["']\]/);
|
|
927
|
+
if (uuidMatch) {
|
|
928
|
+
const meta = this._store.getByUuid(uuidMatch[1]);
|
|
929
|
+
return meta?.uuid ?? null;
|
|
930
|
+
}
|
|
931
|
+
const nameMatch = selector.match(/\[data-name=["']([^"']+)["']\]/);
|
|
932
|
+
if (nameMatch) {
|
|
933
|
+
const metas = this._store.getByName(nameMatch[1]);
|
|
934
|
+
return metas.length > 0 ? metas[0].uuid : null;
|
|
935
|
+
}
|
|
936
|
+
return null;
|
|
937
|
+
}
|
|
938
|
+
/**
|
|
939
|
+
* Find all UUIDs matching a selector pattern.
|
|
940
|
+
*/
|
|
941
|
+
_findAllUuidsBySelector(selector) {
|
|
942
|
+
const uuids = [];
|
|
943
|
+
const testIdMatch = selector.match(/\[data-test-id=["']([^"']+)["']\]/);
|
|
944
|
+
if (testIdMatch) {
|
|
945
|
+
const meta = this._store.getByTestId(testIdMatch[1]);
|
|
946
|
+
if (meta) uuids.push(meta.uuid);
|
|
947
|
+
return uuids;
|
|
948
|
+
}
|
|
949
|
+
const nameMatch = selector.match(/\[data-name=["']([^"']+)["']\]/);
|
|
950
|
+
if (nameMatch) {
|
|
951
|
+
const metas = this._store.getByName(nameMatch[1]);
|
|
952
|
+
for (const m of metas) uuids.push(m.uuid);
|
|
953
|
+
return uuids;
|
|
954
|
+
}
|
|
955
|
+
const tagMatch = selector.match(/^(three-(?:scene|group|mesh|light|camera|object))$/);
|
|
956
|
+
if (tagMatch) {
|
|
957
|
+
const targetTag = tagMatch[1];
|
|
958
|
+
const allObjects = this._store.getFlatList();
|
|
959
|
+
for (const obj of allObjects) {
|
|
960
|
+
const meta = this._store.getMetadata(obj);
|
|
961
|
+
if (meta && getTagForType(meta.type) === targetTag) {
|
|
962
|
+
uuids.push(meta.uuid);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
return uuids;
|
|
966
|
+
}
|
|
967
|
+
return uuids;
|
|
968
|
+
}
|
|
969
|
+
// -------------------------------------------------------------------------
|
|
970
|
+
// Private: LRU doubly-linked list operations
|
|
971
|
+
// -------------------------------------------------------------------------
|
|
972
|
+
/** Add a node to the front (most recently used). */
|
|
973
|
+
_lruPush(node) {
|
|
974
|
+
node.prev = null;
|
|
975
|
+
node.next = this._lruHead;
|
|
976
|
+
if (this._lruHead) {
|
|
977
|
+
this._lruHead.prev = node;
|
|
978
|
+
}
|
|
979
|
+
this._lruHead = node;
|
|
980
|
+
if (!this._lruTail) {
|
|
981
|
+
this._lruTail = node;
|
|
982
|
+
}
|
|
983
|
+
this._lruSize++;
|
|
984
|
+
}
|
|
985
|
+
/** Remove a node from the list. */
|
|
986
|
+
_lruRemove(node) {
|
|
987
|
+
if (node.prev) {
|
|
988
|
+
node.prev.next = node.next;
|
|
989
|
+
} else {
|
|
990
|
+
this._lruHead = node.next;
|
|
991
|
+
}
|
|
992
|
+
if (node.next) {
|
|
993
|
+
node.next.prev = node.prev;
|
|
994
|
+
} else {
|
|
995
|
+
this._lruTail = node.prev;
|
|
996
|
+
}
|
|
997
|
+
node.prev = null;
|
|
998
|
+
node.next = null;
|
|
999
|
+
this._lruSize--;
|
|
1000
|
+
}
|
|
1001
|
+
/** Move a node to the front (most recently used). */
|
|
1002
|
+
_lruTouch(node) {
|
|
1003
|
+
if (this._lruHead === node) return;
|
|
1004
|
+
this._lruRemove(node);
|
|
1005
|
+
this._lruPush(node);
|
|
1006
|
+
}
|
|
1007
|
+
/** Evict the least recently used node. */
|
|
1008
|
+
_evictLRU() {
|
|
1009
|
+
if (!this._lruTail) return;
|
|
1010
|
+
const uuid = this._lruTail.uuid;
|
|
1011
|
+
this.dematerialize(uuid);
|
|
1012
|
+
}
|
|
1013
|
+
};
|
|
1014
|
+
var _patched = false;
|
|
1015
|
+
var _originalAdd = null;
|
|
1016
|
+
var _originalRemove = null;
|
|
1017
|
+
var _activePairs = [];
|
|
1018
|
+
function findTrackingPair(obj) {
|
|
1019
|
+
for (const pair of _activePairs) {
|
|
1020
|
+
if (pair.store.isInTrackedScene(obj)) {
|
|
1021
|
+
return pair;
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
return null;
|
|
1025
|
+
}
|
|
1026
|
+
function registerSubtree(obj, store, mirror) {
|
|
1027
|
+
obj.traverse((child) => {
|
|
1028
|
+
if (!store.has(child)) {
|
|
1029
|
+
store.register(child);
|
|
1030
|
+
mirror.onObjectAdded(child);
|
|
1031
|
+
}
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
function patchObject3D(store, mirror) {
|
|
1035
|
+
_activePairs.push({ store, mirror });
|
|
1036
|
+
if (!_patched) {
|
|
1037
|
+
_originalAdd = three.Object3D.prototype.add;
|
|
1038
|
+
_originalRemove = three.Object3D.prototype.remove;
|
|
1039
|
+
three.Object3D.prototype.add = function patchedAdd(...objects) {
|
|
1040
|
+
_originalAdd.call(this, ...objects);
|
|
1041
|
+
const pair = findTrackingPair(this);
|
|
1042
|
+
if (pair) {
|
|
1043
|
+
for (const obj of objects) {
|
|
1044
|
+
if (obj === this) continue;
|
|
1045
|
+
registerSubtree(obj, pair.store, pair.mirror);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
return this;
|
|
1049
|
+
};
|
|
1050
|
+
three.Object3D.prototype.remove = function patchedRemove(...objects) {
|
|
1051
|
+
const pair = findTrackingPair(this);
|
|
1052
|
+
if (pair) {
|
|
1053
|
+
for (const obj of objects) {
|
|
1054
|
+
if (obj === this) continue;
|
|
1055
|
+
pair.mirror.onObjectRemoved(obj);
|
|
1056
|
+
obj.traverse((child) => {
|
|
1057
|
+
pair.store.unregister(child);
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
_originalRemove.call(this, ...objects);
|
|
1062
|
+
return this;
|
|
1063
|
+
};
|
|
1064
|
+
_patched = true;
|
|
1065
|
+
}
|
|
1066
|
+
return () => {
|
|
1067
|
+
const idx = _activePairs.findIndex(
|
|
1068
|
+
(p) => p.store === store && p.mirror === mirror
|
|
1069
|
+
);
|
|
1070
|
+
if (idx !== -1) {
|
|
1071
|
+
_activePairs.splice(idx, 1);
|
|
1072
|
+
}
|
|
1073
|
+
if (_activePairs.length === 0 && _patched) {
|
|
1074
|
+
restoreObject3D();
|
|
1075
|
+
}
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
function restoreObject3D() {
|
|
1079
|
+
if (!_patched) return;
|
|
1080
|
+
if (_originalAdd) {
|
|
1081
|
+
three.Object3D.prototype.add = _originalAdd;
|
|
1082
|
+
_originalAdd = null;
|
|
1083
|
+
}
|
|
1084
|
+
if (_originalRemove) {
|
|
1085
|
+
three.Object3D.prototype.remove = _originalRemove;
|
|
1086
|
+
_originalRemove = null;
|
|
1087
|
+
}
|
|
1088
|
+
_patched = false;
|
|
1089
|
+
}
|
|
1090
|
+
function isPatched() {
|
|
1091
|
+
return _patched;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
// src/snapshot/snapshot.ts
|
|
1095
|
+
function buildNodeTree(store, meta) {
|
|
1096
|
+
const children = [];
|
|
1097
|
+
for (const childUuid of meta.childrenUuids) {
|
|
1098
|
+
const childMeta = store.getByUuid(childUuid);
|
|
1099
|
+
if (childMeta) {
|
|
1100
|
+
children.push(buildNodeTree(store, childMeta));
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
return {
|
|
1104
|
+
uuid: meta.uuid,
|
|
1105
|
+
name: meta.name,
|
|
1106
|
+
type: meta.type,
|
|
1107
|
+
testId: meta.testId,
|
|
1108
|
+
visible: meta.visible,
|
|
1109
|
+
position: [...meta.position],
|
|
1110
|
+
rotation: [...meta.rotation],
|
|
1111
|
+
scale: [...meta.scale],
|
|
1112
|
+
children
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
function findRoot(store) {
|
|
1116
|
+
const allObjects = store.getFlatList();
|
|
1117
|
+
for (const obj of allObjects) {
|
|
1118
|
+
const meta = store.getMetadata(obj);
|
|
1119
|
+
if (meta && meta.parentUuid === null) {
|
|
1120
|
+
return meta;
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
return null;
|
|
1124
|
+
}
|
|
1125
|
+
function createSnapshot(store) {
|
|
1126
|
+
const rootMeta = findRoot(store);
|
|
1127
|
+
const tree = rootMeta ? buildNodeTree(store, rootMeta) : {
|
|
1128
|
+
uuid: "",
|
|
1129
|
+
name: "empty",
|
|
1130
|
+
type: "Scene",
|
|
1131
|
+
visible: true,
|
|
1132
|
+
position: [0, 0, 0],
|
|
1133
|
+
rotation: [0, 0, 0],
|
|
1134
|
+
scale: [1, 1, 1],
|
|
1135
|
+
children: []
|
|
1136
|
+
};
|
|
1137
|
+
return {
|
|
1138
|
+
timestamp: Date.now(),
|
|
1139
|
+
objectCount: store.getCount(),
|
|
1140
|
+
tree
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1143
|
+
function createFlatSnapshot(store) {
|
|
1144
|
+
const objects = [];
|
|
1145
|
+
const allObjects = store.getFlatList();
|
|
1146
|
+
for (const obj of allObjects) {
|
|
1147
|
+
const meta = store.getMetadata(obj);
|
|
1148
|
+
if (meta) {
|
|
1149
|
+
objects.push({ ...meta });
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
return {
|
|
1153
|
+
timestamp: Date.now(),
|
|
1154
|
+
objectCount: objects.length,
|
|
1155
|
+
objects
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
1158
|
+
var _vec3 = /* @__PURE__ */ new three.Vector3();
|
|
1159
|
+
var _vec3B = /* @__PURE__ */ new three.Vector3();
|
|
1160
|
+
var _box32 = /* @__PURE__ */ new three.Box3();
|
|
1161
|
+
var _frustum = /* @__PURE__ */ new three.Frustum();
|
|
1162
|
+
var _projMatrix = /* @__PURE__ */ new three.Matrix4();
|
|
1163
|
+
function ndcToScreen(ndc, size) {
|
|
1164
|
+
return {
|
|
1165
|
+
x: (ndc.x + 1) / 2 * size.width,
|
|
1166
|
+
y: (-ndc.y + 1) / 2 * size.height
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
function isNdcOnScreen(ndc) {
|
|
1170
|
+
return ndc.x >= -1 && ndc.x <= 1 && ndc.y >= -1 && ndc.y <= 1 && ndc.z >= -1 && ndc.z <= 1;
|
|
1171
|
+
}
|
|
1172
|
+
function isInFrontOfCamera(ndc) {
|
|
1173
|
+
return ndc.z >= -1 && ndc.z <= 1;
|
|
1174
|
+
}
|
|
1175
|
+
function getWorldCenter(obj) {
|
|
1176
|
+
_box32.setFromObject(obj);
|
|
1177
|
+
if (_box32.isEmpty()) {
|
|
1178
|
+
obj.getWorldPosition(_vec3);
|
|
1179
|
+
return _vec3;
|
|
1180
|
+
}
|
|
1181
|
+
_box32.getCenter(_vec3);
|
|
1182
|
+
return _vec3;
|
|
1183
|
+
}
|
|
1184
|
+
function getBboxCorners(obj) {
|
|
1185
|
+
_box32.setFromObject(obj);
|
|
1186
|
+
if (_box32.isEmpty()) return [];
|
|
1187
|
+
const { min, max } = _box32;
|
|
1188
|
+
return [
|
|
1189
|
+
new three.Vector3(min.x, min.y, min.z),
|
|
1190
|
+
new three.Vector3(min.x, min.y, max.z),
|
|
1191
|
+
new three.Vector3(min.x, max.y, min.z),
|
|
1192
|
+
new three.Vector3(min.x, max.y, max.z),
|
|
1193
|
+
new three.Vector3(max.x, min.y, min.z),
|
|
1194
|
+
new three.Vector3(max.x, min.y, max.z),
|
|
1195
|
+
new three.Vector3(max.x, max.y, min.z),
|
|
1196
|
+
new three.Vector3(max.x, max.y, max.z)
|
|
1197
|
+
];
|
|
1198
|
+
}
|
|
1199
|
+
function getBboxFaceCenters(obj) {
|
|
1200
|
+
_box32.setFromObject(obj);
|
|
1201
|
+
if (_box32.isEmpty()) return [];
|
|
1202
|
+
const center = _box32.getCenter(new three.Vector3());
|
|
1203
|
+
const { min, max } = _box32;
|
|
1204
|
+
return [
|
|
1205
|
+
new three.Vector3(min.x, center.y, center.z),
|
|
1206
|
+
// -X face
|
|
1207
|
+
new three.Vector3(max.x, center.y, center.z),
|
|
1208
|
+
// +X face
|
|
1209
|
+
new three.Vector3(center.x, min.y, center.z),
|
|
1210
|
+
// -Y face
|
|
1211
|
+
new three.Vector3(center.x, max.y, center.z),
|
|
1212
|
+
// +Y face
|
|
1213
|
+
new three.Vector3(center.x, center.y, min.z),
|
|
1214
|
+
// -Z face
|
|
1215
|
+
new three.Vector3(center.x, center.y, max.z)
|
|
1216
|
+
// +Z face
|
|
1217
|
+
];
|
|
1218
|
+
}
|
|
1219
|
+
function tryProjectPoint(worldPoint, camera, size) {
|
|
1220
|
+
_vec3B.copy(worldPoint).project(camera);
|
|
1221
|
+
if (!isInFrontOfCamera(_vec3B)) return null;
|
|
1222
|
+
const screen = ndcToScreen(_vec3B, size);
|
|
1223
|
+
return {
|
|
1224
|
+
screen,
|
|
1225
|
+
ndc: { x: _vec3B.x, y: _vec3B.y },
|
|
1226
|
+
ndcZ: _vec3B.z,
|
|
1227
|
+
onScreen: isNdcOnScreen(_vec3B)
|
|
1228
|
+
};
|
|
1229
|
+
}
|
|
1230
|
+
function projectToScreen(obj, camera, size) {
|
|
1231
|
+
obj.updateWorldMatrix(true, false);
|
|
1232
|
+
camera.updateWorldMatrix(true, false);
|
|
1233
|
+
const center = getWorldCenter(obj);
|
|
1234
|
+
const centerResult = tryProjectPoint(center, camera, size);
|
|
1235
|
+
if (centerResult && centerResult.onScreen) {
|
|
1236
|
+
return {
|
|
1237
|
+
point: centerResult.screen,
|
|
1238
|
+
strategy: "center",
|
|
1239
|
+
ndc: centerResult.ndc
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
1242
|
+
const faceCenters = getBboxFaceCenters(obj);
|
|
1243
|
+
const faceResult = findBestOnScreenPoint(faceCenters, camera, size);
|
|
1244
|
+
if (faceResult) {
|
|
1245
|
+
return {
|
|
1246
|
+
point: faceResult.screen,
|
|
1247
|
+
strategy: "face-center",
|
|
1248
|
+
ndc: faceResult.ndc
|
|
1249
|
+
};
|
|
1250
|
+
}
|
|
1251
|
+
const corners = getBboxCorners(obj);
|
|
1252
|
+
const cornerResult = findBestOnScreenPoint(corners, camera, size);
|
|
1253
|
+
if (cornerResult) {
|
|
1254
|
+
return {
|
|
1255
|
+
point: cornerResult.screen,
|
|
1256
|
+
strategy: "corner",
|
|
1257
|
+
ndc: cornerResult.ndc
|
|
1258
|
+
};
|
|
1259
|
+
}
|
|
1260
|
+
obj.getWorldPosition(_vec3);
|
|
1261
|
+
const originResult = tryProjectPoint(_vec3.clone(), camera, size);
|
|
1262
|
+
if (originResult && originResult.onScreen) {
|
|
1263
|
+
return {
|
|
1264
|
+
point: originResult.screen,
|
|
1265
|
+
strategy: "fallback-origin",
|
|
1266
|
+
ndc: originResult.ndc
|
|
1267
|
+
};
|
|
1268
|
+
}
|
|
1269
|
+
if (centerResult) {
|
|
1270
|
+
return {
|
|
1271
|
+
point: centerResult.screen,
|
|
1272
|
+
strategy: "center",
|
|
1273
|
+
ndc: centerResult.ndc
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
return null;
|
|
1277
|
+
}
|
|
1278
|
+
function findBestOnScreenPoint(candidates, camera, size) {
|
|
1279
|
+
let bestResult = null;
|
|
1280
|
+
let bestDistSq = Infinity;
|
|
1281
|
+
const halfW = size.width / 2;
|
|
1282
|
+
const halfH = size.height / 2;
|
|
1283
|
+
for (const point of candidates) {
|
|
1284
|
+
const result = tryProjectPoint(point, camera, size);
|
|
1285
|
+
if (!result || !result.onScreen) continue;
|
|
1286
|
+
const dx = result.screen.x - halfW;
|
|
1287
|
+
const dy = result.screen.y - halfH;
|
|
1288
|
+
const distSq = dx * dx + dy * dy;
|
|
1289
|
+
if (distSq < bestDistSq) {
|
|
1290
|
+
bestDistSq = distSq;
|
|
1291
|
+
bestResult = { screen: result.screen, ndc: result.ndc };
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
return bestResult;
|
|
1295
|
+
}
|
|
1296
|
+
function isInFrustum(obj, camera) {
|
|
1297
|
+
camera.updateWorldMatrix(true, false);
|
|
1298
|
+
obj.updateWorldMatrix(true, false);
|
|
1299
|
+
_projMatrix.multiplyMatrices(
|
|
1300
|
+
camera.projectionMatrix,
|
|
1301
|
+
camera.matrixWorldInverse
|
|
1302
|
+
);
|
|
1303
|
+
_frustum.setFromProjectionMatrix(_projMatrix);
|
|
1304
|
+
_box32.setFromObject(obj);
|
|
1305
|
+
if (_box32.isEmpty()) {
|
|
1306
|
+
obj.getWorldPosition(_vec3);
|
|
1307
|
+
return _frustum.containsPoint(_vec3);
|
|
1308
|
+
}
|
|
1309
|
+
return _frustum.intersectsBox(_box32);
|
|
1310
|
+
}
|
|
1311
|
+
function screenDeltaToWorld(dx, dy, obj, camera, size) {
|
|
1312
|
+
obj.getWorldPosition(_vec3);
|
|
1313
|
+
_vec3.project(camera);
|
|
1314
|
+
const depth = _vec3.z;
|
|
1315
|
+
const start = new three.Vector3(0, 0, depth).unproject(camera);
|
|
1316
|
+
const right = new three.Vector3(2 / size.width, 0, depth).unproject(camera);
|
|
1317
|
+
const up = new three.Vector3(0, -2 / size.height, depth).unproject(camera);
|
|
1318
|
+
const rightDir = right.sub(start);
|
|
1319
|
+
const upDir = up.sub(start);
|
|
1320
|
+
return new three.Vector3().addScaledVector(rightDir, dx).addScaledVector(upDir, dy);
|
|
1321
|
+
}
|
|
1322
|
+
function projectAllSamplePoints(obj, camera, size) {
|
|
1323
|
+
obj.updateWorldMatrix(true, false);
|
|
1324
|
+
camera.updateWorldMatrix(true, false);
|
|
1325
|
+
const points = [];
|
|
1326
|
+
const candidates = [];
|
|
1327
|
+
candidates.push(getWorldCenter(obj).clone());
|
|
1328
|
+
candidates.push(...getBboxFaceCenters(obj));
|
|
1329
|
+
candidates.push(...getBboxCorners(obj));
|
|
1330
|
+
for (const wsPt of candidates) {
|
|
1331
|
+
const result = tryProjectPoint(wsPt, camera, size);
|
|
1332
|
+
if (result && result.onScreen) {
|
|
1333
|
+
points.push(result.screen);
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
return points;
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
// src/interactions/dispatch.ts
|
|
1340
|
+
var _nextPointerId = 1e3;
|
|
1341
|
+
function allocPointerId() {
|
|
1342
|
+
return _nextPointerId++;
|
|
1343
|
+
}
|
|
1344
|
+
function toClientCoords(canvas, point) {
|
|
1345
|
+
const rect = canvas.getBoundingClientRect();
|
|
1346
|
+
return {
|
|
1347
|
+
clientX: rect.left + point.x,
|
|
1348
|
+
clientY: rect.top + point.y
|
|
1349
|
+
};
|
|
1350
|
+
}
|
|
1351
|
+
function makePointerInit(canvas, point, pointerId, overrides) {
|
|
1352
|
+
const { clientX, clientY } = toClientCoords(canvas, point);
|
|
1353
|
+
return {
|
|
1354
|
+
bubbles: true,
|
|
1355
|
+
cancelable: true,
|
|
1356
|
+
composed: true,
|
|
1357
|
+
clientX,
|
|
1358
|
+
clientY,
|
|
1359
|
+
screenX: clientX,
|
|
1360
|
+
screenY: clientY,
|
|
1361
|
+
pointerId,
|
|
1362
|
+
pointerType: "mouse",
|
|
1363
|
+
isPrimary: true,
|
|
1364
|
+
button: 0,
|
|
1365
|
+
buttons: 1,
|
|
1366
|
+
width: 1,
|
|
1367
|
+
height: 1,
|
|
1368
|
+
pressure: 0.5,
|
|
1369
|
+
...overrides
|
|
1370
|
+
};
|
|
1371
|
+
}
|
|
1372
|
+
function dispatchClick(canvas, point) {
|
|
1373
|
+
withSafePointerCapture(() => {
|
|
1374
|
+
const pointerId = allocPointerId();
|
|
1375
|
+
canvas.dispatchEvent(
|
|
1376
|
+
new PointerEvent("pointerdown", makePointerInit(canvas, point, pointerId))
|
|
1377
|
+
);
|
|
1378
|
+
canvas.dispatchEvent(
|
|
1379
|
+
new PointerEvent(
|
|
1380
|
+
"pointerup",
|
|
1381
|
+
makePointerInit(canvas, point, pointerId, { buttons: 0, pressure: 0 })
|
|
1382
|
+
)
|
|
1383
|
+
);
|
|
1384
|
+
canvas.dispatchEvent(
|
|
1385
|
+
new MouseEvent("click", {
|
|
1386
|
+
bubbles: true,
|
|
1387
|
+
cancelable: true,
|
|
1388
|
+
...toClientCoords(canvas, point),
|
|
1389
|
+
button: 0
|
|
1390
|
+
})
|
|
1391
|
+
);
|
|
1392
|
+
});
|
|
1393
|
+
}
|
|
1394
|
+
function dispatchHover(canvas, point) {
|
|
1395
|
+
const pointerId = allocPointerId();
|
|
1396
|
+
const init = makePointerInit(canvas, point, pointerId, {
|
|
1397
|
+
buttons: 0,
|
|
1398
|
+
pressure: 0
|
|
1399
|
+
});
|
|
1400
|
+
canvas.dispatchEvent(new PointerEvent("pointermove", init));
|
|
1401
|
+
canvas.dispatchEvent(new PointerEvent("pointerover", init));
|
|
1402
|
+
canvas.dispatchEvent(new PointerEvent("pointerenter", { ...init, bubbles: false }));
|
|
1403
|
+
}
|
|
1404
|
+
async function dispatchDrag(canvas, start, end, options = {}) {
|
|
1405
|
+
const { steps = 10, stepDelayMs = 0 } = options;
|
|
1406
|
+
const pointerId = allocPointerId();
|
|
1407
|
+
withSafePointerCapture(() => {
|
|
1408
|
+
canvas.dispatchEvent(
|
|
1409
|
+
new PointerEvent("pointerdown", makePointerInit(canvas, start, pointerId))
|
|
1410
|
+
);
|
|
1411
|
+
});
|
|
1412
|
+
for (let i = 1; i <= steps; i++) {
|
|
1413
|
+
const t = i / steps;
|
|
1414
|
+
const intermediate = {
|
|
1415
|
+
x: start.x + (end.x - start.x) * t,
|
|
1416
|
+
y: start.y + (end.y - start.y) * t
|
|
1417
|
+
};
|
|
1418
|
+
if (stepDelayMs > 0) {
|
|
1419
|
+
await sleep(stepDelayMs);
|
|
1420
|
+
}
|
|
1421
|
+
canvas.dispatchEvent(
|
|
1422
|
+
new PointerEvent(
|
|
1423
|
+
"pointermove",
|
|
1424
|
+
makePointerInit(canvas, intermediate, pointerId)
|
|
1425
|
+
)
|
|
1426
|
+
);
|
|
1427
|
+
}
|
|
1428
|
+
withSafePointerCapture(() => {
|
|
1429
|
+
canvas.dispatchEvent(
|
|
1430
|
+
new PointerEvent(
|
|
1431
|
+
"pointerup",
|
|
1432
|
+
makePointerInit(canvas, end, pointerId, { buttons: 0, pressure: 0 })
|
|
1433
|
+
)
|
|
1434
|
+
);
|
|
1435
|
+
});
|
|
1436
|
+
}
|
|
1437
|
+
function dispatchDoubleClick(canvas, point) {
|
|
1438
|
+
withSafePointerCapture(() => {
|
|
1439
|
+
const pointerId = allocPointerId();
|
|
1440
|
+
const coords = toClientCoords(canvas, point);
|
|
1441
|
+
canvas.dispatchEvent(
|
|
1442
|
+
new PointerEvent("pointerdown", makePointerInit(canvas, point, pointerId))
|
|
1443
|
+
);
|
|
1444
|
+
canvas.dispatchEvent(
|
|
1445
|
+
new PointerEvent(
|
|
1446
|
+
"pointerup",
|
|
1447
|
+
makePointerInit(canvas, point, pointerId, { buttons: 0, pressure: 0 })
|
|
1448
|
+
)
|
|
1449
|
+
);
|
|
1450
|
+
canvas.dispatchEvent(
|
|
1451
|
+
new MouseEvent("click", {
|
|
1452
|
+
bubbles: true,
|
|
1453
|
+
cancelable: true,
|
|
1454
|
+
...coords,
|
|
1455
|
+
button: 0,
|
|
1456
|
+
detail: 1
|
|
1457
|
+
})
|
|
1458
|
+
);
|
|
1459
|
+
canvas.dispatchEvent(
|
|
1460
|
+
new PointerEvent("pointerdown", makePointerInit(canvas, point, pointerId))
|
|
1461
|
+
);
|
|
1462
|
+
canvas.dispatchEvent(
|
|
1463
|
+
new PointerEvent(
|
|
1464
|
+
"pointerup",
|
|
1465
|
+
makePointerInit(canvas, point, pointerId, { buttons: 0, pressure: 0 })
|
|
1466
|
+
)
|
|
1467
|
+
);
|
|
1468
|
+
canvas.dispatchEvent(
|
|
1469
|
+
new MouseEvent("click", {
|
|
1470
|
+
bubbles: true,
|
|
1471
|
+
cancelable: true,
|
|
1472
|
+
...coords,
|
|
1473
|
+
button: 0,
|
|
1474
|
+
detail: 2
|
|
1475
|
+
})
|
|
1476
|
+
);
|
|
1477
|
+
canvas.dispatchEvent(
|
|
1478
|
+
new MouseEvent("dblclick", {
|
|
1479
|
+
bubbles: true,
|
|
1480
|
+
cancelable: true,
|
|
1481
|
+
...coords,
|
|
1482
|
+
button: 0,
|
|
1483
|
+
detail: 2
|
|
1484
|
+
})
|
|
1485
|
+
);
|
|
1486
|
+
});
|
|
1487
|
+
}
|
|
1488
|
+
function dispatchContextMenu(canvas, point) {
|
|
1489
|
+
withSafePointerCapture(() => {
|
|
1490
|
+
const pointerId = allocPointerId();
|
|
1491
|
+
canvas.dispatchEvent(
|
|
1492
|
+
new PointerEvent(
|
|
1493
|
+
"pointerdown",
|
|
1494
|
+
makePointerInit(canvas, point, pointerId, { button: 2, buttons: 2 })
|
|
1495
|
+
)
|
|
1496
|
+
);
|
|
1497
|
+
canvas.dispatchEvent(
|
|
1498
|
+
new PointerEvent(
|
|
1499
|
+
"pointerup",
|
|
1500
|
+
makePointerInit(canvas, point, pointerId, {
|
|
1501
|
+
button: 2,
|
|
1502
|
+
buttons: 0,
|
|
1503
|
+
pressure: 0
|
|
1504
|
+
})
|
|
1505
|
+
)
|
|
1506
|
+
);
|
|
1507
|
+
canvas.dispatchEvent(
|
|
1508
|
+
new MouseEvent("contextmenu", {
|
|
1509
|
+
bubbles: true,
|
|
1510
|
+
cancelable: true,
|
|
1511
|
+
...toClientCoords(canvas, point),
|
|
1512
|
+
button: 2
|
|
1513
|
+
})
|
|
1514
|
+
);
|
|
1515
|
+
});
|
|
1516
|
+
}
|
|
1517
|
+
function dispatchWheel(canvas, point, options = {}) {
|
|
1518
|
+
const { deltaY = 100, deltaX = 0, deltaMode = 0 } = options;
|
|
1519
|
+
const coords = toClientCoords(canvas, point);
|
|
1520
|
+
canvas.dispatchEvent(
|
|
1521
|
+
new WheelEvent("wheel", {
|
|
1522
|
+
bubbles: true,
|
|
1523
|
+
cancelable: true,
|
|
1524
|
+
...coords,
|
|
1525
|
+
deltaX,
|
|
1526
|
+
deltaY,
|
|
1527
|
+
deltaZ: 0,
|
|
1528
|
+
deltaMode
|
|
1529
|
+
})
|
|
1530
|
+
);
|
|
1531
|
+
}
|
|
1532
|
+
function dispatchPointerMiss(canvas, point = { x: 1, y: 1 }) {
|
|
1533
|
+
dispatchClick(canvas, point);
|
|
1534
|
+
}
|
|
1535
|
+
function dispatchUnhover(canvas) {
|
|
1536
|
+
const pointerId = allocPointerId();
|
|
1537
|
+
const offScreen = { x: -9999, y: -9999 };
|
|
1538
|
+
const init = makePointerInit(canvas, offScreen, pointerId, {
|
|
1539
|
+
buttons: 0,
|
|
1540
|
+
pressure: 0
|
|
1541
|
+
});
|
|
1542
|
+
canvas.dispatchEvent(new PointerEvent("pointermove", init));
|
|
1543
|
+
canvas.dispatchEvent(new PointerEvent("pointerout", init));
|
|
1544
|
+
canvas.dispatchEvent(new PointerEvent("pointerleave", { ...init, bubbles: false }));
|
|
1545
|
+
}
|
|
1546
|
+
function withSafePointerCapture(fn) {
|
|
1547
|
+
const original = Element.prototype.releasePointerCapture;
|
|
1548
|
+
Element.prototype.releasePointerCapture = function safeRelease(pointerId) {
|
|
1549
|
+
try {
|
|
1550
|
+
original.call(this, pointerId);
|
|
1551
|
+
} catch {
|
|
1552
|
+
}
|
|
1553
|
+
};
|
|
1554
|
+
try {
|
|
1555
|
+
return fn();
|
|
1556
|
+
} finally {
|
|
1557
|
+
Element.prototype.releasePointerCapture = original;
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
function sleep(ms) {
|
|
1561
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1562
|
+
}
|
|
1563
|
+
var _raycaster = /* @__PURE__ */ new three.Raycaster();
|
|
1564
|
+
var _ndc = /* @__PURE__ */ new three.Vector2();
|
|
1565
|
+
function screenToNdc(point, size) {
|
|
1566
|
+
_ndc.set(
|
|
1567
|
+
point.x / size.width * 2 - 1,
|
|
1568
|
+
-(point.y / size.height) * 2 + 1
|
|
1569
|
+
);
|
|
1570
|
+
return _ndc;
|
|
1571
|
+
}
|
|
1572
|
+
function getObjectLabel(obj) {
|
|
1573
|
+
const testId = obj.userData?.testId;
|
|
1574
|
+
if (testId) return `testId="${testId}"`;
|
|
1575
|
+
if (obj.name) return `name="${obj.name}"`;
|
|
1576
|
+
return `uuid="${obj.uuid.slice(0, 8)}\u2026"`;
|
|
1577
|
+
}
|
|
1578
|
+
function isTargetOrDescendant(candidate, target) {
|
|
1579
|
+
let current = candidate;
|
|
1580
|
+
while (current) {
|
|
1581
|
+
if (current === target) return true;
|
|
1582
|
+
current = current.parent;
|
|
1583
|
+
}
|
|
1584
|
+
return false;
|
|
1585
|
+
}
|
|
1586
|
+
function findScene(obj) {
|
|
1587
|
+
let current = obj;
|
|
1588
|
+
while (current) {
|
|
1589
|
+
if (current.isScene) return current;
|
|
1590
|
+
current = current.parent;
|
|
1591
|
+
}
|
|
1592
|
+
return null;
|
|
1593
|
+
}
|
|
1594
|
+
function verifyRaycastHit(point, target, camera, size) {
|
|
1595
|
+
const scene = findScene(target);
|
|
1596
|
+
if (!scene) {
|
|
1597
|
+
return { hit: false, occluderLabel: "object not in scene" };
|
|
1598
|
+
}
|
|
1599
|
+
const ndc = screenToNdc(point, size);
|
|
1600
|
+
_raycaster.setFromCamera(ndc, camera);
|
|
1601
|
+
const intersections = _raycaster.intersectObjects(scene.children, true);
|
|
1602
|
+
if (intersections.length === 0) {
|
|
1603
|
+
return { hit: true };
|
|
1604
|
+
}
|
|
1605
|
+
const firstHit = intersections[0].object;
|
|
1606
|
+
if (isTargetOrDescendant(firstHit, target)) {
|
|
1607
|
+
return { hit: true };
|
|
1608
|
+
}
|
|
1609
|
+
const targetHit = intersections.find(
|
|
1610
|
+
(i) => isTargetOrDescendant(i.object, target)
|
|
1611
|
+
);
|
|
1612
|
+
if (targetHit) {
|
|
1613
|
+
return {
|
|
1614
|
+
hit: false,
|
|
1615
|
+
occluder: firstHit,
|
|
1616
|
+
occluderLabel: getObjectLabel(firstHit)
|
|
1617
|
+
};
|
|
1618
|
+
}
|
|
1619
|
+
return {
|
|
1620
|
+
hit: false,
|
|
1621
|
+
occluder: firstHit,
|
|
1622
|
+
occluderLabel: getObjectLabel(firstHit)
|
|
1623
|
+
};
|
|
1624
|
+
}
|
|
1625
|
+
function verifyRaycastHitMultiPoint(points, target, camera, size) {
|
|
1626
|
+
let lastResult = { hit: false };
|
|
1627
|
+
for (const point of points) {
|
|
1628
|
+
const result = verifyRaycastHit(point, target, camera, size);
|
|
1629
|
+
if (result.hit) return result;
|
|
1630
|
+
lastResult = result;
|
|
1631
|
+
}
|
|
1632
|
+
return lastResult;
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
// src/interactions/resolve.ts
|
|
1636
|
+
var _store2 = null;
|
|
1637
|
+
var _camera = null;
|
|
1638
|
+
var _gl = null;
|
|
1639
|
+
var _size = null;
|
|
1640
|
+
function setInteractionState(store, camera, gl, size) {
|
|
1641
|
+
_store2 = store;
|
|
1642
|
+
_camera = camera;
|
|
1643
|
+
_gl = gl;
|
|
1644
|
+
_size = size;
|
|
1645
|
+
}
|
|
1646
|
+
function clearInteractionState() {
|
|
1647
|
+
_store2 = null;
|
|
1648
|
+
_camera = null;
|
|
1649
|
+
_gl = null;
|
|
1650
|
+
_size = null;
|
|
1651
|
+
}
|
|
1652
|
+
function getStore() {
|
|
1653
|
+
if (!_store2) {
|
|
1654
|
+
throw new Error(
|
|
1655
|
+
"[react-three-dom] Interaction state not initialized. Is <ThreeDom> mounted?"
|
|
1656
|
+
);
|
|
1657
|
+
}
|
|
1658
|
+
return _store2;
|
|
1659
|
+
}
|
|
1660
|
+
function getCamera() {
|
|
1661
|
+
if (!_camera) {
|
|
1662
|
+
throw new Error(
|
|
1663
|
+
"[react-three-dom] Camera not available. Is <ThreeDom> mounted?"
|
|
1664
|
+
);
|
|
1665
|
+
}
|
|
1666
|
+
return _camera;
|
|
1667
|
+
}
|
|
1668
|
+
function getRenderer() {
|
|
1669
|
+
if (!_gl) {
|
|
1670
|
+
throw new Error(
|
|
1671
|
+
"[react-three-dom] Renderer not available. Is <ThreeDom> mounted?"
|
|
1672
|
+
);
|
|
1673
|
+
}
|
|
1674
|
+
return _gl;
|
|
1675
|
+
}
|
|
1676
|
+
function getCanvasSize() {
|
|
1677
|
+
if (!_size) {
|
|
1678
|
+
throw new Error(
|
|
1679
|
+
"[react-three-dom] Canvas size not available. Is <ThreeDom> mounted?"
|
|
1680
|
+
);
|
|
1681
|
+
}
|
|
1682
|
+
return _size;
|
|
1683
|
+
}
|
|
1684
|
+
function resolveObject(idOrUuid) {
|
|
1685
|
+
const store = getStore();
|
|
1686
|
+
const obj = store.getObject3D(idOrUuid);
|
|
1687
|
+
if (!obj) {
|
|
1688
|
+
throw new Error(
|
|
1689
|
+
`[react-three-dom] Object "${idOrUuid}" not found. Check that the object has userData.testId="${idOrUuid}" or uuid="${idOrUuid}".`
|
|
1690
|
+
);
|
|
1691
|
+
}
|
|
1692
|
+
return obj;
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
// src/interactions/click.ts
|
|
1696
|
+
function click3D(idOrUuid, options = {}) {
|
|
1697
|
+
const { verify = true } = options;
|
|
1698
|
+
const obj = resolveObject(idOrUuid);
|
|
1699
|
+
const camera = getCamera();
|
|
1700
|
+
const gl = getRenderer();
|
|
1701
|
+
const size = getCanvasSize();
|
|
1702
|
+
const projection = projectToScreen(obj, camera, size);
|
|
1703
|
+
if (!projection) {
|
|
1704
|
+
throw new Error(
|
|
1705
|
+
`[react-three-dom] click3D("${idOrUuid}") failed: object is not visible on screen. It may be behind the camera or outside the viewport.`
|
|
1706
|
+
);
|
|
1707
|
+
}
|
|
1708
|
+
const canvas = gl.domElement;
|
|
1709
|
+
dispatchClick(canvas, projection.point);
|
|
1710
|
+
let raycast;
|
|
1711
|
+
if (verify) {
|
|
1712
|
+
raycast = verifyRaycastHit(projection.point, obj, camera, size);
|
|
1713
|
+
if (!raycast.hit && raycast.occluderLabel) {
|
|
1714
|
+
console.warn(
|
|
1715
|
+
`[react-three-dom] click3D("${idOrUuid}") dispatched at (${Math.round(projection.point.x)}, ${Math.round(projection.point.y)}) but raycast hit ${raycast.occluderLabel} instead. The object may be occluded.`
|
|
1716
|
+
);
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
return {
|
|
1720
|
+
dispatched: true,
|
|
1721
|
+
raycast,
|
|
1722
|
+
screenPoint: projection.point,
|
|
1723
|
+
strategy: projection.strategy
|
|
1724
|
+
};
|
|
1725
|
+
}
|
|
1726
|
+
function doubleClick3D(idOrUuid, options = {}) {
|
|
1727
|
+
const { verify = true } = options;
|
|
1728
|
+
const obj = resolveObject(idOrUuid);
|
|
1729
|
+
const camera = getCamera();
|
|
1730
|
+
const gl = getRenderer();
|
|
1731
|
+
const size = getCanvasSize();
|
|
1732
|
+
const projection = projectToScreen(obj, camera, size);
|
|
1733
|
+
if (!projection) {
|
|
1734
|
+
throw new Error(
|
|
1735
|
+
`[react-three-dom] doubleClick3D("${idOrUuid}") failed: object is not visible on screen.`
|
|
1736
|
+
);
|
|
1737
|
+
}
|
|
1738
|
+
const canvas = gl.domElement;
|
|
1739
|
+
dispatchDoubleClick(canvas, projection.point);
|
|
1740
|
+
let raycast;
|
|
1741
|
+
if (verify) {
|
|
1742
|
+
raycast = verifyRaycastHit(projection.point, obj, camera, size);
|
|
1743
|
+
if (!raycast.hit && raycast.occluderLabel) {
|
|
1744
|
+
console.warn(
|
|
1745
|
+
`[react-three-dom] doubleClick3D("${idOrUuid}") dispatched at (${Math.round(projection.point.x)}, ${Math.round(projection.point.y)}) but raycast hit ${raycast.occluderLabel} instead.`
|
|
1746
|
+
);
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
return {
|
|
1750
|
+
dispatched: true,
|
|
1751
|
+
raycast,
|
|
1752
|
+
screenPoint: projection.point,
|
|
1753
|
+
strategy: projection.strategy
|
|
1754
|
+
};
|
|
1755
|
+
}
|
|
1756
|
+
function contextMenu3D(idOrUuid, options = {}) {
|
|
1757
|
+
const { verify = true } = options;
|
|
1758
|
+
const obj = resolveObject(idOrUuid);
|
|
1759
|
+
const camera = getCamera();
|
|
1760
|
+
const gl = getRenderer();
|
|
1761
|
+
const size = getCanvasSize();
|
|
1762
|
+
const projection = projectToScreen(obj, camera, size);
|
|
1763
|
+
if (!projection) {
|
|
1764
|
+
throw new Error(
|
|
1765
|
+
`[react-three-dom] contextMenu3D("${idOrUuid}") failed: object is not visible on screen.`
|
|
1766
|
+
);
|
|
1767
|
+
}
|
|
1768
|
+
const canvas = gl.domElement;
|
|
1769
|
+
dispatchContextMenu(canvas, projection.point);
|
|
1770
|
+
let raycast;
|
|
1771
|
+
if (verify) {
|
|
1772
|
+
raycast = verifyRaycastHit(projection.point, obj, camera, size);
|
|
1773
|
+
if (!raycast.hit && raycast.occluderLabel) {
|
|
1774
|
+
console.warn(
|
|
1775
|
+
`[react-three-dom] contextMenu3D("${idOrUuid}") dispatched at (${Math.round(projection.point.x)}, ${Math.round(projection.point.y)}) but raycast hit ${raycast.occluderLabel} instead.`
|
|
1776
|
+
);
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
return {
|
|
1780
|
+
dispatched: true,
|
|
1781
|
+
raycast,
|
|
1782
|
+
screenPoint: projection.point,
|
|
1783
|
+
strategy: projection.strategy
|
|
1784
|
+
};
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
// src/interactions/hover.ts
|
|
1788
|
+
function hover3D(idOrUuid, options = {}) {
|
|
1789
|
+
const { verify = true } = options;
|
|
1790
|
+
const obj = resolveObject(idOrUuid);
|
|
1791
|
+
const camera = getCamera();
|
|
1792
|
+
const gl = getRenderer();
|
|
1793
|
+
const size = getCanvasSize();
|
|
1794
|
+
const projection = projectToScreen(obj, camera, size);
|
|
1795
|
+
if (!projection) {
|
|
1796
|
+
throw new Error(
|
|
1797
|
+
`[react-three-dom] hover3D("${idOrUuid}") failed: object is not visible on screen. It may be behind the camera or outside the viewport.`
|
|
1798
|
+
);
|
|
1799
|
+
}
|
|
1800
|
+
const canvas = gl.domElement;
|
|
1801
|
+
dispatchHover(canvas, projection.point);
|
|
1802
|
+
let raycast;
|
|
1803
|
+
if (verify) {
|
|
1804
|
+
raycast = verifyRaycastHit(projection.point, obj, camera, size);
|
|
1805
|
+
if (!raycast.hit && raycast.occluderLabel) {
|
|
1806
|
+
console.warn(
|
|
1807
|
+
`[react-three-dom] hover3D("${idOrUuid}") dispatched at (${Math.round(projection.point.x)}, ${Math.round(projection.point.y)}) but raycast hit ${raycast.occluderLabel} instead.`
|
|
1808
|
+
);
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
return {
|
|
1812
|
+
dispatched: true,
|
|
1813
|
+
raycast,
|
|
1814
|
+
screenPoint: projection.point,
|
|
1815
|
+
strategy: projection.strategy
|
|
1816
|
+
};
|
|
1817
|
+
}
|
|
1818
|
+
function unhover3D() {
|
|
1819
|
+
const gl = getRenderer();
|
|
1820
|
+
dispatchUnhover(gl.domElement);
|
|
1821
|
+
}
|
|
1822
|
+
async function drag3D(idOrUuid, delta, options = {}) {
|
|
1823
|
+
const { mode = "world", ...dragOptions } = options;
|
|
1824
|
+
const obj = resolveObject(idOrUuid);
|
|
1825
|
+
const camera = getCamera();
|
|
1826
|
+
const gl = getRenderer();
|
|
1827
|
+
const size = getCanvasSize();
|
|
1828
|
+
const projection = projectToScreen(obj, camera, size);
|
|
1829
|
+
if (!projection) {
|
|
1830
|
+
throw new Error(
|
|
1831
|
+
`[react-three-dom] drag3D("${idOrUuid}") failed: object is not visible on screen. It may be behind the camera or outside the viewport.`
|
|
1832
|
+
);
|
|
1833
|
+
}
|
|
1834
|
+
const startPoint = projection.point;
|
|
1835
|
+
let endPoint;
|
|
1836
|
+
if (mode === "screen" && "dx" in delta) {
|
|
1837
|
+
const screenDelta = delta;
|
|
1838
|
+
endPoint = {
|
|
1839
|
+
x: startPoint.x + screenDelta.dx,
|
|
1840
|
+
y: startPoint.y + screenDelta.dy
|
|
1841
|
+
};
|
|
1842
|
+
} else {
|
|
1843
|
+
const worldDelta = delta;
|
|
1844
|
+
const worldPos = new three.Vector3();
|
|
1845
|
+
obj.getWorldPosition(worldPos);
|
|
1846
|
+
const targetPos = worldPos.clone().add(new three.Vector3(worldDelta.x, worldDelta.y, worldDelta.z));
|
|
1847
|
+
targetPos.project(camera);
|
|
1848
|
+
endPoint = {
|
|
1849
|
+
x: (targetPos.x + 1) / 2 * size.width,
|
|
1850
|
+
y: (-targetPos.y + 1) / 2 * size.height
|
|
1851
|
+
};
|
|
1852
|
+
}
|
|
1853
|
+
const canvas = gl.domElement;
|
|
1854
|
+
await dispatchDrag(canvas, startPoint, endPoint, dragOptions);
|
|
1855
|
+
return {
|
|
1856
|
+
dispatched: true,
|
|
1857
|
+
startPoint,
|
|
1858
|
+
endPoint,
|
|
1859
|
+
strategy: projection.strategy
|
|
1860
|
+
};
|
|
1861
|
+
}
|
|
1862
|
+
function previewDragWorldDelta(idOrUuid, screenDx, screenDy) {
|
|
1863
|
+
const obj = resolveObject(idOrUuid);
|
|
1864
|
+
const camera = getCamera();
|
|
1865
|
+
const size = getCanvasSize();
|
|
1866
|
+
return screenDeltaToWorld(screenDx, screenDy, obj, camera, size);
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
// src/interactions/wheel.ts
|
|
1870
|
+
function wheel3D(idOrUuid, options = {}) {
|
|
1871
|
+
const obj = resolveObject(idOrUuid);
|
|
1872
|
+
const camera = getCamera();
|
|
1873
|
+
const gl = getRenderer();
|
|
1874
|
+
const size = getCanvasSize();
|
|
1875
|
+
const projection = projectToScreen(obj, camera, size);
|
|
1876
|
+
if (!projection) {
|
|
1877
|
+
throw new Error(
|
|
1878
|
+
`[react-three-dom] wheel3D("${idOrUuid}") failed: object is not visible on screen.`
|
|
1879
|
+
);
|
|
1880
|
+
}
|
|
1881
|
+
const canvas = gl.domElement;
|
|
1882
|
+
dispatchWheel(canvas, projection.point, options);
|
|
1883
|
+
return {
|
|
1884
|
+
dispatched: true,
|
|
1885
|
+
screenPoint: projection.point,
|
|
1886
|
+
strategy: projection.strategy
|
|
1887
|
+
};
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
// src/interactions/pointerMiss.ts
|
|
1891
|
+
function pointerMiss3D(options = {}) {
|
|
1892
|
+
const gl = getRenderer();
|
|
1893
|
+
const canvas = gl.domElement;
|
|
1894
|
+
dispatchPointerMiss(canvas, options.point);
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
// src/highlight/SelectionManager.ts
|
|
1898
|
+
var SelectionManager = class {
|
|
1899
|
+
constructor() {
|
|
1900
|
+
/** Currently selected objects (ordered by selection time). */
|
|
1901
|
+
this._selected = [];
|
|
1902
|
+
/** Listeners notified on selection change. */
|
|
1903
|
+
this._listeners = [];
|
|
1904
|
+
}
|
|
1905
|
+
// -----------------------------------------------------------------------
|
|
1906
|
+
// Selection API
|
|
1907
|
+
// -----------------------------------------------------------------------
|
|
1908
|
+
/** Select a single object (clears previous selection). */
|
|
1909
|
+
select(obj) {
|
|
1910
|
+
if (this._selected.length === 1 && this._selected[0] === obj) return;
|
|
1911
|
+
this._selected = [obj];
|
|
1912
|
+
this._notify();
|
|
1913
|
+
}
|
|
1914
|
+
/** Add an object to the current selection (multi-select). */
|
|
1915
|
+
addToSelection(obj) {
|
|
1916
|
+
if (this._selected.includes(obj)) return;
|
|
1917
|
+
this._selected.push(obj);
|
|
1918
|
+
this._notify();
|
|
1919
|
+
}
|
|
1920
|
+
/** Remove an object from the selection. */
|
|
1921
|
+
removeFromSelection(obj) {
|
|
1922
|
+
const idx = this._selected.indexOf(obj);
|
|
1923
|
+
if (idx === -1) return;
|
|
1924
|
+
this._selected.splice(idx, 1);
|
|
1925
|
+
this._notify();
|
|
1926
|
+
}
|
|
1927
|
+
/** Toggle an object in/out of the selection. */
|
|
1928
|
+
toggleSelection(obj) {
|
|
1929
|
+
if (this._selected.includes(obj)) {
|
|
1930
|
+
this.removeFromSelection(obj);
|
|
1931
|
+
} else {
|
|
1932
|
+
this.addToSelection(obj);
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
/** Clear all selections. */
|
|
1936
|
+
clearSelection() {
|
|
1937
|
+
if (this._selected.length === 0) return;
|
|
1938
|
+
this._selected = [];
|
|
1939
|
+
this._notify();
|
|
1940
|
+
}
|
|
1941
|
+
/** Get the currently selected objects. */
|
|
1942
|
+
getSelected() {
|
|
1943
|
+
return this._selected;
|
|
1944
|
+
}
|
|
1945
|
+
/** Get the primary (first) selected object, or null. */
|
|
1946
|
+
getPrimary() {
|
|
1947
|
+
return this._selected[0] ?? null;
|
|
1948
|
+
}
|
|
1949
|
+
/** Check if an object is selected. */
|
|
1950
|
+
isSelected(obj) {
|
|
1951
|
+
return this._selected.includes(obj);
|
|
1952
|
+
}
|
|
1953
|
+
/** Number of selected objects. */
|
|
1954
|
+
get count() {
|
|
1955
|
+
return this._selected.length;
|
|
1956
|
+
}
|
|
1957
|
+
// -----------------------------------------------------------------------
|
|
1958
|
+
// Event system
|
|
1959
|
+
// -----------------------------------------------------------------------
|
|
1960
|
+
/** Subscribe to selection changes. Returns unsubscribe function. */
|
|
1961
|
+
subscribe(listener) {
|
|
1962
|
+
this._listeners.push(listener);
|
|
1963
|
+
return () => {
|
|
1964
|
+
const idx = this._listeners.indexOf(listener);
|
|
1965
|
+
if (idx !== -1) this._listeners.splice(idx, 1);
|
|
1966
|
+
};
|
|
1967
|
+
}
|
|
1968
|
+
_notify() {
|
|
1969
|
+
const snapshot = [...this._selected];
|
|
1970
|
+
for (const listener of this._listeners) {
|
|
1971
|
+
listener(snapshot);
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
// -----------------------------------------------------------------------
|
|
1975
|
+
// Cleanup
|
|
1976
|
+
// -----------------------------------------------------------------------
|
|
1977
|
+
dispose() {
|
|
1978
|
+
this._selected = [];
|
|
1979
|
+
this._listeners = [];
|
|
1980
|
+
}
|
|
1981
|
+
};
|
|
1982
|
+
|
|
1983
|
+
// src/bridge/ThreeDom.tsx
|
|
1984
|
+
var _store3 = null;
|
|
1985
|
+
var _mirror = null;
|
|
1986
|
+
var _selectionManager = null;
|
|
1987
|
+
var _highlighter = null;
|
|
1988
|
+
function getStore2() {
|
|
1989
|
+
return _store3;
|
|
1990
|
+
}
|
|
1991
|
+
function getMirror() {
|
|
1992
|
+
return _mirror;
|
|
1993
|
+
}
|
|
1994
|
+
function getSelectionManager() {
|
|
1995
|
+
return _selectionManager;
|
|
1996
|
+
}
|
|
1997
|
+
function getHighlighter() {
|
|
1998
|
+
return _highlighter;
|
|
1999
|
+
}
|
|
2000
|
+
var _box = /* @__PURE__ */ new three.Box3();
|
|
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;
|
|
2089
|
+
}
|
|
2090
|
+
function removeGlobalAPI() {
|
|
2091
|
+
delete window.__R3F_DOM__;
|
|
2092
|
+
}
|
|
2093
|
+
function setElementRect(el, l, t, w, h) {
|
|
2094
|
+
const d = el.dataset;
|
|
2095
|
+
if (d._l !== String(l) || d._t !== String(t) || d._w !== String(w) || d._h !== String(h)) {
|
|
2096
|
+
el.style.left = `${l}px`;
|
|
2097
|
+
el.style.top = `${t}px`;
|
|
2098
|
+
el.style.width = `${w}px`;
|
|
2099
|
+
el.style.height = `${h}px`;
|
|
2100
|
+
d._l = String(l);
|
|
2101
|
+
d._t = String(t);
|
|
2102
|
+
d._w = String(w);
|
|
2103
|
+
d._h = String(h);
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
function ThreeDom({
|
|
2107
|
+
root = "#three-dom-root",
|
|
2108
|
+
batchSize = 500,
|
|
2109
|
+
timeBudgetMs = 0.5,
|
|
2110
|
+
maxDomNodes = 2e3,
|
|
2111
|
+
initialDepth = 3,
|
|
2112
|
+
enabled = true
|
|
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;
|
|
2135
|
+
}
|
|
2136
|
+
canvasParent.style.position = canvasParent.style.position || "relative";
|
|
2137
|
+
canvasParent.appendChild(rootElement);
|
|
2138
|
+
rootElement.style.cssText = [
|
|
2139
|
+
"position: absolute",
|
|
2140
|
+
"top: 0",
|
|
2141
|
+
"left: 0",
|
|
2142
|
+
"width: 100%",
|
|
2143
|
+
"height: 100%",
|
|
2144
|
+
"pointer-events: none",
|
|
2145
|
+
"overflow: hidden",
|
|
2146
|
+
"z-index: 10"
|
|
2147
|
+
].join(";");
|
|
2148
|
+
const store = new ObjectStore();
|
|
2149
|
+
const mirror = new DomMirror(store, maxDomNodes);
|
|
2150
|
+
mirror.setRoot(rootElement);
|
|
2151
|
+
ensureCustomElements(store);
|
|
2152
|
+
store.registerTree(scene);
|
|
2153
|
+
mirror.materializeSubtree(scene.uuid, initialDepth);
|
|
2154
|
+
const unpatch = patchObject3D(store, mirror);
|
|
2155
|
+
setInteractionState(store, camera, gl, size);
|
|
2156
|
+
const selectionManager = new SelectionManager();
|
|
2157
|
+
_selectionManager = selectionManager;
|
|
2158
|
+
_highlighter = null;
|
|
2159
|
+
exposeGlobalAPI(store);
|
|
2160
|
+
_store3 = store;
|
|
2161
|
+
_mirror = mirror;
|
|
2162
|
+
const initialCanvasRect = canvas.getBoundingClientRect();
|
|
2163
|
+
const allObjects = store.getFlatList();
|
|
2164
|
+
for (const obj of allObjects) {
|
|
2165
|
+
if (obj.userData?.__r3fdom_internal) continue;
|
|
2166
|
+
const el = mirror.getElement(obj.uuid);
|
|
2167
|
+
if (!el) continue;
|
|
2168
|
+
if (obj.type === "Scene") {
|
|
2169
|
+
setElementRect(el, 0, 0, Math.round(initialCanvasRect.width), Math.round(initialCanvasRect.height));
|
|
2170
|
+
continue;
|
|
2171
|
+
}
|
|
2172
|
+
const rect = projectToScreenRect(obj, camera, initialCanvasRect);
|
|
2173
|
+
if (rect) {
|
|
2174
|
+
let parentLeft = 0;
|
|
2175
|
+
let parentTop = 0;
|
|
2176
|
+
if (obj.parent && obj.parent.type !== "Scene") {
|
|
2177
|
+
const parentRect = projectToScreenRect(obj.parent, camera, initialCanvasRect);
|
|
2178
|
+
if (parentRect) {
|
|
2179
|
+
parentLeft = Math.round(parentRect.left);
|
|
2180
|
+
parentTop = Math.round(parentRect.top);
|
|
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
|
+
);
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
return () => {
|
|
2193
|
+
unpatch();
|
|
2194
|
+
removeGlobalAPI();
|
|
2195
|
+
clearInteractionState();
|
|
2196
|
+
selectionManager.dispose();
|
|
2197
|
+
mirror.dispose();
|
|
2198
|
+
store.dispose();
|
|
2199
|
+
if (createdRoot && rootElement?.parentNode) {
|
|
2200
|
+
rootElement.parentNode.removeChild(rootElement);
|
|
2201
|
+
}
|
|
2202
|
+
_store3 = null;
|
|
2203
|
+
_mirror = null;
|
|
2204
|
+
_selectionManager = null;
|
|
2205
|
+
_highlighter = null;
|
|
2206
|
+
};
|
|
2207
|
+
}, [scene, camera, gl, size, enabled, root, maxDomNodes, initialDepth]);
|
|
2208
|
+
fiber.useFrame(() => {
|
|
2209
|
+
if (!enabled || !_store3 || !_mirror) return;
|
|
2210
|
+
setInteractionState(_store3, camera, gl, size);
|
|
2211
|
+
const store = _store3;
|
|
2212
|
+
const mirror = _mirror;
|
|
2213
|
+
const canvas = gl.domElement;
|
|
2214
|
+
const canvasRect = canvas.getBoundingClientRect();
|
|
2215
|
+
const start = performance.now();
|
|
2216
|
+
const dirtyObjects = store.drainDirtyQueue();
|
|
2217
|
+
for (const obj of dirtyObjects) {
|
|
2218
|
+
store.update(obj);
|
|
2219
|
+
mirror.syncAttributes(obj);
|
|
2220
|
+
}
|
|
2221
|
+
const budgetRemaining = timeBudgetMs - (performance.now() - start);
|
|
2222
|
+
if (budgetRemaining > 0.1) {
|
|
2223
|
+
const objects2 = store.getFlatList();
|
|
2224
|
+
if (objects2.length > 0) {
|
|
2225
|
+
const end = Math.min(cursorRef.current + batchSize, objects2.length);
|
|
2226
|
+
for (let i = cursorRef.current; i < end; i++) {
|
|
2227
|
+
if (performance.now() - start > timeBudgetMs) break;
|
|
2228
|
+
const obj = objects2[i];
|
|
2229
|
+
const changed = store.update(obj);
|
|
2230
|
+
if (changed) mirror.syncAttributes(obj);
|
|
2231
|
+
}
|
|
2232
|
+
cursorRef.current = end >= objects2.length ? 0 : end;
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
const objects = store.getFlatList();
|
|
2236
|
+
if (objects.length > 0) {
|
|
2237
|
+
const posEnd = Math.min(positionCursorRef.current + 50, objects.length);
|
|
2238
|
+
for (let i = positionCursorRef.current; i < posEnd; i++) {
|
|
2239
|
+
const obj = objects[i];
|
|
2240
|
+
if (obj.userData?.__r3fdom_internal) continue;
|
|
2241
|
+
const el = mirror.getElement(obj.uuid);
|
|
2242
|
+
if (!el) continue;
|
|
2243
|
+
if (obj.type === "Scene") {
|
|
2244
|
+
setElementRect(el, 0, 0, Math.round(canvasRect.width), Math.round(canvasRect.height));
|
|
2245
|
+
continue;
|
|
2246
|
+
}
|
|
2247
|
+
const rect = projectToScreenRect(obj, camera, canvasRect);
|
|
2248
|
+
if (rect) {
|
|
2249
|
+
let parentLeft = 0;
|
|
2250
|
+
let parentTop = 0;
|
|
2251
|
+
if (obj.parent && obj.parent.type !== "Scene") {
|
|
2252
|
+
const parentRect = projectToScreenRect(obj.parent, camera, canvasRect);
|
|
2253
|
+
if (parentRect) {
|
|
2254
|
+
parentLeft = Math.round(parentRect.left);
|
|
2255
|
+
parentTop = Math.round(parentRect.top);
|
|
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;
|
|
2269
|
+
}
|
|
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
|
+
};
|
|
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";
|
|
2400
|
+
}
|
|
2401
|
+
var Highlighter = class {
|
|
2402
|
+
constructor(options = {}) {
|
|
2403
|
+
/** Selected object overlays (persistent until deselected) */
|
|
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;
|
|
2409
|
+
this._unsubscribe = null;
|
|
2410
|
+
/** DevTools hover polling interval */
|
|
2411
|
+
this._hoverPollId = null;
|
|
2412
|
+
this._lastHoveredElement = null;
|
|
2413
|
+
/** Store reference for resolving objects */
|
|
2414
|
+
this._store = null;
|
|
2415
|
+
this._showTooltip = options.showTooltip ?? true;
|
|
2416
|
+
}
|
|
2417
|
+
// -----------------------------------------------------------------------
|
|
2418
|
+
// Lifecycle
|
|
2419
|
+
// -----------------------------------------------------------------------
|
|
2420
|
+
attach(_scene, selectionManager, camera, renderer, store) {
|
|
2421
|
+
this.detach();
|
|
2422
|
+
this._camera = camera;
|
|
2423
|
+
this._renderer = renderer;
|
|
2424
|
+
this._store = store;
|
|
2425
|
+
this._unsubscribe = selectionManager.subscribe((selected) => {
|
|
2426
|
+
this._syncSelectedHighlights(selected);
|
|
2427
|
+
});
|
|
2428
|
+
this._syncSelectedHighlights([...selectionManager.getSelected()]);
|
|
2429
|
+
this._startHoverPolling();
|
|
2430
|
+
}
|
|
2431
|
+
detach() {
|
|
2432
|
+
if (this._unsubscribe) {
|
|
2433
|
+
this._unsubscribe();
|
|
2434
|
+
this._unsubscribe = null;
|
|
2435
|
+
}
|
|
2436
|
+
this._stopHoverPolling();
|
|
2437
|
+
this._clearAllOverlays(this._selectedEntries);
|
|
2438
|
+
this._clearAllOverlays(this._hoverEntries);
|
|
2439
|
+
this._camera = null;
|
|
2440
|
+
this._renderer = null;
|
|
2441
|
+
this._store = null;
|
|
2442
|
+
}
|
|
2443
|
+
// -----------------------------------------------------------------------
|
|
2444
|
+
// Per-frame update — reposition all overlays to follow camera/objects
|
|
2445
|
+
// -----------------------------------------------------------------------
|
|
2446
|
+
update() {
|
|
2447
|
+
if (!this._camera || !this._renderer) return;
|
|
2448
|
+
const canvas = this._renderer.domElement;
|
|
2449
|
+
for (const entry of this._selectedEntries.values()) {
|
|
2450
|
+
const rect = projectBoundsToScreen(entry.target, this._camera, canvas);
|
|
2451
|
+
if (rect) {
|
|
2452
|
+
positionOverlay(entry, rect);
|
|
2453
|
+
} else {
|
|
2454
|
+
hideOverlay(entry);
|
|
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);
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
// -----------------------------------------------------------------------
|
|
2467
|
+
// Public API
|
|
2468
|
+
// -----------------------------------------------------------------------
|
|
2469
|
+
highlight(obj) {
|
|
2470
|
+
this._addSelectedHighlight(obj, false);
|
|
2471
|
+
}
|
|
2472
|
+
unhighlight(obj) {
|
|
2473
|
+
this._removeOverlay(obj, this._selectedEntries);
|
|
2474
|
+
}
|
|
2475
|
+
clearAll() {
|
|
2476
|
+
this._clearAllOverlays(this._selectedEntries);
|
|
2477
|
+
this._clearAllOverlays(this._hoverEntries);
|
|
2478
|
+
}
|
|
2479
|
+
isHighlighted(obj) {
|
|
2480
|
+
return this._selectedEntries.has(obj);
|
|
2481
|
+
}
|
|
2482
|
+
/** Show a temporary hover highlight for an object and its children */
|
|
2483
|
+
showHoverHighlight(obj) {
|
|
2484
|
+
this._clearAllOverlays(this._hoverEntries);
|
|
2485
|
+
this._addHoverHighlightRecursive(obj);
|
|
2486
|
+
}
|
|
2487
|
+
/** Clear the hover highlight */
|
|
2488
|
+
clearHoverHighlight() {
|
|
2489
|
+
this._clearAllOverlays(this._hoverEntries);
|
|
2490
|
+
this._lastHoveredElement = null;
|
|
2491
|
+
}
|
|
2492
|
+
// -----------------------------------------------------------------------
|
|
2493
|
+
// Internal: selection highlights
|
|
2494
|
+
// -----------------------------------------------------------------------
|
|
2495
|
+
_syncSelectedHighlights(selected) {
|
|
2496
|
+
const targetSet = /* @__PURE__ */ new Set();
|
|
2497
|
+
const primarySet = new Set(selected);
|
|
2498
|
+
for (const obj of selected) {
|
|
2499
|
+
targetSet.add(obj);
|
|
2500
|
+
obj.traverse((child) => {
|
|
2501
|
+
targetSet.add(child);
|
|
2502
|
+
});
|
|
2503
|
+
}
|
|
2504
|
+
for (const [obj] of this._selectedEntries) {
|
|
2505
|
+
if (!targetSet.has(obj)) {
|
|
2506
|
+
this._removeOverlay(obj, this._selectedEntries);
|
|
2507
|
+
}
|
|
2508
|
+
}
|
|
2509
|
+
for (const obj of targetSet) {
|
|
2510
|
+
if (obj.userData?.__r3fdom_internal) continue;
|
|
2511
|
+
if (!this._selectedEntries.has(obj)) {
|
|
2512
|
+
const isChild = !primarySet.has(obj);
|
|
2513
|
+
this._addSelectedHighlight(obj, isChild);
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
_addSelectedHighlight(obj, isChild) {
|
|
2518
|
+
if (this._selectedEntries.has(obj)) return;
|
|
2519
|
+
const color = isChild ? COLORS.contentChild : COLORS.content;
|
|
2520
|
+
const overlayEl = createOverlayElement(color, !isChild);
|
|
2521
|
+
const tooltipEl = createTooltipElement();
|
|
2522
|
+
if (isChild || !this._showTooltip) {
|
|
2523
|
+
tooltipEl.style.display = "none";
|
|
2524
|
+
}
|
|
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
|
+
}
|
|
2533
|
+
// -----------------------------------------------------------------------
|
|
2534
|
+
// Internal: hover highlights
|
|
2535
|
+
// -----------------------------------------------------------------------
|
|
2536
|
+
_addHoverHighlightRecursive(obj) {
|
|
2537
|
+
if (obj.userData?.__r3fdom_internal) return;
|
|
2538
|
+
const overlayEl = createOverlayElement(COLORS.hover, false);
|
|
2539
|
+
const tooltipEl = createTooltipElement();
|
|
2540
|
+
if (this._hoverEntries.size === 0 && this._showTooltip) {
|
|
2541
|
+
const label = getObjectLabel2(obj);
|
|
2542
|
+
const dims = getObjectDimensions(obj);
|
|
2543
|
+
tooltipEl.innerHTML = `<span style="color:${COLORS.tooltipTag}">${label}</span>` + (dims ? ` <span style="color:${COLORS.tooltipDim}">${dims}</span>` : "");
|
|
2544
|
+
} else {
|
|
2545
|
+
tooltipEl.style.display = "none";
|
|
2546
|
+
}
|
|
2547
|
+
document.body.appendChild(overlayEl);
|
|
2548
|
+
document.body.appendChild(tooltipEl);
|
|
2549
|
+
this._hoverEntries.set(obj, { overlayEl, tooltipEl, target: obj, isChild: false });
|
|
2550
|
+
for (const child of obj.children) {
|
|
2551
|
+
if (!child.userData?.__r3fdom_internal) {
|
|
2552
|
+
this._addHoverHighlightRecursive(child);
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
// -----------------------------------------------------------------------
|
|
2557
|
+
// Internal: DevTools hover polling
|
|
2558
|
+
// -----------------------------------------------------------------------
|
|
2559
|
+
_startHoverPolling() {
|
|
2560
|
+
this._hoverPollId = setInterval(() => {
|
|
2561
|
+
this._pollDevToolsHover();
|
|
2562
|
+
}, 100);
|
|
2563
|
+
}
|
|
2564
|
+
_stopHoverPolling() {
|
|
2565
|
+
if (this._hoverPollId) {
|
|
2566
|
+
clearInterval(this._hoverPollId);
|
|
2567
|
+
this._hoverPollId = null;
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
_pollDevToolsHover() {
|
|
2571
|
+
if (!this._store) return;
|
|
2572
|
+
try {
|
|
2573
|
+
const hoveredEl = globalThis.__r3fdom_hovered__;
|
|
2574
|
+
if (hoveredEl === this._lastHoveredElement) return;
|
|
2575
|
+
this._lastHoveredElement = hoveredEl ?? null;
|
|
2576
|
+
if (!hoveredEl) {
|
|
2577
|
+
this._clearAllOverlays(this._hoverEntries);
|
|
2578
|
+
return;
|
|
2579
|
+
}
|
|
2580
|
+
const uuid = hoveredEl.getAttribute?.("data-uuid");
|
|
2581
|
+
if (!uuid) {
|
|
2582
|
+
this._clearAllOverlays(this._hoverEntries);
|
|
2583
|
+
return;
|
|
2584
|
+
}
|
|
2585
|
+
const obj = this._store.getObject3D(uuid);
|
|
2586
|
+
if (obj) {
|
|
2587
|
+
this.showHoverHighlight(obj);
|
|
2588
|
+
} else {
|
|
2589
|
+
this._clearAllOverlays(this._hoverEntries);
|
|
2590
|
+
}
|
|
2591
|
+
} catch {
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
2594
|
+
// -----------------------------------------------------------------------
|
|
2595
|
+
// Internal: overlay cleanup
|
|
2596
|
+
// -----------------------------------------------------------------------
|
|
2597
|
+
_removeOverlay(obj, map) {
|
|
2598
|
+
const entry = map.get(obj);
|
|
2599
|
+
if (!entry) return;
|
|
2600
|
+
entry.overlayEl.remove();
|
|
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();
|
|
2608
|
+
}
|
|
2609
|
+
map.clear();
|
|
2610
|
+
}
|
|
2611
|
+
// -----------------------------------------------------------------------
|
|
2612
|
+
// Cleanup
|
|
2613
|
+
// -----------------------------------------------------------------------
|
|
2614
|
+
dispose() {
|
|
2615
|
+
this.detach();
|
|
2616
|
+
}
|
|
2617
|
+
};
|
|
2618
|
+
|
|
2619
|
+
exports.DomMirror = DomMirror;
|
|
2620
|
+
exports.Highlighter = Highlighter;
|
|
2621
|
+
exports.MANAGED_ATTRIBUTES = MANAGED_ATTRIBUTES;
|
|
2622
|
+
exports.ObjectStore = ObjectStore;
|
|
2623
|
+
exports.SelectionManager = SelectionManager;
|
|
2624
|
+
exports.TAG_MAP = TAG_MAP;
|
|
2625
|
+
exports.ThreeDom = ThreeDom;
|
|
2626
|
+
exports.ThreeElement = ThreeElement;
|
|
2627
|
+
exports.applyAttributes = applyAttributes;
|
|
2628
|
+
exports.click3D = click3D;
|
|
2629
|
+
exports.computeAttributes = computeAttributes;
|
|
2630
|
+
exports.contextMenu3D = contextMenu3D;
|
|
2631
|
+
exports.createFlatSnapshot = createFlatSnapshot;
|
|
2632
|
+
exports.createSnapshot = createSnapshot;
|
|
2633
|
+
exports.dispatchClick = dispatchClick;
|
|
2634
|
+
exports.dispatchContextMenu = dispatchContextMenu;
|
|
2635
|
+
exports.dispatchDoubleClick = dispatchDoubleClick;
|
|
2636
|
+
exports.dispatchDrag = dispatchDrag;
|
|
2637
|
+
exports.dispatchHover = dispatchHover;
|
|
2638
|
+
exports.dispatchPointerMiss = dispatchPointerMiss;
|
|
2639
|
+
exports.dispatchUnhover = dispatchUnhover;
|
|
2640
|
+
exports.dispatchWheel = dispatchWheel;
|
|
2641
|
+
exports.doubleClick3D = doubleClick3D;
|
|
2642
|
+
exports.drag3D = drag3D;
|
|
2643
|
+
exports.ensureCustomElements = ensureCustomElements;
|
|
2644
|
+
exports.getHighlighter = getHighlighter;
|
|
2645
|
+
exports.getMirror = getMirror;
|
|
2646
|
+
exports.getSelectionManager = getSelectionManager;
|
|
2647
|
+
exports.getStore = getStore2;
|
|
2648
|
+
exports.getTagForType = getTagForType;
|
|
2649
|
+
exports.hover3D = hover3D;
|
|
2650
|
+
exports.isInFrustum = isInFrustum;
|
|
2651
|
+
exports.isPatched = isPatched;
|
|
2652
|
+
exports.patchObject3D = patchObject3D;
|
|
2653
|
+
exports.pointerMiss3D = pointerMiss3D;
|
|
2654
|
+
exports.previewDragWorldDelta = previewDragWorldDelta;
|
|
2655
|
+
exports.projectAllSamplePoints = projectAllSamplePoints;
|
|
2656
|
+
exports.projectToScreen = projectToScreen;
|
|
2657
|
+
exports.resolveObject = resolveObject;
|
|
2658
|
+
exports.restoreObject3D = restoreObject3D;
|
|
2659
|
+
exports.screenDeltaToWorld = screenDeltaToWorld;
|
|
2660
|
+
exports.unhover3D = unhover3D;
|
|
2661
|
+
exports.verifyRaycastHit = verifyRaycastHit;
|
|
2662
|
+
exports.verifyRaycastHitMultiPoint = verifyRaycastHitMultiPoint;
|
|
2663
|
+
exports.version = version;
|
|
2664
|
+
exports.wheel3D = wheel3D;
|
|
2665
|
+
//# sourceMappingURL=index.cjs.map
|
|
2666
|
+
//# sourceMappingURL=index.cjs.map
|