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
@@ -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 = [];
@@ -26,12 +35,30 @@ export class SelectionFilter {
26
35
  static _historyContextActions = new Map();
27
36
  static _selectionActionSeparator = null;
28
37
  static _contextSuppressReasons = new Set();
38
+ static _selectionFilterIndicator = null;
39
+ static _selectionFilterIndicatorToggle = null;
40
+ static _selectionFilterIndicatorPanel = null;
41
+ static _selectionFilterCheckboxes = new Map();
42
+ static _selectionFilterTypes = null;
43
+ static _selectionFilterOutsideBound = false;
44
+ static _selectionFilterTintBtn = null;
45
+ static _clickWatcherTimer = null;
46
+ static _missingClickLogged = new Map();
47
+ static _clickWatcherIntervalMs = 2000;
48
+ static _onClickWatcherSeq = 1;
49
+ static _selectableTintState = {
50
+ active: false,
51
+ activeColor: null,
52
+ colorIndex: 0,
53
+ colors: ['#34d399', '#f97316', '#60a5fa', '#f43f5e'],
54
+ materials: new Map(),
55
+ };
29
56
 
30
57
  constructor() {
31
58
  throw new Error("SelectionFilter is static and cannot be instantiated.");
32
59
  }
33
60
 
34
- 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]; }
35
62
 
36
63
  // Convenience: return the list of selectable types for the dropdown (excludes ALL)
37
64
  static getAvailableTypes() {
@@ -47,6 +74,144 @@ export class SelectionFilter {
47
74
  return null;
48
75
  }
49
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
+
50
215
  static setCurrentType(_type) {
51
216
  // No-op: current type is no longer tracked.
52
217
  void _type;
@@ -82,7 +247,50 @@ export class SelectionFilter {
82
247
  }
83
248
  }
84
249
 
250
+ static ensureSelectionHandlers(obj, { deep = false } = {}) {
251
+ if (!obj || typeof obj !== 'object') return false;
252
+ let changed = false;
253
+ const attach = (target) => {
254
+ if (!target || typeof target !== 'object') return;
255
+ SelectionState.attach(target);
256
+ SelectionFilter._installOnClickWatcher(target);
257
+ if (typeof target.onClick === 'function') 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
+ }
271
+ }
272
+ };
273
+ });
274
+ try { target.onClick.__brepSelectionHandler = true; } catch (_) { /* ignore */ }
275
+ changed = true;
276
+ };
277
+
278
+ if (!deep) {
279
+ attach(obj);
280
+ return changed;
281
+ }
85
282
 
283
+ const stack = [obj];
284
+ while (stack.length) {
285
+ const current = stack.pop();
286
+ attach(current);
287
+ const kids = Array.isArray(current?.children) ? current.children : [];
288
+ for (const child of kids) {
289
+ if (child) stack.push(child);
290
+ }
291
+ }
292
+ return changed;
293
+ }
86
294
 
87
295
  static allowType(type) {
88
296
  // Legacy support: expand available set; does not change currentType
@@ -127,37 +335,67 @@ export class SelectionFilter {
127
335
  SelectionFilter.#logAllowedTypesChange(SelectionFilter.allowedSelectionTypes, 'Reset');
128
336
  }
129
337
 
130
- static #getBaseMaterial(obj) {
131
- if (!obj) return null;
132
- const ud = obj.userData || {};
133
- if (ud.__baseMaterial) return ud.__baseMaterial;
134
- if (obj.type === SelectionFilter.FACE) return CADmaterials.FACE?.BASE ?? CADmaterials.FACE ?? obj.material;
135
- if (obj.type === SelectionFilter.PLANE) return CADmaterials.PLANE?.BASE ?? CADmaterials.FACE?.BASE ?? obj.material;
136
- if (obj.type === SelectionFilter.EDGE) return CADmaterials.EDGE?.BASE ?? CADmaterials.EDGE ?? obj.material;
137
- if (obj.type === SelectionFilter.SOLID || obj.type === SelectionFilter.COMPONENT) return obj.material;
138
- return obj.material;
139
- }
140
-
141
338
  // ---------------- Hover Highlighting ----------------
142
- static getHoverColor() { return SelectionFilter.hoverColor; }
339
+ static getHoverColor() { return SelectionState.hoverColor || SelectionFilter.hoverColor; }
143
340
  static setHoverColor(hex) {
144
341
  if (!hex) return;
145
342
  try { SelectionFilter.hoverColor = String(hex); } catch (_) { }
343
+ SelectionState.setHoverColor(SelectionFilter.hoverColor);
146
344
  // Update current hovered objects live
147
345
  for (const o of Array.from(SelectionFilter._hovered)) {
148
- if (o && o.material && o.material.color && typeof o.material.color.set === 'function') {
149
- try { o.material.color.set(SelectionFilter.hoverColor); } catch (_) { }
150
- }
346
+ if (!o) continue;
347
+ try {
348
+ SelectionState.attach(o);
349
+ o.hovered = false;
350
+ o.hovered = true;
351
+ } catch { }
151
352
  }
152
353
  }
153
354
 
154
355
  static setHoverObject(obj, options = {}) {
155
- const { ignoreFilter = false } = options;
156
- // Clear existing hover first
157
- SelectionFilter.clearHover();
158
- if (!obj) return;
159
- // Highlight depending on type
160
- 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
+ }
161
399
  }
162
400
 
