brep-io-kernel 1.0.20 → 1.0.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +4 -1
  2. package/dist-kernel/brep-kernel.js +10858 -9938
  3. package/package.json +3 -2
  4. package/src/BREP/Edge.js +2 -0
  5. package/src/BREP/Face.js +2 -0
  6. package/src/BREP/SolidMethods/visualize.js +372 -365
  7. package/src/BREP/Vertex.js +2 -17
  8. package/src/PartHistory.js +4 -25
  9. package/src/SketchSolver2D.js +3 -0
  10. package/src/UI/AccordionWidget.js +1 -1
  11. package/src/UI/EnvMonacoEditor.js +0 -3
  12. package/src/UI/HistoryWidget.js +12 -4
  13. package/src/UI/SceneListing.js +45 -7
  14. package/src/UI/SelectionFilter.js +903 -438
  15. package/src/UI/SelectionState.js +464 -0
  16. package/src/UI/assembly/AssemblyConstraintCollectionWidget.js +41 -1
  17. package/src/UI/assembly/AssemblyConstraintsWidget.js +21 -3
  18. package/src/UI/assembly/constraintSelectionUtils.js +3 -182
  19. package/src/UI/{assembly/constraintFaceUtils.js → faceUtils.js} +30 -5
  20. package/src/UI/featureDialogs.js +154 -69
  21. package/src/UI/history/HistoryCollectionWidget.js +65 -0
  22. package/src/UI/pmi/AnnotationCollectionWidget.js +1 -0
  23. package/src/UI/pmi/BaseAnnotation.js +37 -0
  24. package/src/UI/pmi/LabelOverlay.js +32 -0
  25. package/src/UI/pmi/PMIMode.js +27 -0
  26. package/src/UI/pmi/dimensions/AngleDimensionAnnotation.js +5 -0
  27. package/src/UI/pmi/dimensions/ExplodeBodyAnnotation.js +5 -0
  28. package/src/UI/pmi/dimensions/HoleCalloutAnnotation.js +57 -0
  29. package/src/UI/pmi/dimensions/LeaderAnnotation.js +5 -0
  30. package/src/UI/pmi/dimensions/LinearDimensionAnnotation.js +22 -16
  31. package/src/UI/pmi/dimensions/NoteAnnotation.js +9 -0
  32. package/src/UI/pmi/dimensions/RadialDimensionAnnotation.js +81 -16
  33. package/src/UI/toolbarButtons/orientToFaceButton.js +3 -36
  34. package/src/UI/toolbarButtons/registerDefaultButtons.js +2 -0
  35. package/src/UI/toolbarButtons/selectionStateButton.js +206 -0
  36. package/src/UI/viewer.js +34 -13
  37. package/src/assemblyConstraints/AssemblyConstraintHistory.js +18 -42
  38. package/src/assemblyConstraints/constraints/AngleConstraint.js +1 -0
  39. package/src/assemblyConstraints/constraints/DistanceConstraint.js +1 -0
  40. package/src/features/selectionUtils.js +21 -5
  41. package/src/features/sketch/SketchFeature.js +2 -2
  42. package/src/features/sketch/sketchSolver2D/constraintDefinitions.js +3 -2
  43. package/src/utils/selectionResolver.js +258 -0
