brep-io-kernel 1.0.19 → 1.0.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brep-io-kernel",
3
- "version": "1.0.19",
3
+ "version": "1.0.21",
4
4
  "scripts": {
5
5
  "dev": "pnpm generateLicenses && pnpm build:kernel && vite --host 0.0.0.0",
6
6
  "build": "pnpm generateLicenses && vite build",
@@ -12,6 +12,7 @@ export class HistoryWidget extends HistoryCollectionWidget {
12
12
 
13
13
  // Override configurable hooks from the base widget after super() so they can access `this`.
14
14
  this._autoSyncOpenState = true;
15
+ this._autoFocusOnExpand = true;
15
16
  this._determineExpanded = (entry) => this.#shouldExpandEntry(entry);
16
17
  this._formOptionsProvider = (context) => this.#buildFormOptions(context);
17
18
  this._decorateEntryHeader = (context) => this.#decorateEntryHeader(context);
@@ -380,10 +381,14 @@ export class HistoryWidget extends HistoryCollectionWidget {
380
381
  if (!target) return;
381
382
  if (this._expandedId && String(this._expandedId) === String(target)) return;
382
383
  const entries = this._getEntries();
383
- const exists = entries.some((entry, idx) => this._extractEntryId(entry, idx) === String(target));
384
- if (!exists) return;
385
- this._expandedId = String(target);
386
- this.render();
384
+ for (let i = 0; i < entries.length; i++) {
385
+ const entry = entries[i];
386
+ if (this._extractEntryId(entry, i) !== String(target)) continue;
387
+ if (!this.#shouldExpandEntry(entry)) return;
388
+ this._expandedId = String(target);
389
+ this.render();
390
+ return;
391
+ }
387
392
  }
388
393
 
389
394
  #computeIdsSignature() {
@@ -26,6 +26,20 @@ export class SelectionFilter {
26
26
  static _historyContextActions = new Map();
27
27
  static _selectionActionSeparator = null;
28
28
  static _contextSuppressReasons = new Set();
29
+ static _selectionFilterIndicator = null;
30
+ static _selectionFilterIndicatorToggle = null;
31
+ static _selectionFilterIndicatorPanel = null;
32
+ static _selectionFilterCheckboxes = new Map();
33
+ static _selectionFilterTypes = null;
34
+ static _selectionFilterOutsideBound = false;
35
+ static _selectionFilterTintBtn = null;
36
+ static _selectableTintState = {
37
+ active: false,
38
+ activeColor: null,
39
+ colorIndex: 0,
40
+ colors: ['#34d399', '#f97316', '#60a5fa', '#f43f5e'],
41
+ materials: new Map(),
42
+ };
29
43
 
30
44
  constructor() {
31
45
  throw new Error("SelectionFilter is static and cannot be instantiated.");
@@ -58,6 +72,7 @@ export class SelectionFilter {
58
72
  if (types === SelectionFilter.ALL) {
59
73
  SelectionFilter.allowedSelectionTypes = SelectionFilter.ALL;
60
74
  SelectionFilter.triggerUI();
75
+ SelectionFilter._ensureSceneSelectionHandlers();
61
76
  SelectionFilter.#logAllowedTypesChange(SelectionFilter.allowedSelectionTypes, 'SetSelectionTypes');
62
77
  return;
63
78
  }
@@ -66,6 +81,7 @@ export class SelectionFilter {
66
81
  if (invalid.length) throw new Error(`Unknown selection type(s): ${invalid.join(", ")}`);
67
82
  SelectionFilter.allowedSelectionTypes = new Set(list);
68
83
  SelectionFilter.triggerUI();
84
+ SelectionFilter._ensureSceneSelectionHandlers();
69
85
  SelectionFilter.#logAllowedTypesChange(SelectionFilter.allowedSelectionTypes, 'SetSelectionTypes');
70
86
  }
71
87
 
@@ -78,10 +94,63 @@ export class SelectionFilter {
78
94
  SelectionFilter.allowedSelectionTypes = SelectionFilter.previouseAllowedSelectionTypes;
79
95
  SelectionFilter.previouseAllowedSelectionTypes = null;
80
96
  SelectionFilter.triggerUI();
97
+ SelectionFilter._ensureSceneSelectionHandlers();
81
98
  SelectionFilter.#logAllowedTypesChange(SelectionFilter.allowedSelectionTypes, 'RestoreSelectionTypes');
82
99
  }
83
100
  }
84
101
 
102
+ static ensureSelectionHandlers(obj, { deep = false } = {}) {
103
+ if (!obj || typeof obj !== 'object') return false;
104
+ let changed = false;
105
+ const attach = (target) => {
106
+ if (!target || typeof target !== 'object') return;
107
+ 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;
114
+ }
115
+ SelectionFilter.toggleSelection(target);
116
+ } catch (error) {
117
+ try { console.warn('[SelectionFilter] toggleSelection failed:', error); } catch (_) { /* ignore */ }
118
+ }
119
+ };
120
+ try { target.onClick.__brepSelectionHandler = true; } catch (_) { /* ignore */ }
121
+ changed = true;
122
+ };
123
+
124
+ if (!deep) {
125
+ attach(obj);
126
+ return changed;
127
+ }
128
+
129
+ const stack = [obj];
130
+ while (stack.length) {
131
+ const current = stack.pop();
132
+ attach(current);
133
+ const kids = Array.isArray(current?.children) ? current.children : [];
134
+ for (const child of kids) {
135
+ if (child) stack.push(child);
136
+ }
137
+ }
138
+ return changed;
139
+ }
140
+
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
+
85
154
 
86
155
 
87
156
  static allowType(type) {
@@ -640,7 +709,10 @@ export class SelectionFilter {
640
709
  }
641
710
 
642
711
  static set uiCallback(callback) { SelectionFilter._uiCallback = callback; }
643
- static triggerUI() { if (SelectionFilter._uiCallback) SelectionFilter._uiCallback(); }
712
+ static triggerUI() {
713
+ if (SelectionFilter._uiCallback) SelectionFilter._uiCallback();
714
+ try { SelectionFilter._updateSelectionFilterIndicator(); } catch (_) { }
715
+ }
644
716
 
645
717
  // Emit a global event so UI can react without polling
646
718
  static _emitSelectionChanged() {
@@ -810,49 +882,52 @@ 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
- }
831
-
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;
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
+ for (const FeatureClass of features) {
891
+ if (!FeatureClass) continue;
839
892
  let result = null;
840
- try { result = ConstraintClass.showContexButton?.(items); } catch { result = null; }
893
+ try { result = FeatureClass.showContexButton?.(items); } catch { result = null; }
841
894
  if (!result) continue;
842
895
  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;
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;
845
898
  const params = SelectionFilter._extractContextParams(result);
846
899
  addSpec({
847
- id: safeId('ctx-constraint', typeKey),
900
+ id: safeId('ctx-feature', typeKey),
848
901
  label,
849
902
  title: `Create ${label}`,
850
- onClick: () => SelectionFilter._createConstraintFromContext(viewer, typeKey, params),
903
+ onClick: () => SelectionFilter._createFeatureFromContext(viewer, typeKey, params),
851
904
  });
852
905
  }
906
+
907
+ const constraintRegistry = viewer?.partHistory?.assemblyConstraintRegistry || null;
908
+ const constraintClasses = typeof constraintRegistry?.listAvailable === 'function'
909
+ ? constraintRegistry.listAvailable()
910
+ : (typeof constraintRegistry?.list === 'function' ? constraintRegistry.list() : []);
911
+ if (Array.isArray(constraintClasses)) {
912
+ for (const ConstraintClass of constraintClasses) {
913
+ if (!ConstraintClass) continue;
914
+ let result = null;
915
+ try { result = ConstraintClass.showContexButton?.(items); } catch { result = null; }
916
+ if (!result) continue;
917
+ if (result && typeof result === 'object' && result.show === false) continue;
918
+ const label = (result && typeof result === 'object' && result.label) || ConstraintClass.longName || ConstraintClass.shortName || ConstraintClass.name || 'Constraint';
919
+ const typeKey = ConstraintClass.constraintType || ConstraintClass.shortName || ConstraintClass.name || label;
920
+ const params = SelectionFilter._extractContextParams(result);
921
+ addSpec({
922
+ id: safeId('ctx-constraint', typeKey),
923
+ label,
924
+ title: `Create ${label}`,
925
+ onClick: () => SelectionFilter._createConstraintFromContext(viewer, typeKey, params),
926
+ });
927
+ }
928
+ }
853
929
  }
854
930
 
855
- const pmimode = viewer?._pmiMode || null;
856
931
  const annotationRegistry = viewer?.annotationRegistry || null;
857
932
  if (pmimode && annotationRegistry && typeof annotationRegistry.list === 'function') {
858
933
  const annClasses = annotationRegistry.list();
@@ -1038,6 +1113,369 @@ export class SelectionFilter {
1038
1113
  return bar;
1039
1114
  }
1040
1115
 
1116
+ static _getSelectionFilterTypeList() {
1117
+ if (!SelectionFilter._selectionFilterTypes) {
1118
+ SelectionFilter._selectionFilterTypes = SelectionFilter.TYPES.filter((t) => t !== SelectionFilter.ALL);
1119
+ }
1120
+ return SelectionFilter._selectionFilterTypes;
1121
+ }
1122
+
1123
+ static _getSelectionFilterLabel(type) {
1124
+ const labels = {
1125
+ SOLID: 'Solid',
1126
+ COMPONENT: 'Component',
1127
+ FACE: 'Face',
1128
+ PLANE: 'Plane',
1129
+ SKETCH: 'Sketch',
1130
+ EDGE: 'Edge',
1131
+ LOOP: 'Loop',
1132
+ VERTEX: 'Vertex',
1133
+ };
1134
+ return labels[type] || type;
1135
+ }
1136
+
1137
+ static _summarizeSelectionFilter(types) {
1138
+ const list = Array.isArray(types) ? types : [];
1139
+ const allTypes = SelectionFilter._getSelectionFilterTypeList();
1140
+ if (list.length === 0) return 'None';
1141
+ if (list.length === allTypes.length) return 'All';
1142
+ return list.map((t) => SelectionFilter._getSelectionFilterLabel(t)).join(', ');
1143
+ }
1144
+
1145
+ static _getAllowedTypeList() {
1146
+ const allTypes = SelectionFilter._getSelectionFilterTypeList();
1147
+ if (SelectionFilter.allowedSelectionTypes === SelectionFilter.ALL) return allTypes.slice();
1148
+ const allowed = new Set(Array.from(SelectionFilter.allowedSelectionTypes || []));
1149
+ return allTypes.filter((t) => allowed.has(t));
1150
+ }
1151
+
1152
+ static _updateSelectionFilterIndicator() {
1153
+ const wrap = SelectionFilter._selectionFilterIndicator;
1154
+ if (!wrap) return;
1155
+ const toggle = SelectionFilter._selectionFilterIndicatorToggle;
1156
+ const types = SelectionFilter._getAllowedTypeList();
1157
+ if (SelectionFilter._selectionFilterCheckboxes && SelectionFilter._selectionFilterCheckboxes.size) {
1158
+ const set = new Set(types);
1159
+ for (const [type, cb] of SelectionFilter._selectionFilterCheckboxes.entries()) {
1160
+ if (cb) cb.checked = set.has(type);
1161
+ }
1162
+ }
1163
+ if (toggle) {
1164
+ toggle.textContent = `Selection filter: ${SelectionFilter._summarizeSelectionFilter(types)}`;
1165
+ }
1166
+ SelectionFilter._updateSelectableTintButton();
1167
+ }
1168
+
1169
+ static _getSelectableTintTargets() {
1170
+ const allowed = SelectionFilter.allowedSelectionTypes;
1171
+ const allowAll = allowed === SelectionFilter.ALL;
1172
+ const allowFace = allowAll || (allowed && typeof allowed.has === 'function' && allowed.has(SelectionFilter.FACE));
1173
+ const allowEdge = allowAll || (allowed && typeof allowed.has === 'function' && allowed.has(SelectionFilter.EDGE));
1174
+ return { allowFace, allowEdge };
1175
+ }
1176
+
1177
+ static _updateSelectableTintButton() {
1178
+ const btn = SelectionFilter._selectionFilterTintBtn;
1179
+ if (!btn) return;
1180
+ const state = SelectionFilter._selectableTintState;
1181
+ const active = !!state?.active;
1182
+ const { allowFace, allowEdge } = SelectionFilter._getSelectableTintTargets();
1183
+ const hasTargets = allowFace || allowEdge;
1184
+ const colors = Array.isArray(state?.colors) && state.colors.length ? state.colors : ['#60a5fa'];
1185
+ const nextColor = colors[(state?.colorIndex ?? 0) % colors.length] || '#60a5fa';
1186
+ const displayColor = active ? (state?.activeColor || nextColor) : nextColor;
1187
+ btn.classList.toggle('is-active', active);
1188
+ btn.style.setProperty('--sfi-tint', displayColor);
1189
+ btn.textContent = active ? 'Reset selectable tint' : 'Tint selectable';
1190
+ btn.disabled = !active && !hasTargets;
1191
+ btn.title = active
1192
+ ? 'Restore original face/edge colors'
1193
+ : (hasTargets ? 'Tint selectable faces and edges' : 'Enable Face or Edge selection to tint');
1194
+ }
1195
+
1196
+ static _applySelectableTint(scene, { allowFace, allowEdge, faceColor, edgeColor }) {
1197
+ if (!scene || (!allowFace && !allowEdge)) return;
1198
+ const state = SelectionFilter._selectableTintState;
1199
+ const storeColor = (mat) => {
1200
+ if (!mat || !mat.color || typeof mat.color.getHexString !== 'function') return;
1201
+ if (state.materials.has(mat)) return;
1202
+ try { state.materials.set(mat, `#${mat.color.getHexString()}`); } catch { }
1203
+ };
1204
+ const tintMaterial = (mat, color) => {
1205
+ if (!mat || !mat.color || typeof mat.color.set !== 'function') return;
1206
+ storeColor(mat);
1207
+ try { mat.color.set(color); } catch { }
1208
+ try { mat.needsUpdate = true; } catch { }
1209
+ };
1210
+ const tintObject = (obj, color) => {
1211
+ if (!obj || obj.visible === false) return;
1212
+ if (obj.selected === true) return;
1213
+ const mat = obj.material;
1214
+ if (Array.isArray(mat)) {
1215
+ for (const m of mat) tintMaterial(m, color);
1216
+ } else {
1217
+ tintMaterial(mat, color);
1218
+ }
1219
+ };
1220
+ const isPreview = (obj) => {
1221
+ if (!obj) return true;
1222
+ if (obj.userData?.refPreview) return true;
1223
+ const name = typeof obj.name === 'string' ? obj.name : '';
1224
+ const type = typeof obj.type === 'string' ? obj.type : '';
1225
+ if (name.startsWith('__refPreview__')) return true;
1226
+ if (type.startsWith('REF_PREVIEW')) return true;
1227
+ return false;
1228
+ };
1229
+ scene.traverse((obj) => {
1230
+ if (!obj || isPreview(obj)) return;
1231
+ if (allowFace && obj.type === SelectionFilter.FACE) {
1232
+ tintObject(obj, faceColor);
1233
+ } else if (allowEdge && obj.type === SelectionFilter.EDGE) {
1234
+ tintObject(obj, edgeColor);
1235
+ }
1236
+ });
1237
+ }
1238
+
1239
+ static _restoreSelectableTint() {
1240
+ const state = SelectionFilter._selectableTintState;
1241
+ if (!state || !state.materials) return;
1242
+ for (const [mat, color] of state.materials.entries()) {
1243
+ if (!mat || !mat.color || typeof mat.color.set !== 'function') continue;
1244
+ if (!color) continue;
1245
+ try { mat.color.set(color); } catch { }
1246
+ try { mat.needsUpdate = true; } catch { }
1247
+ }
1248
+ state.materials.clear();
1249
+ state.active = false;
1250
+ state.activeColor = null;
1251
+ SelectionFilter._updateSelectableTintButton();
1252
+ try { SelectionFilter.viewer?.render?.(); } catch { }
1253
+ }
1254
+
1255
+ static _toggleSelectableTint(viewer) {
1256
+ const state = SelectionFilter._selectableTintState;
1257
+ if (!state) return;
1258
+ if (state.active) {
1259
+ SelectionFilter._restoreSelectableTint();
1260
+ return;
1261
+ }
1262
+ const { allowFace, allowEdge } = SelectionFilter._getSelectableTintTargets();
1263
+ if (!allowFace && !allowEdge) return;
1264
+ const scene = viewer?.partHistory?.scene || viewer?.scene || SelectionFilter.viewer?.partHistory?.scene || SelectionFilter.viewer?.scene || null;
1265
+ if (!scene) return;
1266
+ const colors = Array.isArray(state.colors) && state.colors.length ? state.colors : ['#60a5fa'];
1267
+ const color = colors[state.colorIndex % colors.length] || '#60a5fa';
1268
+ state.colorIndex = (state.colorIndex + 1) % colors.length;
1269
+ SelectionFilter._applySelectableTint(scene, {
1270
+ allowFace,
1271
+ allowEdge,
1272
+ faceColor: color,
1273
+ edgeColor: color,
1274
+ });
1275
+ state.active = true;
1276
+ state.activeColor = color;
1277
+ SelectionFilter._updateSelectableTintButton();
1278
+ try { (viewer || SelectionFilter.viewer)?.render?.(); } catch { }
1279
+ }
1280
+
1281
+ static _ensureSelectionFilterIndicator(viewer) {
1282
+ if (SelectionFilter._selectionFilterIndicator && SelectionFilter._selectionFilterIndicator.isConnected) {
1283
+ SelectionFilter._updateSelectionFilterIndicator();
1284
+ return SelectionFilter._selectionFilterIndicator;
1285
+ }
1286
+ if (typeof document === 'undefined') return null;
1287
+ const host = viewer?.container || document.body || null;
1288
+ if (!host) return null;
1289
+ try {
1290
+ if (!document.getElementById('selection-filter-indicator-styles')) {
1291
+ const style = document.createElement('style');
1292
+ style.id = 'selection-filter-indicator-styles';
1293
+ style.textContent = `
1294
+ .selection-filter-indicator {
1295
+ position: fixed;
1296
+ bottom: 8px;
1297
+ left: 50%;
1298
+ transform: translateX(-50%);
1299
+ display: flex;
1300
+ flex-direction: column;
1301
+ gap: 6px;
1302
+ background: rgba(20,24,30,.85);
1303
+ border: 1px solid #262b36;
1304
+ border-radius: 10px;
1305
+ padding: 6px;
1306
+ color: #ddd;
1307
+ z-index: 12;
1308
+ user-select: none;
1309
+ min-width: 220px;
1310
+ max-width: min(440px, calc(100vw - 16px));
1311
+ box-shadow: 0 6px 18px rgba(0,0,0,.35);
1312
+ }
1313
+ .selection-filter-indicator .sfi-toggle {
1314
+ background: transparent;
1315
+ border-radius: 8px;
1316
+ padding: 6px 10px;
1317
+ width: 100%;
1318
+ min-height: 32px;
1319
+ box-sizing: border-box;
1320
+ color: #ddd;
1321
+ border: 1px solid #364053;
1322
+ cursor: pointer;
1323
+ text-align: left;
1324
+ }
1325
+ .selection-filter-indicator .sfi-toggle:hover { filter: brightness(1.08); }
1326
+ .selection-filter-indicator .sfi-toggle:active { filter: brightness(1.15); }
1327
+ .selection-filter-indicator .sfi-panel {
1328
+ border: 1px solid #2b3240;
1329
+ border-radius: 8px;
1330
+ padding: 8px 10px;
1331
+ background: rgba(17,22,31,.95);
1332
+ }
1333
+ .selection-filter-indicator .sfi-panel[hidden] { display: none; }
1334
+ .selection-filter-indicator .sfi-list {
1335
+ display: grid;
1336
+ grid-template-columns: repeat(2, minmax(0, 1fr));
1337
+ gap: 6px 10px;
1338
+ }
1339
+ .selection-filter-indicator .sfi-option {
1340
+ display: flex;
1341
+ align-items: center;
1342
+ gap: 6px;
1343
+ font-size: 12px;
1344
+ color: #cbd5e1;
1345
+ }
1346
+ .selection-filter-indicator input[type="checkbox"] {
1347
+ width: 16px;
1348
+ height: 16px;
1349
+ accent-color: #60a5fa;
1350
+ }
1351
+ .selection-filter-indicator .sfi-actions {
1352
+ display: flex;
1353
+ gap: 6px;
1354
+ margin-top: 8px;
1355
+ }
1356
+ .selection-filter-indicator .sfi-btn {
1357
+ flex: 1;
1358
+ background: rgba(255,255,255,.04);
1359
+ border: 1px solid #364053;
1360
+ border-radius: 8px;
1361
+ color: #e2e8f0;
1362
+ padding: 6px 10px;
1363
+ font-size: 12px;
1364
+ cursor: pointer;
1365
+ text-align: center;
1366
+ min-height: 28px;
1367
+ }
1368
+ .selection-filter-indicator .sfi-btn:hover { filter: brightness(1.08); }
1369
+ .selection-filter-indicator .sfi-btn:active { filter: brightness(1.15); }
1370
+ .selection-filter-indicator .sfi-btn.is-active {
1371
+ border-color: var(--sfi-tint, #60a5fa);
1372
+ color: var(--sfi-tint, #60a5fa);
1373
+ }
1374
+ `;
1375
+ document.head.appendChild(style);
1376
+ }
1377
+ } catch { }
1378
+
1379
+ const wrap = document.createElement('div');
1380
+ wrap.className = 'selection-filter-indicator';
1381
+
1382
+ const toggle = document.createElement('button');
1383
+ toggle.type = 'button';
1384
+ toggle.className = 'sfi-toggle';
1385
+ const panelId = `selection-filter-panel-${Math.random().toString(36).slice(2, 8)}`;
1386
+ toggle.setAttribute('aria-expanded', 'false');
1387
+ toggle.setAttribute('aria-controls', panelId);
1388
+ wrap.appendChild(toggle);
1389
+
1390
+ const panel = document.createElement('div');
1391
+ panel.className = 'sfi-panel';
1392
+ panel.id = panelId;
1393
+ panel.hidden = true;
1394
+
1395
+ const list = document.createElement('div');
1396
+ list.className = 'sfi-list';
1397
+ panel.appendChild(list);
1398
+
1399
+ const checkboxByType = new Map();
1400
+ const types = SelectionFilter._getSelectionFilterTypeList();
1401
+ for (const type of types) {
1402
+ const option = document.createElement('label');
1403
+ option.className = 'sfi-option';
1404
+
1405
+ const box = document.createElement('input');
1406
+ box.type = 'checkbox';
1407
+ box.dataset.type = type;
1408
+ box.addEventListener('click', (ev) => ev.stopPropagation());
1409
+ box.addEventListener('change', (ev) => {
1410
+ ev.stopPropagation();
1411
+ const next = [];
1412
+ for (const t of types) {
1413
+ const cb = checkboxByType.get(t);
1414
+ if (cb && cb.checked) next.push(t);
1415
+ }
1416
+ const nextValue = next.length === types.length ? SelectionFilter.ALL : next;
1417
+ try { SelectionFilter.SetSelectionTypes(nextValue); } catch { }
1418
+ if (SelectionFilter.previouseAllowedSelectionTypes !== null) {
1419
+ SelectionFilter.previouseAllowedSelectionTypes = SelectionFilter.allowedSelectionTypes;
1420
+ }
1421
+ SelectionFilter._updateSelectionFilterIndicator();
1422
+ });
1423
+
1424
+ const label = document.createElement('span');
1425
+ label.textContent = SelectionFilter._getSelectionFilterLabel(type);
1426
+
1427
+ option.appendChild(box);
1428
+ option.appendChild(label);
1429
+ list.appendChild(option);
1430
+ checkboxByType.set(type, box);
1431
+ }
1432
+
1433
+ const actions = document.createElement('div');
1434
+ actions.className = 'sfi-actions';
1435
+ const tintBtn = document.createElement('button');
1436
+ tintBtn.type = 'button';
1437
+ tintBtn.className = 'sfi-btn';
1438
+ tintBtn.addEventListener('click', (ev) => {
1439
+ ev.stopPropagation();
1440
+ SelectionFilter._toggleSelectableTint(viewer);
1441
+ });
1442
+ actions.appendChild(tintBtn);
1443
+ panel.appendChild(actions);
1444
+
1445
+ toggle.addEventListener('click', (ev) => {
1446
+ ev.stopPropagation();
1447
+ const nextOpen = panel.hidden;
1448
+ panel.hidden = !nextOpen;
1449
+ toggle.setAttribute('aria-expanded', String(nextOpen));
1450
+ if (nextOpen) SelectionFilter._updateSelectionFilterIndicator();
1451
+ });
1452
+ panel.addEventListener('click', (ev) => ev.stopPropagation());
1453
+
1454
+ if (!SelectionFilter._selectionFilterOutsideBound) {
1455
+ SelectionFilter._selectionFilterOutsideBound = true;
1456
+ document.addEventListener('mousedown', (ev) => {
1457
+ const panelEl = SelectionFilter._selectionFilterIndicatorPanel;
1458
+ const toggleEl = SelectionFilter._selectionFilterIndicatorToggle;
1459
+ const wrapEl = SelectionFilter._selectionFilterIndicator;
1460
+ if (wrapEl && ev && wrapEl.contains(ev.target)) return;
1461
+ if (!panelEl || panelEl.hidden) return;
1462
+ panelEl.hidden = true;
1463
+ if (toggleEl) toggleEl.setAttribute('aria-expanded', 'false');
1464
+ });
1465
+ }
1466
+
1467
+ wrap.appendChild(panel);
1468
+ host.appendChild(wrap);
1469
+
1470
+ SelectionFilter._selectionFilterIndicator = wrap;
1471
+ SelectionFilter._selectionFilterIndicatorToggle = toggle;
1472
+ SelectionFilter._selectionFilterIndicatorPanel = panel;
1473
+ SelectionFilter._selectionFilterCheckboxes = checkboxByType;
1474
+ SelectionFilter._selectionFilterTintBtn = tintBtn;
1475
+ SelectionFilter._updateSelectionFilterIndicator();
1476
+ return wrap;
1477
+ }
1478
+
1041
1479
  static _createSelectionActionButton(entry) {
1042
1480
  try {
1043
1481
  const btn = document.createElement('button');
@@ -164,6 +164,7 @@ export class AssemblyConstraintCollectionWidget extends HistoryCollectionWidget
164
164
 
165
165
  this.partHistory = partHistory || null;
166
166
  this.viewer = viewer || null;
167
+ this._autoFocusOnExpand = true;
167
168
  this._highlightCallback = typeof onHighlightRequest === 'function' ? onHighlightRequest : null;
168
169
  this._clearHighlightCallback = typeof onClearHighlight === 'function' ? onClearHighlight : null;
169
170
  this._beforeConstraintChangeHandler = callBeforeChange;
@@ -209,6 +209,10 @@ export class AssemblyConstraintsWidget {
209
209
  this._scheduleSync();
210
210
  }
211
211
 
212
+ collapseExpandedDialogs() {
213
+ try { this._constraintList?.collapseExpandedEntries?.({ clearOpenState: true }); } catch { /* ignore */ }
214
+ }
215
+
212
216
  #handleHistoryChange() {
213
217
  this._scheduleSync();
214
218
  if (this._ignoreFullSolveChangeCount > 0) {