brep-io-kernel 1.0.17 → 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.
Files changed (32) hide show
  1. package/dist-kernel/brep-kernel.js +13547 -12034
  2. package/package.json +1 -1
  3. package/src/FeatureRegistry.js +3 -0
  4. package/src/PartHistory.js +141 -0
  5. package/src/UI/CADmaterials.js +2 -2
  6. package/src/UI/MainToolbar.js +39 -2
  7. package/src/UI/SelectionFilter.js +438 -0
  8. package/src/UI/featureDialogWidgets/booleanOperationField.js +33 -52
  9. package/src/UI/featureDialogWidgets/referenceSelectionField.js +10 -0
  10. package/src/UI/featureDialogs.js +841 -8
  11. package/src/UI/history/HistoryCollectionWidget.js +20 -1
  12. package/src/UI/pmi/AnnotationRegistry.js +3 -0
  13. package/src/UI/toolbarButtons/orientToFaceButton.js +3 -0
  14. package/src/UI/toolbarButtons/registerDefaultButtons.js +0 -6
  15. package/src/UI/toolbarButtons/registerSelectionButtons.js +68 -0
  16. package/src/UI/viewer.js +22 -4
  17. package/src/assemblyConstraints/AssemblyConstraintRegistry.js +3 -0
  18. package/src/features/boolean/BooleanFeature.js +15 -0
  19. package/src/features/chamfer/ChamferFeature.js +12 -0
  20. package/src/features/extrude/ExtrudeFeature.js +11 -0
  21. package/src/features/fillet/FilletFeature.js +12 -0
  22. package/src/features/hole/HoleFeature.js +15 -0
  23. package/src/features/loft/LoftFeature.js +17 -0
  24. package/src/features/mirror/MirrorFeature.js +14 -0
  25. package/src/features/patternLinear/PatternLinearFeature.js +9 -0
  26. package/src/features/patternRadial/PatternRadialFeature.js +13 -0
  27. package/src/features/plane/PlaneFeature.js +10 -0
  28. package/src/features/revolve/RevolveFeature.js +15 -0
  29. package/src/features/sketch/SketchFeature.js +11 -0
  30. package/src/features/sweep/SweepFeature.js +17 -0
  31. package/src/features/transform/TransformFeature.js +12 -0
  32. package/src/features/tube/TubeFeature.js +12 -0