@@ -1,185 +1,6 @@
1
+ import { resolveSelectionObject as resolveSelectionObjectBase, scoreObjectForNormal } from '../../utils/selectionResolver.js';
2
+
1
3
  export function resolveSelectionObject(scene, selection, options = {}) {
2
4
  const scoreFn = typeof options.scoreFn === 'function' ? options.scoreFn : scoreObjectForNormal;
3
- return internalResolveSelectionObject(scene, selection, { scoreFn });
4
- }
5
-
6
- function internalResolveSelectionObject(scene, selection, options) {
7
- if (!scene || selection == null) return null;
8
- if (selection.isObject3D) return selection;
9
-
10
- if (Array.isArray(selection)) {
11
- for (const item of selection) {
12
- const resolved = internalResolveSelectionObject(scene, item, options);
13
- if (resolved) return resolved;
14
- }
15
- return null;
16
- }
17
-
18
- if (typeof selection === 'string') {
19
- return resolveObjectFromString(scene, selection, options);
20
- }
21
-
22
- if (typeof selection === 'object') {
23
- if (selection.isObject3D) return selection;
24
- const {
25
- uuid,
26
- name,
27
- id,
28
- path,
29
- reference,
30
- target,
31
- } = selection;
32
-
33
- if (typeof uuid === 'string') {
34
- try {
35
- const found = scene.getObjectByProperty?.('uuid', uuid);
36
- if (found) return found;
37
- } catch { /* ignore */ }
38
- }
39
-
40
- const resolveCandidate = (candidate) => (
41
- typeof candidate === 'string'
42
- ? resolveObjectFromString(scene, candidate, options)
43
- : null
44
- );
45
-
46
- const nameCandidate = typeof name === 'string'
47
- ? name
48
- : (typeof selection?.selectionName === 'string' ? selection.selectionName : null);
49
- const idCandidate = typeof id === 'string' ? id : null;
50
-
51
- const nameResolved = resolveCandidate(nameCandidate);
52
- if (nameResolved) return nameResolved;
53
-
54
- const idResolved = resolveCandidate(idCandidate);
55
- if (idResolved) return idResolved;
56
-
57
- if (Array.isArray(path)) {
58
- for (let i = path.length - 1; i >= 0; i -= 1) {
59
- const segment = path[i];
60
- if (typeof segment !== 'string') continue;
61
- const resolved = resolveObjectFromString(scene, segment, options);
62
- if (resolved) return resolved;
63
- }
64
- }
65
-
66
- if (reference != null) {
67
- const resolved = internalResolveSelectionObject(scene, reference, options);
68
- if (resolved) return resolved;
69
- }
70
-
71
- if (target != null) {
72
- const resolved = internalResolveSelectionObject(scene, target, options);
73
- if (resolved) return resolved;
74
- }
75
- }
76
-
77
- return null;
78
- }
79
-
80
- function resolveObjectFromString(scene, value, options) {
81
- if (!scene || typeof value !== 'string') return null;
82
- const trimmed = value.trim();
83
- if (!trimmed) return null;
84
-
85
- if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
86
- try {
87
- const parsed = JSON.parse(trimmed);
88
- if (parsed != null) {
89
- const resolved = internalResolveSelectionObject(scene, parsed, options);
90
- if (resolved) return resolved;
91
- }
92
- } catch { /* ignore JSON parse errors */ }
93
- }
94
-
95
- const direct = findObjectByName(scene, trimmed, options.scoreFn);
96
- if (direct) return direct;
97
-
98
- if (looksLikeUUID(trimmed)) {
99
- try {
100
- const byUuid = scene.getObjectByProperty?.('uuid', trimmed);
101
- if (byUuid) return byUuid;
102
- } catch { /* ignore */ }
103
- }
104
-
105
- const candidates = new Set();
106
- candidates.add(trimmed);
107
-
108
- const splitByDelims = trimmed.split(/›|>|\/|\||→|->/);
109
- if (splitByDelims.length > 1) {
110
- for (const segment of splitByDelims) {
111
- const s = segment.trim();
112
- if (s) candidates.add(s);
113
- }
114
- }
115
-
116
- if (trimmed.includes(':')) {
117
- for (const segment of trimmed.split(':')) {
118
- const s = segment.trim();
119
- if (s) candidates.add(s);
120
- }
121
- }
122
-
123
- for (const candidate of candidates) {
124
- const found = findObjectByName(scene, candidate, options.scoreFn);
125
- if (found) return found;
126
- }
127
-
128
- let fallback = null;
129
- try {
130
- scene.traverse?.((obj) => {
131
- if (fallback || !obj?.name) return;
132
- if (!trimmed.includes(obj.name)) return;
133
- if (!fallback) {
134
- fallback = obj;
135
- return;
136
- }
137
- const currentScore = options.scoreFn(fallback);
138
- const nextScore = options.scoreFn(obj);
139
- if (nextScore > currentScore || obj.name.length > fallback.name.length) {
140
- fallback = obj;
141
- }
142
- });
143
- } catch { /* ignore */ }
144
-
145
- return fallback;
146
- }
147
-
148
- function findObjectByName(scene, name, scoreFn) {
149
- if (!scene || typeof name !== 'string' || !name) return null;
150
-
151
- if (typeof scene.traverse !== 'function') {
152
- return scene?.getObjectByName?.(name) || null;
153
- }
154
-
155
- let best = null;
156
- scene.traverse((obj) => {
157
- if (!obj || obj.name !== name) return;
158
- if (!best) {
159
- best = obj;
160
- return;
161
- }
162
- const currentScore = scoreFn(best);
163
- const newScore = scoreFn(obj);
164
- if (newScore > currentScore) best = obj;
165
- });
166
-
167
- if (best) return best;
168
- if (typeof scene.getObjectByName === 'function') return scene.getObjectByName(name);
169
- return null;
170
- }
171
-
172
- function looksLikeUUID(value) {
173
- if (typeof value !== 'string') return false;
174
- const trimmed = value.trim();
175
- if (trimmed.length !== 36) return false;
176
- return /^[0-9a-fA-F-]{36}$/.test(trimmed);
177
- }
178
-
179
- function scoreObjectForNormal(object) {
180
- if (!object) return -Infinity;
181
- const type = object.userData?.type || object.userData?.brepType || object.type;
182
- if (String(type).toUpperCase() === 'FACE') return 3;
183
- if (object.geometry) return 2;
184
- return 1;
5
+ return resolveSelectionObjectBase(scene, selection, { ...options, scoreFn });
185
6
  }
