brep-io-kernel 1.0.21 → 1.0.23

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.
Files changed (37) hide show
  1. package/README.md +4 -1
  2. package/dist-kernel/brep-kernel.js +17545 -16874
  3. package/package.json +3 -2
  4. package/src/BREP/Edge.js +2 -0
  5. package/src/BREP/Face.js +2 -0
  6. package/src/BREP/SolidMethods/fillet.js +17 -3
  7. package/src/BREP/SolidMethods/visualize.js +372 -365
  8. package/src/BREP/Vertex.js +2 -17
  9. package/src/BREP/fillets/fillet.js +193 -39
  10. package/src/PartHistory.js +4 -25
  11. package/src/SketchSolver2D.js +3 -0
  12. package/src/UI/AccordionWidget.js +1 -1
  13. package/src/UI/EnvMonacoEditor.js +0 -3
  14. package/src/UI/HistoryWidget.js +3 -0
  15. package/src/UI/SceneListing.js +45 -7
  16. package/src/UI/SelectionFilter.js +469 -442
  17. package/src/UI/SelectionState.js +464 -0
  18. package/src/UI/assembly/AssemblyConstraintCollectionWidget.js +40 -1
  19. package/src/UI/assembly/AssemblyConstraintsWidget.js +17 -3
  20. package/src/UI/assembly/constraintSelectionUtils.js +3 -182
  21. package/src/UI/{assembly/constraintFaceUtils.js → faceUtils.js} +30 -5
  22. package/src/UI/featureDialogs.js +99 -69
  23. package/src/UI/pmi/LabelOverlay.js +32 -0
  24. package/src/UI/pmi/PMIMode.js +23 -0
  25. package/src/UI/pmi/dimensions/HoleCalloutAnnotation.js +7 -1
  26. package/src/UI/toolbarButtons/orientToFaceButton.js +3 -36
  27. package/src/UI/toolbarButtons/registerDefaultButtons.js +2 -0
  28. package/src/UI/toolbarButtons/selectionStateButton.js +206 -0
  29. package/src/UI/viewer.js +16 -16
  30. package/src/assemblyConstraints/AssemblyConstraintHistory.js +18 -42
  31. package/src/assemblyConstraints/constraints/AngleConstraint.js +1 -0
  32. package/src/assemblyConstraints/constraints/DistanceConstraint.js +1 -0
  33. package/src/features/fillet/FilletFeature.js +7 -0
  34. package/src/features/selectionUtils.js +21 -5
  35. package/src/features/sketch/SketchFeature.js +2 -2
  36. package/src/features/sketch/sketchSolver2D/constraintDefinitions.js +3 -2
  37. package/src/utils/selectionResolver.js +258 -0
@@ -1,5 +1,6 @@
1
1
  import * as THREE from "three";
2
2
  import { CADmaterials } from "../UI/CADmaterials.js";
3
+ import { SelectionState } from "../UI/SelectionState.js";
3
4
 
4
5
  // Vertex: container at a specific position with a point marker.
5
6
  // When selected, swaps to the selected PointsMaterial; no extra sphere.
@@ -22,22 +23,6 @@ export class Vertex extends THREE.Object3D {
22
23
  this._point = new THREE.Points(ptGeom, ptMat);
23
24
  this.add(this._point);
24
25
 
25
- // Selection flag accessor toggles point material
26
- this._selected = false;
27
- Object.defineProperty(this, 'selected', {
28
- get: () => this._selected,
29
- set: (v) => {
30
- const nv = !!v;
31
- this._selected = nv;
32
- try {
33
- if (this._point && this._point.material && CADmaterials?.VERTEX) {
34
- this._point.material = nv ? (CADmaterials.VERTEX.SELECTED || this._point.material)
35
- : (CADmaterials.VERTEX.BASE || this._point.material);
36
- }
37
- } catch { }
38
- },
39
- configurable: true,
40
- enumerable: true,
41
- });
26
+ SelectionState.attach(this);
42
27
  }
43
28
  }
