brep-io-kernel 1.0.18 → 1.0.19

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.
@@ -205,6 +205,7 @@ export class SchemaForm {
205
205
  // If target is inside the same active element (e.g., clicking within the input), skip
206
206
  if (target && typeof target.closest === 'function') {
207
207
  if (target.closest('[active-reference-selection]')) return;
208
+ if (target.closest('.ref-active')) return;
208
209
  }
209
210
  this._stopActiveReferenceSelection();
210
211
  } catch (_) { }
@@ -632,7 +633,7 @@ export class SchemaForm {
632
633
  }
633
634
  if (!mat) {
634
635
  try {
635
- mat = new LineMaterial({ color: colorHex, linewidth: 3, transparent: true, opacity: 0.95, worldUnits: false });
636
+ mat = new LineMaterial({ color: colorHex, linewidth: 3, transparent: true, opacity: 0.95, worldUnits: false, dashed: false });
636
637
  } catch (_) { mat = null; }
637
638
  }
638
639
  if (mat && mat.color && typeof mat.color.set === 'function') {
@@ -643,6 +644,10 @@ export class SchemaForm {
643
644
  try { mat.opacity = Number.isFinite(mat.opacity) ? Math.min(0.95, mat.opacity) : 0.95; } catch (_) { }
644
645
  try { mat.depthTest = false; } catch (_) { }
645
646
  try { mat.depthWrite = false; } catch (_) { }
647
+ try { if (typeof mat.dashed !== 'undefined') mat.dashed = false; } catch (_) { }
648
+ try { if (typeof mat.dashScale !== 'undefined') mat.dashScale = 1; } catch (_) { }
649
+ try { if (typeof mat.dashSize !== 'undefined') mat.dashSize = 1; } catch (_) { }
650
+ try { if (typeof mat.gapSize !== 'undefined') mat.gapSize = 0; } catch (_) { }
646
651
  try { this._syncPreviewLineResolution(mat); } catch (_) { }
647
652
  }
648
653
  return mat;
@@ -724,6 +729,7 @@ export class SchemaForm {
724
729
  child.userData = child.userData || {};
725
730
  child.userData.refPreview = true;
726
731
  child.userData.refName = refName;
732
+ if (!child.userData.previewType && child.type) child.userData.previewType = child.type;
727
733
  } catch (_) { }
728
734
  });
729
735
  } catch (_) { }
@@ -741,20 +747,54 @@ export class SchemaForm {
741
747
  return null;
742
748
  }
743
749
 
