brep-io-kernel 1.0.20 → 1.0.22

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 (43) hide show
  1. package/README.md +4 -1
  2. package/dist-kernel/brep-kernel.js +10858 -9938
  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/visualize.js +372 -365
  7. package/src/BREP/Vertex.js +2 -17
  8. package/src/PartHistory.js +4 -25
  9. package/src/SketchSolver2D.js +3 -0
  10. package/src/UI/AccordionWidget.js +1 -1
  11. package/src/UI/EnvMonacoEditor.js +0 -3
  12. package/src/UI/HistoryWidget.js +12 -4
  13. package/src/UI/SceneListing.js +45 -7
  14. package/src/UI/SelectionFilter.js +903 -438
  15. package/src/UI/SelectionState.js +464 -0
  16. package/src/UI/assembly/AssemblyConstraintCollectionWidget.js +41 -1
  17. package/src/UI/assembly/AssemblyConstraintsWidget.js +21 -3
  18. package/src/UI/assembly/constraintSelectionUtils.js +3 -182
  19. package/src/UI/{assembly/constraintFaceUtils.js → faceUtils.js} +30 -5
  20. package/src/UI/featureDialogs.js +154 -69
  21. package/src/UI/history/HistoryCollectionWidget.js +65 -0
  22. package/src/UI/pmi/AnnotationCollectionWidget.js +1 -0
  23. package/src/UI/pmi/BaseAnnotation.js +37 -0
  24. package/src/UI/pmi/LabelOverlay.js +32 -0
  25. package/src/UI/pmi/PMIMode.js +27 -0
  26. package/src/UI/pmi/dimensions/AngleDimensionAnnotation.js +5 -0
  27. package/src/UI/pmi/dimensions/ExplodeBodyAnnotation.js +5 -0
  28. package/src/UI/pmi/dimensions/HoleCalloutAnnotation.js +57 -0
  29. package/src/UI/pmi/dimensions/LeaderAnnotation.js +5 -0
  30. package/src/UI/pmi/dimensions/LinearDimensionAnnotation.js +22 -16
  31. package/src/UI/pmi/dimensions/NoteAnnotation.js +9 -0
  32. package/src/UI/pmi/dimensions/RadialDimensionAnnotation.js +81 -16
  33. package/src/UI/toolbarButtons/orientToFaceButton.js +3 -36
  34. package/src/UI/toolbarButtons/registerDefaultButtons.js +2 -0
  35. package/src/UI/toolbarButtons/selectionStateButton.js +206 -0
  36. package/src/UI/viewer.js +34 -13
  37. package/src/assemblyConstraints/AssemblyConstraintHistory.js +18 -42
  38. package/src/assemblyConstraints/constraints/AngleConstraint.js +1 -0
  39. package/src/assemblyConstraints/constraints/DistanceConstraint.js +1 -0
  40. package/src/features/selectionUtils.js +21 -5
  41. package/src/features/sketch/SketchFeature.js +2 -2
  42. package/src/features/sketch/sketchSolver2D/constraintDefinitions.js +3 -2
  43. package/src/utils/selectionResolver.js +258 -0
