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