brep-io-kernel 1.0.16 → 1.0.18

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.16",
3
+ "version": "1.0.18",
4
4
  "scripts": {
5
5
  "dev": "pnpm generateLicenses && pnpm build:kernel && vite --host 0.0.0.0",
6
6
  "build": "pnpm generateLicenses && vite build",
@@ -341,6 +341,7 @@ export class PartHistory {
341
341
  if (JSON.stringify(feature.inputParams) !== feature.lastRunInputParams) feature.dirty = true;
342
342
 
343
343
  instance.inputParams = await this.sanitizeInputParams(FeatureClass.inputParamsSchema, feature.inputParams);
344
+ try { this._captureReferencePreviewSnapshots(feature, FeatureClass.inputParamsSchema, instance.inputParams, instance.persistentData); } catch { }
344
345
  // check the timestamps of any objects referenced by reference_selection inputs; if any are newer than the feature timestamp, mark dirty
345
346
  for (const key in FeatureClass.inputParamsSchema) {
346
347
  if (Object.prototype.hasOwnProperty.call(FeatureClass.inputParamsSchema, key)) {
@@ -460,6 +461,111 @@ export class PartHistory {
460
461
  return this;
461
462
  }
462
463
 
464
+ _captureReferencePreviewSnapshots(feature, schema, resolvedParams, persistentTarget = null) {
465
+ if (!schema || !resolvedParams) return;
466
+ const stores = [];
467
+ if (feature) {
468
+ feature.persistentData = feature.persistentData || {};
469
+ stores.push(feature.persistentData);
470
+ }
471
+ if (persistentTarget && typeof persistentTarget === 'object') {
472
+ stores.push(persistentTarget);
473
+ }
474
+ if (!stores.length) return;
475
+ const ensureBucket = (store, key) => {
476
+ if (!store.__refPreviewSnapshots || typeof store.__refPreviewSnapshots !== 'object') {
477
+ store.__refPreviewSnapshots = {};
478
+ }
479
+ if (!store.__refPreviewSnapshots[key] || typeof store.__refPreviewSnapshots[key] !== 'object') {
480
+ store.__refPreviewSnapshots[key] = {};
481
+ }
482
+ return store.__refPreviewSnapshots[key];
483
+ };
484
+
485
+ const normalizeRefName = (obj) => {
486
+ if (!obj) return null;
487
+ const raw = obj.name != null ? String(obj.name).trim() : '';
488
+ if (raw) return raw;
489
+ const type = obj.type || 'OBJECT';
490
+ const pos = obj.position || {};
491
+ const x = Number.isFinite(pos.x) ? pos.x : 0;
492
+ const y = Number.isFinite(pos.y) ? pos.y : 0;
493
+ const z = Number.isFinite(pos.z) ? pos.z : 0;
494
+ return `${type}(${x},${y},${z})`;
495
+ };
496
+
497
+ const extractEdgeWorldPositions = (obj) => {
498
+ if (!obj) return [];
499
+ try { obj.updateMatrixWorld?.(true); } catch { }
500
+ try {
501
+ if (typeof obj.points === 'function') {
502
+ const pts = obj.points(true);
503
+ if (Array.isArray(pts) && pts.length) {
504
+ const flat = [];
505
+ for (const p of pts) {
506
+ if (!p) continue;
507
+ const x = Number(p.x);
508
+ const y = Number(p.y);
509
+ const z = Number(p.z);
510
+ if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(z)) continue;
511
+ flat.push(x, y, z);
512
+ }
513
+ if (flat.length >= 6) return flat;
514
+ }
515
+ }
516
+ } catch { /* ignore */ }
517
+
518
+ try {
519
+ const geom = obj.geometry;
520
+ const pos = geom && typeof geom.getAttribute === 'function' ? geom.getAttribute('position') : null;
521
+ if (!pos || pos.itemSize !== 3 || pos.count < 2) return [];
522
+ const tmp = new THREE.Vector3();
523
+ const flat = [];
524
+ for (let i = 0; i < pos.count; i++) {
525
+ tmp.set(pos.getX(i), pos.getY(i), pos.getZ(i));
526
+ tmp.applyMatrix4(obj.matrixWorld);
527
+ flat.push(tmp.x, tmp.y, tmp.z);
528
+ }
529
+ return flat.length >= 6 ? flat : [];
530
+ } catch { /* ignore */ }
531
+ return [];
532
+ };
533
+
534
+ for (const key in schema) {
535
+ if (!Object.prototype.hasOwnProperty.call(schema, key)) continue;
536
+ const def = schema[key];
537
+ if (!def || def.type !== 'reference_selection') continue;
538
+ const selected = Array.isArray(resolvedParams[key]) ? resolvedParams[key] : [];
539
+ if (!selected.length) continue;
540
+ const buckets = stores.map((store) => ensureBucket(store, key));
541
+ for (const obj of selected) {
542
+ if (!obj || typeof obj !== 'object') continue;
543
+ const refName = normalizeRefName(obj);
544
+ if (!refName) continue;
545
+ const objType = String(obj.type || '').toUpperCase();
546
+ const sourceUuid = obj.uuid || null;
547
+ const sourceFeatureId = obj.owningFeatureID ?? null;
548
+ if (objType === SelectionFilter.EDGE || objType === 'EDGE') {
549
+ const positions = extractEdgeWorldPositions(obj);
550
+ if (positions && positions.length >= 6) {
551
+ for (const bucket of buckets) {
552
+ bucket[refName] = { type: 'EDGE', positions, sourceUuid, sourceFeatureId };
553
+ }
554
+ }
555
+ } else if (objType === SelectionFilter.VERTEX || objType === 'VERTEX') {
556
+ const pos = new THREE.Vector3();
557
+ try {
558
+ if (typeof obj.getWorldPosition === 'function') obj.getWorldPosition(pos);
559
+ else pos.set(obj.position?.x || 0, obj.position?.y || 0, obj.position?.z || 0);
560
+ } catch { }
561
+ for (const bucket of buckets) {
562
+ bucket[refName] = { type: 'VERTEX', position: [pos.x, pos.y, pos.z], sourceUuid, sourceFeatureId };
563
+ }
564
+ }
565
+ }
566
+ }
567
+ }
568
+
463
569
 
464
570
  async _coerceRunEffects(result, featureType, featureID) {
465
571
  if (result == null) return { added: [], removed: [] };
@@ -127,8 +127,8 @@ export const CADmaterials = {
127
127
  depthWrite: true,
128
128
  // Keep selected faces slightly behind edges as well.
129
129
  polygonOffset: true,
130
- polygonOffsetFactor: 0,
131
- polygonOffsetUnits: 0,
130
+ polygonOffsetFactor: -2,
131
+ polygonOffsetUnits: 1,
132
132
  emissiveIntensity: 0,
133
133
  })
134
134
  },
@@ -79,6 +79,14 @@ export class MainToolbar {
79
79
  color: #e9f0ff;
80
80
  box-shadow: 0 0 0 1px rgba(110,168,254,.2) inset;
81
81
  }
82
+ .mtb-selection {
83
+ display: none;
84
+ align-items: center;
85
+ gap: 6px;
86
+ padding-left: 6px;
87
+ margin-left: 2px;
88
+ border-left: 1px solid #364053;
89
+ }
82
90
  `;
83
91
  document.head.appendChild(style);
84
92
  }
@@ -103,7 +111,8 @@ export class MainToolbar {
103
111
  b.className = 'mtb-btn';
104
112
  b.textContent = label;
105
113
  b.title = title || label;
106
- b.addEventListener('click', (e) => { e.stopPropagation(); try { onClick && onClick(); } catch {} });
114
+ b.__mtbOnClick = onClick;
115
+ b.addEventListener('click', (e) => { e.stopPropagation(); try { b.__mtbOnClick && b.__mtbOnClick(); } catch {} });
107
116
  return b;
108
117
  }
109
118
 
@@ -111,11 +120,39 @@ export class MainToolbar {
111
120
  addCustomButton({ label, title, onClick }) {
112
121
  try {
113
122
  const btn = this._btn(String(label ?? '🔧'), String(title || ''), onClick);
114
- this._left?.appendChild(btn);
123
+ const anchor = this._selectionContainer && this._selectionContainer.parentNode === this._left
124
+ ? this._selectionContainer
125
+ : null;
126
+ if (anchor) this._left?.insertBefore(btn, anchor);
127
+ else this._left?.appendChild(btn);
115
128
  return btn;
116
129
  } catch { return null; }
117
130
  }
118
131
 
132
+ _ensureSelectionContainer() {
133
+ if (this._selectionContainer) return this._selectionContainer;
134
+ const wrap = document.createElement('div');
135
+ wrap.className = 'mtb-selection';
136
+ this._selectionContainer = wrap;
137
+ this._left?.appendChild(wrap);
138
+ return wrap;
139
+ }
140
+
141
+ // Public: allow selection-based buttons in their own cluster
142
+ addSelectionButton({ label, title, onClick }) {
143
+ try {
144
+ const btn = this._btn(String(label ?? '🔧'), String(title || ''), onClick);
145
+ if (label && String(label).length <= 2) btn.classList.add('mtb-icon');
146
+ const wrap = this._ensureSelectionContainer();
147
+ wrap.appendChild(btn);
148
+ return btn;
149
+ } catch { return null; }
150
+ }
151
+
152
+ getSelectionContainer() {
153
+ return this._ensureSelectionContainer();
154
+ }
155
+
119
156
  _positionWithSidebar() {
120
157
  try {
121
158
  const sb = this.viewer?.sidebar;
@@ -17,6 +17,12 @@ 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;
20
26
 
21
27
  constructor() {
22
28
  throw new Error("SelectionFilter is static and cannot be instantiated.");
@@ -429,6 +435,11 @@ export class SelectionFilter {
429
435
  if (!targetObj) return false;
430
436
 
431
437
  // Update the reference input with the chosen object
438
+ try {
439
+ if (activeRefInput && typeof activeRefInput.__captureReferencePreview === 'function') {
440
+ activeRefInput.__captureReferencePreview(targetObj);
441
+ }
442
+ } catch (_) { /* ignore preview capture errors */ }
432
443
  const objType = targetObj.type;
433
444
  const objectName = targetObj.name || `${objType}(${targetObj.position?.x || 0},${targetObj.position?.y || 0},${targetObj.position?.z || 0})`;
434
445
 
@@ -617,6 +628,189 @@ export class SelectionFilter {
617
628
  } catch (_) { /* noop */ }
618
629
  }
619
630
 
631
+ static getSelectedObjects(options = {}) {
632
+ const scene = options.scene
633
+ || SelectionFilter.viewer?.partHistory?.scene
634
+ || SelectionFilter.viewer?.scene
635
+ || null;
636
+ const selected = [];
637
+ if (!scene || typeof scene.traverse !== 'function') return selected;
638
+ scene.traverse((obj) => {
639
+ if (obj && obj.selected) selected.push(obj);
640
+ });
641
+ return selected;
642
+ }
643
+
644
+ static registerSelectionAction(spec = {}) {
645
+ if (!spec) return null;
646
+ const id = String(spec.id || `selection-action-${SelectionFilter._selectionActionSeq++}`);
647
+ const entry = SelectionFilter._selectionActions.get(id) || { id };
648
+ entry.label = spec.label ?? entry.label ?? '';
649
+ entry.title = spec.title ?? entry.title ?? entry.label ?? '';
650
+ entry.onClick = spec.onClick ?? entry.onClick ?? null;
651
+ entry.shouldShow = typeof spec.shouldShow === 'function' ? spec.shouldShow : (entry.shouldShow || null);
652
+ SelectionFilter._selectionActions.set(id, entry);
653
+ if (!SelectionFilter._selectionActionOrder.includes(id)) {
654
+ SelectionFilter._selectionActionOrder.push(id);
655
+ }
656
+ SelectionFilter._ensureSelectionActionListener();
657
+ SelectionFilter._syncSelectionActions();
658
+ return id;
659
+ }
660
+
661
+ static unregisterSelectionAction(id) {
662
+ if (!id) return;
663
+ const entry = SelectionFilter._selectionActions.get(id);
664
+ if (entry?.btn && entry.btn.parentNode) {
665
+ try { entry.btn.parentNode.removeChild(entry.btn); } catch { }
666
+ }
667
+ SelectionFilter._selectionActions.delete(id);
668
+ SelectionFilter._selectionActionOrder = SelectionFilter._selectionActionOrder.filter((k) => k !== id);
669
+ SelectionFilter._syncSelectionActions();
670
+ }
671
+
672
+ static refreshSelectionActions() {
673
+ SelectionFilter._syncSelectionActions();
674
+ }
675
+
676
+ static _ensureSelectionActionListener() {
677
+ if (SelectionFilter._selectionActionListenerBound) return;
678
+ if (typeof window === 'undefined') return;
679
+ SelectionFilter._selectionActionListenerBound = true;
680
+ window.addEventListener('selection-changed', () => SelectionFilter._syncSelectionActions());
681
+ }
682
+
683
+ static _syncSelectionActions() {
684
+ const actions = SelectionFilter._selectionActions;
685
+ if (!actions || actions.size === 0) {
686
+ try {
687
+ const bar = SelectionFilter._selectionActionBar;
688
+ if (bar) bar.style.display = 'none';
689
+ } catch { }
690
+ return;
691
+ }
692
+ const viewer = SelectionFilter.viewer;
693
+ const bar = SelectionFilter._ensureSelectionActionBar(viewer);
694
+ if (!bar) {
695
+ SelectionFilter._selectionActionsPending = true;
696
+ return;
697
+ }
698
+ SelectionFilter._selectionActionsPending = false;
699
+ const selection = SelectionFilter.getSelectedObjects();
700
+ const hideAll = !!viewer?._sketchMode;
701
+ let visibleCount = 0;
702
+ const updateButton = (entry, show) => {
703
+ if (!entry || !entry.btn) return;
704
+ try { entry.btn.style.display = show ? '' : 'none'; } catch { }
705
+ if (show) visibleCount += 1;
706
+ };
707
+ for (const id of SelectionFilter._selectionActionOrder) {
708
+ const entry = actions.get(id);
709
+ if (!entry) continue;
710
+ if (!entry.btn) {
711
+ entry.btn = SelectionFilter._createSelectionActionButton(entry);
712
+ }
713
+ if (!entry.btn) continue;
714
+ try {
715
+ entry.btn.textContent = String(entry.label ?? '');
716
+ entry.btn.title = String(entry.title ?? entry.label ?? '');
717
+ entry.btn.__sabOnClick = entry.onClick;
718
+ const isIcon = String(entry.label || '').length <= 2;
719
+ entry.btn.classList.toggle('sab-icon', isIcon);
720
+ } catch { }
721
+ let show = !hideAll;
722
+ if (show) {
723
+ if (typeof entry.shouldShow === 'function') {
724
+ try { show = !!entry.shouldShow(selection, viewer); } catch { show = false; }
725
+ } else {
726
+ show = selection.length > 0;
727
+ }
728
+ }
729
+ updateButton(entry, show);
730
+ if (entry.btn.parentNode !== bar) {
731
+ try { bar.appendChild(entry.btn); } catch { }
732
+ }
733
+ }
734
+ try {
735
+ if (bar) bar.style.display = visibleCount > 0 ? 'flex' : 'none';
736
+ } catch { }
737
+ }
738
+
739
+ static _ensureSelectionActionBar(viewer) {
740
+ if (SelectionFilter._selectionActionBar && SelectionFilter._selectionActionBar.isConnected) {
741
+ return SelectionFilter._selectionActionBar;
742
+ }
743
+ if (typeof document === 'undefined') return null;
744
+ const host = viewer?.container || document.body || null;
745
+ if (!host) return null;
746
+ try {
747
+ if (!document.getElementById('selection-action-bar-styles')) {
748
+ const style = document.createElement('style');
749
+ style.id = 'selection-action-bar-styles';
750
+ style.textContent = `
751
+ .selection-action-bar {
752
+ position: absolute;
753
+ top: 100px;
754
+ right: 8px;
755
+ display: flex;
756
+ flex-direction: column;
757
+ gap: 6px;
758
+ align-items: stretch;
759
+ background: rgba(20,24,30,.85);
760
+ border: 1px solid #262b36;
761
+ border-radius: 8px;
762
+ padding: 6px;
763
+ color: #ddd;
764
+ min-width: 40px;
765
+ max-width: 150px;
766
+ z-index: 12;
767
+ user-select: none;
768
+ }
769
+ .selection-action-bar .sab-btn {
770
+ background: transparent;
771
+ border-radius: 6px;
772
+ padding: 4px 8px;
773
+ width: 100%;
774
+ min-height: 34px;
775
+ box-sizing: border-box;
776
+ color: #ddd;
777
+ border: 1px solid #364053;
778
+ cursor: pointer;
779
+ }
780
+ .selection-action-bar .sab-btn:hover { filter: brightness(1.08); }
781
+ .selection-action-bar .sab-btn:active { filter: brightness(1.15); }
782
+ .selection-action-bar .sab-btn.sab-icon {
783
+ font-size: 16px;
784
+ min-width: 36px;
785
+ }
786
+ `;
787
+ document.head.appendChild(style);
788
+ }
789
+ } catch { }
790
+ const bar = document.createElement('div');
791
+ bar.className = 'selection-action-bar';
792
+ host.appendChild(bar);
793
+ SelectionFilter._selectionActionBar = bar;
794
+ return bar;
795
+ }
796
+
797
+ static _createSelectionActionButton(entry) {
798
+ try {
799
+ const btn = document.createElement('button');
800
+ btn.className = 'sab-btn';
801
+ btn.textContent = String(entry?.label ?? '');
802
+ btn.title = String(entry?.title ?? entry?.label ?? '');
803
+ btn.__sabOnClick = entry?.onClick ?? null;
804
+ btn.addEventListener('click', (e) => {
805
+ e.stopPropagation();
806
+ try { btn.__sabOnClick && btn.__sabOnClick(); } catch { }
807
+ });
808
+ const isIcon = String(entry?.label || '').length <= 2;
809
+ if (isIcon) btn.classList.add('sab-icon');
810
+ return btn;
811
+ } catch { return null; }
812
+ }
813
+
620
814
  static #logAllowedTypesChange(next, reason = '') {
621
815
  try {
622
816
  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', () => {