@@ -17,6 +17,15 @@ export class SelectionFilter {
17
17
  static previouseAllowedSelectionTypes = null;
18
18
  static _hovered = new Set(); // objects currently hover-highlighted
19
19
  static hoverColor = '#fbff00'; // default hover tint
20
+ static _selectionActions = new Map();
21
+ static _selectionActionOrder = [];
22
+ static _selectionActionSeq = 1;
23
+ static _selectionActionListenerBound = false;
24
+ static _selectionActionsPending = false;
25
+ static _selectionActionBar = null;
26
+ static _historyContextActions = new Map();
27
+ static _selectionActionSeparator = null;
28
+ static _contextSuppressReasons = new Set();
20
29
 
21
30
  constructor() {
22
31
  throw new Error("SelectionFilter is static and cannot be instantiated.");
@@ -188,6 +197,25 @@ export class SelectionFilter {
188
197
  let clone;
189
198
  try { clone = typeof origMat.clone === 'function' ? origMat.clone() : origMat; } catch { clone = origMat; }
190
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 { }
191
219
  try {
192
220
  t.userData.__hoverOrigMat = origMat;
193
221
  t.userData.__hoverMatApplied = true;
@@ -429,6 +457,11 @@ export class SelectionFilter {
429
457
  if (!targetObj) return false;
430
458
 
431
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 */ }
432
465
  const objType = targetObj.type;
433
466
  const objectName = targetObj.name || `${objType}(${targetObj.position?.x || 0},${targetObj.position?.y || 0},${targetObj.position?.z || 0})`;
434
467
 
@@ -617,6 +650,411 @@ export class SelectionFilter {
617
650
  } catch (_) { /* noop */ }
618
651
  }
619
652
 
653
+ static getSelectedObjects(options = {}) {
654
+ const scene = options.scene
655
+ || SelectionFilter.viewer?.partHistory?.scene
656
+ || SelectionFilter.viewer?.scene
657
+ || null;
658
+ const selected = [];
659
+ if (!scene || typeof scene.traverse !== 'function') return selected;
660
+ scene.traverse((obj) => {
661
+ if (obj && obj.selected) selected.push(obj);
662
+ });
663
+ return selected;
664
+ }
665
+
666
+ static registerSelectionAction(spec = {}) {
667
+ if (!spec) return null;
668
+ const id = String(spec.id || `selection-action-${SelectionFilter._selectionActionSeq++}`);
669
+ const entry = SelectionFilter._selectionActions.get(id) || { id };
670
+ entry.label = spec.label ?? entry.label ?? '';
671
+ entry.title = spec.title ?? entry.title ?? entry.label ?? '';
672
+ entry.onClick = spec.onClick ?? entry.onClick ?? null;
673
+ entry.shouldShow = typeof spec.shouldShow === 'function' ? spec.shouldShow : (entry.shouldShow || null);
674
+ SelectionFilter._selectionActions.set(id, entry);
675
+ if (!SelectionFilter._selectionActionOrder.includes(id)) {
676
+ SelectionFilter._selectionActionOrder.push(id);
677
+ }
678
+ SelectionFilter._ensureSelectionActionListener();
679
+ SelectionFilter._syncSelectionActions();
680
+ return id;
681
+ }
682
+
683
+ static unregisterSelectionAction(id) {
684
+ if (!id) return;
685
+ const entry = SelectionFilter._selectionActions.get(id);
686
+ if (entry?.btn && entry.btn.parentNode) {
687
+ try { entry.btn.parentNode.removeChild(entry.btn); } catch { }
688
+ }
689
+ SelectionFilter._selectionActions.delete(id);
690
+ SelectionFilter._selectionActionOrder = SelectionFilter._selectionActionOrder.filter((k) => k !== id);
691
+ SelectionFilter._syncSelectionActions();
692
+ }
693
+
694
+ static refreshSelectionActions() {
695
+ SelectionFilter._syncSelectionActions();
696
+ }
697
+
698
+ static _ensureSelectionActionListener() {
699
+ if (SelectionFilter._selectionActionListenerBound) return;
700
+ if (typeof window === 'undefined') return;
701
+ SelectionFilter._selectionActionListenerBound = true;
702
+ window.addEventListener('selection-changed', () => SelectionFilter._syncSelectionActions());
703
+ }
704
+
705
+ static _syncSelectionActions() {
706
+ const viewer = SelectionFilter.viewer;
707
+ const bar = SelectionFilter._ensureSelectionActionBar(viewer);
708
+ if (!bar) {
709
+ SelectionFilter._selectionActionsPending = true;
710
+ return;
711
+ }
712
+ SelectionFilter._selectionActionsPending = false;
713
+ const suppressed = SelectionFilter._contextSuppressReasons?.size > 0;
714
+ if (suppressed) {
715
+ try { bar.style.display = 'none'; } catch { }
716
+ return;
717
+ }
718
+ const selection = SelectionFilter.getSelectedObjects();
719
+ const hideAll = !!viewer?._sketchMode;
720
+ const utilityButtons = [];
721
+ const actions = SelectionFilter._selectionActions;
722
+
723
+ for (const id of SelectionFilter._selectionActionOrder) {
724
+ const entry = actions.get(id);
725
+ if (!entry) continue;
726
+ if (!entry.btn) {
727
+ entry.btn = SelectionFilter._createSelectionActionButton(entry);
728
+ }
729
+ if (!entry.btn) continue;
730
+ try {
731
+ entry.btn.textContent = String(entry.label ?? '');
732
+ entry.btn.title = String(entry.title ?? entry.label ?? '');
733
+ entry.btn.__sabOnClick = entry.onClick;
734
+ const isIcon = String(entry.label || '').length <= 2;
735
+ entry.btn.classList.toggle('sab-icon', isIcon);
736
+ } catch { }
737
+ let show = !hideAll;
738
+ if (show) {
739
+ if (typeof entry.shouldShow === 'function') {
740
+ try { show = !!entry.shouldShow(selection, viewer); } catch { show = false; }
741
+ } else {
742
+ show = selection.length > 0;
743
+ }
744
+ }
745
+ if (show) utilityButtons.push(entry.btn);
746
+ }
747
+
748
+ const historySpecs = SelectionFilter._getHistoryContextActionSpecs(selection, viewer);
749
+ const contextButtons = [];
750
+ const desiredIds = new Set();
751
+ for (const spec of historySpecs) {
752
+ if (!spec || !spec.id) continue;
753
+ desiredIds.add(spec.id);
754
+ const existing = SelectionFilter._historyContextActions.get(spec.id) || { id: spec.id };
755
+ existing.label = spec.label ?? existing.label ?? '';
756
+ existing.title = spec.title ?? existing.title ?? existing.label ?? '';
757
+ existing.onClick = spec.onClick ?? existing.onClick ?? null;
758
+ existing.shouldShow = typeof spec.shouldShow === 'function' ? spec.shouldShow : null;
759
+ if (!existing.btn) {
760
+ existing.btn = SelectionFilter._createSelectionActionButton(existing);
761
+ }
762
+ if (!existing.btn) continue;
763
+ try {
764
+ existing.btn.textContent = String(existing.label ?? '');
765
+ existing.btn.title = String(existing.title ?? existing.label ?? '');
766
+ existing.btn.__sabOnClick = existing.onClick;
767
+ const isIcon = String(existing.label || '').length <= 2;
768
+ existing.btn.classList.toggle('sab-icon', isIcon);
769
+ } catch { }
770
+ let show = !hideAll;
771
+ if (show && typeof existing.shouldShow === 'function') {
772
+ try { show = !!existing.shouldShow(selection, viewer); } catch { show = false; }
773
+ } else if (!existing.shouldShow) {
774
+ show = show && selection.length > 0;
775
+ }
776
+ if (show) contextButtons.push(existing.btn);
777
+ SelectionFilter._historyContextActions.set(spec.id, existing);
778
+ }
779
+
780
+ for (const [id, entry] of SelectionFilter._historyContextActions.entries()) {
781
+ if (desiredIds.has(id)) continue;
782
+ try { entry.btn?.remove?.(); } catch { }
783
+ SelectionFilter._historyContextActions.delete(id);
784
+ }
785
+
786
+ try { bar.textContent = ''; } catch { }
787
+ for (const btn of utilityButtons) {
788
+ try { bar.appendChild(btn); } catch { }
789
+ }
790
+ if (utilityButtons.length && contextButtons.length) {
791
+ const sep = SelectionFilter._ensureSelectionActionSeparator();
792
+ if (sep) {
793
+ try { bar.appendChild(sep); } catch { }
794
+ }
795
+ }
796
+ for (const btn of contextButtons) {
797
+ try { bar.appendChild(btn); } catch { }
798
+ }
799
+ try { bar.style.display = (utilityButtons.length + contextButtons.length) > 0 ? 'flex' : 'none'; } catch { }
800
+ }
801
+
802
+ static _getHistoryContextActionSpecs(selection, viewer) {
803
+ const out = [];
804
+ const items = Array.isArray(selection) ? selection : [];
805
+ const safeId = (prefix, key) => {
806
+ const raw = String(key || '').toLowerCase().replace(/[^a-z0-9_-]+/g, '-');
807
+ return `${prefix}-${raw || 'item'}`;
808
+ };
809
+ const addSpec = (spec) => {
810
+ if (spec && spec.id) out.push(spec);
811
+ };
812
+
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;
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
+ });
852
+ }
853
+ }
854
+
855
+ const pmimode = viewer?._pmiMode || null;
856
+ const annotationRegistry = viewer?.annotationRegistry || null;
857
+ if (pmimode && annotationRegistry && typeof annotationRegistry.list === 'function') {
858
+ const annClasses = annotationRegistry.list();
859
+ for (const AnnClass of annClasses) {
860
+ if (!AnnClass) continue;
861
+ let result = null;
862
+ try { result = AnnClass.showContexButton?.(items); } catch { result = null; }
863
+ if (!result) continue;
864
+ if (result && typeof result === 'object' && result.show === false) continue;
865
+ const label = (result && typeof result === 'object' && result.label) || AnnClass.longName || AnnClass.shortName || AnnClass.name || 'Annotation';
866
+ const typeKey = AnnClass.type || AnnClass.entityType || AnnClass.shortName || AnnClass.name || label;
867
+ const params = SelectionFilter._extractContextParams(result);
868
+ addSpec({
869
+ id: safeId('ctx-annotation', typeKey),
870
+ label,
871
+ title: `Create ${label}`,
872
+ onClick: () => SelectionFilter._createAnnotationFromContext(viewer, typeKey, params),
873
+ });
874
+ }
875
+ }
876
+
877
+ return out;
878
+ }
879
+
880
+ static async _createFeatureFromContext(viewer, typeKey, params = null) {
881
+ if (!viewer || !typeKey) return;
882
+ SelectionFilter.setContextBarSuppressed('context-create', true);
883
+ setTimeout(() => SelectionFilter.setContextBarSuppressed('context-create', false), 0);
884
+ let entry = null;
885
+ if (viewer.historyWidget && typeof viewer.historyWidget._handleAddEntry === 'function') {
886
+ try { entry = await viewer.historyWidget._handleAddEntry(typeKey); } catch { }
887
+ } else {
888
+ try { entry = await viewer.partHistory?.newFeature?.(typeKey); } catch { }
889
+ }
890
+ if (entry && params && typeof params === 'object') {
891
+ SelectionFilter._applyContextParamsToEntry(viewer, entry, params);
892
+ }
893
+ return entry;
894
+ }
895
+
896
+ static _createConstraintFromContext(viewer, typeKey, params = null) {
897
+ if (!viewer || !typeKey) return;
898
+ SelectionFilter.setContextBarSuppressed('context-create', true);
899
+ setTimeout(() => SelectionFilter.setContextBarSuppressed('context-create', false), 0);
900
+ try { viewer.partHistory?.assemblyConstraintHistory?.addConstraint?.(typeKey, params || null); } catch { }
901
+ }
902
+
903
+ static _createAnnotationFromContext(viewer, typeKey, params = null) {
904
+ if (!viewer || !typeKey) return;
905
+ SelectionFilter.setContextBarSuppressed('context-create', true);
906
+ setTimeout(() => SelectionFilter.setContextBarSuppressed('context-create', false), 0);
907
+ try { viewer._pmiMode?._annotationHistory?.createAnnotation?.(typeKey, params || null); } catch { }
908
+ }
909
+
910
+ static _extractContextParams(result) {
911
+ if (!result || result === true) return null;
912
+ if (typeof result !== 'object') return null;
913
+ if (result.params && typeof result.params === 'object') return result.params;
914
+ if (result.field) {
915
+ return { [result.field]: result.value };
916
+ }
917
+ return null;
918
+ }
919
+
920
+ static _applyContextParamsToEntry(viewer, entry, params = {}) {
921
+ if (!entry || !params || typeof params !== 'object') return;
922
+ try {
923
+ for (const [key, value] of Object.entries(params)) {
924
+ entry.inputParams = entry.inputParams || {};
925
+ entry.inputParams[key] = value;
926
+ }
927
+ } catch { }
928
+ const historyWidget = viewer?.historyWidget || null;
929
+ if (!historyWidget || typeof historyWidget._handleSchemaChange !== 'function') return;
930
+ try {
931
+ const id = entry?.inputParams?.id ?? entry?.id ?? null;
932
+ const entryId = String(id ?? '');
933
+ historyWidget._handleSchemaChange(entryId, entry, { key: '__context', value: params });
934
+ const refresh = () => {
935
+ try {
936
+ const form = historyWidget.getFormForEntry?.(entryId);
937
+ if (form && typeof form.refreshFromParams === 'function') {
938
+ form.refreshFromParams();
939
+ return true;
940
+ }
941
+ } catch { }
942
+ return false;
943
+ };
944
+ if (!refresh()) {
945
+ setTimeout(() => { try { refresh(); } catch { } }, 0);
946
+ }
947
+ } catch { }
948
+ }
949
+
950
+ static setContextBarSuppressed(key, active) {
951
+ if (!key) return;
952
+ const reasons = SelectionFilter._contextSuppressReasons || new Set();
953
+ SelectionFilter._contextSuppressReasons = reasons;
954
+ const had = reasons.has(key);
955
+ if (active) {
956
+ reasons.add(key);
957
+ } else {
958
+ reasons.delete(key);
959
+ }
960
+ if (had !== reasons.has(key)) {
961
+ SelectionFilter._syncSelectionActions();
962
+ }
963
+ }
964
+
965
+ static _ensureSelectionActionSeparator() {
966
+ if (SelectionFilter._selectionActionSeparator && SelectionFilter._selectionActionSeparator.isConnected) {
967
+ return SelectionFilter._selectionActionSeparator;
968
+ }
969
+ if (typeof document === 'undefined') return null;
970
+ const el = document.createElement('div');
971
+ el.className = 'selection-action-sep';
972
+ SelectionFilter._selectionActionSeparator = el;
973
+ return el;
974
+ }
975
+
976
+ static _ensureSelectionActionBar(viewer) {
977
+ if (SelectionFilter._selectionActionBar && SelectionFilter._selectionActionBar.isConnected) {
978
+ return SelectionFilter._selectionActionBar;
979
+ }
980
+ if (typeof document === 'undefined') return null;
981
+ const host = viewer?.container || document.body || null;
982
+ if (!host) return null;
983
+ try {
984
+ if (!document.getElementById('selection-action-bar-styles')) {
985
+ const style = document.createElement('style');
986
+ style.id = 'selection-action-bar-styles';
987
+ style.textContent = `
988
+ .selection-action-bar {
989
+ position: absolute;
990
+ top: 100px;
991
+ right: 8px;
992
+ display: flex;
993
+ flex-direction: column;
994
+ gap: 6px;
995
+ align-items: stretch;
996
+ background: rgba(20,24,30,.85);
997
+ border: 1px solid #262b36;
998
+ border-radius: 8px;
999
+ padding: 6px;
1000
+ color: #ddd;
1001
+ min-width: 40px;
1002
+ max-width: 150px;
1003
+ z-index: 12;
1004
+ user-select: none;
1005
+ }
1006
+ .selection-action-bar .sab-btn {
1007
+ background: transparent;
1008
+ border-radius: 6px;
1009
+ padding: 4px 8px;
1010
+ width: 100%;
1011
+ min-height: 34px;
1012
+ box-sizing: border-box;
1013
+ color: #ddd;
1014
+ border: 1px solid #364053;
1015
+ cursor: pointer;
1016
+ }
1017
+ .selection-action-bar .sab-btn:hover { filter: brightness(1.08); }
1018
+ .selection-action-bar .sab-btn:active { filter: brightness(1.15); }
1019
+ .selection-action-bar .sab-btn.sab-icon {
1020
+ font-size: 16px;
1021
+ min-width: 36px;
1022
+ }
1023
+ .selection-action-bar .selection-action-sep {
1024
+ height: 1px;
1025
+ width: 100%;
1026
+ background: #2c3443;
1027
+ opacity: 0.9;
1028
+ margin: 4px 0;
1029
+ }
1030
+ `;
1031
+ document.head.appendChild(style);
1032
+ }
1033
+ } catch { }
1034
+ const bar = document.createElement('div');
1035
+ bar.className = 'selection-action-bar';
1036
+ host.appendChild(bar);
1037
+ SelectionFilter._selectionActionBar = bar;
1038
+ return bar;
1039
+ }
1040
+
1041
+ static _createSelectionActionButton(entry) {
1042
+ try {
1043
+ const btn = document.createElement('button');
1044
+ btn.className = 'sab-btn';
1045
+ btn.textContent = String(entry?.label ?? '');
1046
+ btn.title = String(entry?.title ?? entry?.label ?? '');
1047
+ btn.__sabOnClick = entry?.onClick ?? null;
1048
+ btn.addEventListener('click', (e) => {
1049
+ e.stopPropagation();
1050
+ try { btn.__sabOnClick && btn.__sabOnClick(); } catch { }
1051
+ });
1052
+ const isIcon = String(entry?.label || '').length <= 2;
1053
+ if (isIcon) btn.classList.add('sab-icon');
1054
+ return btn;
1055
+ } catch { return null; }
1056
+ }
1057
+
620
1058
  static #logAllowedTypesChange(next, reason = '') {