@@ -22,6 +22,83 @@ import { computeFaceAreaFromTriangles } from "./filletGeometry.js";
22
22
  export { clearFilletCaches, trimFilletCaches } from './inset.js';
23
23
  export { fixTJunctionsAndPatchHoles } from './outset.js';
24
24
 
25
+ function buildPointInsideTester(solid) {
26
+ if (!solid) return null;
27
+ const tv = solid._triVerts;
28
+ const vp = solid._vertProperties;
29
+ if (!tv || !vp || typeof tv.length !== 'number' || typeof vp.length !== 'number') return null;
30
+ const triCount = (tv.length / 3) | 0;
31
+ if (triCount === 0 || vp.length < 9) return null;
32
+
33
+ let minX = Infinity, minY = Infinity, minZ = Infinity;
34
+ let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
35
+ for (let i = 0; i < vp.length; i += 3) {
36
+ const x = vp[i], y = vp[i + 1], z = vp[i + 2];
37
+ if (x < minX) minX = x; if (x > maxX) maxX = x;
38
+ if (y < minY) minY = y; if (y > maxY) maxY = y;
39
+ if (z < minZ) minZ = z; if (z > maxZ) maxZ = z;
40
+ }
41
+ const diag = Math.hypot(maxX - minX, maxY - minY, maxZ - minZ) || 1;
42
+ const jitter = 1e-6 * diag;
43
+
44
+ const rayTri = (ox, oy, oz, dx, dy, dz, ax, ay, az, bx, by, bz, cx, cy, cz) => {
45
+ const EPS = 1e-12;
46
+ const e1x = bx - ax, e1y = by - ay, e1z = bz - az;
47
+ const e2x = cx - ax, e2y = cy - ay, e2z = cz - az;
48
+ const px = dy * e2z - dz * e2y;
49
+ const py = dz * e2x - dx * e2z;
50
+ const pz = dx * e2y - dy * e2x;
51
+ const det = e1x * px + e1y * py + e1z * pz;
52
+ if (Math.abs(det) < EPS) return null;
53
+ const invDet = 1.0 / det;
54
+ const tvecx = ox - ax, tvecy = oy - ay, tvecz = oz - az;
55
+ const u = (tvecx * px + tvecy * py + tvecz * pz) * invDet;
56
+ if (u < -1e-12 || u > 1 + 1e-12) return null;
57
+ const qx = tvecy * e1z - tvecz * e1y;
58
+ const qy = tvecz * e1x - tvecx * e1z;
59
+ const qz = tvecx * e1y - tvecy * e1x;
60
+ const v = (dx * qx + dy * qy + dz * qz) * invDet;
61
+ if (v < -1e-12 || u + v > 1 + 1e-12) return null;
62
+ const tHit = (e2x * qx + e2y * qy + e2z * qz) * invDet;
63
+ return tHit > 1e-10 ? tHit : null;
64
+ };
65
+
66
+ const dirs = [
67
+ [1, 0, 0],
68
+ [0, 1, 0],
69
+ [0, 0, 1],
70
+ ];
71
+
72
+ return (pt) => {
73
+ if (!pt || !Number.isFinite(pt.x) || !Number.isFinite(pt.y) || !Number.isFinite(pt.z)) return false;
74
+ const px = pt.x, py = pt.y, pz = pt.z;
75
+ let votes = 0;
76
+ for (let k = 0; k < dirs.length; k++) {
77
+ const dir = dirs[k];
78
+ const ox = px + (k + 1) * jitter;
79
+ const oy = py + (k + 2) * jitter;
80
+ const oz = pz + (k + 3) * jitter;
81
+ let hits = 0;
82
+ for (let t = 0; t < triCount; t++) {
83
+ const b = t * 3;
84
+ const ia = (tv[b + 0] >>> 0) * 3;
85
+ const ib = (tv[b + 1] >>> 0) * 3;
86
+ const ic = (tv[b + 2] >>> 0) * 3;
87
+ const hit = rayTri(
88
+ ox, oy, oz,
89
+ dir[0], dir[1], dir[2],
90
+ vp[ia + 0], vp[ia + 1], vp[ia + 2],
91
+ vp[ib + 0], vp[ib + 1], vp[ib + 2],
92
+ vp[ic + 0], vp[ic + 1], vp[ic + 2]
93
+ );
94
+ if (hit !== null) hits++;
95
+ }
96
+ if ((hits % 2) === 1) votes++;
97
+ }
98
+ return votes >= 2;
99
+ };
100
+ }
101
+
25
102
  /**
26
103
  * Compute the fillet centerline polyline for an input edge without building the fillet solid.
27
104
  *
@@ -823,52 +900,129 @@ export function filletSolid({ edgeToFillet, radius = 1, sideMode = 'INSET', debu
823
900
  // large displacements on big models.
824
901
  const outsetInsetMagnitude = Math.max(1e-4, Math.min(0.05, Math.abs(radius) * 0.05));
825
902
  const wedgeInsetMagnitude = closedLoop ? 0 : ((side === 'INSET') ? Math.abs(inflate) : outsetInsetMagnitude);
826
- for (let i = 0; i < edgeWedgeCopy.length; i++) {
827
- const edgeWedgePt = edgeWedgeCopy[i];
828
- const centerPt = centerlineCopy[i] || centerlineCopy[centerlineCopy.length - 1]; // Fallback to last point
829
-
830
- if (edgeWedgePt && centerPt) {
831
- try {
832
- const origWedgeEdge = { ...edgeWedgePt };
833
-
834
- // Calculate direction from edge point toward the centerline (inward direction)
835
- const inwardDir = {
836
- x: centerPt.x - edgeWedgePt.x,
837
- y: centerPt.y - edgeWedgePt.y,
838
- z: centerPt.z - edgeWedgePt.z
839
- };
840
- const inwardLength = Math.sqrt(inwardDir.x * inwardDir.x + inwardDir.y * inwardDir.y + inwardDir.z * inwardDir.z);
841
-
842
- if (inwardLength > 1e-12) {
843
- // Normalize and apply inset
844
- const normalizedInward = {
845
- x: inwardDir.x / inwardLength,
846
- y: inwardDir.y / inwardLength,
847
- z: inwardDir.z / inwardLength
903
+ const useInsideCheck = wedgeInsetMagnitude && side === 'OUTSET';
904
+ const pointInsideTarget = useInsideCheck
905
+ ? buildPointInsideTester(edgeToFillet?.parentSolid || edgeToFillet?.parent || null)
906
+ : null;
907
+ let preferredDirSign = null;
908
+ let insideResults = null;
909
+ if (pointInsideTarget) {
910
+ insideResults = new Array(edgeWedgeCopy.length);
911
+ let countIn = 0;
912
+ let countOut = 0;
913
+ for (let i = 0; i < edgeWedgeCopy.length; i++) {
914
+ const edgeWedgePt = edgeWedgeCopy[i];
915
+ const centerPt = centerlineCopy[i] || centerlineCopy[centerlineCopy.length - 1];
916
+ if (!edgeWedgePt || !centerPt) continue;
917
+ const inwardDir = {
918
+ x: centerPt.x - edgeWedgePt.x,
919
+ y: centerPt.y - edgeWedgePt.y,
920
+ z: centerPt.z - edgeWedgePt.z
921
+ };
922
+ const inwardLength = Math.sqrt(inwardDir.x * inwardDir.x + inwardDir.y * inwardDir.y + inwardDir.z * inwardDir.z);
923
+ if (inwardLength <= 1e-12) continue;
924
+ const nx = inwardDir.x / inwardLength;
925
+ const ny = inwardDir.y / inwardLength;
926
+ const nz = inwardDir.z / inwardLength;
927
+ const candidateIn = {
928
+ x: edgeWedgePt.x + nx * wedgeInsetMagnitude,
929
+ y: edgeWedgePt.y + ny * wedgeInsetMagnitude,
930
+ z: edgeWedgePt.z + nz * wedgeInsetMagnitude
931
+ };
932
+ const candidateOut = {
933
+ x: edgeWedgePt.x - nx * wedgeInsetMagnitude,
934
+ y: edgeWedgePt.y - ny * wedgeInsetMagnitude,
935
+ z: edgeWedgePt.z - nz * wedgeInsetMagnitude
936
+ };
937
+ const inInside = pointInsideTarget(candidateIn);
938
+ const outInside = pointInsideTarget(candidateOut);
939
+ insideResults[i] = { inInside, outInside };
940
+ if (inInside !== outInside) {
941
+ if (inInside) countIn++; else countOut++;
942
+ }
943
+ }
944
+ if (countIn || countOut) {
945
+ preferredDirSign = countIn >= countOut ? 1 : -1;
946
+ }
947
+ }
948
+ if (wedgeInsetMagnitude) {
949
+ for (let i = 0; i < edgeWedgeCopy.length; i++) {
950
+ const edgeWedgePt = edgeWedgeCopy[i];
951
+ const centerPt = centerlineCopy[i] || centerlineCopy[centerlineCopy.length - 1]; // Fallback to last point
952
+
953
+ if (edgeWedgePt && centerPt) {
954
+ try {
955
+ const origWedgeEdge = { ...edgeWedgePt };
956
+
957
+ // Calculate direction from edge point toward the centerline (inward direction)
958
+ const inwardDir = {
959
+ x: centerPt.x - edgeWedgePt.x,
960
+ y: centerPt.y - edgeWedgePt.y,
961
+ z: centerPt.z - edgeWedgePt.z
848
962
  };
849
- // Determine direction: OUTSET -> inward, INSET -> outward (opposite)
850
- const dirSign = (side === 'INSET') ? -1 : 1;
851
- const step = dirSign * wedgeInsetMagnitude;
852
- // Apply
853
- edgeWedgePt.x += normalizedInward.x * step;
854
- edgeWedgePt.y += normalizedInward.y * step;
855
- edgeWedgePt.z += normalizedInward.z * step;
856
-
857
- // Validate the result
858
- if (!isFiniteVec3(edgeWedgePt)) {
859
- console.warn(`Invalid wedge edge point after inset at index ${i}, reverting to original`);
860
- Object.assign(edgeWedgePt, origWedgeEdge);
963
+ const inwardLength = Math.sqrt(inwardDir.x * inwardDir.x + inwardDir.y * inwardDir.y + inwardDir.z * inwardDir.z);
964
+
965
+ if (inwardLength > 1e-12) {
966
+ // Normalize and apply inset
967
+ const normalizedInward = {
968
+ x: inwardDir.x / inwardLength,
969
+ y: inwardDir.y / inwardLength,
970
+ z: inwardDir.z / inwardLength
971
+ };
972
+ const candidateIn = {
973
+ x: origWedgeEdge.x + normalizedInward.x * wedgeInsetMagnitude,
974
+ y: origWedgeEdge.y + normalizedInward.y * wedgeInsetMagnitude,
975
+ z: origWedgeEdge.z + normalizedInward.z * wedgeInsetMagnitude
976
+ };
977
+ const candidateOut = {
978
+ x: origWedgeEdge.x - normalizedInward.x * wedgeInsetMagnitude,
979
+ y: origWedgeEdge.y - normalizedInward.y * wedgeInsetMagnitude,
980
+ z: origWedgeEdge.z - normalizedInward.z * wedgeInsetMagnitude
981
+ };
982
+ let chosen = null;
983
+
984
+ if (pointInsideTarget) {
985
+ const insideRes = insideResults ? insideResults[i] : null;
986
+ const inInside = insideRes ? insideRes.inInside : pointInsideTarget(candidateIn);
987
+ const outInside = insideRes ? insideRes.outInside : pointInsideTarget(candidateOut);
988
+ if (inInside !== outInside) {
989
+ chosen = inInside ? candidateIn : candidateOut;
990
+ }
991
+ }
992
+
993
+ if (!chosen) {
994
+ // Fallback to previous sign-based behavior.
995
+ const dirSign = (preferredDirSign !== null)
996
+ ? preferredDirSign
997
+ : ((side === 'INSET') ? -1 : 1);
998
+ const step = dirSign * wedgeInsetMagnitude;
999
+ chosen = {
1000
+ x: origWedgeEdge.x + normalizedInward.x * step,
1001
+ y: origWedgeEdge.y + normalizedInward.y * step,
1002
+ z: origWedgeEdge.z + normalizedInward.z * step
1003
+ };
1004
+ }
1005
+
1006
+ edgeWedgePt.x = chosen.x;
1007
+ edgeWedgePt.y = chosen.y;
1008
+ edgeWedgePt.z = chosen.z;
1009
+
1010
+ // Validate the result
1011
+ if (!isFiniteVec3(edgeWedgePt)) {
1012
+ console.warn(`Invalid wedge edge point after inset at index ${i}, reverting to original`);
1013
+ Object.assign(edgeWedgePt, origWedgeEdge);
1014
+ }
1015
+ } else {
1016
+ console.warn(`Edge point ${i} is too close to centerline, skipping wedge inset`);
861
1017
  }
862
- } else {
863
- console.warn(`Edge point ${i} is too close to centerline, skipping wedge inset`);
1018
+ } catch (insetError) {
1019
+ console.warn(`Wedge edge inset failed at index ${i}: ${insetError?.message || insetError}`);
864
1020
  }
865
- } catch (insetError) {
866
- console.warn(`Wedge edge inset failed at index ${i}: ${insetError?.message || insetError}`);
867
1021
  }
868
1022
  }
869
1023
  }
870
1024
 
871
- if (wedgeInsetMagnitude) logDebug(`Applied wedge inset of ${wedgeInsetMagnitude} units (${side === 'INSET' ? 'outward' : 'inward'}) to ${edgeWedgeCopy.length} edge points`);
1025
+ if (wedgeInsetMagnitude) logDebug(`Applied wedge inset of ${wedgeInsetMagnitude} units (inside-aware) to ${edgeWedgeCopy.length} edge points`);
872
1026
 
873
1027
 
874
1028
  // Do not reorder edge points. Centerline/tangent/edge points are produced in
@@ -572,11 +572,12 @@ export class PartHistory {
572
572
  const objType = String(obj.type || '').toUpperCase();
573
573
  const sourceUuid = obj.uuid || null;
574
574
  const sourceFeatureId = obj.owningFeatureID ?? null;
575
+ const sourceTimestamp = (obj.timestamp ?? obj.userData?.timestamp ?? null);
575
576
  if (objType === SelectionFilter.EDGE || objType === 'EDGE') {
576
577
  const positions = extractEdgeWorldPositions(obj);
577
578
  if (positions && positions.length >= 6) {
578
579
  for (const bucket of buckets) {
579
- bucket[refName] = { type: 'EDGE', positions, sourceUuid, sourceFeatureId };
580
+ bucket[refName] = { type: 'EDGE', positions, sourceUuid, sourceFeatureId, sourceTimestamp };
580
581
  }
581
582
  }
582
583
  } else if (objType === SelectionFilter.FACE || objType === 'FACE' || objType === SelectionFilter.PLANE || objType === 'PLANE') {
@@ -584,7 +585,7 @@ export class PartHistory {
584
585
  if (edgePositions && edgePositions.length) {
585
586
  const snapType = (objType === SelectionFilter.PLANE || objType === 'PLANE') ? 'PLANE' : 'FACE';
586
587
  for (const bucket of buckets) {
587
- bucket[refName] = { type: snapType, edgePositions, sourceUuid, sourceFeatureId };
588
+ bucket[refName] = { type: snapType, edgePositions, sourceUuid, sourceFeatureId, sourceTimestamp };
588
589
  }
589
590
  }
590
591
  } else if (objType === SelectionFilter.VERTEX || objType === 'VERTEX') {
@@ -594,7 +595,7 @@ export class PartHistory {
594
595
  else pos.set(obj.position?.x || 0, obj.position?.y || 0, obj.position?.z || 0);
595
596
  } catch { }
596
597
  for (const bucket of buckets) {
597
- bucket[refName] = { type: 'VERTEX', position: [pos.x, pos.y, pos.z], sourceUuid, sourceFeatureId };
598
+ bucket[refName] = { type: 'VERTEX', position: [pos.x, pos.y, pos.z], sourceUuid, sourceFeatureId, sourceTimestamp };
598
599
  }
599
600
  }
600
601
  }
@@ -662,7 +663,6 @@ export class PartHistory {
662
663
  applyTimeStampToChildrenRecursively(a, feature.timestamp);
663
664
  } catch { }
664
665
 
665
- this._attachSelectionHandlers(a);
666
666
  }
667
667
  }
668
668
 
@@ -677,27 +677,6 @@ export class PartHistory {
677
677
 
678
678
 
679
679
 
680
- _attachSelectionHandlers(obj) {
681
- if (!obj || typeof obj !== 'object') return;
682
- obj.onClick = () => {
683
- try {
684
- if (obj.type === SelectionFilter.SOLID && obj.parent && obj.parent.type === SelectionFilter.COMPONENT) {
685
- const handledByParent = SelectionFilter.toggleSelection(obj.parent);
686
- if (!handledByParent) SelectionFilter.toggleSelection(obj);
687
- return;
688
- }
689
- SelectionFilter.toggleSelection(obj);
690
- } catch (error) {
691
- try { console.warn('[PartHistory] toggleSelection failed:', error); }
692
- catch (_) { /* no-op */ }
693
- }
694
- };
695
- const children = Array.isArray(obj.children) ? obj.children : [];
696
- for (const child of children) {
697
- this._attachSelectionHandlers(child);
698
- }
699
- }
700
-
701
680
  _safeRemove(obj) {
702
681
  if (!obj) return;
703
682
  try {
@@ -0,0 +1,3 @@
1
+ // Standalone 2D sketch solver entry point
2
+ export { ConstraintSolver, ConstraintEngine } from './features/sketch/sketchSolver2D/ConstraintEngine.js';
3
+ export { constraints } from './features/sketch/sketchSolver2D/constraintDefinitions.js';
@@ -2,7 +2,7 @@
2
2
  // ES6, framework-free, dark mode, no animations.
3
3
  // All public methods are async and resolve AFTER the DOM has painted.
4
4
 
5
- export class AccordionSection {
5
+ class AccordionSection {
6
6
  /**
7
7
  * Represents a single accordion section.
8
8
  * Properties:
@@ -517,6 +517,3 @@ EnvMonacoEditor._runtimeCompletionRegistered = false;
517
517
  if (!customElements.get('env-monaco-editor')) {
518
518
  customElements.define('env-monaco-editor', EnvMonacoEditor);
519
519
  }
520
-
521
- export { EnvMonacoEditor };
522
- export default EnvMonacoEditor;
@@ -386,6 +386,9 @@ export class HistoryWidget extends HistoryCollectionWidget {
386
386
  if (this._extractEntryId(entry, i) !== String(target)) continue;
387
387
  if (!this.#shouldExpandEntry(entry)) return;
388
388
  this._expandedId = String(target);
389
+ if (this._autoFocusOnExpand) {
390
+ this._pendingFocusEntryId = String(target);
391
+ }
389
392
  this.render();
390
393
  return;
391
394
  }
@@ -38,11 +38,14 @@ export class SceneListing {
38
38
  this._expandedByName = new Map(); // name -> boolean
39
39
  // Remember expand/collapse state for non-solid nodes when showing all objects
40
40
  this._expandedByUuid = new Map(); // uuid -> boolean
41
+ this._hoveredUuids = new Set();
41
42
  this._running = false;
42
43
  this._raf = 0;
43
44
 
44
45
  this.#attachTypeVisibilityButtons();
45
46
  this.#attachDisplayModeToggle();
47
+ this._hoverListener = (ev) => this.#syncHoverFromScene(ev);
48
+ window.addEventListener('hover-changed', this._hoverListener);
46
49
 
47
50
  // Wire toolbar
48
51
  // this.toolbar.querySelector(".st-expand").addEventListener("click", () => this.#setAllOpen(true));
@@ -99,6 +102,10 @@ export class SceneListing {
99
102
  dispose() {
100
103
  this.stop();
101
104
  this.clear();
105
+ if (this._hoverListener) {
106
+ try { window.removeEventListener('hover-changed', this._hoverListener); } catch { }
107
+ this._hoverListener = null;
108
+ }
102
109
  this.uiElement.remove();
103
110
  }
104
111
 
@@ -228,6 +235,13 @@ export class SceneListing {
228
235
  e.stopPropagation();
229
236
  });
230
237
 
238
+ const hoverOn = () => {
239
+ try { SelectionFilter.setHoverObject(obj, { ignoreFilter: true }); } catch (_) { }
240
+ };
241
+ const hoverOff = () => {
242
+ try { SelectionFilter.clearHover(); } catch (_) { }
243
+ };
244
+
231
245
  // Selection: name click -> recursive toggle selection (unchanged)
232
246
  nameBtn.addEventListener("click", (e) => {
233
247
  // If any descendant (including self) is not selected, select all; otherwise deselect all.
@@ -243,13 +257,9 @@ export class SceneListing {
243
257
  e.stopPropagation();
244
258
  });
245
259
 
246
- // Hover highlight respecting selection filter
247
- nameBtn.addEventListener('mouseenter', () => {
248
- try { SelectionFilter.setHoverByName(this.scene, obj.name); } catch (_) { }
249
- });
250
- nameBtn.addEventListener('mouseleave', () => {
251
- try { SelectionFilter.clearHover(); } catch (_) { }
252
- });
260
+ // Hover highlight for the full row (ignore selection filter)
261
+ row.addEventListener('pointerenter', hoverOn);
262
+ row.addEventListener('pointerleave', hoverOff);
253
263
 
254
264
  // Row assembly
255
265
  row.appendChild(toggle);
@@ -300,6 +310,8 @@ export class SceneListing {
300
310
  info.lastSelected = sel;
301
311
  info.li.classList.toggle("is-selected", sel);
302
312
  }
313
+ const hovered = this._hoveredUuids.has(obj.uuid);
314
+ info.li.classList.toggle("is-hovered", hovered);
303
315
 
304
316
  // Keep label fresh (names may change externally)
305
317
  const wantLabel = this.#labelFor(obj);
@@ -310,6 +322,28 @@ export class SceneListing {
310
322
  }
311
323
  }
312
324
 
325
+ #syncHoverFromScene(ev) {
326
+ const detail = ev?.detail || {};
327
+ let uuids = [];
328
+ if (Array.isArray(detail.uuids) && detail.uuids.length) {
329
+ uuids = detail.uuids;
330
+ } else if (Array.isArray(detail.objects)) {
331
+ uuids = detail.objects.map((obj) => obj?.uuid).filter(Boolean);
332
+ }
333
+ const next = new Set(uuids);
334
+ for (const uuid of this._hoveredUuids) {
335
+ if (next.has(uuid)) continue;
336
+ const info = this.nodes.get(uuid);
337
+ if (info) info.li.classList.remove('is-hovered');
338
+ }
339
+ for (const uuid of next) {
340
+ if (this._hoveredUuids.has(uuid)) continue;
341
+ const info = this.nodes.get(uuid);
342
+ if (info) info.li.classList.add('is-hovered');
343
+ }
344
+ this._hoveredUuids = next;
345
+ }
346
+
313
347
  // ---- Actions --------------------------------------------------------------
314
348
 
315
349
  #setSelectedRecursive(obj, sel) {
@@ -568,6 +602,10 @@ export class SceneListing {
568
602
  background: var(--sel);
569
603
  box-shadow: inset 0 0 0 1px var(--accent);
570
604
  }
605
+ .scene-tree__item.is-hovered > .scene-tree__row{
606
+ background: #142033;
607
+ box-shadow: inset 0 0 0 1px rgba(74,163,255,.45);
608
+ }
571
609
  .scene-tree__item.is-parent.open > .scene-tree__row{ border-bottom:1px solid #111a26; }
572
610
  .st-caret{
573
611
  width:20px; height:20px; border:0; background:transparent; color:var(--muted);