163
401
  static setHoverByName(scene, name) {
@@ -168,157 +406,38 @@ export class SelectionFilter {
168
406
  }
169
407
 
170
408
  static clearHover() {
171
- if (!SelectionFilter._hovered || SelectionFilter._hovered.size === 0) return;
172
- for (const o of Array.from(SelectionFilter._hovered)) {
173
- SelectionFilter.#restoreHover(o);
174
- }
175
- SelectionFilter._hovered.clear();
409
+ SelectionFilter._clearHoverState({ emit: true });
176
410
  }
177
411
 
178
- static #applyHover(obj, options = {}) {
179
- const { ignoreFilter = false } = options;
180
- if (!obj) return;
181
- // Respect selection filter: skip if disallowed
182
- const allowed = ignoreFilter
183
- || SelectionFilter.IsAllowed(obj.type)
184
- || SelectionFilter.matchesAllowedType(obj.type);
185
- if (!allowed) return;
186
-
187
- // Only ever highlight one object: the exact object provided, if it has a color
188
- const target = obj;
189
- if (!target) return;
190
-
191
- const applyToOne = (t) => {
192
- if (!t) return;
193
- if (!t.userData) t.userData = {};
194
- const origMat = t.material;
195
- if (!origMat) return;
196
- if (t.userData.__hoverMatApplied) { SelectionFilter._hovered.add(t); return; }
197
- let clone;
198
- try { clone = typeof origMat.clone === 'function' ? origMat.clone() : origMat; } catch { clone = origMat; }
199
- try { if (clone && clone.color && typeof clone.color.set === 'function') clone.color.set(SelectionFilter.hoverColor); } catch { }
200
- try {
201
- if (origMat && clone && origMat.resolution && clone.resolution && typeof clone.resolution.copy === 'function') {
202
- clone.resolution.copy(origMat.resolution);
203
- }
204
- } catch { }
205
- try {
206
- if (origMat && clone && typeof origMat.dashed !== 'undefined' && typeof clone.dashed !== 'undefined') {
207
- clone.dashed = origMat.dashed;
208
- }
209
- if (origMat && clone && typeof origMat.dashSize !== 'undefined' && typeof clone.dashSize !== 'undefined') {
210
- clone.dashSize = origMat.dashSize;
211
- }
212
- if (origMat && clone && typeof origMat.gapSize !== 'undefined' && typeof clone.gapSize !== 'undefined') {
213
- clone.gapSize = origMat.gapSize;
214
- }
215
- if (origMat && clone && typeof origMat.dashScale !== 'undefined' && typeof clone.dashScale !== 'undefined') {
216
- clone.dashScale = origMat.dashScale;
217
- }
218
- } catch { }
219
- try {
220
- t.userData.__hoverOrigMat = origMat;
221
- t.userData.__hoverMatApplied = true;
222
- if (clone !== origMat) t.material = clone;
223
- t.userData.__hoverMat = clone;
224
- } catch { }
225
- SelectionFilter._hovered.add(t);
226
- };
227
-
228
- if (target.type === SelectionFilter.SOLID || target.type === SelectionFilter.COMPONENT) {
229
- // Highlight all immediate child faces/edges for SOLID or COMPONENT children
230
- if (Array.isArray(target.children)) {
231
- for (const ch of target.children) {
232
- if (!ch) continue;
233
- if (ch.type === SelectionFilter.SOLID || ch.type === SelectionFilter.COMPONENT) {
234
- if (Array.isArray(ch.children)) {
235
- for (const nested of ch.children) {
236
- if (nested && (nested.type === SelectionFilter.FACE || nested.type === SelectionFilter.EDGE)) applyToOne(nested);
237
- }
238
- }
239
- } else if (ch.type === SelectionFilter.FACE || ch.type === SelectionFilter.EDGE) {
240
- applyToOne(ch);
241
- }
242
- }
243
- }
244
- // Track the solid as a logical hovered root to clear later
245
- SelectionFilter._hovered.add(target);
246
- return;
247
- }
248
-
249
- if (target.type === SelectionFilter.VERTEX) {
250
- // Apply to the vertex object and any drawable children (e.g., Points)
251
- applyToOne(target);
252
- if (Array.isArray(target.children)) {
253
- for (const ch of target.children) {
254
- applyToOne(ch);
255
- }
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);
256
418
  }
257
- return;
258
- }
259
-
260
- applyToOne(target);
419
+ const ev = new CustomEvent('hover-changed', { detail: { objects: list, uuids } });
420
+ window.dispatchEvent(ev);
421
+ } catch { /* ignore */ }
261
422
  }
262
423
 
263
- static #restoreHover(obj) {
264
- if (!obj) return;
265
- const restoreOne = (t) => {
266
- if (!t) return;
267
- const ud = t.userData || {};
268
- if (!ud.__hoverMatApplied) return;
269
- const cloneWithColor = (mat, colorHex) => {
270
- if (!mat) return mat;
271
- 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)) {
272
428
  try {
273
- c = typeof mat.clone === 'function' ? mat.clone() : mat;
274
- if (c && c.color && typeof c.color.set === 'function' && colorHex) c.color.set(colorHex);
275
- } catch { c = mat; }
276
- return c;
277
- };
278
- const applySelectionMaterial = () => {
279
- if (t.type === SelectionFilter.FACE) return CADmaterials.FACE?.SELECTED ?? CADmaterials.FACE ?? ud.__hoverOrigMat ?? t.material;
280
- if (t.type === SelectionFilter.PLANE) return CADmaterials.PLANE?.SELECTED ?? CADmaterials.FACE?.SELECTED ?? ud.__hoverOrigMat ?? t.material;
281
- if (t.type === SelectionFilter.EDGE) {
282
- const base = ud.__hoverOrigMat ?? t.material;
283
- const selColor = CADmaterials.EDGE?.SELECTED?.color || CADmaterials.EDGE?.SELECTED?.color?.getHexString?.();
284
- return cloneWithColor(base, selColor || '#ff00ff');
285
- }
286
- if (t.type === SelectionFilter.SOLID || t.type === SelectionFilter.COMPONENT) return CADmaterials.SOLID?.SELECTED ?? ud.__hoverOrigMat ?? t.material;
287
- return ud.__hoverOrigMat ?? t.material;
288
- };
289
- const applyDeselectedMaterial = () => {
290
- if (t.type === SelectionFilter.FACE) return ud.__hoverOrigMat ?? SelectionFilter.#getBaseMaterial(t) ?? t.material;
291
- if (t.type === SelectionFilter.PLANE) return ud.__hoverOrigMat ?? SelectionFilter.#getBaseMaterial(t) ?? t.material;
292
- if (t.type === SelectionFilter.EDGE) return ud.__hoverOrigMat ?? SelectionFilter.#getBaseMaterial(t) ?? t.material;
293
- if (t.type === SelectionFilter.SOLID || t.type === SelectionFilter.COMPONENT) return ud.__hoverOrigMat ?? t.material;
294
- return ud.__hoverOrigMat ?? t.material;
295
- };
296
- try {
297
- if (t.selected) {
298
- const selMat = applySelectionMaterial();
299
- if (selMat) t.material = selMat;
300
- } else {
301
- const baseMat = applyDeselectedMaterial();
302
- if (baseMat) t.material = baseMat;
303
- }
304
- if (ud.__hoverMat && ud.__hoverMat !== ud.__hoverOrigMat && typeof ud.__hoverMat.dispose === 'function') ud.__hoverMat.dispose();
305
- } catch { }
306
- try { delete t.userData.__hoverMatApplied; } catch { }
307
- try { delete t.userData.__hoverOrigMat; } catch { }
308
- try { delete t.userData.__hoverMat; } catch { }
309
- };
310
-
311
- if (obj.type === SelectionFilter.SOLID || obj.type === SelectionFilter.COMPONENT) {
312
- if (Array.isArray(obj.children)) {
313
- for (const ch of obj.children) restoreOne(ch);
429
+ SelectionState.attach(o);
430
+ o.hovered = false;
431
+ } catch { }
314
432
  }
433
+ SelectionFilter._hovered.clear();
315
434
  }
316
- if (obj.type === SelectionFilter.VERTEX) {
317
- if (Array.isArray(obj.children)) {
318
- for (const ch of obj.children) restoreOne(ch);
319
- }
435
+ if (SelectionFilter._hoveredSourceMap.size) {
436
+ SelectionFilter._hoveredSourceMap.clear();
437
+ }
438
+ if (emit && hadHover) {
439
+ SelectionFilter._emitHoverChanged([]);
320
440
  }
321
- restoreOne(obj);
322
441
  }
323
442
 
324
443
  static #isInputUsable(el) {