621
1059
  try {
622
1060
  const desc = next === SelectionFilter.ALL
@@ -1,9 +1,11 @@
1
+ import { renderReferenceSelectionField } from './referenceSelectionField.js';
2
+
1
3
  export function renderBooleanOperationField({ ui, key, def, controlWrap }) {
2
4
  if (!ui.params[key] || typeof ui.params[key] !== 'object') {
3
- ui.params[key] = { targets: [], operation: 'NONE', operation: 'NONE' };
5
+ ui.params[key] = { targets: [], operation: 'NONE' };
4
6
  } else {
5
7
  if (!Array.isArray(ui.params[key].targets)) ui.params[key].targets = [];
6
- if (!ui.params[key].operation && !ui.params[key].operation) ui.params[key].operation = 'NONE';
8
+ if (!ui.params[key].operation) ui.params[key].operation = 'NONE';
7
9
  }
8
10
 
9
11
  const wrap = document.createElement('div');
@@ -19,70 +21,49 @@ export function renderBooleanOperationField({ ui, key, def, controlWrap }) {
19
21
  opt.textContent = String(op);
20
22
  sel.appendChild(opt);
21
23
  }
22
- sel.value = String((ui.params[key].operation ?? ui.params[key].operation) || 'NONE');
24
+ sel.value = String(ui.params[key].operation || 'NONE');
23
25
  sel.addEventListener('change', () => {
24
26
  if (!ui.params[key] || typeof ui.params[key] !== 'object') ui.params[key] = { targets: [], operation: 'NONE' };
25
27
  ui.params[key].operation = sel.value;
26
- ui.params[key].operation = sel.value;
27
28
  ui._emitParamsChange(key, ui.params[key]);
28
29
  });
29
30
  wrap.appendChild(sel);
30
31
 
31
- const refWrap = document.createElement('div');
32
- refWrap.className = 'ref-multi-wrap';
33
- const chipsWrap = document.createElement('div');
34
- chipsWrap.className = 'ref-chips';
35
- refWrap.appendChild(chipsWrap);
36
-
37
- const inputElTargets = document.createElement('input');
38
- inputElTargets.type = 'text';
39
- inputElTargets.className = 'input';
40
- inputElTargets.dataset.multiple = 'true';
41
- inputElTargets.placeholder = 'Click then select solids…';
42
- ui._renderChips(chipsWrap, key, Array.isArray(ui.params[key].targets) ? ui.params[key].targets : []);
43
-
44
- const activate = () => {
45
- ui._activateReferenceSelection(inputElTargets, { selectionFilter: ['SOLID'] });
32
+ const refMount = document.createElement('div');
33
+ const targetsDef = {
34
+ type: 'reference_selection',
35
+ multiple: true,
36
+ selectionFilter: ['SOLID'],
46
37
  };
47
- chipsWrap.addEventListener('click', activate);
48
- inputElTargets.addEventListener('click', activate);
49
-
50
- inputElTargets.addEventListener('change', () => {
51
- if (inputElTargets.dataset && inputElTargets.dataset.forceClear === 'true') {
52
- if (!ui.params[key] || typeof ui.params[key] !== 'object') ui.params[key] = { targets: [], operation: 'NONE' };
53
- ui.params[key].targets = [];
54
- ui._renderChips(chipsWrap, key, ui.params[key].targets);
55
- inputElTargets.value = '';
56
- delete inputElTargets.dataset.forceClear;
38
+ const valueAdapter = {
39
+ read: () => {
40
+ const current = ui.params[key];
41
+ if (!current || typeof current !== 'object') return [];
42
+ return Array.isArray(current.targets) ? current.targets : [];
43
+ },
44
+ write: (next) => {
45
+ if (!ui.params[key] || typeof ui.params[key] !== 'object') ui.params[key] = { targets: [], operation: sel.value || 'NONE' };
46
+ ui.params[key].targets = Array.isArray(next) ? next : [];
47
+ },
48
+ emit: () => {
57
49
  ui._emitParamsChange(key, ui.params[key]);
58
- return;
59
- }
60
- if (!ui.params[key] || typeof ui.params[key] !== 'object') ui.params[key] = { targets: [], operation: 'NONE' };
61
- let incoming = [];
62
- try {
63
- const parsed = JSON.parse(inputElTargets.value);
64
- if (Array.isArray(parsed)) incoming = parsed;
65
- } catch (_) {
66
- if (inputElTargets.value && String(inputElTargets.value).trim() !== '') incoming = [String(inputElTargets.value).trim()];
67
- }
68
- const cur = Array.isArray(ui.params[key].targets) ? ui.params[key].targets : [];
69
- for (const name of incoming) {
70
- if (!cur.includes(name)) cur.push(name);
71
- }
72
- ui.params[key].targets = cur;
73
- ui._renderChips(chipsWrap, key, cur);
74
- inputElTargets.value = '';
75
- ui._emitParamsChange(key, ui.params[key]);
50
+ },
51
+ };
52
+ const refField = renderReferenceSelectionField({
53
+ ui,
54
+ key,
55
+ def: targetsDef,
56
+ id: `${key}-targets`,
57
+ controlWrap: refMount,
58
+ valueAdapter,
76
59
  });
77
-
78
- refWrap.appendChild(inputElTargets);
79
- wrap.appendChild(refWrap);
60
+ wrap.appendChild(refMount);
80
61
 
81
62
  controlWrap.appendChild(wrap);
82
63
 
83
64
  return {
84
- inputEl: inputElTargets,
85
- activate,
65
+ inputEl: refField.inputEl,
66
+ activate: refField.activate,
86
67
  readValue() {
87
68
  const current = ui.params[key];
88
69
  if (!current || typeof current !== 'object') {
@@ -5,6 +5,7 @@ export function renderReferenceSelectionField({ ui, key, def, id, controlWrap, v
5
5
  inputEl.type = 'hidden';
6
6
  inputEl.id = id;
7
7
  try { inputEl.dataset.key = String(key); } catch (_) { }
8
+ try { inputEl.__refSelectionDef = def; } catch (_) { }
8
9
 
9
10
  const isMulti = !!def.multiple;
10
11
  if (isMulti) inputEl.dataset.multiple = 'true';
@@ -132,6 +133,15 @@ export function renderReferenceSelectionField({ ui, key, def, id, controlWrap, v
132
133
  inputEl.value = initial ?? '';
133
134
 
134
135
  valueWrap.addEventListener('click', () => ui._activateReferenceSelection(inputEl, def));
136
+ valueWrap.addEventListener('mouseenter', () => {
137
+ const normalized = normalizeReferenceName(inputEl.value || readRawValue());
138
+ if (normalized) {
139
+ try { ui._hoverReferenceSelectionItem?.(inputEl, def, normalized); } catch (_) { }
140
+ }
141
+ });
142
+ valueWrap.addEventListener('mouseleave', () => {
143
+ try { ui._clearReferenceSelectionHover?.(inputEl); } catch (_) { }
144
+ });
135
145
  refWrap.appendChild(valueWrap);
136
146
 
137
147
  inputEl.addEventListener('change', () => {