@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 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