@@ -346,246 +465,208 @@ export class SelectionFilter {
346
465
  return true;
347
466
  }
348
467
 
349
- static toggleSelection(objectToToggleSelectionOn) {
350
- // get the type of the object
351
- const type = objectToToggleSelectionOn.type;
352
- if (!type) throw new Error("Object to toggle selection on must have a type.");
353
-
354
- // Check if a reference selection is active
468
+ static #handleReferenceSelection(objectToToggleSelectionOn) {
355
469
  try {
356
470
  let activeRefInput = document.querySelector('[active-reference-selection="true"],[active-reference-selection=true]');
357
471
  if (!activeRefInput) {
358
472
  try { activeRefInput = window.__BREP_activeRefInput || null; } catch (_) { /* ignore */ }
359
473
  }
360
- if (activeRefInput) {
361
- const usable = SelectionFilter.#isInputUsable(activeRefInput);
362
- if (!usable) {
363
- try { activeRefInput.removeAttribute('active-reference-selection'); } catch (_) { }
364
- try { activeRefInput.style.filter = 'none'; } catch (_) { }
365
- try { if (window.__BREP_activeRefInput === activeRefInput) window.__BREP_activeRefInput = null; } catch (_) { }
366
- SelectionFilter.restoreAllowedSelectionTypes();
367
- return false;
368
- }
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
+ }
369
484
 
370
- const dataset = activeRefInput.dataset || {};
371
- const isMultiRef = dataset.multiple === 'true';
372
- const maxSelections = Number(dataset.maxSelections);
373
- const hasMax = Number.isFinite(maxSelections) && maxSelections > 0;
374
- if (isMultiRef) {
375
- 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) {
376
501
  try {
377
- if (typeof activeRefInput.__getSelectionList === 'function') {
378
- const list = activeRefInput.__getSelectionList();
379
- if (Array.isArray(list)) currentCount = list.length;
380
- } else if (dataset.selectedCount !== undefined) {
381
- const parsed = Number(dataset.selectedCount);
382
- 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);
383
508
  }
384
- } catch (_) { /* ignore */ }
385
- if (hasMax && currentCount >= maxSelections) {
386
- try {
387
- const wrap = activeRefInput.closest('.ref-single-wrap, .ref-multi-wrap');
388
- if (wrap) {
389
- wrap.classList.add('ref-limit-reached');
390
- setTimeout(() => {
391
- try { wrap.classList.remove('ref-limit-reached'); } catch (_) { }
392
- }, 480);
393
- }
394
- } catch (_) { }
395
- return true;
396
- }
509
+ } catch (_) { }
510
+ return true;
397
511
  }
398
- const allowed = SelectionFilter.allowedSelectionTypes;
399
- const allowAll = allowed === SelectionFilter.ALL;
400
- const priorityOrder = [
401
- SelectionFilter.VERTEX,
402
- SelectionFilter.EDGE,
403
- SelectionFilter.FACE,
404
- SelectionFilter.PLANE,
405
- SelectionFilter.SOLID,
406
- SelectionFilter.COMPONENT,
407
- ];
408
- const allowedHas = (t) => !!(allowed && typeof allowed.has === 'function' && allowed.has(t));
409
- const allowedPriority = allowAll ? priorityOrder : priorityOrder.filter(t => allowedHas(t));
410
-
411
- // Helper: find first descendant matching a type
412
- const findDescendantOfType = (root, desired) => {
413
- if (!root || !desired) return null;
414
- let found = null;
415
- try {
416
- root.traverse?.((ch) => {
417
- if (!found && ch && ch.type === desired) found = ch;
418
- });
419
- } catch (_) { /* ignore */ }
420
- return found;
421
- };
512
+ }
422
513
 
423
- const findAncestorOfType = (obj, desired) => {
424
- let cur = obj;
425
- while (cur && cur.parent) {
426
- if (cur.type === desired) return cur;
427
- cur = cur.parent;
428
- }
429
- return null;
430
- };
431
- const pickByTypeList = (typeList) => {
432
- if (!Array.isArray(typeList)) return null;
433
- for (const desired of typeList) {
434
- if (!desired) continue;
435
- if (objectToToggleSelectionOn?.type === desired) return objectToToggleSelectionOn;
436
- let picked = findDescendantOfType(objectToToggleSelectionOn, desired);
437
- if (!picked) picked = findAncestorOfType(objectToToggleSelectionOn, desired);
438
- if (picked) return picked;
439
- }
440
- return null;
441
- };
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
+ };
442
539
 
443
- let targetObj = null;
444
- // Prefer the exact object the user clicked if it is allowed.
445
- if (allowAll || allowedHas(objectToToggleSelectionOn?.type)) {
446
- targetObj = objectToToggleSelectionOn;
447
- }
448
- if (!targetObj) {
449
- targetObj = pickByTypeList(allowedPriority);
450
- }
451
- if (!targetObj && !allowAll && allowed && typeof allowed[Symbol.iterator] === 'function') {
452
- targetObj = pickByTypeList(Array.from(allowed));
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;
453
545
  }
454
- if (!targetObj && allowAll) {
455
- 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;
456
557
  }
457
- if (!targetObj) return false;
558
+ return null;
559
+ };
458
560
 
459
- // Update the reference input with the chosen object
460
- try {
461
- if (activeRefInput && typeof activeRefInput.__captureReferencePreview === 'function') {
462
- activeRefInput.__captureReferencePreview(targetObj);
463
- }
464
- } catch (_) { /* ignore preview capture errors */ }
465
- const objType = targetObj.type;
466
- const objectName = targetObj.name || `${objType}(${targetObj.position?.x || 0},${targetObj.position?.y || 0},${targetObj.position?.z || 0})`;
467
-
468
- const snapshotSelections = (inputEl) => {
469
- const data = inputEl?.dataset || {};
470
- let values = null;
471
- if (data.selectedValues) {
472
- try {
473
- const parsed = JSON.parse(data.selectedValues);
474
- if (Array.isArray(parsed)) values = parsed;
475
- } catch (_) { /* ignore */ }
476
- }
477
- if (!Array.isArray(values) && typeof inputEl?.__getSelectionList === 'function') {
478
- try {
479
- const list = inputEl.__getSelectionList();
480
- if (Array.isArray(list)) values = list.slice();
481
- } catch (_) { /* ignore */ }
482
- }
483
- let count = 0;
484
- if (Array.isArray(values)) {
485
- count = values.length;
486
- } else if (data.selectedCount !== undefined) {
487
- const parsed = Number(data.selectedCount);
488
- if (Number.isFinite(parsed) && parsed >= 0) count = parsed;
489
- }
490
- return count;
491
- };
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;
492
575
 
493
- activeRefInput.value = objectName;
494
- activeRefInput.dispatchEvent(new Event('change'));
495
- const afterSelectionCount = snapshotSelections(activeRefInput);
576
+ try {
577
+ if (activeRefInput && typeof activeRefInput.__captureReferencePreview === 'function') {
578
+ activeRefInput.__captureReferencePreview(targetObj);
579
+ }
580
+ } catch (_) { /* ignore preview capture errors */ }
496
581
 