750
+ _buildEdgePreviewFromObject(obj, refName, colorHex = REF_PREVIEW_COLORS.EDGE) {
751
+ if (!obj) return null;
752
+ const positions = this._extractEdgeWorldPositions(obj);
753
+ if (!positions || positions.length < 6) return null;
754
+ const geom = new LineGeometry();
755
+ geom.setPositions(positions);
756
+ try { geom.computeBoundingSphere(); } catch (_) { }
757
+ const mat = this._createPreviewLineMaterial(obj.material, colorHex);
758
+ const line = new Line2(geom, mat || undefined);
759
+ try { line.computeLineDistances?.(); } catch (_) { }
760
+ try { if (line.material && typeof line.material.dashed !== 'undefined') line.material.dashed = false; } catch (_) { }
761
+ line.type = 'REF_PREVIEW_EDGE';
762
+ return this._configurePreviewObject(line, refName, 'EDGE');
763
+ }
764
+
765
+ _extractFaceEdgePositions(face) {
766
+ if (!face) return [];
767
+ const out = [];
768
+ const addEdge = (edge) => {
769
+ const positions = this._extractEdgeWorldPositions(edge);
770
+ if (positions && positions.length >= 6) out.push(positions);
771
+ };
772
+
773
+ if (Array.isArray(face.edges) && face.edges.length) {
774
+ for (const edge of face.edges) addEdge(edge);
775
+ return out;
776
+ }
777
+
778
+ const faceName = face?.name || face?.userData?.faceName || null;
779
+ const parentSolid = face?.parentSolid || face?.userData?.parentSolid || face?.parent || null;
780
+ if (!faceName || !parentSolid || !Array.isArray(parentSolid.children)) return out;
781
+
782
+ for (const child of parentSolid.children) {
783
+ if (!child || child.type !== SelectionFilter.EDGE) continue;
784
+ const faceA = child?.userData?.faceA || null;
785
+ const faceB = child?.userData?.faceB || null;
786
+ if (faceA === faceName || faceB === faceName) {
787
+ addEdge(child);
788
+ }
789
+ }
790
+ return out;
791
+ }
792
+
744
793
  _buildReferencePreviewObject(obj, refName) {
745
794
  if (!obj) return null;
746
795
  const type = String(obj.type || '').toUpperCase();
747
796
  if (type === SelectionFilter.EDGE || type === 'EDGE') {
748
- const positions = this._extractEdgeWorldPositions(obj);
749
- if (!positions || positions.length < 6) return null;
750
- const geom = new LineGeometry();
751
- geom.setPositions(positions);
752
- try { geom.computeBoundingSphere(); } catch (_) { }
753
- const mat = this._createPreviewLineMaterial(obj.material, REF_PREVIEW_COLORS.EDGE);
754
- const line = new Line2(geom, mat || undefined);
755
- try { line.computeLineDistances?.(); } catch (_) { }
756
- line.type = 'REF_PREVIEW_EDGE';
757
- return this._configurePreviewObject(line, refName, 'EDGE');
797
+ return this._buildEdgePreviewFromObject(obj, refName, REF_PREVIEW_COLORS.EDGE);
758
798
  }
759
799
  if (type === SelectionFilter.FACE || type === SelectionFilter.PLANE || type === 'FACE' || type === 'PLANE') {
760
800
  const geom = obj.geometry && typeof obj.geometry.clone === 'function' ? obj.geometry.clone() : null;
@@ -765,6 +805,20 @@ export class SchemaForm {
765
805
  const mesh = new THREE.Mesh(geom, mat || undefined);
766
806
  mesh.type = (type === SelectionFilter.PLANE || type === 'PLANE') ? 'REF_PREVIEW_PLANE' : 'REF_PREVIEW_FACE';
767
807
  try { mesh.matrixAutoUpdate = false; } catch (_) { }
808
+ const edges = Array.isArray(obj.edges) ? obj.edges : [];
809
+ if (edges.length) {
810
+ const group = new THREE.Group();
811
+ group.type = mesh.type;
812
+ try { group.userData = group.userData || {}; } catch (_) { }
813
+ try { group.userData.previewHasEdges = true; } catch (_) { }
814
+ try { group.userData.previewHasFace = true; } catch (_) { }
815
+ group.add(mesh);
816
+ for (const edge of edges) {
817
+ const edgePreview = this._buildEdgePreviewFromObject(edge, refName, REF_PREVIEW_COLORS.EDGE);
818
+ if (edgePreview) group.add(edgePreview);
819
+ }
820
+ return this._configurePreviewObject(group, refName, mesh.type);
821
+ }
768
822
  return this._configurePreviewObject(mesh, refName, mesh.type);
769
823
  }
770
824
  if (type === SelectionFilter.VERTEX || type === 'VERTEX') {
@@ -796,9 +850,32 @@ export class SchemaForm {
796
850
  const mat = this._createPreviewLineMaterial(null, REF_PREVIEW_COLORS.EDGE);
797
851
  const line = new Line2(geom, mat || undefined);
798
852
  try { line.computeLineDistances?.(); } catch (_) { }
853
+ try { if (line.material && typeof line.material.dashed !== 'undefined') line.material.dashed = false; } catch (_) { }
799
854
  line.type = 'REF_PREVIEW_EDGE';
800
855
  return this._configurePreviewObject(line, refName, 'EDGE');
801
856
  }
857
+ if (type === 'FACE' || type === 'PLANE') {
858
+ const group = new THREE.Group();
859
+ group.type = type === 'PLANE' ? 'REF_PREVIEW_PLANE' : 'REF_PREVIEW_FACE';
860
+ try { group.userData = group.userData || {}; } catch (_) { }
861
+ try { group.userData.previewHasEdges = true; } catch (_) { }
862
+ const edges = Array.isArray(snapshot.edgePositions) ? snapshot.edgePositions : [];
863
+ for (const positions of edges) {
864
+ if (!Array.isArray(positions) || positions.length < 6) continue;
865
+ const geom = new LineGeometry();
866
+ geom.setPositions(positions);
867
+ try { geom.computeBoundingSphere(); } catch (_) { }
868
+ const mat = this._createPreviewLineMaterial(null, REF_PREVIEW_COLORS.EDGE);
869
+ const line = new Line2(geom, mat || undefined);
870
+ try { line.computeLineDistances?.(); } catch (_) { }
871
+ try { if (line.material && typeof line.material.dashed !== 'undefined') line.material.dashed = false; } catch (_) { }
872
+ line.type = 'REF_PREVIEW_EDGE';
873
+ this._configurePreviewObject(line, refName, 'EDGE');
874
+ group.add(line);
875
+ }
876
+ if (group.children.length === 0) return null;
877
+ return this._configurePreviewObject(group, refName, group.type);
878
+ }
802
879
  if (type === 'VERTEX') {
803
880
  const pos = snapshot.position;
804
881
  if (!Array.isArray(pos) || pos.length < 3) return null;
@@ -881,8 +958,26 @@ export class SchemaForm {
881
958
  originalPresent = !!real;
882
959
  }
883
960
  if (originalPresent) {
884
- if (ghost.parent === group) {
885
- try { group.remove(ghost); } catch (_) { }
961
+ const keepGhost = !!(entry?.showWhenOriginalPresent || ghost?.userData?.previewHasEdges);
962
+ if (!keepGhost) {
963
+ if (ghost.parent === group) {
964
+ try { group.remove(ghost); } catch (_) { }
965
+ }
966
+ continue;
967
+ }
968
+ try {
969
+ ghost.traverse?.((child) => {
970
+ if (!child || !child.userData?.refPreview) return;
971
+ const pType = String(child.userData.previewType || child.type || '').toUpperCase();
972
+ if (pType.includes('REF_PREVIEW_FACE') || pType.includes('REF_PREVIEW_PLANE')) {
973
+ child.visible = false;
974
+ } else if (child.type === 'REF_PREVIEW_EDGE') {
975
+ child.visible = true;
976
+ }
977
+ });
978
+ } catch (_) { }
979
+ if (ghost.parent !== group) {
980
+ try { group.add(ghost); } catch (_) { }
886
981
  }
887
982
  continue;
888
983
  }
@@ -904,7 +999,34 @@ export class SchemaForm {
904
999
  if (!cache) return;
905
1000
  const store = this._getReferencePreviewPersistentBucket(inputEl);
906
1001
  for (const name of list) {
907
- if (!name || cache.has(name)) continue;
1002
+ if (!name) continue;
1003
+ const existing = cache.get(name) || null;
1004
+ const snapshot = store ? store[name] : null;
1005
+ if (snapshot) {
1006
+ const snapType = String(snapshot.type || '').toUpperCase();
1007
+ const snapEdges = Array.isArray(snapshot.edgePositions) ? snapshot.edgePositions : null;
1008
+ const isFaceSnap = (snapType === 'FACE' || snapType === 'PLANE') && snapEdges && snapEdges.length;
1009
+ const isEdgeSnap = snapType === 'EDGE' && Array.isArray(snapshot.positions) && snapshot.positions.length >= 6;
1010
+ const isVertexSnap = snapType === 'VERTEX' && Array.isArray(snapshot.position) && snapshot.position.length >= 3;
1011
+ if (isFaceSnap || isEdgeSnap || isVertexSnap) {
1012
+ const shouldOverride = !existing || !existing.fromSnapshot || isFaceSnap;
1013
+ if (shouldOverride) {
1014
+ const ghost = this._buildReferencePreviewFromSnapshot(name, snapshot);
1015
+ if (ghost) {
1016
+ cache.set(name, {
1017
+ object: ghost,
1018
+ type: snapshot.type || null,
1019
+ sourceUuid: snapshot.sourceUuid || null,
1020
+ sourceFeatureId: snapshot.sourceFeatureId || null,
1021
+ showWhenOriginalPresent: isFaceSnap || !!ghost?.userData?.previewHasEdges,
1022
+ fromSnapshot: true,
1023
+ });
1024
+ continue;
1025
+ }
1026
+ }
1027
+ }
1028
+ }
1029
+ if (cache.has(name)) continue;
908
1030
  const obj = scene.getObjectByName(name);
909
1031
  if (obj && !obj?.userData?.refPreview) {
910
1032
  this._storeReferencePreviewSnapshot(inputEl, def, obj);
@@ -919,6 +1041,8 @@ export class SchemaForm {
919
1041
  type: snapshot.type || null,
920
1042
  sourceUuid: snapshot.sourceUuid || null,
921
1043
  sourceFeatureId: snapshot.sourceFeatureId || null,
1044
+ showWhenOriginalPresent: String(snapshot.type || '').toUpperCase() === 'FACE' || String(snapshot.type || '').toUpperCase() === 'PLANE' || !!ghost?.userData?.previewHasEdges,
1045
+ fromSnapshot: true,
922
1046
  });
923
1047
  }
924
1048
  }
@@ -941,6 +1065,7 @@ export class SchemaForm {
941
1065
  type: obj.type || null,
942
1066
  sourceUuid,
943
1067
  sourceFeatureId,
1068
+ showWhenOriginalPresent: !!ghost?.userData?.previewHasEdges,
944
1069
  });
945
1070
  try {
946
1071
  const store = this._getReferencePreviewPersistentBucket(inputEl);
@@ -1043,13 +1168,78 @@ export class SchemaForm {
1043
1168
  } catch (_) { }
1044
1169
  }
1045
1170
 
1171
+ _ensureReferencePreviewSnapshots(inputEl, def) {
1172
+ try {
1173
+ if (!inputEl) return;
1174
+ if (inputEl.__refPreviewBackfillPromise) return;
1175
+ const names = this._collectReferenceSelectionNames(inputEl, def);
1176
+ if (!names.length) return;
1177
+ const store = this._getReferencePreviewPersistentBucket(inputEl);
1178
+ let missing = false;
1179
+ if (!store) missing = true;
1180
+ if (!missing) {
1181
+ for (const name of names) {
1182
+ const snap = store ? store[name] : null;
1183
+ const type = String(snap?.type || '').toUpperCase();
1184
+ if (!snap) { missing = true; break; }
1185
+ if (type === 'EDGE') {
1186
+ if (!Array.isArray(snap.positions) || snap.positions.length < 6) { missing = true; break; }
1187
+ } else if (type === 'VERTEX') {
1188
+ if (!Array.isArray(snap.position) || snap.position.length < 3) { missing = true; break; }
1189
+ } else if (type === 'FACE' || type === 'PLANE') {
1190
+ if (!Array.isArray(snap.edgePositions) || snap.edgePositions.length === 0) { missing = true; break; }
1191
+ } else {
1192
+ missing = true;
1193
+ break;
1194
+ }
1195
+ }
1196
+ }
1197
+ if (!missing) return;
1198
+
1199
+ const partHistory = this.options?.partHistory || this.options?.viewer?.partHistory || null;
1200
+ if (!partHistory || typeof partHistory.runHistory !== 'function') return;
1201
+
1202
+ const prevStep = partHistory.currentHistoryStepId;
1203
+ const featureId = this.params?.id ?? this.params?.featureID ?? this.params?.featureId ?? null;
1204
+ if (featureId != null) {
1205
+ try { partHistory.currentHistoryStepId = String(featureId); } catch (_) { }
1206
+ }
1207
+ inputEl.__refPreviewBackfillPromise = Promise.resolve()
1208
+ .then(() => partHistory.runHistory())
1209
+ .catch(() => { /* ignore */ })
1210
+ .then(() => {
1211
+ if (featureId != null) {
1212
+ try { partHistory.currentHistoryStepId = prevStep; } catch (_) { }
1213
+ }
1214
+ })
1215
+ .then(() => {
1216
+ inputEl.__refPreviewBackfillPromise = null;
1217
+ if (SchemaForm.__activeRefInput !== inputEl) return;
1218
+ const scene = this._getReferenceSelectionScene();
1219
+ const latest = this._collectReferenceSelectionNames(inputEl, def);
1220
+ try { this._seedReferencePreviewCacheFromScene(inputEl, def, latest, scene); } catch (_) { }
1221
+ try { this._syncActiveReferenceSelectionPreview(inputEl, def); } catch (_) { }
1222
+ })
1223
+ .finally(() => {
1224
+ inputEl.__refPreviewBackfillPromise = null;
1225
+ });
1226
+ } catch (_) { }
1227
+ }
1228
+
1046
1229
  _hoverReferenceSelectionItem(inputEl, def, name) {
1047
1230
  try {
1048
- if (!inputEl || SchemaForm.__activeRefInput !== inputEl) return;
1231
+ if (!inputEl) return;
1232
+ const activeInput = SchemaForm.__activeRefInput || null;
1233
+ if (activeInput !== inputEl) {
1234
+ const wrap = inputEl.closest?.('.ref-active') || null;
1235
+ if (!wrap) return;
1236
+ }
1049
1237
  const normalized = normalizeReferenceName(name);
1050
1238
  if (!normalized) return;
1051
1239
  const scene = this._getReferenceSelectionScene();
1052
1240
  if (!scene) return;
1241
+ try { console.log('[ReferenceSelection] Hover', { name: normalized }); } catch (_) { }
1242
+ try { this._ensureReferencePreviewSnapshots(inputEl, def); } catch (_) { }
1053
1243
  try { this._seedReferencePreviewCacheFromScene(inputEl, def, [normalized], scene); } catch (_) { }
1054
1244
  try { this._syncActiveReferenceSelectionPreview(inputEl, def); } catch (_) { }
1055
1245
 
@@ -1064,6 +1254,24 @@ export class SchemaForm {
1064
1254
  try { target = scene.getObjectByName(`__refPreview__${normalized}`); } catch (_) { target = null; }
1065
1255
  }
1066
1256
  if (!target) return;
1257
+ if (!target.material && target.traverse) {
1258
+ let candidate = null;
1259
+ try {
1260
+ target.traverse((child) => {
1261
+ if (!child || candidate) return;
1262
+ if (child.type === 'REF_PREVIEW_EDGE') { candidate = child; return; }
1263
+ if (child.material) candidate = child;
1264
+ });
1265
+ } catch (_) { }
1266
+ if (candidate) target = candidate;
1267
+ }
1268
+ try {
1269
+ console.log('[ReferenceSelection] Hover target', {
1270
+ name: normalized,
1271
+ target: target || null,
1272
+ inScene: !!(target && target.parent),
1273
+ });
1274
+ } catch (_) { }
1067
1275
  inputEl.__refChipHoverActive = true;
1068
1276
  try { SelectionFilter.setHoverObject(target, { ignoreFilter: true }); } catch (_) { }
1069
1277
  } catch (_) { }
@@ -1146,6 +1354,7 @@ export class SchemaForm {
1146
1354
 
1147
1355
  // Highlight existing selections while this reference field is active
1148
1356
  try { this._syncActiveReferenceSelectionHighlight(inputEl, def); } catch (_) { }
1357
+ try { this._ensureReferencePreviewSnapshots(inputEl, def); } catch (_) { }
1149
1358
  try {
1150
1359
  if (typeof inputEl.__captureReferencePreview !== 'function') {
1151
1360
  inputEl.__captureReferencePreview = (obj) => this._storeReferencePreviewSnapshot(inputEl, def, obj);
@@ -1647,8 +1856,41 @@ export class SchemaForm {
1647
1856
  chipsWrap.textContent = '';
1648
1857
  const arr = Array.isArray(values) ? values : [];
1649
1858
  const normalizedValues = normalizeReferenceList(arr);
1650
- const inputEl = (this._inputs && typeof this._inputs.get === 'function') ? this._inputs.get(key) : null;
1651
- const def = (inputEl && inputEl.__refSelectionDef) || (this.schema ? (this.schema[key] || null) : null);
1859
+ let inputEl = (this._inputs && typeof this._inputs.get === 'function') ? this._inputs.get(key) : null;
1860
+ const resolveInput = () => {
1861
+ if (inputEl) return inputEl;
1862
+ const wrap = chipsWrap?.closest?.('.ref-multi-wrap, .ref-single-wrap') || null;
1863
+ const hidden = wrap ? wrap.querySelector('input[type="hidden"]') : null;
1864
+ if (hidden) inputEl = hidden;
1865
+ return inputEl;
1866
+ };
1867
+ const resolveDef = () => {
1868
+ const el = resolveInput();
1869
+ return (el && el.__refSelectionDef) || (this.schema ? (this.schema[key] || null) : null);
1870
+ };
1871
+ const def = resolveDef();
1872
+ if (chipsWrap && !chipsWrap.__refHoverBound) {
1873
+ chipsWrap.__refHoverBound = true;
1874
+ chipsWrap.__refHoverName = null;
1875
+ chipsWrap.addEventListener('mousemove', (ev) => {
1876
+ try {
1877
+ const chip = ev.target?.closest?.('.ref-chip');
1878
+ const refName = chip?.dataset?.refName || null;
1879
+ if (!refName) {
1880
+ chipsWrap.__refHoverName = null;
1881
+ this._clearReferenceSelectionHover(resolveInput());
1882
+ return;
1883
+ }
1884
+ if (chipsWrap.__refHoverName === refName) return;
1885
+ chipsWrap.__refHoverName = refName;
1886
+ this._hoverReferenceSelectionItem(resolveInput(), resolveDef(), refName);
1887
+ } catch (_) { }
1888
+ });
1889
+ chipsWrap.addEventListener('mouseleave', () => {
1890
+ chipsWrap.__refHoverName = null;
1891
+ this._clearReferenceSelectionHover(resolveInput());
1892
+ });
1893
+ }
1652
1894
  if (inputEl) {
1653
1895
  if (typeof inputEl.__updateSelectionMetadata === 'function') {
1654
1896
  try { inputEl.__updateSelectionMetadata(normalizedValues); } catch (_) { }
@@ -1665,6 +1907,7 @@ export class SchemaForm {
1665
1907
  for (const name of normalizedValues) {
1666
1908
  const chip = document.createElement('span');
1667
1909
  chip.className = 'ref-chip';
1910
+ try { chip.dataset.refName = name; } catch (_) { }
1668
1911
 
1669
1912
  const label = document.createElement('span');
1670
1913
  label.className = 'ref-chip-label';
@@ -1673,15 +1916,19 @@ export class SchemaForm {
1673
1916
 
1674
1917
  // Hover highlight on chip hover
1675
1918
  chip.addEventListener('mouseenter', () => {
1676
- if (def && def.type === 'reference_selection') {
1677
- this._hoverReferenceSelectionItem(inputEl, def, name);
1919
+ const liveInput = resolveInput();
1920
+ const liveDef = resolveDef();
1921
+ if (liveDef && liveDef.type === 'reference_selection') {
1922
+ this._hoverReferenceSelectionItem(liveInput, liveDef, name);
1678
1923
  return;
1679
1924
  }
1680
1925
  try { SelectionFilter.setHoverByName(this.options?.scene || null, name); } catch (_) { }
1681
1926
  });
1682
1927
  chip.addEventListener('mouseleave', () => {
1683
- if (def && def.type === 'reference_selection') {
1684
- this._clearReferenceSelectionHover(inputEl);
1928
+ const liveInput = resolveInput();
1929
+ const liveDef = resolveDef();
1930
+ if (liveDef && liveDef.type === 'reference_selection') {
1931
+ this._clearReferenceSelectionHover(liveInput);
1685
1932
  return;
1686
1933
  }
1687
1934
  try { SelectionFilter.clearHover(); } catch (_) { }
@@ -1,4 +1,5 @@
1
1
  import { SchemaForm } from '../featureDialogs.js';
2
+ import { SelectionFilter } from '../SelectionFilter.js';
2
3
  import { resolveEntryId, resolveHistoryDisplayInfo } from './historyDisplayInfo.js';
3
4
  import { HISTORY_COLLECTION_WIDGET_CSS } from './historyCollectionWidget.css.js';
4
5
 
@@ -43,6 +44,8 @@ export class HistoryCollectionWidget {
43
44
  this._addMenu = null;
44
45
  this._onGlobalClick = null;
45
46
  this._globalRefreshHandler = null;
47
+ this._contextSuppressKey = `hc-${Math.random().toString(36).slice(2, 9)}`;
48
+ this._contextSuppressActive = false;
46
49
 
47
50
  this.uiElement = document.createElement('div');
48
51
  this.uiElement.className = 'history-collection-widget-host';
@@ -100,6 +103,7 @@ export class HistoryCollectionWidget {
100
103
  }
101
104
 
102
105
  dispose() {
106
+ this._setContextSuppression(false);
103
107
  if (typeof this._listenerUnsub === 'function') {
104
108
  try { this._listenerUnsub(); } catch (_) {}
105
109
  }
@@ -152,6 +156,7 @@ export class HistoryCollectionWidget {
152
156
  this._listEl.textContent = '';
153
157
 
154
158
  if (!entries.length) {
159
+ this._setContextSuppression(false);
155
160
  const empty = document.createElement('div');
156
161
  empty.className = 'hc-empty';
157
162
  empty.textContent = 'No entries yet.';
@@ -185,6 +190,7 @@ export class HistoryCollectionWidget {
185
190
  targetId = null;
186
191
  }
187
192
  this._expandedId = targetId;
193
+ this._setContextSuppression(!!this._expandedId);
188
194
 
189
195
  for (let i = 0; i < entries.length; i++) {
190
196
  const entry = entries[i];
@@ -589,12 +595,24 @@ export class HistoryCollectionWidget {
589
595
  }
590
596
 
591
597
  _notifyEntryToggle(entry, isOpen) {
598
+ this._setContextSuppression(!!isOpen);
592
599
  if (!this._onEntryToggle) return;
593
600
  try {
594
601
  this._onEntryToggle(entry || null, isOpen);
595
602
  } catch (_) { /* ignore toggle hook errors */ }
596
603
  }
597
604
 
605
+ _setContextSuppression(isOpen) {
606
+ const next = !!isOpen;
607
+ if (this._contextSuppressActive === next) return;
608
+ this._contextSuppressActive = next;
609
+ if (SelectionFilter && typeof SelectionFilter.setContextBarSuppressed === 'function') {
610
+ try {
611
+ SelectionFilter.setContextBarSuppressed(this._contextSuppressKey, next);
612
+ } catch (_) { /* ignore */ }
613
+ }
614
+ }
615
+
598
616
  async _moveEntry(id, delta) {
599
617
  if (!id) return;
600
618
  const entries = this._getEntries();
@@ -659,7 +677,7 @@ export class HistoryCollectionWidget {
659
677
  this.render();
660
678
  this._emitCollectionChange('add', entry);
661
679
  this._deferScrollToEntry(createdEntryId);
662
- return;
680
+ return entry;
663
681
  }
664
682
  const entry = await this._instantiateEntryForType(typeStr);
665
683
  if (!entry) return;
@@ -680,6 +698,7 @@ export class HistoryCollectionWidget {
680
698
  this.render();
681
699
  this._emitCollectionChange('add', entry);
682
700
  this._deferScrollToEntry(createdEntryId);
701
+ return entry;
683
702
  }
684
703
 
685
704
  _handleSchemaChange(id, entry, details) {
@@ -29,6 +29,9 @@ class AnnotationRegistry {
29
29
  if (!ctor.longName) {
30
30
  ctor.longName = ctor.featureName || ctor.name || ctor.shortName || ctor.type || 'Annotation';
31
31
  }
32
+ if (typeof ctor.showContexButton !== 'function') {
33
+ ctor.showContexButton = () => false;
34
+ }
32
35
  }
33
36
  const typeKey = normalizeKey(
34
37
  handler.type
@@ -54,6 +54,9 @@ export class AssemblyConstraintRegistry {
54
54
  if (!ConstraintClass.longName) {
55
55
  ConstraintClass.longName = ConstraintClass.constraintName || ConstraintClass.name || ConstraintClass.shortName || 'Constraint';
56
56
  }
57
+ if (typeof ConstraintClass.showContexButton !== 'function') {
58
+ ConstraintClass.showContexButton = () => false;
59
+ }
57
60
  const keys = this.#collectKeys(ConstraintClass);
58
61
  if (!keys.typeKey) return;
59
62
 
@@ -25,6 +25,21 @@ export class BooleanFeature {
25
25
  static shortName = "B";
26
26
  static longName = "Boolean";
27
27
  static inputParamsSchema = inputParamsSchema;
28
+ static showContexButton(selectedItems) {
29
+ const items = Array.isArray(selectedItems) ? selectedItems : [];
30
+ const solids = items
31
+ .filter((it) => String(it?.type || '').toUpperCase() === 'SOLID')
32
+ .map((it) => it?.name)
33
+ .filter((name) => !!name);
34
+ if (solids.length < 2) return false;
35
+ const [targetSolid, ...tools] = solids;
36
+ return {
37
+ params: {
38
+ targetSolid,
39
+ boolean: { operation: 'UNION', targets: tools },
40
+ },
41
+ };
42
+ }
28
43
 
29
44
  constructor() {
30
45
  this.inputParams = {};
@@ -46,6 +46,18 @@ export class ChamferFeature {
46
46
  static shortName = "CH";
47
47
  static longName = "Chamfer";
48
48
  static inputParamsSchema = inputParamsSchema;
49
+ static showContexButton(selectedItems) {
50
+ const items = Array.isArray(selectedItems) ? selectedItems : [];
51
+ const edges = items
52
+ .filter((it) => {
53
+ const type = String(it?.type || '').toUpperCase();
54
+ return type === 'EDGE' || type === 'FACE';
55
+ })
56
+ .map((it) => it?.name || it?.userData?.edgeName || it?.userData?.faceName)
57
+ .filter((name) => !!name);
58
+ if (!edges.length) return false;
59
+ return { params: { edges } };
60
+ }
49
61
 
50
62
  constructor() {
51
63
  this.inputParams = {};
@@ -41,6 +41,17 @@ export class ExtrudeFeature {
41
41
  static shortName = "E";
42
42
  static longName = "Extrude";
43
43
  static inputParamsSchema = inputParamsSchema;
44
+ static showContexButton(selectedItems) {
45
+ const items = Array.isArray(selectedItems) ? selectedItems : [];
46
+ const pick = items.find((it) => {
47
+ const type = String(it?.type || '').toUpperCase();
48
+ return type === 'FACE' || type === 'SKETCH';
49
+ });
50
+ if (!pick) return false;
51
+ const name = pick?.name || pick?.userData?.faceName || pick?.userData?.edgeName || null;
52
+ if (!name) return false;
53
+ return { field: 'profile', value: name };
54
+ }
44
55
 
45
56
  constructor() {
46
57
  this.inputParams = {};
@@ -63,6 +63,18 @@ export class FilletFeature {
63
63
  static shortName = "F";
64
64
  static longName = "Fillet";
65
65
  static inputParamsSchema = inputParamsSchema;
66
+ static showContexButton(selectedItems) {
67
+ const items = Array.isArray(selectedItems) ? selectedItems : [];
68
+ const edges = items
69
+ .filter((it) => {
70
+ const type = String(it?.type || '').toUpperCase();
71
+ return type === 'EDGE' || type === 'FACE';
72
+ })
73
+ .map((it) => it?.name || it?.userData?.edgeName || it?.userData?.faceName)
74
+ .filter((name) => !!name);
75
+ if (!edges.length) return false;
76
+ return { params: { edges } };
77
+ }
66
78
 
67
79
  constructor() {
68
80
  this.inputParams = {};
@@ -569,6 +569,21 @@ export class HoleFeature {
569
569
  static shortName = 'H';
570
570
  static longName = 'Hole';
571
571
  static inputParamsSchema = inputParamsSchema;
572
+ static showContexButton(selectedItems) {
573
+ const items = Array.isArray(selectedItems) ? selectedItems : [];
574
+ const sketch = items.find((it) => {
575
+ const type = String(it?.type || '').toUpperCase();
576
+ if (type === 'SKETCH') return true;
577
+ if (it?.parent && String(it.parent.type || '').toUpperCase() === 'SKETCH') return true;
578
+ return false;
579
+ });
580
+ if (!sketch) return false;
581
+ const name = (String(sketch?.type || '').toUpperCase() === 'SKETCH')
582
+ ? sketch.name
583
+ : sketch.parent?.name;
584
+ if (!name) return false;
585
+ return { field: 'face', value: name };
586
+ }
572
587
 
573
588
  constructor() {
574
589
  this.inputParams = {};
@@ -58,6 +58,23 @@ export class LoftFeature {
58
58
  static shortName = "LOFT";
59
59
  static longName = "Loft";
60
60
  static inputParamsSchema = inputParamsSchema;
61
+ static showContexButton(selectedItems) {
62
+ const items = Array.isArray(selectedItems) ? selectedItems : [];
63
+ const profiles = items
64
+ .filter((it) => {
65
+ const type = String(it?.type || '').toUpperCase();
66
+ return type === 'FACE' || type === 'SKETCH';
67
+ })
68
+ .map((it) => {
69
+ if (!it) return null;
70
+ if (String(it.type || '').toUpperCase() === 'SKETCH') return it.name || null;
71
+ if (it.parent && String(it.parent.type || '').toUpperCase() === 'SKETCH') return it.parent.name || null;
72
+ return it.name || it.userData?.faceName || null;
73
+ })
74
+ .filter((name) => !!name);
75
+ if (profiles.length < 2) return false;
76
+ return { params: { profiles } };
77
+ }
61
78
 
62
79
  constructor() {
63
80
  this.inputParams = {};
@@ -35,6 +35,20 @@ export class MirrorFeature {
35
35
  static longName = "Mirror";
36
36
 
37
37
  static inputParamsSchema = inputParamsSchema;
38
+ static showContexButton(selectedItems) {
39
+ const items = Array.isArray(selectedItems) ? selectedItems : [];
40
+ const solids = items
41
+ .filter((it) => String(it?.type || '').toUpperCase() === 'SOLID')
42
+ .map((it) => it?.name)
43
+ .filter((name) => !!name);
44
+ const plane = items.find((it) => {
45
+ const type = String(it?.type || '').toUpperCase();
46
+ return type === 'FACE' || type === 'PLANE';
47
+ });
48
+ const planeName = plane?.name || plane?.userData?.faceName || null;
49
+ if (!solids.length || !planeName) return false;
50
+ return { params: { solids, mirrorPlane: planeName } };
51
+ }
38
52
 
39
53
  constructor() {
40
54
  this.inputParams = {};