brep-io-kernel 1.0.21 → 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 (34) hide show
  1. package/README.md +4 -1
  2. package/dist-kernel/brep-kernel.js +11065 -10512
  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 +3 -0
  13. package/src/UI/SceneListing.js +45 -7
  14. package/src/UI/SelectionFilter.js +469 -442
  15. package/src/UI/SelectionState.js +464 -0
  16. package/src/UI/assembly/AssemblyConstraintCollectionWidget.js +40 -1
  17. package/src/UI/assembly/AssemblyConstraintsWidget.js +17 -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 +99 -69
  21. package/src/UI/pmi/LabelOverlay.js +32 -0
  22. package/src/UI/pmi/PMIMode.js +23 -0
  23. package/src/UI/pmi/dimensions/HoleCalloutAnnotation.js +7 -1
  24. package/src/UI/toolbarButtons/orientToFaceButton.js +3 -36
  25. package/src/UI/toolbarButtons/registerDefaultButtons.js +2 -0
  26. package/src/UI/toolbarButtons/selectionStateButton.js +206 -0
  27. package/src/UI/viewer.js +16 -16
  28. package/src/assemblyConstraints/AssemblyConstraintHistory.js +18 -42
  29. package/src/assemblyConstraints/constraints/AngleConstraint.js +1 -0
  30. package/src/assemblyConstraints/constraints/DistanceConstraint.js +1 -0
  31. package/src/features/selectionUtils.js +21 -5
  32. package/src/features/sketch/SketchFeature.js +2 -2
  33. package/src/features/sketch/sketchSolver2D/constraintDefinitions.js +3 -2
  34. package/src/utils/selectionResolver.js +258 -0
@@ -1,11 +1,19 @@
1
- import { CADmaterials } from "./CADmaterials.js";
1
+ import { SelectionState } from "./SelectionState.js";
2
2
  import {BREP} from '../BREP/BREP.js';