@@ -1,5 +1,5 @@
1
1
  import * as THREE from 'three';
2
- import { objectRepresentativePoint } from '../pmi/annUtils.js';
2
+ import { objectRepresentativePoint } from './pmi/annUtils.js';
3
3
 
4
4
  export function isFaceObject(object) {
5
5
  if (!object) return false;
@@ -35,6 +35,21 @@ export function computeFaceOrigin(object) {
35
35
  return null;
36
36
  }
37
37
 
38
+ export function computeFaceCenter(object) {
39
+ if (!object) return null;
40
+ try { object.updateMatrixWorld?.(true); } catch { /* ignore */ }
41
+ try {
42
+ const box = new THREE.Box3().setFromObject(object);
43
+ if (!box.isEmpty()) return box.getCenter(new THREE.Vector3());
44
+ } catch { /* ignore */ }
45
+ try {
46
+ const geom = object.geometry;
47
+ const bs = geom?.boundingSphere || (geom?.computeBoundingSphere && (geom.computeBoundingSphere(), geom.boundingSphere));
48
+ if (bs) return object.localToWorld(bs.center.clone());
49
+ } catch { /* ignore */ }
50
+ return computeFaceOrigin(object);
51
+ }
52
+
38
53
  export function computeFaceNormal(object) {
39
54
  if (!object) return null;
40
55
  try {
@@ -45,9 +60,11 @@ export function computeFaceNormal(object) {
45
60
  } catch { /* ignore */ }
46
61
 
47
62
  const geom = object.geometry;
48
- if (!geom?.isBufferGeometry) return null;
63
+ if (!geom?.isBufferGeometry) {
64
+ return fallbackQuaternionNormal(object);
65
+ }
49
66
  const pos = geom.getAttribute?.('position');
50
- if (!pos || pos.itemSize !== 3 || pos.count < 3) return null;
67
+ if (!pos || pos.itemSize !== 3 || pos.count < 3) return fallbackQuaternionNormal(object);
51
68
  const index = geom.getIndex?.();
52
69
 
53
70
  const v0 = new THREE.Vector3();
@@ -94,10 +111,10 @@ export function computeFaceNormal(object) {
94
111
  }
95
112
  }
96
113
 
97
- if (count === 0) return null;
114
+ if (count === 0) return fallbackQuaternionNormal(object);
98
115
 
99
116
  accum.divideScalar(count);
100
- if (accum.lengthSq() <= 1e-10) return null;
117
+ if (accum.lengthSq() <= 1e-10) return fallbackQuaternionNormal(object);
101
118
 
102
119
  return accum.normalize();
103
120
  }
@@ -113,3 +130,11 @@ export function estimateArrowLength(object) {
113
130
  }
114
131
  return 10;
115
132
  }
133
+
134
+ function fallbackQuaternionNormal(object) {
135
+ try {
136
+ const q = object?.getWorldQuaternion?.(new THREE.Quaternion());
137
+ if (q) return new THREE.Vector3(0, 0, 1).applyQuaternion(q).normalize();
138
+ } catch { /* ignore */ }
139
+ return null;
140
+ }
@@ -455,6 +455,52 @@ export class SchemaForm {
455
455
  return false;
456
456
  }