497
- const didReachLimit = isMultiRef && hasMax && afterSelectionCount >= maxSelections;
498
- 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})`;
499
584
 
500
- if (!keepActive) {
501
- // Clean up the reference selection state
502
- activeRefInput.removeAttribute('active-reference-selection');
503
- activeRefInput.style.filter = 'none';
585
+ const snapshotSelections = (inputEl) => {
586
+ const data = inputEl?.dataset || {};
587
+ let values = null;
588
+ if (data.selectedValues) {
504
589
  try {
505
- const wrap = activeRefInput.closest('.ref-single-wrap, .ref-multi-wrap');
506
- if (wrap) wrap.classList.remove('ref-active');
507
- } catch (_) { }
508
- // Restore selection filter
509
- SelectionFilter.restoreAllowedSelectionTypes();
510
- try { if (window.__BREP_activeRefInput === activeRefInput) window.__BREP_activeRefInput = null; } catch (_) { }
511
- } else {
512
- activeRefInput.setAttribute('active-reference-selection', 'true');
513
- 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') {
514
595
  try {
515
- const wrap = activeRefInput.closest('.ref-single-wrap, .ref-multi-wrap');
516
- if (wrap) wrap.classList.add('ref-active');
517
- } catch (_) { }
518
- 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;
519
606
  }
520
- 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 (_) { }
521
634
  }
635
+ return true;
522
636
  } catch (error) {
523
- console.warn("Error handling reference selection:", error);
637
+ if (debugMode) {
638
+ console.warn("Error handling reference selection:", error);
639
+ }
640
+ return false;
524
641
  }
642
+ }
525
643
 
644
+ static #toggleStandardSelection(objectToToggleSelectionOn) {
645
+ const type = objectToToggleSelectionOn.type;
526
646
  let parentSelectedAction = false;
527
- // check if the object is selectable and if it is toggle the .selected atribute on the object.
528
- // Allow toggling off even if type is currently disallowed; only block new selections
529
647
  if (SelectionFilter.IsAllowed(type) || objectToToggleSelectionOn.selected === true) {
648
+ SelectionState.attach(objectToToggleSelectionOn);
530
649
  objectToToggleSelectionOn.selected = !objectToToggleSelectionOn.selected;
531
- // change the material on the object to indicate it is selected or not.
532
- if (objectToToggleSelectionOn.selected) {
533
- if (objectToToggleSelectionOn.type === SelectionFilter.FACE) {
534
- objectToToggleSelectionOn.material = CADmaterials.FACE?.SELECTED ?? CADmaterials.FACE;
535
- } else if (objectToToggleSelectionOn.type === SelectionFilter.PLANE) {
536
- objectToToggleSelectionOn.material = CADmaterials.PLANE?.SELECTED ?? CADmaterials.FACE?.SELECTED ?? objectToToggleSelectionOn.material;
537
- } else if (objectToToggleSelectionOn.type === SelectionFilter.EDGE) {
538
- objectToToggleSelectionOn.material = CADmaterials.EDGE?.SELECTED ?? CADmaterials.EDGE;
539
- } else if (objectToToggleSelectionOn.type === SelectionFilter.VERTEX) {
540
- // Vertex visuals are handled by its selected accessor (point + sphere)
541
- } else if (objectToToggleSelectionOn.type === SelectionFilter.SOLID || objectToToggleSelectionOn.type === SelectionFilter.COMPONENT) {
542
- parentSelectedAction = true;
543
- objectToToggleSelectionOn.children.forEach(child => {
544
- // apply selected material based on object type for faces and edges
545
- if (child.type === SelectionFilter.FACE) child.material = CADmaterials.FACE?.SELECTED ?? CADmaterials.FACE;
546
- if (child.type === SelectionFilter.PLANE) child.material = CADmaterials.PLANE?.SELECTED ?? CADmaterials.FACE?.SELECTED ?? child.material;
547
- if (child.type === SelectionFilter.EDGE) child.material = CADmaterials.EDGE?.SELECTED ?? CADmaterials.EDGE;
548
- });
549
- }
550
-
551
- } else {
552
- if (objectToToggleSelectionOn.type === SelectionFilter.FACE) {
553
- objectToToggleSelectionOn.material = SelectionFilter.#getBaseMaterial(objectToToggleSelectionOn) ?? objectToToggleSelectionOn.material;
554
- } else if (objectToToggleSelectionOn.type === SelectionFilter.PLANE) {
555
- objectToToggleSelectionOn.material = SelectionFilter.#getBaseMaterial(objectToToggleSelectionOn) ?? objectToToggleSelectionOn.material;
556
- } else if (objectToToggleSelectionOn.type === SelectionFilter.EDGE) {
557
- objectToToggleSelectionOn.material = SelectionFilter.#getBaseMaterial(objectToToggleSelectionOn) ?? objectToToggleSelectionOn.material;
558
- } else if (objectToToggleSelectionOn.type === SelectionFilter.VERTEX) {
559
- // Vertex accessor handles its own visual reset
560
- } else if (objectToToggleSelectionOn.type === SelectionFilter.SOLID || objectToToggleSelectionOn.type === SelectionFilter.COMPONENT) {
561
- parentSelectedAction = true;
562
- objectToToggleSelectionOn.children.forEach(child => {
563
- // apply selected material based on object type for faces and edges
564
- if (child.type === SelectionFilter.FACE) child.material = child.selected ? CADmaterials.FACE?.SELECTED ?? CADmaterials.FACE : (SelectionFilter.#getBaseMaterial(child) ?? child.material);
565
- if (child.type === SelectionFilter.PLANE) child.material = child.selected ? (CADmaterials.PLANE?.SELECTED ?? CADmaterials.FACE?.SELECTED ?? child.material) : (SelectionFilter.#getBaseMaterial(child) ?? child.material);
566
- if (child.type === SelectionFilter.EDGE) child.material = child.selected ? CADmaterials.EDGE?.SELECTED ?? CADmaterials.EDGE : (SelectionFilter.#getBaseMaterial(child) ?? child.material);
567
- });
568
- }
650
+ if (type === SelectionFilter.SOLID || type === SelectionFilter.COMPONENT) {
651
+ parentSelectedAction = true;
569
652
  }
570
653
  SelectionFilter._emitSelectionChanged();
571
654
  }
572
-
573
655
  return parentSelectedAction;
574
656
  }
575
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
+
576
665
  static unselectAll(scene) {
577
666
  // itterate over all children and nested children of the scene and set the .selected atribute to false.
578
667
  scene.traverse((child) => {
668
+ SelectionState.attach(child);
579
669
  child.selected = false;
580
- // reset material to base
581
- if (child.type === SelectionFilter.FACE) {
582
- child.material = SelectionFilter.#getBaseMaterial(child) ?? child.material;
583
- } else if (child.type === SelectionFilter.PLANE) {
584
- child.material = SelectionFilter.#getBaseMaterial(child) ?? child.material;
585
- } else if (child.type === SelectionFilter.EDGE) {
586
- child.material = SelectionFilter.#getBaseMaterial(child) ?? child.material;
587
- }
588
-
589
670
  });
590
671
  SelectionFilter._emitSelectionChanged();
591
672
  }
@@ -593,17 +674,8 @@ export class SelectionFilter {
593
674
  static selectItem(scene, itemName) {
594
675
  scene.traverse((child) => {
595
676
  if (child && child.name === itemName) {
677
+ SelectionState.attach(child);
596
678
  child.selected = true;
597
- // change material to selected
598
- if (child.type === SelectionFilter.FACE) {
599
- child.material = CADmaterials.FACE?.SELECTED ?? CADmaterials.FACE;
600
- } else if (child.type === SelectionFilter.PLANE) {
601
- child.material = CADmaterials.PLANE?.SELECTED ?? CADmaterials.FACE?.SELECTED ?? child.material;
602
- } else if (child.type === SelectionFilter.EDGE) {
603
- child.material = CADmaterials.EDGE?.SELECTED ?? CADmaterials.EDGE;
604
- } else if (child.type === SelectionFilter.SOLID || child.type === SelectionFilter.COMPONENT) {
605
- child.material = CADmaterials.SOLID?.SELECTED ?? CADmaterials.SOLID;
606
- }
607
679
  }
608
680
  });
609
681
  SelectionFilter._emitSelectionChanged();
@@ -613,37 +685,36 @@ export class SelectionFilter {
613
685
  // Traverse scene and deselect a single item by name, updating materials appropriately
614
686
  scene.traverse((child) => {
615
687
  if (child.name === itemName) {
688
+ SelectionState.attach(child);
616
689
  child.selected = false;
617
- if (child.type === SelectionFilter.FACE) {
618
- child.material = SelectionFilter.#getBaseMaterial(child) ?? child.material;
619
- } else if (child.type === SelectionFilter.PLANE) {
620
- child.material = SelectionFilter.#getBaseMaterial(child) ?? child.material;
621
- } else if (child.type === SelectionFilter.EDGE) {
622
- child.material = SelectionFilter.#getBaseMaterial(child) ?? child.material;
623
- } else if (child.type === SelectionFilter.SOLID || child.type === SelectionFilter.COMPONENT) {
624
- // For solids, keep children materials consistent with their own selected flags
625
- child.children.forEach(grandchild => {
626
- if (grandchild.type === SelectionFilter.FACE) {
627
- grandchild.material = grandchild.selected ? (CADmaterials.FACE?.SELECTED ?? CADmaterials.FACE) : (SelectionFilter.#getBaseMaterial(grandchild) ?? grandchild.material);
628
- }
629
- if (grandchild.type === SelectionFilter.PLANE) {
630
- grandchild.material = grandchild.selected ? (CADmaterials.PLANE?.SELECTED ?? CADmaterials.FACE?.SELECTED ?? grandchild.material) : (SelectionFilter.#getBaseMaterial(grandchild) ?? grandchild.material);
631
- }
632
- if (grandchild.type === SelectionFilter.EDGE) {
633
- grandchild.material = grandchild.selected ? (CADmaterials.EDGE?.SELECTED ?? CADmaterials.EDGE) : (SelectionFilter.#getBaseMaterial(grandchild) ?? grandchild.material);
634
- }
635
- });
636
- }
637
690
  }
638
691
  });
639
692
  SelectionFilter._emitSelectionChanged();
640
693
  }
641
694
 
642
695
  static set uiCallback(callback) { SelectionFilter._uiCallback = callback; }
643
- static triggerUI() { if (SelectionFilter._uiCallback) SelectionFilter._uiCallback(); }
696
+ static triggerUI() {
697
+ if (SelectionFilter._uiCallback) SelectionFilter._uiCallback();
698
+ try { SelectionFilter._updateSelectionFilterIndicator(); } catch (_) { }
699
+ }
644
700
 
645
701
  // Emit a global event so UI can react without polling
646
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 */ }
647
718
  try {
648
719
  const ev = new CustomEvent('selection-changed');
649
720
  window.dispatchEvent(ev);
@@ -802,6 +873,7 @@ export class SelectionFilter {
802
873
  static _getHistoryContextActionSpecs(selection, viewer) {
803
874
  const out = [];
804
875
  const items = Array.isArray(selection) ? selection : [];
876
+ const suppressFeatureButtons = SelectionFilter._hasAssemblyComponentSelection(items, viewer);
805
877
  const safeId = (prefix, key) => {
806
878
  const raw = String(key || '').toLowerCase().replace(/[^a-z0-9_-]+/g, '-');
807
879
  return `${prefix}-${raw || 'item'}`;
@@ -810,49 +882,54 @@ export class SelectionFilter {
810
882
  if (spec && spec.id) out.push(spec);
811
883
  };
812
884
 
813
- const featureRegistry = viewer?.partHistory?.featureRegistry || null;
814
- const features = Array.isArray(featureRegistry?.features) ? featureRegistry.features : [];
815
- for (const FeatureClass of features) {
816
- if (!FeatureClass) continue;
817
- let result = null;
818
- try { result = FeatureClass.showContexButton?.(items); } catch { result = null; }
819
- if (!result) continue;
820
- if (result && typeof result === 'object' && result.show === false) continue;
821
- const label = (result && typeof result === 'object' && result.label) || FeatureClass.longName || FeatureClass.shortName || FeatureClass.name || 'Feature';
822
- const typeKey = FeatureClass.shortName || FeatureClass.type || FeatureClass.name || label;
823
- const params = SelectionFilter._extractContextParams(result);
824
- addSpec({
825
- id: safeId('ctx-feature', typeKey),
826
- label,
827
- title: `Create ${label}`,
828
- onClick: () => SelectionFilter._createFeatureFromContext(viewer, typeKey, params),
829
- });
830
- }
885
+ const pmimode = viewer?._pmiMode || null;
886
+ const pmiActive = !!pmimode;
887
+ if (!pmiActive) {
888
+ const featureRegistry = viewer?.partHistory?.featureRegistry || null;
889
+ const features = Array.isArray(featureRegistry?.features) ? featureRegistry.features : [];
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
+ }
907
+ }
831
908
 
832
- const constraintRegistry = viewer?.partHistory?.assemblyConstraintRegistry || null;
833
- const constraintClasses = typeof constraintRegistry?.listAvailable === 'function'
834
- ? constraintRegistry.listAvailable()
835
- : (typeof constraintRegistry?.list === 'function' ? constraintRegistry.list() : []);
836
- if (Array.isArray(constraintClasses)) {
837
- for (const ConstraintClass of constraintClasses) {
838
- if (!ConstraintClass) continue;
839
- let result = null;
840
- try { result = ConstraintClass.showContexButton?.(items); } catch { result = null; }
841
- if (!result) continue;
842
- if (result && typeof result === 'object' && result.show === false) continue;
843
- const label = (result && typeof result === 'object' && result.label) || ConstraintClass.longName || ConstraintClass.shortName || ConstraintClass.name || 'Constraint';
844
- const typeKey = ConstraintClass.constraintType || ConstraintClass.shortName || ConstraintClass.name || label;
845
- const params = SelectionFilter._extractContextParams(result);
846
- addSpec({
847
- id: safeId('ctx-constraint', typeKey),
848
- label,
849
- title: `Create ${label}`,
850
- onClick: () => SelectionFilter._createConstraintFromContext(viewer, typeKey, params),
851
- });
909
+ const constraintRegistry = viewer?.partHistory?.assemblyConstraintRegistry || null;
910
+ const constraintClasses = typeof constraintRegistry?.listAvailable === 'function'
911
+ ? constraintRegistry.listAvailable()
912
+ : (typeof constraintRegistry?.list === 'function' ? constraintRegistry.list() : []);
913
+ if (Array.isArray(constraintClasses)) {
914
+ for (const ConstraintClass of constraintClasses) {
915
+ if (!ConstraintClass) continue;
916
+ let result = null;
917
+ try { result = ConstraintClass.showContexButton?.(items); } catch { result = null; }
918
+ if (!result) continue;
919
+ if (result && typeof result === 'object' && result.show === false) continue;
920
+ const label = (result && typeof result === 'object' && result.label) || ConstraintClass.longName || ConstraintClass.shortName || ConstraintClass.name || 'Constraint';
921
+ const typeKey = ConstraintClass.constraintType || ConstraintClass.shortName || ConstraintClass.name || label;
922
+ const params = SelectionFilter._extractContextParams(result);
923
+ addSpec({
924
+ id: safeId('ctx-constraint', typeKey),
925
+ label,
926
+ title: `Create ${label}`,
927
+ onClick: () => SelectionFilter._createConstraintFromContext(viewer, typeKey, params),
928
+ });
929
+ }
852
930
  }
853
931
  }
854
932
 
855
- const pmimode = viewer?._pmiMode || null;
856
933
  const annotationRegistry = viewer?.annotationRegistry || null;
857
934
  if (pmimode && annotationRegistry && typeof annotationRegistry.list === 'function') {
858
935
  const annClasses = annotationRegistry.list();
@@ -877,6 +954,28 @@ export class SelectionFilter {
877
954
  return out;
878
955
  }
879
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
+
880
979
  static async _createFeatureFromContext(viewer, typeKey, params = null) {
881
980
  if (!viewer || !typeKey) return;
882
981
  SelectionFilter.setContextBarSuppressed('context-create', true);
@@ -1038,6 +1137,371 @@ export class SelectionFilter {
1038
1137
  return bar;
1039
1138
  }
1040
1139
 
1140
+ static _getSelectionFilterTypeList() {
1141
+ if (!SelectionFilter._selectionFilterTypes) {
1142
+ SelectionFilter._selectionFilterTypes = SelectionFilter.TYPES.filter((t) => t !== SelectionFilter.ALL);
1143
+ }
1144
+ return SelectionFilter._selectionFilterTypes;
1145
+ }
1146
+
1147
+ static _getSelectionFilterLabel(type) {
1148
+ const labels = {
1149
+ SOLID: 'Solid',
1150
+ COMPONENT: 'Component',
1151
+ FACE: 'Face',
1152
+ PLANE: 'Plane',
1153
+ SKETCH: 'Sketch',
1154
+ DATUM: 'Datum',
1155
+ HELIX: 'Helix',
1156
+ EDGE: 'Edge',
1157
+ LOOP: 'Loop',
1158
+ VERTEX: 'Vertex',
1159
+ };
1160
+ return labels[type] || type;
1161
+ }
1162
+
1163
+ static _summarizeSelectionFilter(types) {
1164
+ const list = Array.isArray(types) ? types : [];
1165
+ const allTypes = SelectionFilter._getSelectionFilterTypeList();
1166
+ if (list.length === 0) return 'None';
1167
+ if (list.length === allTypes.length) return 'All';
1168
+ return list.map((t) => SelectionFilter._getSelectionFilterLabel(t)).join(', ');
1169
+ }
1170
+
1171
+ static _getAllowedTypeList() {
1172
+ const allTypes = SelectionFilter._getSelectionFilterTypeList();
1173
+ if (SelectionFilter.allowedSelectionTypes === SelectionFilter.ALL) return allTypes.slice();
1174
+ const allowed = new Set(Array.from(SelectionFilter.allowedSelectionTypes || []));
1175
+ return allTypes.filter((t) => allowed.has(t));
1176
+ }
1177
+
1178
+ static _updateSelectionFilterIndicator() {
1179
+ const wrap = SelectionFilter._selectionFilterIndicator;
1180
+ if (!wrap) return;
1181
+ const toggle = SelectionFilter._selectionFilterIndicatorToggle;
1182
+ const types = SelectionFilter._getAllowedTypeList();
1183
+ if (SelectionFilter._selectionFilterCheckboxes && SelectionFilter._selectionFilterCheckboxes.size) {
1184
+ const set = new Set(types);
1185
+ for (const [type, cb] of SelectionFilter._selectionFilterCheckboxes.entries()) {
1186
+ if (cb) cb.checked = set.has(type);
1187
+ }
1188
+ }
1189
+ if (toggle) {
1190
+ toggle.textContent = `Selection filter: ${SelectionFilter._summarizeSelectionFilter(types)}`;
1191
+ }
1192
+ SelectionFilter._updateSelectableTintButton();
1193
+ }
1194
+
1195
+ static _getSelectableTintTargets() {
1196
+ const allowed = SelectionFilter.allowedSelectionTypes;
1197
+ const allowAll = allowed === SelectionFilter.ALL;
1198
+ const allowFace = allowAll || (allowed && typeof allowed.has === 'function' && allowed.has(SelectionFilter.FACE));
1199
+ const allowEdge = allowAll || (allowed && typeof allowed.has === 'function' && allowed.has(SelectionFilter.EDGE));
1200
+ return { allowFace, allowEdge };
1201
+ }
1202
+
1203
+ static _updateSelectableTintButton() {
1204
+ const btn = SelectionFilter._selectionFilterTintBtn;
1205
+ if (!btn) return;
1206
+ const state = SelectionFilter._selectableTintState;
1207
+ const active = !!state?.active;
1208
+ const { allowFace, allowEdge } = SelectionFilter._getSelectableTintTargets();
1209
+ const hasTargets = allowFace || allowEdge;
1210
+ const colors = Array.isArray(state?.colors) && state.colors.length ? state.colors : ['#60a5fa'];
1211
+ const nextColor = colors[(state?.colorIndex ?? 0) % colors.length] || '#60a5fa';
1212
+ const displayColor = active ? (state?.activeColor || nextColor) : nextColor;
1213
+ btn.classList.toggle('is-active', active);
1214
+ btn.style.setProperty('--sfi-tint', displayColor);
1215
+ btn.textContent = active ? 'Reset selectable tint' : 'Tint selectable';
1216
+ btn.disabled = !active && !hasTargets;
1217
+ btn.title = active
1218
+ ? 'Restore original face/edge colors'
1219
+ : (hasTargets ? 'Tint selectable faces and edges' : 'Enable Face or Edge selection to tint');
1220
+ }
1221
+
1222
+ static _applySelectableTint(scene, { allowFace, allowEdge, faceColor, edgeColor }) {
1223
+ if (!scene || (!allowFace && !allowEdge)) return;
1224
+ const state = SelectionFilter._selectableTintState;
1225
+ const storeColor = (mat) => {
1226
+ if (!mat || !mat.color || typeof mat.color.getHexString !== 'function') return;
1227
+ if (state.materials.has(mat)) return;
1228
+ try { state.materials.set(mat, `#${mat.color.getHexString()}`); } catch { }
1229
+ };
1230
+ const tintMaterial = (mat, color) => {
1231
+ if (!mat || !mat.color || typeof mat.color.set !== 'function') return;
1232
+ storeColor(mat);
1233
+ try { mat.color.set(color); } catch { }
1234
+ try { mat.needsUpdate = true; } catch { }
1235
+ };
1236
+ const tintObject = (obj, color) => {
1237
+ if (!obj || obj.visible === false) return;
1238
+ if (obj.selected === true) return;
1239
+ const mat = obj.material;
1240
+ if (Array.isArray(mat)) {
1241
+ for (const m of mat) tintMaterial(m, color);
1242
+ } else {
1243
+ tintMaterial(mat, color);
1244
+ }
1245
+ };
1246
+ const isPreview = (obj) => {
1247
+ if (!obj) return true;
1248
+ if (obj.userData?.refPreview) return true;
1249
+ const name = typeof obj.name === 'string' ? obj.name : '';
1250
+ const type = typeof obj.type === 'string' ? obj.type : '';
1251
+ if (name.startsWith('__refPreview__')) return true;
1252
+ if (type.startsWith('REF_PREVIEW')) return true;
1253
+ return false;
1254
+ };
1255
+ scene.traverse((obj) => {
1256
+ if (!obj || isPreview(obj)) return;
1257
+ if (allowFace && obj.type === SelectionFilter.FACE) {
1258
+ tintObject(obj, faceColor);
1259
+ } else if (allowEdge && obj.type === SelectionFilter.EDGE) {
1260
+ tintObject(obj, edgeColor);
1261
+ }
1262
+ });
1263
+ }
1264
+
1265
+ static _restoreSelectableTint() {
1266
+ const state = SelectionFilter._selectableTintState;
1267
+ if (!state || !state.materials) return;
1268
+ for (const [mat, color] of state.materials.entries()) {
1269
+ if (!mat || !mat.color || typeof mat.color.set !== 'function') continue;
1270
+ if (!color) continue;
1271
+ try { mat.color.set(color); } catch { }
1272
+ try { mat.needsUpdate = true; } catch { }
1273
+ }
1274
+ state.materials.clear();
1275
+ state.active = false;
1276
+ state.activeColor = null;
1277
+ SelectionFilter._updateSelectableTintButton();
1278
+ try { SelectionFilter.viewer?.render?.(); } catch { }
1279
+ }
1280
+
1281
+ static _toggleSelectableTint(viewer) {
1282
+ const state = SelectionFilter._selectableTintState;
1283
+ if (!state) return;
1284
+ if (state.active) {
1285
+ SelectionFilter._restoreSelectableTint();
1286
+ return;
1287
+ }
1288
+ const { allowFace, allowEdge } = SelectionFilter._getSelectableTintTargets();
1289
+ if (!allowFace && !allowEdge) return;
1290
+ const scene = viewer?.partHistory?.scene || viewer?.scene || SelectionFilter.viewer?.partHistory?.scene || SelectionFilter.viewer?.scene || null;
1291
+ if (!scene) return;
1292
+ const colors = Array.isArray(state.colors) && state.colors.length ? state.colors : ['#60a5fa'];
1293
+ const color = colors[state.colorIndex % colors.length] || '#60a5fa';
1294
+ state.colorIndex = (state.colorIndex + 1) % colors.length;
1295
+ SelectionFilter._applySelectableTint(scene, {
1296
+ allowFace,
1297
+ allowEdge,
1298
+ faceColor: color,
1299
+ edgeColor: color,
1300
+ });
1301
+ state.active = true;
1302
+ state.activeColor = color;
1303
+ SelectionFilter._updateSelectableTintButton();
1304
+ try { (viewer || SelectionFilter.viewer)?.render?.(); } catch { }
1305
+ }
1306
+
1307
+ static _ensureSelectionFilterIndicator(viewer) {
1308
+ if (SelectionFilter._selectionFilterIndicator && SelectionFilter._selectionFilterIndicator.isConnected) {
1309
+ SelectionFilter._updateSelectionFilterIndicator();
1310
+ return SelectionFilter._selectionFilterIndicator;
1311
+ }
1312
+ if (typeof document === 'undefined') return null;
1313
+ const host = viewer?.container || document.body || null;
1314
+ if (!host) return null;
1315
+ try {
1316
+ if (!document.getElementById('selection-filter-indicator-styles')) {
1317
+ const style = document.createElement('style');
1318
+ style.id = 'selection-filter-indicator-styles';
1319
+ style.textContent = `
1320
+ .selection-filter-indicator {
1321
+ position: fixed;
1322
+ bottom: 8px;
1323
+ left: 50%;
1324
+ transform: translateX(-50%);
1325
+ display: flex;
1326
+ flex-direction: column;
1327
+ gap: 6px;
1328
+ background: rgba(20,24,30,.85);
1329
+ border: 1px solid #262b36;
1330
+ border-radius: 10px;
1331
+ padding: 6px;
1332
+ color: #ddd;
1333
+ z-index: 12;
1334
+ user-select: none;
1335
+ min-width: 220px;
1336
+ max-width: min(440px, calc(100vw - 16px));
1337
+ box-shadow: 0 6px 18px rgba(0,0,0,.35);
1338
+ }
1339
+ .selection-filter-indicator .sfi-toggle {
1340
+ background: transparent;
1341
+ border-radius: 8px;
1342
+ padding: 6px 10px;
1343
+ width: 100%;
1344
+ min-height: 32px;
1345
+ box-sizing: border-box;
1346
+ color: #ddd;
1347
+ border: 1px solid #364053;
1348
+ cursor: pointer;
1349
+ text-align: left;
1350
+ }
1351
+ .selection-filter-indicator .sfi-toggle:hover { filter: brightness(1.08); }
1352
+ .selection-filter-indicator .sfi-toggle:active { filter: brightness(1.15); }
1353
+ .selection-filter-indicator .sfi-panel {
1354
+ border: 1px solid #2b3240;
1355
+ border-radius: 8px;
1356
+ padding: 8px 10px;
1357
+ background: rgba(17,22,31,.95);
1358
+ }
1359
+ .selection-filter-indicator .sfi-panel[hidden] { display: none; }
1360
+ .selection-filter-indicator .sfi-list {
1361
+ display: grid;
1362
+ grid-template-columns: repeat(2, minmax(0, 1fr));
1363
+ gap: 6px 10px;
1364
+ }
1365
+ .selection-filter-indicator .sfi-option {
1366
+ display: flex;
1367
+ align-items: center;
1368
+ gap: 6px;
1369
+ font-size: 12px;
1370
+ color: #cbd5e1;
1371
+ }
1372
+ .selection-filter-indicator input[type="checkbox"] {
1373
+ width: 16px;
1374
+ height: 16px;
1375
+ accent-color: #60a5fa;
1376
+ }
1377
+ .selection-filter-indicator .sfi-actions {
1378
+ display: flex;
1379
+ gap: 6px;
1380
+ margin-top: 8px;
1381
+ }
1382
+ .selection-filter-indicator .sfi-btn {
1383
+ flex: 1;
1384
+ background: rgba(255,255,255,.04);
1385
+ border: 1px solid #364053;
1386
+ border-radius: 8px;
1387
+ color: #e2e8f0;
1388
+ padding: 6px 10px;
1389
+ font-size: 12px;
1390
+ cursor: pointer;
1391
+ text-align: center;
1392
+ min-height: 28px;
1393
+ }
1394
+ .selection-filter-indicator .sfi-btn:hover { filter: brightness(1.08); }
1395
+ .selection-filter-indicator .sfi-btn:active { filter: brightness(1.15); }
1396
+ .selection-filter-indicator .sfi-btn.is-active {
1397
+ border-color: var(--sfi-tint, #60a5fa);
1398
+ color: var(--sfi-tint, #60a5fa);
1399
+ }
1400
+ `;
1401
+ document.head.appendChild(style);
1402
+ }
1403
+ } catch { }
1404
+
1405
+ const wrap = document.createElement('div');
1406
+ wrap.className = 'selection-filter-indicator';
1407
+
1408
+ const toggle = document.createElement('button');
1409
+ toggle.type = 'button';
1410
+ toggle.className = 'sfi-toggle';
1411
+ const panelId = `selection-filter-panel-${Math.random().toString(36).slice(2, 8)}`;
1412
+ toggle.setAttribute('aria-expanded', 'false');
1413
+ toggle.setAttribute('aria-controls', panelId);
1414
+ wrap.appendChild(toggle);
1415
+
1416
+ const panel = document.createElement('div');
1417
+ panel.className = 'sfi-panel';
1418
+ panel.id = panelId;
1419
+ panel.hidden = true;
1420
+
1421
+ const list = document.createElement('div');
1422
+ list.className = 'sfi-list';
1423
+ panel.appendChild(list);
1424
+
1425
+ const checkboxByType = new Map();
1426
+ const types = SelectionFilter._getSelectionFilterTypeList();
1427
+ for (const type of types) {
1428
+ const option = document.createElement('label');
1429
+ option.className = 'sfi-option';
1430
+
1431
+ const box = document.createElement('input');
1432
+ box.type = 'checkbox';
1433
+ box.dataset.type = type;
1434
+ box.addEventListener('click', (ev) => ev.stopPropagation());
1435
+ box.addEventListener('change', (ev) => {
1436
+ ev.stopPropagation();
1437
+ const next = [];
1438
+ for (const t of types) {
1439
+ const cb = checkboxByType.get(t);
1440
+ if (cb && cb.checked) next.push(t);
1441
+ }
1442
+ const nextValue = next.length === types.length ? SelectionFilter.ALL : next;
1443
+ try { SelectionFilter.SetSelectionTypes(nextValue); } catch { }
1444
+ if (SelectionFilter.previouseAllowedSelectionTypes !== null) {
1445
+ SelectionFilter.previouseAllowedSelectionTypes = SelectionFilter.allowedSelectionTypes;
1446
+ }
1447
+ SelectionFilter._updateSelectionFilterIndicator();
1448
+ });
1449
+
1450
+ const label = document.createElement('span');
1451
+ label.textContent = SelectionFilter._getSelectionFilterLabel(type);
1452
+
1453
+ option.appendChild(box);
1454
+ option.appendChild(label);
1455
+ list.appendChild(option);
1456
+ checkboxByType.set(type, box);
1457
+ }
1458
+
1459
+ const actions = document.createElement('div');
1460
+ actions.className = 'sfi-actions';
1461
+ const tintBtn = document.createElement('button');
1462
+ tintBtn.type = 'button';
1463
+ tintBtn.className = 'sfi-btn';
1464
+ tintBtn.addEventListener('click', (ev) => {
1465
+ ev.stopPropagation();
1466
+ SelectionFilter._toggleSelectableTint(viewer);
1467
+ });
1468
+ actions.appendChild(tintBtn);
1469
+ panel.appendChild(actions);
1470
+
1471
+ toggle.addEventListener('click', (ev) => {
1472
+ ev.stopPropagation();
1473
+ const nextOpen = panel.hidden;
1474
+ panel.hidden = !nextOpen;
1475
+ toggle.setAttribute('aria-expanded', String(nextOpen));
1476
+ if (nextOpen) SelectionFilter._updateSelectionFilterIndicator();
1477
+ });
1478
+ panel.addEventListener('click', (ev) => ev.stopPropagation());
1479
+
1480
+ if (!SelectionFilter._selectionFilterOutsideBound) {
1481
+ SelectionFilter._selectionFilterOutsideBound = true;
1482
+ document.addEventListener('mousedown', (ev) => {
1483
+ const panelEl = SelectionFilter._selectionFilterIndicatorPanel;
1484
+ const toggleEl = SelectionFilter._selectionFilterIndicatorToggle;
1485
+ const wrapEl = SelectionFilter._selectionFilterIndicator;
1486
+ if (wrapEl && ev && wrapEl.contains(ev.target)) return;
1487
+ if (!panelEl || panelEl.hidden) return;
1488
+ panelEl.hidden = true;
1489
+ if (toggleEl) toggleEl.setAttribute('aria-expanded', 'false');
1490
+ });
1491
+ }
1492
+
1493
+ wrap.appendChild(panel);
1494
+ host.appendChild(wrap);
1495
+
1496
+ SelectionFilter._selectionFilterIndicator = wrap;
1497
+ SelectionFilter._selectionFilterIndicatorToggle = toggle;
1498
+ SelectionFilter._selectionFilterIndicatorPanel = panel;
1499
+ SelectionFilter._selectionFilterCheckboxes = checkboxByType;
1500
+ SelectionFilter._selectionFilterTintBtn = tintBtn;
1501
+ SelectionFilter._updateSelectionFilterIndicator();
1502
+ return wrap;
1503
+ }
1504
+
1041
1505
  static _createSelectionActionButton(entry) {
1042
1506
  try {
1043
1507
  const btn = document.createElement('button');
@@ -1056,6 +1520,7 @@ export class SelectionFilter {
1056
1520
  }
1057
1521
 
1058
1522
  static #logAllowedTypesChange(next, reason = '') {
1523
+ if (!debugMode) return;
1059
1524
  try {
1060
1525
  const desc = next === SelectionFilter.ALL
1061
1526
  ? 'ALL'