@@ -0,0 +1,206 @@
1
+ import { FloatingWindow } from '../FloatingWindow.js';
2
+ import { SelectionFilter } from '../SelectionFilter.js';
3
+
4
+ const PANEL_KEY = '__selectionStatePanel';
5
+
6
+ function _ensureStyles() {
7
+ if (document.getElementById('selection-state-styles')) return;
8
+ const style = document.createElement('style');
9
+ style.id = 'selection-state-styles';
10
+ style.textContent = `
11
+ .selection-state-content {
12
+ display: flex;
13
+ flex-direction: column;
14
+ gap: 8px;
15
+ padding: 10px;
16
+ height: 100%;
17
+ box-sizing: border-box;
18
+ color: #e5e7eb;
19
+ font-size: 12px;
20
+ }
21
+ .selection-state-header {
22
+ display: flex;
23
+ align-items: center;
24
+ justify-content: space-between;
25
+ gap: 8px;
26
+ font-weight: 700;
27
+ }
28
+ .selection-state-count {
29
+ color: #cbd5f5;
30
+ font-size: 12px;
31
+ }
32
+ .selection-state-list {
33
+ flex: 1 1 auto;
34
+ min-height: 0;
35
+ overflow: auto;
36
+ background: rgba(8,10,14,.55);
37
+ border: 1px solid #1f2937;
38
+ border-radius: 8px;
39
+ padding: 8px;
40
+ display: flex;
41
+ flex-direction: column;
42
+ gap: 6px;
43
+ }
44
+ .selection-state-item {
45
+ display: grid;
46
+ grid-template-columns: auto 1fr;
47
+ gap: 6px 10px;
48
+ align-items: center;
49
+ padding: 4px 6px;
50
+ border-radius: 6px;
51
+ background: rgba(255,255,255,.02);
52
+ border: 1px solid rgba(148,163,184,.18);
53
+ }
54
+ .selection-state-type {
55
+ font-size: 10px;
56
+ font-weight: 700;
57
+ letter-spacing: 0.04em;
58
+ text-transform: uppercase;
59
+ color: #9aa0aa;
60
+ }
61
+ .selection-state-name {
62
+ font-size: 12px;
63
+ color: #e5e7eb;
64
+ word-break: break-word;
65
+ }
66
+ .selection-state-empty {
67
+ color: #9aa0aa;
68
+ font-style: italic;
69
+ padding: 6px;
70
+ }
71
+ `;
72
+ document.head.appendChild(style);
73
+ }
74
+
75
+ function _labelFor(obj) {
76
+ if (!obj) return 'Unknown';
77
+ const name = obj.name
78
+ || obj.userData?.faceName
79
+ || obj.userData?.edgeName
80
+ || obj.userData?.vertexName
81
+ || obj.userData?.solidName
82
+ || obj.userData?.name
83
+ || null;
84
+ return name || '(unnamed)';
85
+ }
86
+
87
+ class SelectionStatePanel {
88
+ constructor(viewer) {
89
+ this.viewer = viewer || null;
90
+ this.window = null;
91
+ this.root = null;
92
+ this.content = null;
93
+ this.countEl = null;
94
+ this.listEl = null;
95
+ this._boundSelectionChanged = this._handleSelectionChanged.bind(this);
96
+ try { window.addEventListener('selection-changed', this._boundSelectionChanged); } catch { }
97
+ }
98
+
99
+ toggle() {
100
+ if (this.root && this.root.style.display !== 'none') this.close();
101
+ else this.open();
102
+ }
103
+
104
+ open() {
105
+ this._ensureWindow();
106
+ if (!this.root) return;
107
+ this.root.style.display = 'flex';
108
+ this._render();
109
+ }
110
+
111
+ close() {
112
+ if (this.root) {
113
+ try { this.root.style.display = 'none'; } catch { }
114
+ }
115
+ }
116
+
117
+ _ensureWindow() {
118
+ if (this.window && this.root) return;
119
+ _ensureStyles();
120
+ const fw = new FloatingWindow({
121
+ title: 'Selection State',
122
+ width: 360,
123
+ height: 320,
124
+ right: 14,
125
+ top: 120,
126
+ shaded: false,
127
+ onClose: () => this.close(),
128
+ });
129
+
130
+ const content = document.createElement('div');
131
+ content.className = 'selection-state-content';
132
+
133
+ const header = document.createElement('div');
134
+ header.className = 'selection-state-header';
135
+ const title = document.createElement('div');
136
+ title.textContent = 'Currently selected';
137
+ const count = document.createElement('div');
138
+ count.className = 'selection-state-count';
139
+ header.appendChild(title);
140
+ header.appendChild(count);
141
+
142
+ const list = document.createElement('div');
143
+ list.className = 'selection-state-list';
144
+
145
+ content.appendChild(header);
146
+ content.appendChild(list);
147
+ fw.content.appendChild(content);
148
+
149
+ this.window = fw;
150
+ this.root = fw.root;
151
+ this.content = content;
152
+ this.countEl = count;
153
+ this.listEl = list;
154
+ }
155
+
156
+ _handleSelectionChanged() {
157
+ if (!this.root || this.root.style.display === 'none') return;
158
+ this._render();
159
+ }
160
+
161
+ _render() {
162
+ const list = this.listEl;
163
+ const countEl = this.countEl;
164
+ if (!list || !countEl) return;
165
+
166
+ const scene = this.viewer?.partHistory?.scene || this.viewer?.scene || null;
167
+ const selection = SelectionFilter.getSelectedObjects({ scene }) || [];
168
+ countEl.textContent = `${selection.length} selected`;
169
+
170
+ list.textContent = '';
171
+ if (!selection.length) {
172
+ const empty = document.createElement('div');
173
+ empty.className = 'selection-state-empty';
174
+ empty.textContent = 'No items selected.';
175
+ list.appendChild(empty);
176
+ return;
177
+ }
178
+
179
+ for (const obj of selection) {
180
+ const row = document.createElement('div');
181
+ row.className = 'selection-state-item';
182
+ const type = document.createElement('div');
183
+ type.className = 'selection-state-type';
184
+ type.textContent = String(obj?.type || 'object').toUpperCase();
185
+ const name = document.createElement('div');
186
+ name.className = 'selection-state-name';
187
+ name.textContent = _labelFor(obj);
188
+ row.appendChild(type);
189
+ row.appendChild(name);
190
+ list.appendChild(row);
191
+ }
192
+ }
193
+ }
194
+
195
+ export function createSelectionStateButton(viewer) {
196
+ if (!viewer) return null;
197
+ if (!viewer[PANEL_KEY]) {
198
+ viewer[PANEL_KEY] = new SelectionStatePanel(viewer);
199
+ }
200
+ const panel = viewer[PANEL_KEY];
201
+ return {
202
+ label: 'Sel',
203
+ title: 'Toggle selection state window',
204
+ onClick: () => panel.toggle(),
205
+ };
206
+ }
package/src/UI/viewer.js CHANGED
@@ -16,6 +16,7 @@ import { HistoryWidget } from './HistoryWidget.js';
16
16
  import { AssemblyConstraintsWidget } from './assembly/AssemblyConstraintsWidget.js';