457
457
 
458
+ // Focus the first available field in this form (or activate a reference selection when needed).
459
+ focusFirstField() {
460
+ const canFocus = (el) => {
461
+ if (!el || typeof el.focus !== 'function') return false;
462
+ if (el.disabled) return false;
463
+ const ariaDisabled = el.getAttribute ? el.getAttribute('aria-disabled') : null;
464
+ if (ariaDisabled === 'true') return false;
465
+ return true;
466
+ };
467
+ const tryFocus = (el) => {
468
+ if (!canFocus(el)) return false;
469
+ try { el.focus(); } catch (_) { return false; }
470
+ return true;
471
+ };
472
+
473
+ for (const key in this.schema) {
474
+ if (!Object.prototype.hasOwnProperty.call(this.schema, key)) continue;
475
+ if (this._excludedKeys.has(key)) continue;
476
+ const def = this.schema[key];
477
+ const row = this._fieldsWrap?.querySelector?.(`[data-key="${key}"]`) || null;
478
+
479
+ // Reference selections should auto-activate instead of just focusing the display button.
480
+ if (def && def.type === 'reference_selection') {
481
+ if (this.activateField(key)) return true;
482
+ }
483
+
484
+ // Prefer direct inputs first.
485
+ if (row) {
486
+ const input = row.querySelector('input:not([type="hidden"]), select, textarea');
487
+ if (tryFocus(input)) return true;
488
+ const btn = row.querySelector('button, [tabindex]:not([tabindex="-1"])');
489
+ if (tryFocus(btn)) return true;
490
+ }
491
+
492
+ const inputEl = this._inputs.get(key);
493
+ if (inputEl && inputEl.getAttribute && inputEl.getAttribute('type') !== 'hidden') {
494
+ if (tryFocus(inputEl)) return true;
495
+ }
496
+ }
497
+
498
+ const root = this._shadow || this.uiElement;
499
+ const any = root?.querySelector?.('input:not([type="hidden"]), select, textarea, button, [tabindex]:not([tabindex="-1"])');
500
+ if (tryFocus(any)) return true;
501
+ return false;
502
+ }
503
+
458
504
  readFieldValue(key) {
459
505
  const widget = this._widgets.get(key);
460
506
  if (widget && typeof widget.readValue === 'function') {
@@ -1001,50 +1047,50 @@ export class SchemaForm {
1001
1047
  for (const name of list) {
1002
1048
  if (!name) continue;
1003
1049
  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;
1030
1050
  const obj = scene.getObjectByName(name);
1031
1051
  if (obj && !obj?.userData?.refPreview) {
1032
- this._storeReferencePreviewSnapshot(inputEl, def, obj);
1052
+ const objType = String(obj.type || '').toUpperCase();
1053
+ const isEdgeObj = objType === SelectionFilter.EDGE || objType === 'EDGE';
1054
+ const objTimestamp = (obj.timestamp ?? obj.userData?.timestamp ?? null);
1055
+ const existingTimestamp = existing?.sourceTimestamp ?? null;
1056
+ const shouldRefresh = !existing
1057
+ || existing.fromSnapshot
1058
+ || (!!existing?.sourceUuid && !!obj.uuid && existing.sourceUuid !== obj.uuid)
1059
+ || (!existing?.sourceUuid && !!obj.uuid)
1060
+ || (objTimestamp != null && existingTimestamp !== objTimestamp)
1061
+ || (isEdgeObj && !existing?.showWhenOriginalPresent);
1062
+ if (shouldRefresh) {
1063
+ this._storeReferencePreviewSnapshot(inputEl, def, obj);
1064
+ }
1033
1065
  continue;
1034
1066
  }
1035
- if (store && store[name]) {
1036
- const snapshot = store[name];
1037
- const ghost = this._buildReferencePreviewFromSnapshot(name, snapshot);
1038
- if (ghost) {
1039
- cache.set(name, {
1040
- object: ghost,
1041
- type: snapshot.type || null,
1042
- sourceUuid: snapshot.sourceUuid || null,
1043
- sourceFeatureId: snapshot.sourceFeatureId || null,
1044
- showWhenOriginalPresent: String(snapshot.type || '').toUpperCase() === 'FACE' || String(snapshot.type || '').toUpperCase() === 'PLANE' || !!ghost?.userData?.previewHasEdges,
1045
- fromSnapshot: true,
1046
- });
1047
- }
1067
+ const snapshot = store ? store[name] : null;
1068
+ if (!snapshot) continue;
1069
+ const snapType = String(snapshot.type || '').toUpperCase();
1070
+ const snapEdges = Array.isArray(snapshot.edgePositions) ? snapshot.edgePositions : null;
1071
+ const isFaceSnap = (snapType === 'FACE' || snapType === 'PLANE') && snapEdges && snapEdges.length;
1072
+ const isEdgeSnap = snapType === 'EDGE' && Array.isArray(snapshot.positions) && snapshot.positions.length >= 6;
1073
+ const isVertexSnap = snapType === 'VERTEX' && Array.isArray(snapshot.position) && snapshot.position.length >= 3;
1074
+ if (!(isFaceSnap || isEdgeSnap || isVertexSnap)) continue;
1075
+ const snapTimestamp = snapshot.sourceTimestamp ?? null;
1076
+ const shouldOverride = !existing
1077
+ || !existing.fromSnapshot
1078
+ || isFaceSnap
1079
+ || (!!snapshot.sourceUuid && !!existing?.sourceUuid && snapshot.sourceUuid !== existing.sourceUuid)
1080
+ || (snapTimestamp != null && (existing?.sourceTimestamp ?? null) !== snapTimestamp)
1081
+ || (isEdgeSnap && !existing?.showWhenOriginalPresent);
1082
+ if (!shouldOverride) continue;
1083
+ const ghost = this._buildReferencePreviewFromSnapshot(name, snapshot);
1084
+ if (ghost) {
1085
+ cache.set(name, {
1086
+ object: ghost,
1087
+ type: snapshot.type || null,
1088
+ sourceUuid: snapshot.sourceUuid || null,
1089
+ sourceFeatureId: snapshot.sourceFeatureId || null,
1090
+ sourceTimestamp: snapTimestamp,
1091
+ showWhenOriginalPresent: isFaceSnap || isEdgeSnap || !!ghost?.userData?.previewHasEdges,
1092
+ fromSnapshot: true,
1093
+ });
1048
1094
  }
1049
1095
  }
1050
1096
  }
@@ -1060,21 +1106,24 @@ export class SchemaForm {
1060
1106
  if (!ghost) return;
1061
1107
  const sourceUuid = obj.uuid || null;
1062
1108
  const sourceFeatureId = this._getOwningFeatureIdForObject(obj);
1109
+ const sourceTimestamp = (obj.timestamp ?? obj.userData?.timestamp ?? null);
1110
+ const objType = String(obj.type || '').toUpperCase();
1111
+ const isEdge = objType === SelectionFilter.EDGE || objType === 'EDGE';
1063
1112
  cache.set(refName, {
1064
1113
  object: ghost,
1065
1114
  type: obj.type || null,
1066
1115
  sourceUuid,
1067
1116
  sourceFeatureId,
1068
- showWhenOriginalPresent: !!ghost?.userData?.previewHasEdges,
1117
+ sourceTimestamp,
1118
+ showWhenOriginalPresent: isEdge || !!ghost?.userData?.previewHasEdges,
1069
1119
  });
1070
1120
  try {
1071
1121
  const store = this._getReferencePreviewPersistentBucket(inputEl);
1072
1122
  if (store) {
1073
- const objType = String(obj.type || '').toUpperCase();
1074
1123
  if (objType === SelectionFilter.EDGE || objType === 'EDGE') {
1075
1124
  const positions = this._extractEdgeWorldPositions(obj);
1076
1125
  if (positions && positions.length >= 6) {
1077
- store[refName] = { type: 'EDGE', positions, sourceUuid, sourceFeatureId };
1126
+ store[refName] = { type: 'EDGE', positions, sourceUuid, sourceFeatureId, sourceTimestamp };
1078
1127
  }
1079
1128
  } else if (objType === SelectionFilter.VERTEX || objType === 'VERTEX') {
1080
1129
  const pos = new THREE.Vector3();
@@ -1082,7 +1131,7 @@ export class SchemaForm {
1082
1131
  if (typeof obj.getWorldPosition === 'function') obj.getWorldPosition(pos);
1083
1132
  else pos.set(obj.position?.x || 0, obj.position?.y || 0, obj.position?.z || 0);
1084
1133
  } catch (_) { }
1085
- store[refName] = { type: 'VERTEX', position: [pos.x, pos.y, pos.z], sourceUuid, sourceFeatureId };
1134
+ store[refName] = { type: 'VERTEX', position: [pos.x, pos.y, pos.z], sourceUuid, sourceFeatureId, sourceTimestamp };
1086
1135
  }
1087
1136
  }
1088
1137
  } catch (_) { }
@@ -1229,64 +1278,100 @@ export class SchemaForm {
1229
1278
  _hoverReferenceSelectionItem(inputEl, def, name) {
1230
1279
  try {
1231
1280
  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
- }
1281
+ const isActive = (SchemaForm.__activeRefInput || null) === inputEl;
1237
1282
  const normalized = normalizeReferenceName(name);
1238
1283
  if (!normalized) return;
1239
1284
  const scene = this._getReferenceSelectionScene();
1240
1285
  if (!scene) return;
1241
1286
  try { console.log('[ReferenceSelection] Hover', { name: normalized }); } catch (_) { }
1242
- try { this._ensureReferencePreviewSnapshots(inputEl, def); } catch (_) { }
1243
- try { this._seedReferencePreviewCacheFromScene(inputEl, def, [normalized], scene); } catch (_) { }
1244
- try { this._syncActiveReferenceSelectionPreview(inputEl, def); } catch (_) { }
1245
-
1246
- let target = null;
1247
- try { target = scene.getObjectByName(normalized); } catch (_) { target = null; }
1248
- if (!target) {
1249
- const cache = this._getReferencePreviewCache(inputEl);
1250
- const entry = cache ? cache.get(normalized) : null;
1251
- target = entry?.object || entry || null;
1287
+ if (isActive) {
1288
+ try { this._ensureReferencePreviewSnapshots(inputEl, def); } catch (_) { }
1252
1289
  }
1253
- if (!target) {
1254
- try { target = scene.getObjectByName(`__refPreview__${normalized}`); } catch (_) { target = null; }
1290
+ try { this._seedReferencePreviewCacheFromScene(inputEl, def, [normalized], scene); } catch (_) { }
1291
+ if (isActive) {
1292
+ try { this._syncActiveReferenceSelectionPreview(inputEl, def); } catch (_) { }
1255
1293
  }
1256
- if (!target) return;
1257
- if (!target.material && target.traverse) {
1294
+
1295
+ const resolveHoverCandidate = (obj) => {
1296
+ if (!obj) return null;
1297
+ if (obj.material) return obj;
1298
+ if (!obj.traverse) return obj;
1258
1299
  let candidate = null;
1259
1300
  try {
1260
- target.traverse((child) => {
1301
+ obj.traverse((child) => {
1261
1302
  if (!child || candidate) return;
1262
1303
  if (child.type === 'REF_PREVIEW_EDGE') { candidate = child; return; }
1263
1304
  if (child.material) candidate = child;
1264
1305
  });
1265
1306
  } catch (_) { }
1266
- if (candidate) target = candidate;
1307
+ return candidate || obj;
1308
+ };
1309
+
1310
+ const targets = [];
1311
+ const pushTarget = (obj) => {
1312
+ if (!obj || !obj.isObject3D) return;
1313
+ const candidate = resolveHoverCandidate(obj);
1314
+ if (!candidate) return;
1315
+ if (!targets.includes(candidate)) targets.push(candidate);
1316
+ };
1317
+
1318
+ let sceneObj = null;
1319
+ try { sceneObj = scene.getObjectByName(normalized); } catch (_) { sceneObj = null; }
1320
+ if (sceneObj && !sceneObj?.userData?.refPreview) pushTarget(sceneObj);
1321
+
1322
+ const cache = this._getReferencePreviewCache(inputEl);
1323
+ const entry = cache ? cache.get(normalized) : null;
1324
+ const ghost = entry?.object || entry || null;
1325
+ if (ghost && ghost !== sceneObj) pushTarget(ghost);
1326
+
1327
+ let namedPreview = null;
1328
+ try { namedPreview = scene.getObjectByName(`__refPreview__${normalized}`); } catch (_) { namedPreview = null; }
1329
+ if (namedPreview && namedPreview !== sceneObj && namedPreview !== ghost) pushTarget(namedPreview);
1330
+
1331
+ if (!targets.length) return;
1332
+ if (!isActive && ghost && ghost.isObject3D && !ghost.parent) {
1333
+ try {
1334
+ const group = this._ensureReferencePreviewGroup(inputEl);
1335
+ if (group && ghost.parent !== group) {
1336
+ group.add(ghost);
1337
+ inputEl.__refPreviewHoverGroup = true;
1338
+ }
1339
+ } catch (_) { }
1267
1340
  }
1268
1341
  try {
1269
1342
  console.log('[ReferenceSelection] Hover target', {
1270
1343
  name: normalized,
1271
- target: target || null,
1272
- inScene: !!(target && target.parent),
1344
+ targetCount: targets.length,
1273
1345
  });
1274
1346
  } catch (_) { }
1275
1347
  inputEl.__refChipHoverActive = true;
1276
- try { SelectionFilter.setHoverObject(target, { ignoreFilter: true }); } catch (_) { }
1348
+ try { SelectionFilter.setHoverObjects(targets, { ignoreFilter: true }); } catch (_) { }
1277
1349
  } catch (_) { }
1278
1350
  }
1279
1351
 
1280
1352
  _clearReferenceSelectionHover(inputEl) {
1281
1353
  try {
1282
- if (!inputEl || SchemaForm.__activeRefInput !== inputEl) return;
1354
+ if (!inputEl) return;
1283
1355
  if (!inputEl.__refChipHoverActive) return;
1284
1356
  inputEl.__refChipHoverActive = false;
1285
1357
  SelectionFilter.clearHover();
1358
+ if (SchemaForm.__activeRefInput !== inputEl && inputEl.__refPreviewHoverGroup) {
1359
+ inputEl.__refPreviewHoverGroup = false;
1360
+ try { this._removeReferencePreviewGroup(inputEl); } catch (_) { }
1361
+ }
1286
1362
  } catch (_) { }
1287
1363
  }
1288
1364
 
1289
1365
  _activateReferenceSelection(inputEl, def) {
1366
+ // If switching between reference fields, fully stop the previous session so
1367
+ // selection filters restore correctly (prevents sticky FACE-only state).
1368
+ try {
1369
+ const prevActive = SchemaForm.__activeRefInput || null;
1370
+ if (prevActive && prevActive !== inputEl) {
1371
+ this._stopActiveReferenceSelection();
1372
+ }
1373
+ } catch (_) { }
1374
+
1290
1375
  // Clear any lingering scene selection so the new reference starts fresh
1291
1376
  try {
1292
1377
  const scene = this._getReferenceSelectionScene();
@@ -95,6 +95,8 @@ export class HistoryCollectionWidget {
95
95
  this._itemEls = new Map();
96
96
  this._forms = new Map();
97
97
  this._uiFieldSignatures = new Map();
98
+ this._autoFocusOnExpand = false;
99
+ this._pendingFocusEntryId = null;
98
100
  this._boundHistoryListener = null;
99
101
  this._listenerUnsub = null;
100
102
  this._suppressHistoryListener = false;
@@ -157,6 +159,7 @@ export class HistoryCollectionWidget {
157
159
 
158
160
  if (!entries.length) {
159
161
  this._setContextSuppression(false);
162
+ this._pendingFocusEntryId = null;
160
163
  const empty = document.createElement('div');
161
164
  empty.className = 'hc-empty';
162
165
  empty.textContent = 'No entries yet.';
@@ -198,6 +201,8 @@ export class HistoryCollectionWidget {
198
201
  const itemEl = this._renderEntry(entry, id, i, targetId === id, entries.length);
199
202
  this._listEl.appendChild(itemEl);
200
203
  }
204
+
205
+ this._applyPendingFocus();
201
206
  }
202
207
 
203
208
  _destroyForm(id) {
@@ -223,6 +228,36 @@ export class HistoryCollectionWidget {
223
228
  return this._forms.get(String(id)) || null;
224
229
  }
225
230
 
231
+ // Close any expanded entry dialog and optionally clear stored open state.
232
+ collapseExpandedEntries({ clearOpenState = true, notify = true } = {}) {
233
+ const prevId = this._expandedId != null ? String(this._expandedId) : null;
234
+ let prevEntry = null;
235
+ if (prevId) {
236
+ const info = this._findEntryInfoById(prevId);
237
+ prevEntry = info?.entry || null;
238
+ }
239
+
240
+ if (clearOpenState && this._autoSyncOpenState) {
241
+ if (prevEntry) {
242
+ this._applyOpenState(prevEntry, false);
243
+ } else {
244
+ const entries = this._getEntries();
245
+ for (const entry of entries) {
246
+ this._applyOpenState(entry, false);
247
+ }
248
+ }
249
+ }
250
+
251
+ if (!prevId) {
252
+ this._setContextSuppression(false);
253
+ return;
254
+ }
255
+
256
+ this._expandedId = null;
257
+ this.render();
258
+ if (notify) this._notifyEntryToggle(prevEntry, false);
259
+ }
260
+
226
261
  _getEntries() {
227
262
  if (!this.history) return [];
228
263
  if (Array.isArray(this.history.entries)) return this.history.entries;
@@ -414,6 +449,7 @@ export class HistoryCollectionWidget {
414
449
  this._applyOpenState(targetEntry, false);
415
450
  }
416
451
  this._expandedId = null;
452
+ this._pendingFocusEntryId = null;
417
453
  this.render();
418
454
  this._notifyEntryToggle(targetEntry, false);
419
455
  return;
@@ -425,6 +461,9 @@ export class HistoryCollectionWidget {
425
461
  if (targetEntry) this._applyOpenState(targetEntry, true);
426
462
  }
427
463
  this._expandedId = targetEntry ? targetId : null;
464
+ if (this._autoFocusOnExpand && targetEntry) {
465
+ this._pendingFocusEntryId = targetId;
466
+ }
428
467
  this.render();
429
468
  if (previousInfo?.entry) this._notifyEntryToggle(previousInfo.entry, false);
430
469
  if (targetEntry) this._notifyEntryToggle(targetEntry, true);
@@ -613,6 +652,30 @@ export class HistoryCollectionWidget {
613
652
  }
614
653
  }
615
654
 
655
+ _applyPendingFocus() {
656
+ if (!this._autoFocusOnExpand) return;
657
+ const targetId = this._pendingFocusEntryId;
658
+ if (!targetId) return;
659
+ if (!this._expandedId || String(this._expandedId) !== String(targetId)) {
660
+ this._pendingFocusEntryId = null;
661
+ return;
662
+ }
663
+ const form = this.getFormForEntry(targetId);
664
+ if (!form) {
665
+ this._pendingFocusEntryId = null;
666
+ return;
667
+ }
668
+ const focus = () => {
669
+ try {
670
+ if (typeof form.focusFirstField === 'function') form.focusFirstField();
671
+ else if (typeof form.activateFirstReferenceSelection === 'function') form.activateFirstReferenceSelection();
672
+ } catch (_) { /* ignore */ }
673
+ };
674
+ if (typeof requestAnimationFrame === 'function') requestAnimationFrame(() => focus());
675
+ else setTimeout(focus, 0);
676
+ this._pendingFocusEntryId = null;
677
+ }
678
+
616
679
  async _moveEntry(id, delta) {
617
680
  if (!id) return;
618
681
  const entries = this._getEntries();
@@ -671,6 +734,7 @@ export class HistoryCollectionWidget {
671
734
  }
672
735
  if (this._autoSyncOpenState) this._applyOpenState(entry, true);
673
736
  this._expandedId = normalizedId;
737
+ if (this._autoFocusOnExpand) this._pendingFocusEntryId = normalizedId;
674
738
  createdEntryId = normalizedId;
675
739
  }
676
740
  } catch (_) { /* ignore */ }
@@ -693,6 +757,7 @@ export class HistoryCollectionWidget {
693
757
  }
694
758
  if (this._autoSyncOpenState) this._applyOpenState(entry, true);
695
759
  this._expandedId = normalizedId;
760
+ if (this._autoFocusOnExpand) this._pendingFocusEntryId = normalizedId;
696
761
  createdEntryId = normalizedId;
697
762
  }
698
763
  this.render();