3
+
4
+ const debugMode = false;
5
+
6
+
7
+
8
+
3
9
  export class SelectionFilter {
4
10
  static SOLID = "SOLID";
5
11
  static COMPONENT = "COMPONENT";
6
12
  static FACE = "FACE";
7
13
  static PLANE = "PLANE";
8
14
  static SKETCH = "SKETCH";
15
+ static DATUM = "DATUM";
16
+ static HELIX = "HELIX";
9
17
  static EDGE = "EDGE";
10
18
  static LOOP = "LOOP";
11
19
  static VERTEX = "VERTEX";
@@ -16,6 +24,7 @@ export class SelectionFilter {
16
24
  static viewer = null;
17
25
  static previouseAllowedSelectionTypes = null;
18
26
  static _hovered = new Set(); // objects currently hover-highlighted
27
+ static _hoveredSourceMap = new Map(); // key -> source object for hover
19
28
  static hoverColor = '#fbff00'; // default hover tint
20
29
  static _selectionActions = new Map();
21
30
  static _selectionActionOrder = [];
@@ -33,6 +42,10 @@ export class SelectionFilter {
33
42
  static _selectionFilterTypes = null;
34
43
  static _selectionFilterOutsideBound = false;
35
44
  static _selectionFilterTintBtn = null;
45
+ static _clickWatcherTimer = null;
46
+ static _missingClickLogged = new Map();
47
+ static _clickWatcherIntervalMs = 2000;
48
+ static _onClickWatcherSeq = 1;
36
49
  static _selectableTintState = {
37
50
  active: false,
38
51
  activeColor: null,
@@ -45,7 +58,7 @@ export class SelectionFilter {
45
58
  throw new Error("SelectionFilter is static and cannot be instantiated.");
46
59
  }
47
60
 
48
- static get TYPES() { return [this.SOLID, this.COMPONENT, this.FACE, this.PLANE, this.SKETCH, this.EDGE, this.LOOP, this.VERTEX, this.ALL]; }
61
+ static get TYPES() { return [this.SOLID, this.COMPONENT, this.FACE, this.PLANE, this.SKETCH, this.DATUM, this.HELIX, this.EDGE, this.LOOP, this.VERTEX, this.ALL]; }
49
62
 
50
63
  // Convenience: return the list of selectable types for the dropdown (excludes ALL)
51
64
  static getAvailableTypes() {
@@ -61,6 +74,144 @@ export class SelectionFilter {
61
74
  return null;
62
75
  }
63
76
 
77
+ static _withSilentOnClick(target, fn) {
78
+ if (!target || typeof fn !== 'function') return;
79
+ try { target.__brepOnClickSilent = true; } catch { }
80
+ try { fn(); } catch { } finally {
81
+ try { target.__brepOnClickSilent = false; } catch { }
82
+ }
83
+ }
84
+
85
+ static _installOnClickWatcher(target) {
86
+ if (!target || typeof target !== 'object') return;
87
+ const existingDesc = Object.getOwnPropertyDescriptor(target, 'onClick');
88
+ if (existingDesc?.get && existingDesc?.get.__brepOnClickWatcher) return;
89
+ let current = typeof target.onClick !== 'undefined' ? target.onClick : undefined;
90
+ const getter = function () { return current; };
91
+ getter.__brepOnClickWatcher = true;
92
+ const setter = function (v) {
93
+ const prev = current;
94
+ current = v;
95
+ try {
96
+ target.__brepOnClickLastSetAt = Date.now();
97
+ target.__brepOnClickLastSetStack = new Error('[SelectionFilter] onClick set').stack;
98
+ } catch { }
99
+ const silent = !!target.__brepOnClickSilent;
100
+ const prevFn = typeof prev === 'function';
101
+ const nextFn = typeof v === 'function';
102
+ if (!silent && prev !== v) {
103
+ if (!nextFn || (prevFn && !nextFn)) {
104
+ if (debugMode) {
105
+ try {
106
+ console.log('[SelectionFilter] onClick removed/overwritten', {
107
+ name: target?.name,
108
+ type: target?.type,
109
+ uuid: target?.uuid,
110
+ prev,
111
+ next: v,
112
+ target,
113
+ });
114
+ console.trace('[SelectionFilter] onClick change stack');
115
+ } catch { }
116
+ }
117
+ } else if (!v?.__brepSelectionHandler) {
118
+ if (debugMode) {
119
+ try {
120
+ console.log('[SelectionFilter] onClick replaced', {
121
+ name: target?.name,
122
+ type: target?.type,
123
+ uuid: target?.uuid,
124
+ prev,
125
+ next: v,
126
+ target,
127
+ });
128
+ console.trace('[SelectionFilter] onClick set stack');
129
+ } catch { }
130
+ }
131
+ }
132
+ }
133
+ };
134
+ setter.__brepOnClickWatcher = true;
135
+ try {
136
+ Object.defineProperty(target, 'onClick', {
137
+ get: getter,
138
+ set: setter,
139
+ configurable: true,
140
+ enumerable: true,
141
+ });
142
+ } catch {
143
+ try { target.onClick = current; } catch { }
144
+ }
145
+ }
146
+
147
+ static startClickWatcher(viewer = null, { intervalMs = 2000 } = {}) {
148
+ const v = viewer || SelectionFilter.viewer;
149
+ SelectionFilter._clickWatcherIntervalMs = Math.max(250, Number(intervalMs) || 2000);
150
+ if (SelectionFilter._clickWatcherTimer) return;
151
+ const scan = () => {
152
+ try {
153
+ const scene = v?.partHistory?.scene || v?.scene || SelectionFilter.viewer?.partHistory?.scene || SelectionFilter.viewer?.scene || null;
154
+ if (!scene) return;
155
+ const selectionTypes = new Set(SelectionFilter.TYPES.filter(t => t && t !== SelectionFilter.ALL));
156
+ const missingNow = new Set();
157
+ const stack = Array.isArray(scene.children) ? [...scene.children] : [];
158
+ while (stack.length) {
159
+ const current = stack.pop();
160
+ if (!current) continue;
161
+ const kids = Array.isArray(current?.children) ? current.children : [];
162
+ for (const child of kids) stack.push(child);
163
+
164
+ const type = String(current.type || '').toUpperCase();
165
+ if (!selectionTypes.has(type)) continue;
166
+
167
+ SelectionFilter._installOnClickWatcher(current);
168
+ let hasClick = typeof current.onClick === 'function';
169
+ if (!hasClick) {
170
+ try {
171
+ SelectionFilter.ensureSelectionHandlers(current, { deep: false });
172
+ } catch { }
173
+ hasClick = typeof current.onClick === 'function';
174
+ }
175
+ if (!hasClick) {
176
+ missingNow.add(current.uuid);
177
+ const last = SelectionFilter._missingClickLogged.get(current.uuid);
178
+ if (!last) {
179
+ SelectionFilter._missingClickLogged.set(current.uuid, Date.now());
180
+ if (debugMode) {
181
+ try {
182
+ console.log('[SelectionFilter] Missing onClick', {
183
+ name: current?.name,
184
+ type: current?.type,
185
+ uuid: current?.uuid,
186
+ parentName: current?.parent?.name,
187
+ parentType: current?.parent?.type,
188
+ lastSetAt: current?.__brepOnClickLastSetAt || null,
189
+ lastSetStack: current?.__brepOnClickLastSetStack || null,
190
+ object: current,
191
+ });
192
+ } catch { }
193
+ }
194
+ }
195
+ }
196
+ }
197
+ // Clear recovered entries
198
+ for (const key of SelectionFilter._missingClickLogged.keys()) {
199
+ if (!missingNow.has(key)) SelectionFilter._missingClickLogged.delete(key);
200
+ }
201
+ } catch { }
202
+ };
203
+ try { scan(); } catch { }
204
+ SelectionFilter._clickWatcherTimer = setInterval(scan, SelectionFilter._clickWatcherIntervalMs);
205
+ }
206
+
207
+ static stopClickWatcher() {
208
+ if (SelectionFilter._clickWatcherTimer) {
209
+ clearInterval(SelectionFilter._clickWatcherTimer);
210
+ SelectionFilter._clickWatcherTimer = null;
211
+ }
212
+ SelectionFilter._missingClickLogged.clear();
213
+ }
214
+
64
215
  static setCurrentType(_type) {
65
216
  // No-op: current type is no longer tracked.
66
217
  void _type;
@@ -72,7 +223,6 @@ export class SelectionFilter {
72
223
  if (types === SelectionFilter.ALL) {
73
224
  SelectionFilter.allowedSelectionTypes = SelectionFilter.ALL;
74
225
  SelectionFilter.triggerUI();
75
- SelectionFilter._ensureSceneSelectionHandlers();
76
226
  SelectionFilter.#logAllowedTypesChange(SelectionFilter.allowedSelectionTypes, 'SetSelectionTypes');
77
227
  return;
78
228
  }
@@ -81,7 +231,6 @@ export class SelectionFilter {
81
231
  if (invalid.length) throw new Error(`Unknown selection type(s): ${invalid.join(", ")}`);
82
232
  SelectionFilter.allowedSelectionTypes = new Set(list);
83
233
  SelectionFilter.triggerUI();
84
- SelectionFilter._ensureSceneSelectionHandlers();
85
234
  SelectionFilter.#logAllowedTypesChange(SelectionFilter.allowedSelectionTypes, 'SetSelectionTypes');
86
235
  }
87
236
 
@@ -94,7 +243,6 @@ export class SelectionFilter {
94
243
  SelectionFilter.allowedSelectionTypes = SelectionFilter.previouseAllowedSelectionTypes;
95
244
  SelectionFilter.previouseAllowedSelectionTypes = null;
96
245
  SelectionFilter.triggerUI();
97
- SelectionFilter._ensureSceneSelectionHandlers();
98
246
  SelectionFilter.#logAllowedTypesChange(SelectionFilter.allowedSelectionTypes, 'RestoreSelectionTypes');
99
247
  }
100
248
  }
@@ -104,19 +252,25 @@ export class SelectionFilter {
104
252
  let changed = false;
105
253
  const attach = (target) => {
106
254
  if (!target || typeof target !== 'object') return;
255
+ SelectionState.attach(target);
256
+ SelectionFilter._installOnClickWatcher(target);
107
257
  if (typeof target.onClick === 'function') return;
108
- target.onClick = () => {
109
- try {
110
- if (target.type === SelectionFilter.SOLID && target.parent && target.parent.type === SelectionFilter.COMPONENT) {
111
- const handledByParent = SelectionFilter.toggleSelection(target.parent);
112
- if (!handledByParent) SelectionFilter.toggleSelection(target);
113
- return;
258
+ SelectionFilter._withSilentOnClick(target, () => {
259
+ target.onClick = () => {
260
+ try {
261
+ if (target.type === SelectionFilter.SOLID && target.parent && target.parent.type === SelectionFilter.COMPONENT) {
262
+ const handledByParent = SelectionFilter.toggleSelection(target.parent);
263
+ if (!handledByParent) SelectionFilter.toggleSelection(target);
264
+ return;
265
+ }
266
+ SelectionFilter.toggleSelection(target);
267
+ } catch (error) {
268
+ if (debugMode) {
269
+ try { console.warn('[SelectionFilter] toggleSelection failed:', error); } catch (_) { /* ignore */ }
270
+ }
114
271
  }
115
- SelectionFilter.toggleSelection(target);
116
- } catch (error) {
117
- try { console.warn('[SelectionFilter] toggleSelection failed:', error); } catch (_) { /* ignore */ }
118
- }
119
- };
272
+ };
273
+ });
120
274
  try { target.onClick.__brepSelectionHandler = true; } catch (_) { /* ignore */ }
121
275
  changed = true;
122
276
  };
@@ -138,21 +292,6 @@ export class SelectionFilter {
138
292
  return changed;
139
293
  }
140
294
 
141
- static _ensureSceneSelectionHandlers() {
142
- try {
143
- const scene = SelectionFilter.viewer?.partHistory?.scene
144
- || SelectionFilter.viewer?.scene
145
- || null;
146
- if (!scene || !Array.isArray(scene.children)) return;
147
- for (const child of scene.children) {
148
- if (!child || child.type !== SelectionFilter.SOLID) continue;
149
- SelectionFilter.ensureSelectionHandlers(child, { deep: true });
150
- }
151
- } catch { }
152
- }
153
-
154
-
155
-
156
295
  static allowType(type) {
157
296
  // Legacy support: expand available set; does not change currentType
158
297
  if (type === SelectionFilter.ALL) { SelectionFilter.allowedSelectionTypes = SelectionFilter.ALL; SelectionFilter.triggerUI(); return; }
@@ -196,37 +335,67 @@ export class SelectionFilter {
196
335
  SelectionFilter.#logAllowedTypesChange(SelectionFilter.allowedSelectionTypes, 'Reset');
197
336
  }
198
337
 
199
- static #getBaseMaterial(obj) {
200
- if (!obj) return null;
201
- const ud = obj.userData || {};
202
- if (ud.__baseMaterial) return ud.__baseMaterial;
203
- if (obj.type === SelectionFilter.FACE) return CADmaterials.FACE?.BASE ?? CADmaterials.FACE ?? obj.material;
204
- if (obj.type === SelectionFilter.PLANE) return CADmaterials.PLANE?.BASE ?? CADmaterials.FACE?.BASE ?? obj.material;
205
- if (obj.type === SelectionFilter.EDGE) return CADmaterials.EDGE?.BASE ?? CADmaterials.EDGE ?? obj.material;
206
- if (obj.type === SelectionFilter.SOLID || obj.type === SelectionFilter.COMPONENT) return obj.material;
207
- return obj.material;
208
- }
209
-
210
338
  // ---------------- Hover Highlighting ----------------
211
- static getHoverColor() { return SelectionFilter.hoverColor; }
339
+ static getHoverColor() { return SelectionState.hoverColor || SelectionFilter.hoverColor; }
212
340
  static setHoverColor(hex) {
213
341
  if (!hex) return;
214
342
  try { SelectionFilter.hoverColor = String(hex); } catch (_) { }
343
+ SelectionState.setHoverColor(SelectionFilter.hoverColor);
215
344
  // Update current hovered objects live
216
345
  for (const o of Array.from(SelectionFilter._hovered)) {
217
- if (o && o.material && o.material.color && typeof o.material.color.set === 'function') {
218
- try { o.material.color.set(SelectionFilter.hoverColor); } catch (_) { }
219
- }
346
+ if (!o) continue;
347
+ try {
348
+ SelectionState.attach(o);
349
+ o.hovered = false;
350
+ o.hovered = true;
351
+ } catch { }
220
352
  }
221
353
  }
222
354
 
223
355
  static setHoverObject(obj, options = {}) {
224
- const { ignoreFilter = false } = options;
225
- // Clear existing hover first
226
- SelectionFilter.clearHover();
227
- if (!obj) return;
228
- // Highlight depending on type
229
- SelectionFilter.#applyHover(obj, { ignoreFilter });
356
+ SelectionFilter.setHoverObjects(obj ? [obj] : [], options);
357
+ }
358
+
359
+ static setHoverObjects(objs, options = {}) {
360
+ const { ignoreFilter = false, append = false } = options;
361
+ const prevKeys = new Set(SelectionFilter._hoveredSourceMap.keys());
362
+ if (!append) {
363
+ SelectionFilter._clearHoverState({ emit: false });
364
+ }
365
+ if (!objs) return;
366
+ const list = Array.isArray(objs) ? objs : [objs];
367
+ const seen = new Set();
368
+ const keyFor = (obj) => obj?.uuid || obj?.id || obj?.name || obj;
369
+ for (const obj of list) {
370
+ if (!obj) continue;
371
+ const key = keyFor(obj);
372
+ if (seen.has(key)) continue;
373
+ seen.add(key);
374
+ const allowed = ignoreFilter || SelectionFilter.IsAllowed(obj.type);
375
+ if (!allowed) continue;
376
+ if (key && !SelectionFilter._hoveredSourceMap.has(key)) {
377
+ SelectionFilter._hoveredSourceMap.set(key, obj);
378
+ }
379
+ const targets = SelectionState.getHoverTargets(obj);
380
+ for (const t of targets) {
381
+ if (!t) continue;
382
+ try {
383
+ SelectionState.attach(t);
384
+ t.hovered = true;
385
+ SelectionFilter._hovered.add(t);
386
+ } catch { }
387
+ }
388
+ }
389
+ const nextKeys = new Set(SelectionFilter._hoveredSourceMap.keys());
390
+ let changed = prevKeys.size !== nextKeys.size;
391
+ if (!changed) {
392
+ for (const k of nextKeys) {
393
+ if (!prevKeys.has(k)) { changed = true; break; }
394
+ }
395
+ }
396
+ if (changed) {
397
+ SelectionFilter._emitHoverChanged(Array.from(SelectionFilter._hoveredSourceMap.values()));
398
+ }
230
399
  }
231
400
 
232
401
  static setHoverByName(scene, name) {
@@ -237,157 +406,38 @@ export class SelectionFilter {
237
406
  }
238
407
 
239
408
  static clearHover() {
240
- if (!SelectionFilter._hovered || SelectionFilter._hovered.size === 0) return;
241
- for (const o of Array.from(SelectionFilter._hovered)) {
242
- SelectionFilter.#restoreHover(o);
243
- }
244
- SelectionFilter._hovered.clear();
409
+ SelectionFilter._clearHoverState({ emit: true });
245
410
  }
246
411
 
247
- static #applyHover(obj, options = {}) {
248
- const { ignoreFilter = false } = options;
249
- if (!obj) return;
250
- // Respect selection filter: skip if disallowed
251
- const allowed = ignoreFilter
252
- || SelectionFilter.IsAllowed(obj.type)
253
- || SelectionFilter.matchesAllowedType(obj.type);
254
- if (!allowed) return;
255
-
256
- // Only ever highlight one object: the exact object provided, if it has a color
257
- const target = obj;
258
- if (!target) return;
259
-
260
- const applyToOne = (t) => {
261
- if (!t) return;
262
- if (!t.userData) t.userData = {};
263
- const origMat = t.material;
264
- if (!origMat) return;
265
- if (t.userData.__hoverMatApplied) { SelectionFilter._hovered.add(t); return; }
266
- let clone;
267
- try { clone = typeof origMat.clone === 'function' ? origMat.clone() : origMat; } catch { clone = origMat; }
268
- try { if (clone && clone.color && typeof clone.color.set === 'function') clone.color.set(SelectionFilter.hoverColor); } catch { }
269
- try {
270
- if (origMat && clone && origMat.resolution && clone.resolution && typeof clone.resolution.copy === 'function') {
271
- clone.resolution.copy(origMat.resolution);
272
- }
273
- } catch { }
274
- try {
275
- if (origMat && clone && typeof origMat.dashed !== 'undefined' && typeof clone.dashed !== 'undefined') {
276
- clone.dashed = origMat.dashed;
277
- }
278
- if (origMat && clone && typeof origMat.dashSize !== 'undefined' && typeof clone.dashSize !== 'undefined') {
279
- clone.dashSize = origMat.dashSize;
280
- }
281
- if (origMat && clone && typeof origMat.gapSize !== 'undefined' && typeof clone.gapSize !== 'undefined') {
282
- clone.gapSize = origMat.gapSize;
283
- }
284
- if (origMat && clone && typeof origMat.dashScale !== 'undefined' && typeof clone.dashScale !== 'undefined') {
285
- clone.dashScale = origMat.dashScale;
286
- }
287
- } catch { }
288
- try {
289
- t.userData.__hoverOrigMat = origMat;
290
- t.userData.__hoverMatApplied = true;
291
- if (clone !== origMat) t.material = clone;
292
- t.userData.__hoverMat = clone;
293
- } catch { }
294
- SelectionFilter._hovered.add(t);
295
- };
296
-
297
- if (target.type === SelectionFilter.SOLID || target.type === SelectionFilter.COMPONENT) {
298
- // Highlight all immediate child faces/edges for SOLID or COMPONENT children
299
- if (Array.isArray(target.children)) {
300
- for (const ch of target.children) {
301
- if (!ch) continue;
302
- if (ch.type === SelectionFilter.SOLID || ch.type === SelectionFilter.COMPONENT) {
303
- if (Array.isArray(ch.children)) {
304
- for (const nested of ch.children) {
305
- if (nested && (nested.type === SelectionFilter.FACE || nested.type === SelectionFilter.EDGE)) applyToOne(nested);
306
- }
307
- }
308
- } else if (ch.type === SelectionFilter.FACE || ch.type === SelectionFilter.EDGE) {
309
- applyToOne(ch);
310
- }
311
- }
312
- }
313
- // Track the solid as a logical hovered root to clear later
314
- SelectionFilter._hovered.add(target);
315
- return;
316
- }
317
-
318
- if (target.type === SelectionFilter.VERTEX) {
319
- // Apply to the vertex object and any drawable children (e.g., Points)
320
- applyToOne(target);
321
- if (Array.isArray(target.children)) {
322
- for (const ch of target.children) {
323
- applyToOne(ch);
324
- }
412
+ static _emitHoverChanged(objs = []) {
413
+ try {
414
+ const list = Array.isArray(objs) ? objs : [];
415
+ const uuids = [];
416
+ for (const obj of list) {
417
+ if (obj && obj.uuid) uuids.push(obj.uuid);
325
418
  }
326
- return;
327
- }
328
-
329
- applyToOne(target);
419
+ const ev = new CustomEvent('hover-changed', { detail: { objects: list, uuids } });
420
+ window.dispatchEvent(ev);
421
+ } catch { /* ignore */ }
330
422
  }
331
423
 
332
- static #restoreHover(obj) {
333
- if (!obj) return;
334
- const restoreOne = (t) => {
335
- if (!t) return;
336
- const ud = t.userData || {};
337
- if (!ud.__hoverMatApplied) return;
338
- const cloneWithColor = (mat, colorHex) => {
339
- if (!mat) return mat;
340
- let c = mat;
424
+ static _clearHoverState({ emit = true } = {}) {
425
+ const hadHover = (SelectionFilter._hovered && SelectionFilter._hovered.size) || SelectionFilter._hoveredSourceMap.size;
426
+ if (SelectionFilter._hovered && SelectionFilter._hovered.size) {
427
+ for (const o of Array.from(SelectionFilter._hovered)) {
341
428
  try {
342
- c = typeof mat.clone === 'function' ? mat.clone() : mat;
343
- if (c && c.color && typeof c.color.set === 'function' && colorHex) c.color.set(colorHex);
344
- } catch { c = mat; }
345
- return c;
346
- };
347
- const applySelectionMaterial = () => {
348
- if (t.type === SelectionFilter.FACE) return CADmaterials.FACE?.SELECTED ?? CADmaterials.FACE ?? ud.__hoverOrigMat ?? t.material;
349
- if (t.type === SelectionFilter.PLANE) return CADmaterials.PLANE?.SELECTED ?? CADmaterials.FACE?.SELECTED ?? ud.__hoverOrigMat ?? t.material;
350
- if (t.type === SelectionFilter.EDGE) {
351
- const base = ud.__hoverOrigMat ?? t.material;
352
- const selColor = CADmaterials.EDGE?.SELECTED?.color || CADmaterials.EDGE?.SELECTED?.color?.getHexString?.();
353
- return cloneWithColor(base, selColor || '#ff00ff');
354
- }
355
- if (t.type === SelectionFilter.SOLID || t.type === SelectionFilter.COMPONENT) return CADmaterials.SOLID?.SELECTED ?? ud.__hoverOrigMat ?? t.material;
356
- return ud.__hoverOrigMat ?? t.material;
357
- };
358
- const applyDeselectedMaterial = () => {
359
- if (t.type === SelectionFilter.FACE) return ud.__hoverOrigMat ?? SelectionFilter.#getBaseMaterial(t) ?? t.material;
360
- if (t.type === SelectionFilter.PLANE) return ud.__hoverOrigMat ?? SelectionFilter.#getBaseMaterial(t) ?? t.material;
361
- if (t.type === SelectionFilter.EDGE) return ud.__hoverOrigMat ?? SelectionFilter.#getBaseMaterial(t) ?? t.material;
362
- if (t.type === SelectionFilter.SOLID || t.type === SelectionFilter.COMPONENT) return ud.__hoverOrigMat ?? t.material;
363
- return ud.__hoverOrigMat ?? t.material;
364
- };
365
- try {
366
- if (t.selected) {
367
- const selMat = applySelectionMaterial();
368
- if (selMat) t.material = selMat;
369
- } else {
370
- const baseMat = applyDeselectedMaterial();
371
- if (baseMat) t.material = baseMat;
372
- }
373
- if (ud.__hoverMat && ud.__hoverMat !== ud.__hoverOrigMat && typeof ud.__hoverMat.dispose === 'function') ud.__hoverMat.dispose();
374
- } catch { }
375
- try { delete t.userData.__hoverMatApplied; } catch { }
376
- try { delete t.userData.__hoverOrigMat; } catch { }
377
- try { delete t.userData.__hoverMat; } catch { }
378
- };
379
-
380
- if (obj.type === SelectionFilter.SOLID || obj.type === SelectionFilter.COMPONENT) {
381
- if (Array.isArray(obj.children)) {
382
- for (const ch of obj.children) restoreOne(ch);
429
+ SelectionState.attach(o);
430
+ o.hovered = false;
431
+ } catch { }
383
432
  }
433
+ SelectionFilter._hovered.clear();
384
434
  }
385
- if (obj.type === SelectionFilter.VERTEX) {
386
- if (Array.isArray(obj.children)) {
387
- for (const ch of obj.children) restoreOne(ch);
388
- }
435
+ if (SelectionFilter._hoveredSourceMap.size) {
436
+ SelectionFilter._hoveredSourceMap.clear();
437
+ }
438
+ if (emit && hadHover) {
439
+ SelectionFilter._emitHoverChanged([]);
389
440
  }
390
- restoreOne(obj);
391
441
  }
392
442
 
393
443
  static #isInputUsable(el) {
@@ -415,246 +465,208 @@ export class SelectionFilter {
415
465
  return true;
416
466
  }
417
467
 
418
- static toggleSelection(objectToToggleSelectionOn) {
419
- // get the type of the object
420
- const type = objectToToggleSelectionOn.type;
421
- if (!type) throw new Error("Object to toggle selection on must have a type.");
422
-
423
- // Check if a reference selection is active
468
+ static #handleReferenceSelection(objectToToggleSelectionOn) {
424
469
  try {
425
470
  let activeRefInput = document.querySelector('[active-reference-selection="true"],[active-reference-selection=true]');
426
471
  if (!activeRefInput) {
427
472
  try { activeRefInput = window.__BREP_activeRefInput || null; } catch (_) { /* ignore */ }
428
473
  }
429
- if (activeRefInput) {
430
- const usable = SelectionFilter.#isInputUsable(activeRefInput);
431
- if (!usable) {
432
- try { activeRefInput.removeAttribute('active-reference-selection'); } catch (_) { }
433
- try { activeRefInput.style.filter = 'none'; } catch (_) { }
434
- try { if (window.__BREP_activeRefInput === activeRefInput) window.__BREP_activeRefInput = null; } catch (_) { }
435
- SelectionFilter.restoreAllowedSelectionTypes();
436
- return false;
437
- }
474
+ if (!activeRefInput) return false;
475
+
476
+ const usable = SelectionFilter.#isInputUsable(activeRefInput);
477
+ if (!usable) {
478
+ try { activeRefInput.removeAttribute('active-reference-selection'); } catch (_) { }
479
+ try { activeRefInput.style.filter = 'none'; } catch (_) { }
480
+ try { if (window.__BREP_activeRefInput === activeRefInput) window.__BREP_activeRefInput = null; } catch (_) { }
481
+ SelectionFilter.restoreAllowedSelectionTypes();
482
+ return false;
483
+ }
438
484
 
439
- const dataset = activeRefInput.dataset || {};
440
- const isMultiRef = dataset.multiple === 'true';
441
- const maxSelections = Number(dataset.maxSelections);
442
- const hasMax = Number.isFinite(maxSelections) && maxSelections > 0;
443
- if (isMultiRef) {
444
- let currentCount = 0;
485
+ const dataset = activeRefInput.dataset || {};
486
+ const isMultiRef = dataset.multiple === 'true';
487
+ const maxSelections = Number(dataset.maxSelections);
488
+ const hasMax = Number.isFinite(maxSelections) && maxSelections > 0;
489
+ if (isMultiRef) {
490
+ let currentCount = 0;
491
+ try {
492
+ if (typeof activeRefInput.__getSelectionList === 'function') {
493
+ const list = activeRefInput.__getSelectionList();
494
+ if (Array.isArray(list)) currentCount = list.length;
495
+ } else if (dataset.selectedCount !== undefined) {
496
+ const parsed = Number(dataset.selectedCount);
497
+ if (Number.isFinite(parsed)) currentCount = parsed;
498
+ }
499
+ } catch (_) { /* ignore */ }
500
+ if (hasMax && currentCount >= maxSelections) {
445
501
  try {
446
- if (typeof activeRefInput.__getSelectionList === 'function') {
447
- const list = activeRefInput.__getSelectionList();
448
- if (Array.isArray(list)) currentCount = list.length;
449
- } else if (dataset.selectedCount !== undefined) {
450
- const parsed = Number(dataset.selectedCount);
451
- if (Number.isFinite(parsed)) currentCount = parsed;
502
+ const wrap = activeRefInput.closest('.ref-single-wrap, .ref-multi-wrap');
503
+ if (wrap) {
504
+ wrap.classList.add('ref-limit-reached');
505
+ setTimeout(() => {
506
+ try { wrap.classList.remove('ref-limit-reached'); } catch (_) { }
507
+ }, 480);
452
508
  }
453
- } catch (_) { /* ignore */ }
454
- if (hasMax && currentCount >= maxSelections) {
455
- try {
456
- const wrap = activeRefInput.closest('.ref-single-wrap, .ref-multi-wrap');
457
- if (wrap) {
458
- wrap.classList.add('ref-limit-reached');
459
- setTimeout(() => {
460
- try { wrap.classList.remove('ref-limit-reached'); } catch (_) { }
461
- }, 480);
462
- }
463
- } catch (_) { }
464
- return true;
465
- }
509
+ } catch (_) { }
510
+ return true;
466
511
  }
467
- const allowed = SelectionFilter.allowedSelectionTypes;
468
- const allowAll = allowed === SelectionFilter.ALL;
469
- const priorityOrder = [
470
- SelectionFilter.VERTEX,
471
- SelectionFilter.EDGE,
472
- SelectionFilter.FACE,
473
- SelectionFilter.PLANE,
474
- SelectionFilter.SOLID,
475
- SelectionFilter.COMPONENT,
476
- ];
477
- const allowedHas = (t) => !!(allowed && typeof allowed.has === 'function' && allowed.has(t));
478
- const allowedPriority = allowAll ? priorityOrder : priorityOrder.filter(t => allowedHas(t));
479
-
480
- // Helper: find first descendant matching a type
481
- const findDescendantOfType = (root, desired) => {
482
- if (!root || !desired) return null;
483
- let found = null;
484
- try {
485
- root.traverse?.((ch) => {
486
- if (!found && ch && ch.type === desired) found = ch;
487
- });
488
- } catch (_) { /* ignore */ }
489
- return found;
490
- };
512
+ }
491
513
 
492
- const findAncestorOfType = (obj, desired) => {
493
- let cur = obj;
494
- while (cur && cur.parent) {
495
- if (cur.type === desired) return cur;
496
- cur = cur.parent;
497
- }
498
- return null;
499
- };
500
- const pickByTypeList = (typeList) => {
501
- if (!Array.isArray(typeList)) return null;
502
- for (const desired of typeList) {
503
- if (!desired) continue;
504
- if (objectToToggleSelectionOn?.type === desired) return objectToToggleSelectionOn;
505
- let picked = findDescendantOfType(objectToToggleSelectionOn, desired);
506
- if (!picked) picked = findAncestorOfType(objectToToggleSelectionOn, desired);
507
- if (picked) return picked;
508
- }
509
- return null;
510
- };
514
+ const allowed = SelectionFilter.allowedSelectionTypes;
515
+ const allowAll = allowed === SelectionFilter.ALL;
516
+ const priorityOrder = [
517
+ SelectionFilter.VERTEX,
518
+ SelectionFilter.EDGE,
519
+ SelectionFilter.FACE,
520
+ SelectionFilter.PLANE,
521
+ SelectionFilter.SKETCH,
522
+ SelectionFilter.LOOP,
523
+ SelectionFilter.SOLID,
524
+ SelectionFilter.COMPONENT,
525
+ ];
526
+ const allowedHas = (t) => !!(allowed && typeof allowed.has === 'function' && allowed.has(t));
527
+ const allowedPriority = allowAll ? priorityOrder : priorityOrder.filter(t => allowedHas(t));
528
+
529
+ const findDescendantOfType = (root, desired) => {
530
+ if (!root || !desired) return null;
531
+ let found = null;
532
+ try {
533
+ root.traverse?.((ch) => {
534
+ if (!found && ch && ch.type === desired) found = ch;
535
+ });
536
+ } catch (_) { /* ignore */ }
537
+ return found;
538
+ };
511
539
 
512
- let targetObj = null;
513
- // Prefer the exact object the user clicked if it is allowed.
514
- if (allowAll || allowedHas(objectToToggleSelectionOn?.type)) {
515
- targetObj = objectToToggleSelectionOn;
516
- }
517
- if (!targetObj) {
518
- targetObj = pickByTypeList(allowedPriority);
540
+ const findAncestorOfType = (obj, desired) => {
541
+ let cur = obj;
542
+ while (cur && cur.parent) {
543
+ if (cur.type === desired) return cur;
544
+ cur = cur.parent;
519
545
  }
520
- if (!targetObj && !allowAll && allowed && typeof allowed[Symbol.iterator] === 'function') {
521
- targetObj = pickByTypeList(Array.from(allowed));
522
- }
523
- if (!targetObj && allowAll) {
524
- targetObj = objectToToggleSelectionOn;
546
+ return null;
547
+ };
548
+
549
+ const pickByTypeList = (typeList) => {
550
+ if (!Array.isArray(typeList)) return null;
551
+ for (const desired of typeList) {
552
+ if (!desired) continue;
553
+ if (objectToToggleSelectionOn?.type === desired) return objectToToggleSelectionOn;
554
+ let picked = findDescendantOfType(objectToToggleSelectionOn, desired);
555
+ if (!picked) picked = findAncestorOfType(objectToToggleSelectionOn, desired);
556
+ if (picked) return picked;
525
557
  }
526
- if (!targetObj) return false;
558
+ return null;
559
+ };
527
560
 
528
- // Update the reference input with the chosen object
529
- try {
530
- if (activeRefInput && typeof activeRefInput.__captureReferencePreview === 'function') {
531
- activeRefInput.__captureReferencePreview(targetObj);
532
- }
533
- } catch (_) { /* ignore preview capture errors */ }
534
- const objType = targetObj.type;
535
- const objectName = targetObj.name || `${objType}(${targetObj.position?.x || 0},${targetObj.position?.y || 0},${targetObj.position?.z || 0})`;
536
-
537
- const snapshotSelections = (inputEl) => {
538
- const data = inputEl?.dataset || {};
539
- let values = null;
540
- if (data.selectedValues) {
541
- try {
542
- const parsed = JSON.parse(data.selectedValues);
543
- if (Array.isArray(parsed)) values = parsed;
544
- } catch (_) { /* ignore */ }
545
- }
546
- if (!Array.isArray(values) && typeof inputEl?.__getSelectionList === 'function') {
547
- try {
548
- const list = inputEl.__getSelectionList();
549
- if (Array.isArray(list)) values = list.slice();
550
- } catch (_) { /* ignore */ }
551
- }
552
- let count = 0;
553
- if (Array.isArray(values)) {
554
- count = values.length;
555
- } else if (data.selectedCount !== undefined) {
556
- const parsed = Number(data.selectedCount);
557
- if (Number.isFinite(parsed) && parsed >= 0) count = parsed;
558
- }
559
- return count;
560
- };
561
+ let targetObj = null;
562
+ if (allowAll || allowedHas(objectToToggleSelectionOn?.type)) {
563
+ targetObj = objectToToggleSelectionOn;
564
+ }
565
+ if (!targetObj) {
566
+ targetObj = pickByTypeList(allowedPriority);
567
+ }
568
+ if (!targetObj && !allowAll && allowed && typeof allowed[Symbol.iterator] === 'function') {
569
+ targetObj = pickByTypeList(Array.from(allowed));
570
+ }
571
+ if (!targetObj && allowAll) {
572
+ targetObj = objectToToggleSelectionOn;
573
+ }
574
+ if (!targetObj) return false;
561
575
 
562
- activeRefInput.value = objectName;
563
- activeRefInput.dispatchEvent(new Event('change'));
564
- const afterSelectionCount = snapshotSelections(activeRefInput);
576
+ try {
577
+ if (activeRefInput && typeof activeRefInput.__captureReferencePreview === 'function') {
578
+ activeRefInput.__captureReferencePreview(targetObj);
579
+ }
580
+ } catch (_) { /* ignore preview capture errors */ }
565
581
 
566
- const didReachLimit = isMultiRef && hasMax && afterSelectionCount >= maxSelections;
567
- const keepActive = isMultiRef && !didReachLimit;
582
+ const objType = targetObj.type;
583
+ const objectName = targetObj.name || `${objType}(${targetObj.position?.x || 0},${targetObj.position?.y || 0},${targetObj.position?.z || 0})`;
568
584
 
569
- if (!keepActive) {
570
- // Clean up the reference selection state
571
- activeRefInput.removeAttribute('active-reference-selection');
572
- activeRefInput.style.filter = 'none';
585
+ const snapshotSelections = (inputEl) => {
586
+ const data = inputEl?.dataset || {};
587
+ let values = null;
588
+ if (data.selectedValues) {
573
589
  try {
574
- const wrap = activeRefInput.closest('.ref-single-wrap, .ref-multi-wrap');
575
- if (wrap) wrap.classList.remove('ref-active');
576
- } catch (_) { }
577
- // Restore selection filter
578
- SelectionFilter.restoreAllowedSelectionTypes();
579
- try { if (window.__BREP_activeRefInput === activeRefInput) window.__BREP_activeRefInput = null; } catch (_) { }
580
- } else {
581
- activeRefInput.setAttribute('active-reference-selection', 'true');
582
- activeRefInput.style.filter = 'invert(1)';
590
+ const parsed = JSON.parse(data.selectedValues);
591
+ if (Array.isArray(parsed)) values = parsed;
592
+ } catch (_) { /* ignore */ }
593
+ }
594
+ if (!Array.isArray(values) && typeof inputEl?.__getSelectionList === 'function') {
583
595
  try {
584
- const wrap = activeRefInput.closest('.ref-single-wrap, .ref-multi-wrap');
585
- if (wrap) wrap.classList.add('ref-active');
586
- } catch (_) { }
587
- try { window.__BREP_activeRefInput = activeRefInput; } catch (_) { }
596
+ const list = inputEl.__getSelectionList();
597
+ if (Array.isArray(list)) values = list.slice();
598
+ } catch (_) { /* ignore */ }
599
+ }
600
+ let count = 0;
601
+ if (Array.isArray(values)) {
602
+ count = values.length;
603
+ } else if (data.selectedCount !== undefined) {
604
+ const parsed = Number(data.selectedCount);
605
+ if (Number.isFinite(parsed) && parsed >= 0) count = parsed;
588
606
  }
589
- return true; // handled as a reference selection
607
+ return count;
608
+ };
609
+
610
+ activeRefInput.value = objectName;
611
+ activeRefInput.dispatchEvent(new Event('change'));
612
+ const afterSelectionCount = snapshotSelections(activeRefInput);
613
+
614
+ const didReachLimit = isMultiRef && hasMax && afterSelectionCount >= maxSelections;
615
+ const keepActive = isMultiRef && !didReachLimit;
616
+
617
+ if (!keepActive) {
618
+ activeRefInput.removeAttribute('active-reference-selection');
619
+ activeRefInput.style.filter = 'none';
620
+ try {
621
+ const wrap = activeRefInput.closest('.ref-single-wrap, .ref-multi-wrap');
622
+ if (wrap) wrap.classList.remove('ref-active');
623
+ } catch (_) { }
624
+ SelectionFilter.restoreAllowedSelectionTypes();
625
+ try { if (window.__BREP_activeRefInput === activeRefInput) window.__BREP_activeRefInput = null; } catch (_) { }
626
+ } else {
627
+ activeRefInput.setAttribute('active-reference-selection', 'true');
628
+ activeRefInput.style.filter = 'invert(1)';
629
+ try {
630
+ const wrap = activeRefInput.closest('.ref-single-wrap, .ref-multi-wrap');
631
+ if (wrap) wrap.classList.add('ref-active');
632
+ } catch (_) { }
633
+ try { window.__BREP_activeRefInput = activeRefInput; } catch (_) { }
590
634
  }
635
+ return true;
591
636
  } catch (error) {
592
- console.warn("Error handling reference selection:", error);
637
+ if (debugMode) {
638
+ console.warn("Error handling reference selection:", error);
639
+ }
640
+ return false;
593
641
  }
642
+ }
594
643
 
644
+ static #toggleStandardSelection(objectToToggleSelectionOn) {
645
+ const type = objectToToggleSelectionOn.type;
595
646
  let parentSelectedAction = false;
596
- // check if the object is selectable and if it is toggle the .selected atribute on the object.
597
- // Allow toggling off even if type is currently disallowed; only block new selections
598
647
  if (SelectionFilter.IsAllowed(type) || objectToToggleSelectionOn.selected === true) {
648
+ SelectionState.attach(objectToToggleSelectionOn);
599
649
  objectToToggleSelectionOn.selected = !objectToToggleSelectionOn.selected;
600
- // change the material on the object to indicate it is selected or not.
601
- if (objectToToggleSelectionOn.selected) {
602
- if (objectToToggleSelectionOn.type === SelectionFilter.FACE) {
603
- objectToToggleSelectionOn.material = CADmaterials.FACE?.SELECTED ?? CADmaterials.FACE;
604
- } else if (objectToToggleSelectionOn.type === SelectionFilter.PLANE) {
605
- objectToToggleSelectionOn.material = CADmaterials.PLANE?.SELECTED ?? CADmaterials.FACE?.SELECTED ?? objectToToggleSelectionOn.material;
606
- } else if (objectToToggleSelectionOn.type === SelectionFilter.EDGE) {
607
- objectToToggleSelectionOn.material = CADmaterials.EDGE?.SELECTED ?? CADmaterials.EDGE;
608
- } else if (objectToToggleSelectionOn.type === SelectionFilter.VERTEX) {
609
- // Vertex visuals are handled by its selected accessor (point + sphere)
610
- } else if (objectToToggleSelectionOn.type === SelectionFilter.SOLID || objectToToggleSelectionOn.type === SelectionFilter.COMPONENT) {
611
- parentSelectedAction = true;
612
- objectToToggleSelectionOn.children.forEach(child => {
613
- // apply selected material based on object type for faces and edges
614
- if (child.type === SelectionFilter.FACE) child.material = CADmaterials.FACE?.SELECTED ?? CADmaterials.FACE;
615
- if (child.type === SelectionFilter.PLANE) child.material = CADmaterials.PLANE?.SELECTED ?? CADmaterials.FACE?.SELECTED ?? child.material;
616
- if (child.type === SelectionFilter.EDGE) child.material = CADmaterials.EDGE?.SELECTED ?? CADmaterials.EDGE;
617
- });
618
- }
619
-
620
- } else {
621
- if (objectToToggleSelectionOn.type === SelectionFilter.FACE) {
622
- objectToToggleSelectionOn.material = SelectionFilter.#getBaseMaterial(objectToToggleSelectionOn) ?? objectToToggleSelectionOn.material;
623
- } else if (objectToToggleSelectionOn.type === SelectionFilter.PLANE) {
624
- objectToToggleSelectionOn.material = SelectionFilter.#getBaseMaterial(objectToToggleSelectionOn) ?? objectToToggleSelectionOn.material;
625
- } else if (objectToToggleSelectionOn.type === SelectionFilter.EDGE) {
626
- objectToToggleSelectionOn.material = SelectionFilter.#getBaseMaterial(objectToToggleSelectionOn) ?? objectToToggleSelectionOn.material;
627
- } else if (objectToToggleSelectionOn.type === SelectionFilter.VERTEX) {
628
- // Vertex accessor handles its own visual reset
629
- } else if (objectToToggleSelectionOn.type === SelectionFilter.SOLID || objectToToggleSelectionOn.type === SelectionFilter.COMPONENT) {
630
- parentSelectedAction = true;
631
- objectToToggleSelectionOn.children.forEach(child => {
632
- // apply selected material based on object type for faces and edges
633
- if (child.type === SelectionFilter.FACE) child.material = child.selected ? CADmaterials.FACE?.SELECTED ?? CADmaterials.FACE : (SelectionFilter.#getBaseMaterial(child) ?? child.material);
634
- if (child.type === SelectionFilter.PLANE) child.material = child.selected ? (CADmaterials.PLANE?.SELECTED ?? CADmaterials.FACE?.SELECTED ?? child.material) : (SelectionFilter.#getBaseMaterial(child) ?? child.material);
635
- if (child.type === SelectionFilter.EDGE) child.material = child.selected ? CADmaterials.EDGE?.SELECTED ?? CADmaterials.EDGE : (SelectionFilter.#getBaseMaterial(child) ?? child.material);
636
- });
637
- }
650
+ if (type === SelectionFilter.SOLID || type === SelectionFilter.COMPONENT) {
651
+ parentSelectedAction = true;
638
652
  }
639
653
  SelectionFilter._emitSelectionChanged();
640
654
  }
641
-
642
655
  return parentSelectedAction;
643
656
  }
644
657
 
658
+ static toggleSelection(objectToToggleSelectionOn) {
659
+ const type = objectToToggleSelectionOn.type;
660
+ if (!type) throw new Error("Object to toggle selection on must have a type.");
661
+ if (SelectionFilter.#handleReferenceSelection(objectToToggleSelectionOn)) return true;
662
+ return SelectionFilter.#toggleStandardSelection(objectToToggleSelectionOn);
663
+ }
664
+
645
665
  static unselectAll(scene) {
646
666
  // itterate over all children and nested children of the scene and set the .selected atribute to false.
647
667
  scene.traverse((child) => {
668
+ SelectionState.attach(child);
648
669
  child.selected = false;
649
- // reset material to base
650
- if (child.type === SelectionFilter.FACE) {
651
- child.material = SelectionFilter.#getBaseMaterial(child) ?? child.material;
652
- } else if (child.type === SelectionFilter.PLANE) {
653
- child.material = SelectionFilter.#getBaseMaterial(child) ?? child.material;
654
- } else if (child.type === SelectionFilter.EDGE) {
655
- child.material = SelectionFilter.#getBaseMaterial(child) ?? child.material;
656
- }
657
-
658
670
  });
659
671
  SelectionFilter._emitSelectionChanged();
660
672
  }
@@ -662,17 +674,8 @@ export class SelectionFilter {
662
674
  static selectItem(scene, itemName) {
663
675
  scene.traverse((child) => {
664
676
  if (child && child.name === itemName) {
677
+ SelectionState.attach(child);
665
678
  child.selected = true;
666
- // change material to selected
667
- if (child.type === SelectionFilter.FACE) {
668
- child.material = CADmaterials.FACE?.SELECTED ?? CADmaterials.FACE;
669
- } else if (child.type === SelectionFilter.PLANE) {
670
- child.material = CADmaterials.PLANE?.SELECTED ?? CADmaterials.FACE?.SELECTED ?? child.material;
671
- } else if (child.type === SelectionFilter.EDGE) {
672
- child.material = CADmaterials.EDGE?.SELECTED ?? CADmaterials.EDGE;
673
- } else if (child.type === SelectionFilter.SOLID || child.type === SelectionFilter.COMPONENT) {
674
- child.material = CADmaterials.SOLID?.SELECTED ?? CADmaterials.SOLID;
675
- }
676
679
  }
677
680
  });
678
681
  SelectionFilter._emitSelectionChanged();
@@ -682,27 +685,8 @@ export class SelectionFilter {
682
685
  // Traverse scene and deselect a single item by name, updating materials appropriately
683
686
  scene.traverse((child) => {
684
687
  if (child.name === itemName) {
688
+ SelectionState.attach(child);
685
689
  child.selected = false;
686
- if (child.type === SelectionFilter.FACE) {
687
- child.material = SelectionFilter.#getBaseMaterial(child) ?? child.material;
688
- } else if (child.type === SelectionFilter.PLANE) {
689
- child.material = SelectionFilter.#getBaseMaterial(child) ?? child.material;
690
- } else if (child.type === SelectionFilter.EDGE) {
691
- child.material = SelectionFilter.#getBaseMaterial(child) ?? child.material;
692
- } else if (child.type === SelectionFilter.SOLID || child.type === SelectionFilter.COMPONENT) {
693
- // For solids, keep children materials consistent with their own selected flags
694
- child.children.forEach(grandchild => {
695
- if (grandchild.type === SelectionFilter.FACE) {
696
- grandchild.material = grandchild.selected ? (CADmaterials.FACE?.SELECTED ?? CADmaterials.FACE) : (SelectionFilter.#getBaseMaterial(grandchild) ?? grandchild.material);
697
- }
698
- if (grandchild.type === SelectionFilter.PLANE) {
699
- grandchild.material = grandchild.selected ? (CADmaterials.PLANE?.SELECTED ?? CADmaterials.FACE?.SELECTED ?? grandchild.material) : (SelectionFilter.#getBaseMaterial(grandchild) ?? grandchild.material);
700
- }
701
- if (grandchild.type === SelectionFilter.EDGE) {
702
- grandchild.material = grandchild.selected ? (CADmaterials.EDGE?.SELECTED ?? CADmaterials.EDGE) : (SelectionFilter.#getBaseMaterial(grandchild) ?? grandchild.material);
703
- }
704
- });
705
- }
706
690
  }
707
691
  });
708
692
  SelectionFilter._emitSelectionChanged();
@@ -716,6 +700,21 @@ export class SelectionFilter {
716
700
 
717
701
  // Emit a global event so UI can react without polling
718
702
  static _emitSelectionChanged() {
703
+ try {
704
+ const selection = SelectionFilter.getSelectedObjects();
705
+ const names = selection.map((obj) => (
706
+ obj?.name
707
+ || obj?.userData?.faceName
708
+ || obj?.userData?.edgeName
709
+ || obj?.userData?.vertexName
710
+ || obj?.userData?.solidName
711
+ || obj?.userData?.name
712
+ || obj?.type
713
+ || 'Object'
714
+ ));
715
+ const desc = names.length ? names.join(', ') : '(none)';
716
+ console.log(`[SelectionFilter] selection changed -> ${desc}`);
717
+ } catch { /* noop */ }
719
718
  try {
720
719
  const ev = new CustomEvent('selection-changed');
721
720
  window.dispatchEvent(ev);
@@ -874,6 +873,7 @@ export class SelectionFilter {
874
873
  static _getHistoryContextActionSpecs(selection, viewer) {
875
874
  const out = [];
876
875
  const items = Array.isArray(selection) ? selection : [];
876
+ const suppressFeatureButtons = SelectionFilter._hasAssemblyComponentSelection(items, viewer);
877
877
  const safeId = (prefix, key) => {
878
878
  const raw = String(key || '').toLowerCase().replace(/[^a-z0-9_-]+/g, '-');
879
879
  return `${prefix}-${raw || 'item'}`;
@@ -887,21 +887,23 @@ export class SelectionFilter {
887
887
  if (!pmiActive) {
888
888
  const featureRegistry = viewer?.partHistory?.featureRegistry || null;
889
889
  const features = Array.isArray(featureRegistry?.features) ? featureRegistry.features : [];
890
- for (const FeatureClass of features) {
891
- if (!FeatureClass) continue;
892
- let result = null;
893
- try { result = FeatureClass.showContexButton?.(items); } catch { result = null; }
894
- if (!result) continue;
895
- if (result && typeof result === 'object' && result.show === false) continue;
896
- const label = (result && typeof result === 'object' && result.label) || FeatureClass.longName || FeatureClass.shortName || FeatureClass.name || 'Feature';
897
- const typeKey = FeatureClass.shortName || FeatureClass.type || FeatureClass.name || label;
898
- const params = SelectionFilter._extractContextParams(result);
899
- addSpec({
900
- id: safeId('ctx-feature', typeKey),
901
- label,
902
- title: `Create ${label}`,
903
- onClick: () => SelectionFilter._createFeatureFromContext(viewer, typeKey, params),
904
- });
890
+ if (!suppressFeatureButtons) {
891
+ for (const FeatureClass of features) {
892
+ if (!FeatureClass) continue;
893
+ let result = null;
894
+ try { result = FeatureClass.showContexButton?.(items); } catch { result = null; }
895
+ if (!result) continue;
896
+ if (result && typeof result === 'object' && result.show === false) continue;
897
+ const label = (result && typeof result === 'object' && result.label) || FeatureClass.longName || FeatureClass.shortName || FeatureClass.name || 'Feature';
898
+ const typeKey = FeatureClass.shortName || FeatureClass.type || FeatureClass.name || label;
899
+ const params = SelectionFilter._extractContextParams(result);
900
+ addSpec({
901
+ id: safeId('ctx-feature', typeKey),
902
+ label,
903
+ title: `Create ${label}`,
904
+ onClick: () => SelectionFilter._createFeatureFromContext(viewer, typeKey, params),
905
+ });
906
+ }
905
907
  }
906
908
 
907
909
  const constraintRegistry = viewer?.partHistory?.assemblyConstraintRegistry || null;
@@ -952,6 +954,28 @@ export class SelectionFilter {
952
954
  return out;
953
955
  }
954
956
 
957
+ static _hasAssemblyComponentSelection(items, viewer) {
958
+ if (!Array.isArray(items) || !items.length) return false;
959
+ const findComponent = (obj) => {
960
+ if (!obj) return null;
961
+ if (viewer && typeof viewer._findOwningComponent === 'function') {
962
+ try { return viewer._findOwningComponent(obj); } catch { /* ignore */ }
963
+ }
964
+ let cur = obj;
965
+ while (cur) {
966
+ if (cur.isAssemblyComponent || cur.type === SelectionFilter.COMPONENT || cur.type === 'COMPONENT') return cur;
967
+ cur = cur.parent || null;
968
+ }
969
+ return null;
970
+ };
971
+ for (const item of items) {
972
+ const obj = item?.object || item?.target || item;
973
+ if (!obj) continue;
974
+ if (findComponent(obj)) return true;
975
+ }
976
+ return false;
977
+ }
978
+
955
979
  static async _createFeatureFromContext(viewer, typeKey, params = null) {
956
980
  if (!viewer || !typeKey) return;
957
981
  SelectionFilter.setContextBarSuppressed('context-create', true);
@@ -1127,6 +1151,8 @@ export class SelectionFilter {
1127
1151
  FACE: 'Face',
1128
1152
  PLANE: 'Plane',
1129
1153
  SKETCH: 'Sketch',
1154
+ DATUM: 'Datum',
1155
+ HELIX: 'Helix',
1130
1156
  EDGE: 'Edge',
1131
1157
  LOOP: 'Loop',
1132
1158
  VERTEX: 'Vertex',
@@ -1494,6 +1520,7 @@ export class SelectionFilter {
1494
1520
  }
1495
1521
 
1496
1522
  static #logAllowedTypesChange(next, reason = '') {
1523
+ if (!debugMode) return;
1497
1524
  try {
1498
1525
  const desc = next === SelectionFilter.ALL
1499
1526
  ? 'ALL'