17
17
  import { PartHistory } from '../PartHistory.js';
18
18
  import { SelectionFilter } from './SelectionFilter.js';
19
+ import { SelectionState } from './SelectionState.js';
19
20
  import './expressionsManager.js'
20
21
  import { expressionsManager } from './expressionsManager.js';
21
22
  import { MainToolbar } from './MainToolbar.js';
@@ -533,6 +534,8 @@ export class Viewer {
533
534
  this._attachRendererEvents(el);
534
535
 
535
536
  SelectionFilter.viewer = this;
537
+ try { SelectionFilter.startClickWatcher(this); } catch (_) { }
538
+ try { SelectionFilter._ensureSelectionFilterIndicator?.(this); } catch (_) { }
536
539
  // Use capture on pointerup to ensure we end interactions even if pointerup fires off-element
537
540
  window.addEventListener('pointerup', this._onPointerUp, { passive: false, capture: true });
538
541
  document.addEventListener('dblclick', this._onGlobalDoubleClick, { passive: false, capture: true });
@@ -1476,8 +1479,15 @@ export class Viewer {
1476
1479
  // ----------------------------------------
1477
1480
  // PMI Edit Mode API
1478
1481
  // ----------------------------------------
1482
+ _collapseExpandedDialogsForModeSwitch() {
1483
+ try { this.historyWidget?.collapseExpandedEntries?.({ clearOpenState: true, notify: false }); } catch { }
1484
+ try { this.assemblyConstraintsWidget?.collapseExpandedDialogs?.(); } catch { }
1485
+ try { this._pmiMode?.collapseExpandedDialogs?.(); } catch { }
1486
+ }
1487
+
1479
1488
  startPMIMode(viewEntry, viewIndex, widget = this.pmiViewsWidget) {
1480
1489
  const alreadyActive = !!this._pmiMode;
1490
+ try { this._collapseExpandedDialogsForModeSwitch(); } catch { }
1481
1491
  if (!alreadyActive) {
1482
1492
  try { this.assemblyConstraintsWidget?.onPMIModeEnter?.(); } catch { }
1483
1493
  }
@@ -1506,6 +1516,9 @@ export class Viewer {
1506
1516
 
1507
1517
  endPMIMode() {
1508
1518
  const hadMode = !!this._pmiMode;
1519
+ if (hadMode) {
1520
+ try { this._collapseExpandedDialogsForModeSwitch(); } catch { }
1521
+ }
1509
1522
  try { if (this._pmiMode) this._pmiMode.dispose(); } catch { }
1510
1523
  this._pmiMode = null;
1511
1524
  if (hadMode) {
@@ -2174,16 +2187,8 @@ export class Viewer {
2174
2187
  const ud = obj.userData;
2175
2188
  const defaultMaterial = ud.__defaultMaterial ?? baseMaterial;
2176
2189
  if (!ud.__defaultMaterial) ud.__defaultMaterial = baseMaterial;
2177
- const isHovered = !!ud.__hoverMatApplied;
2178
- const isSelected = obj.selected === true;
2179
-
2180
2190
  const applyBase = (mat) => {
2181
- ud.__baseMaterial = mat;
2182
- if (isHovered) {
2183
- ud.__hoverOrigMat = mat;
2184
- } else if (!isSelected && mat) {
2185
- obj.material = mat;
2186
- }
2191
+ SelectionState.setBaseMaterial(obj, mat);
2187
2192
  };
2188
2193
 
2189
2194
  if (!color) {
@@ -2365,21 +2370,25 @@ export class Viewer {
2365
2370
 
2366
2371
  // Prefer the intersected object if it is clickable
2367
2372
  let obj = intersection.object;
2373
+ if (obj && obj.type === 'POINTS' && obj.parent && String(obj.parent.type || '').toUpperCase() === SelectionFilter.VERTEX) {
2374
+ obj = obj.parent;
2375
+ }
2368
2376
 
2369
2377
  // If the object (or its ancestors) doesn't expose onClick, climb to one that does
2370
2378
  let target = obj;
2371
2379
  while (target && typeof target.onClick !== 'function' && target.visible) target = target.parent;
2380
+ if (!target) target = obj;
2372
2381
  if (!target) return null;
2373
2382
 
2374
2383
  // Respect selection filter: ensure target is a permitted type, or ALL
2375
2384
  if (typeof isAllowed === 'function') {
2376
2385
  // Allow selecting already-selected items regardless (toggle off), consistent with SceneListing
2377
2386
  if (!isAllowed(target.type) && !target.selected) {
2378
- // Try to find a closer ancestor/descendant of allowed type that is clickable
2387
+ // Try to find a closer ancestor of allowed type
2379
2388
  // Ascend first (e.g., FACE hit while EDGE is active should try parent SOLID only if allowed)
2380
2389
  let t = target.parent;
2381
- while (t && typeof t.onClick === 'function' && !isAllowed(t.type)) t = t.parent;
2382
- if (t && typeof t.onClick === 'function' && isAllowed(t.type)) target = t;
2390
+ while (t && !isAllowed(t.type)) t = t.parent;
2391
+ if (t && isAllowed(t.type)) target = t;
2383
2392
  else return null;
2384
2393
  }
2385
2394
  }
@@ -2566,11 +2575,23 @@ export class Viewer {
2566
2575
  SelectionFilter.EDGE,
2567
2576
  SelectionFilter.FACE,
2568
2577
  SelectionFilter.PLANE,
2578
+ SelectionFilter.SKETCH,
2579
+ SelectionFilter.DATUM,
2580
+ SelectionFilter.HELIX,
2581
+ SelectionFilter.LOOP,
2569
2582
  SelectionFilter.SOLID,
2570
2583
  SelectionFilter.COMPONENT,
2571
2584
  ].map(t => normType(t));
2585
+ const normSolid = normType(SelectionFilter.SOLID);
2586
+ const normComponent = normType(SelectionFilter.COMPONENT);
2587
+ const nonSolidAllowed = Array.from(allowedSet).some(t => t && t !== normSolid && t !== normComponent);
2572
2588
  const getPriority = (type) => {
2573
- const idx = priorityOrder.indexOf(normType(type));
2589
+ const nt = normType(type);
2590
+ if (nonSolidAllowed && (nt === normSolid || nt === normComponent)) {
2591
+ // Always push SOLID/COMPONENT to the end when any other type is allowed.
2592
+ return priorityOrder.length + 2;
2593
+ }
2594
+ const idx = priorityOrder.indexOf(nt);
2574
2595
  return idx === -1 ? priorityOrder.length : idx;
2575
2596
  };
2576
2597
  const isAllowedType = (type) => {
@@ -3,6 +3,7 @@ import { AssemblyConstraintRegistry } from './AssemblyConstraintRegistry.js';
3
3
  import { evaluateConstraintNumericValue } from './constraintExpressionUtils.js';
4
4
  import { deepClone } from '../utils/deepClone.js';
5
5
  import { normalizeTypeString } from '../utils/normalizeTypeString.js';
6
+ import { resolveSelectionObject } from '../utils/selectionResolver.js';
6
7
 
7
8
  const RESERVED_KEYS = new Set(['type', 'persistentData', '__open']);
8
9
 
@@ -102,47 +103,6 @@ function removeExistingDebugArrows(scene) {
102
103
  }
103
104
  }
104
105
 
105
- function resolveSelectionObject(scene, selection) {
106
- if (!scene || selection == null) return null;
107
- let target = selection;
108
- if (Array.isArray(selection)) {
109
- target = selection.find((item) => item != null) ?? null;
110
- }
111
- if (!target) return null;
112
- if (target.isObject3D) return target;
113
- try {
114
- if (typeof target === 'string') {
115
- if (typeof scene.traverse === 'function') {
116
- let best = null;
117
- scene.traverse((obj) => {
118
- if (!obj || obj.name !== target) return;
119
- if (!best) best = obj;
120
- const component = resolveComponentFromObject(obj);
121
- const bestComponent = best ? resolveComponentFromObject(best) : null;
122
- if (component && !bestComponent) {
123
- best = obj;
124
- }
125
- });
126
- if (best) return best;
127
- }
128
- return typeof scene.getObjectByName === 'function'
129
- ? scene.getObjectByName(target)
130
- : null;
131
- }
132
- if (typeof target?.uuid === 'string' && typeof scene.getObjectByProperty === 'function') {
133
- const found = scene.getObjectByProperty('uuid', target.uuid);
134
- if (found) return found;
135
- }
136
- if (typeof target?.name === 'string' && typeof scene.getObjectByName === 'function') {
137
- const found = scene.getObjectByName(target.name);
138
- if (found) return found;
139
- }
140
- } catch {
141
- return null;
142
- }
143
- return null;
144
- }
145
-
146
106
  function resolveComponentFromObject(obj) {
147
107
  let current = obj;
148
108
  while (current) {
@@ -152,6 +112,10 @@ function resolveComponentFromObject(obj) {
152
112
  return null;
153
113
  }
154
114
 
115
+ function scoreObjectForComponent(object) {
116
+ return resolveComponentFromObject(object) ? 1 : 0;
117
+ }
118
+
155
119
  function vectorFrom(value) {
156
120
  if (!value) return null;
157
121
  if (value instanceof THREE.Vector3) return value.clone();
@@ -894,7 +858,19 @@ export class AssemblyConstraintHistory {
894
858
 
895
859
  const updatedComponents = new Set();
896
860
 
897
- const resolveObject = (selection) => resolveSelectionObject(scene, selection);
861
+ const resolveObject = (selection) => resolveSelectionObject(scene, selection, {
862
+ scoreFn: scoreObjectForComponent,
863
+ allowJson: false,
864
+ allowUuidString: false,
865
+ allowUuidObject: true,
866
+ allowFuzzyName: false,
867
+ allowNameContains: false,
868
+ allowPath: false,
869
+ allowReference: false,
870
+ allowTarget: false,
871
+ allowSelectionName: false,
872
+ arrayMode: 'first',
873
+ });
898
874
  const resolveComponent = (selection) => {
899
875
  const obj = resolveObject(selection);
900
876
  return resolveComponentFromObject(obj);
@@ -33,6 +33,7 @@ export class AngleConstraint extends BaseAssemblyConstraint {
33
33
  static shortName = '∠';
34
34
  static longName = '∠ Angle Constraint';
35
35
  static constraintType = 'angle';
36
+ static focusField = 'angle';
36
37
  static aliases = ['angle', 'angle_between', 'angular', 'ANGL'];
37
38
  static inputParamsSchema = inputParamsSchema;
38
39
 
@@ -37,6 +37,7 @@ export class DistanceConstraint extends BaseAssemblyConstraint {
37
37
  static shortName = '⟺';
38
38
  static longName = '⟺ Distance Constraint';
39
39
  static constraintType = 'distance';
40
+ static focusField = 'distance';
40
41
  static aliases = ['distance', 'offset', 'gap', 'DIST'];
41
42
  static inputParamsSchema = inputParamsSchema;
42
43
 
@@ -1,16 +1,32 @@
1
+ import { resolveSelectionObject as resolveSelectionObjectBase } from '../utils/selectionResolver.js';
2
+
1
3
  export function resolveSelectionObject(selection, partHistory) {
2
4
  if (!selection) return null;
3
- if (typeof selection === 'string') {
5
+ if (typeof selection !== 'string') return selection;
6
+
7
+ const nameResolver = (name) => {
4
8
  if (partHistory && typeof partHistory.getObjectByName === 'function') {
5
- return partHistory.getObjectByName(selection);
9
+ return partHistory.getObjectByName(name);
6
10
  }
7
11
  const scene = partHistory?.scene;
8
12
  if (scene && typeof scene.getObjectByName === 'function') {
9
- return scene.getObjectByName(selection);
13
+ return scene.getObjectByName(name);
10
14
  }
11
15
  return null;
12
- }
13
- return selection;
16
+ };
17
+
18
+ return resolveSelectionObjectBase(partHistory?.scene || null, selection, {
19
+ nameResolver,
20
+ allowJson: false,
21
+ allowUuid: false,
22
+ allowFuzzyName: false,
23
+ allowNameContains: false,
24
+ allowPath: false,
25
+ allowReference: false,
26
+ allowTarget: false,
27
+ allowSelectionName: false,
28
+ arrayMode: 'first',
29
+ });
14
30
  }
15
31
 
16
32
  function getFeatureEntryId(entry) {
@@ -4,6 +4,7 @@ import { BREP } from "../../BREP/BREP.js";
4
4
  const THREE = BREP.THREE;
5
5
  import { LineGeometry } from 'three/examples/jsm/Addons.js';
6
6
  import { deepClone } from '../../utils/deepClone.js';
7
+ import { SelectionState } from '../../UI/SelectionState.js';
7
8
 
8
9
  const inputParamsSchema = {
9
10
  id: {
@@ -892,8 +893,7 @@ export class SketchFeature {
892
893
  if (sketchMat) {
893
894
  sketchMat.side = THREE.DoubleSide;
894
895
  sketchMat.needsUpdate = true;
895
- face.material = sketchMat;
896
- face.userData.__baseMaterial = sketchMat;
896
+ SelectionState.setBaseMaterial(face, sketchMat);
897
897
  }
898
898
  } catch { }
899
899
  sceneGroup.add(face);
@@ -689,9 +689,10 @@ const shortestAngleDelta = (target, current) => {
689
689
 
690
690
 
691
691
  export const constraints = {
692
- tolerance,
692
+ get tolerance() { return tolerance; },
693
+ set tolerance(value) { tolerance = value; },
693
694
  constraintFunctions,
694
- }
695
+ };
695
696
 
696
697
 
697
698