brep-io-kernel 1.0.21 → 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 (34) hide show
  1. package/README.md +4 -1
  2. package/dist-kernel/brep-kernel.js +11065 -10512
  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 +3 -0
  13. package/src/UI/SceneListing.js +45 -7
  14. package/src/UI/SelectionFilter.js +469 -442
  15. package/src/UI/SelectionState.js +464 -0
  16. package/src/UI/assembly/AssemblyConstraintCollectionWidget.js +40 -1
  17. package/src/UI/assembly/AssemblyConstraintsWidget.js +17 -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 +99 -69
  21. package/src/UI/pmi/LabelOverlay.js +32 -0
  22. package/src/UI/pmi/PMIMode.js +23 -0
  23. package/src/UI/pmi/dimensions/HoleCalloutAnnotation.js +7 -1
  24. package/src/UI/toolbarButtons/orientToFaceButton.js +3 -36
  25. package/src/UI/toolbarButtons/registerDefaultButtons.js +2 -0
  26. package/src/UI/toolbarButtons/selectionStateButton.js +206 -0
  27. package/src/UI/viewer.js +16 -16
  28. package/src/assemblyConstraints/AssemblyConstraintHistory.js +18 -42
  29. package/src/assemblyConstraints/constraints/AngleConstraint.js +1 -0
  30. package/src/assemblyConstraints/constraints/DistanceConstraint.js +1 -0
  31. package/src/features/selectionUtils.js +21 -5
  32. package/src/features/sketch/SketchFeature.js +2 -2
  33. package/src/features/sketch/sketchSolver2D/constraintDefinitions.js +3 -2
  34. 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
+ }
@@ -1047,50 +1047,50 @@ export class SchemaForm {
1047
1047
  for (const name of list) {
1048
1048
  if (!name) continue;
1049
1049
  const existing = cache.get(name) || null;
1050
- const snapshot = store ? store[name] : null;
1051
- if (snapshot) {
1052
- const snapType = String(snapshot.type || '').toUpperCase();
1053
- const snapEdges = Array.isArray(snapshot.edgePositions) ? snapshot.edgePositions : null;
1054
- const isFaceSnap = (snapType === 'FACE' || snapType === 'PLANE') && snapEdges && snapEdges.length;
1055
- const isEdgeSnap = snapType === 'EDGE' && Array.isArray(snapshot.positions) && snapshot.positions.length >= 6;
1056
- const isVertexSnap = snapType === 'VERTEX' && Array.isArray(snapshot.position) && snapshot.position.length >= 3;
1057
- if (isFaceSnap || isEdgeSnap || isVertexSnap) {
1058
- const shouldOverride = !existing || !existing.fromSnapshot || isFaceSnap;
1059
- if (shouldOverride) {
1060
- const ghost = this._buildReferencePreviewFromSnapshot(name, snapshot);
1061
- if (ghost) {
1062
- cache.set(name, {
1063
- object: ghost,
1064
- type: snapshot.type || null,
1065
- sourceUuid: snapshot.sourceUuid || null,
1066
- sourceFeatureId: snapshot.sourceFeatureId || null,
1067
- showWhenOriginalPresent: isFaceSnap || !!ghost?.userData?.previewHasEdges,
1068
- fromSnapshot: true,
1069
- });
1070
- continue;
1071
- }
1072
- }
1073
- }
1074
- }
1075
- if (cache.has(name)) continue;
1076
1050
  const obj = scene.getObjectByName(name);
1077
1051
  if (obj && !obj?.userData?.refPreview) {
1078
- 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
+ }
1079
1065
  continue;
1080
1066
  }
1081
- if (store && store[name]) {
1082
- const snapshot = store[name];
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
- showWhenOriginalPresent: String(snapshot.type || '').toUpperCase() === 'FACE' || String(snapshot.type || '').toUpperCase() === 'PLANE' || !!ghost?.userData?.previewHasEdges,
1091
- fromSnapshot: true,
1092
- });
1093
- }
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
+ });
1094
1094
  }
1095
1095
  }
1096
1096
  }
@@ -1106,21 +1106,24 @@ export class SchemaForm {
1106
1106
  if (!ghost) return;
1107
1107
  const sourceUuid = obj.uuid || null;
1108
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';
1109
1112
  cache.set(refName, {
1110
1113
  object: ghost,
1111
1114
  type: obj.type || null,
1112
1115
  sourceUuid,
1113
1116
  sourceFeatureId,
1114
- showWhenOriginalPresent: !!ghost?.userData?.previewHasEdges,
1117
+ sourceTimestamp,
1118
+ showWhenOriginalPresent: isEdge || !!ghost?.userData?.previewHasEdges,
1115
1119
  });
1116
1120
  try {
1117
1121
  const store = this._getReferencePreviewPersistentBucket(inputEl);
1118
1122
  if (store) {
1119
- const objType = String(obj.type || '').toUpperCase();
1120
1123
  if (objType === SelectionFilter.EDGE || objType === 'EDGE') {
1121
1124
  const positions = this._extractEdgeWorldPositions(obj);
1122
1125
  if (positions && positions.length >= 6) {
1123
- store[refName] = { type: 'EDGE', positions, sourceUuid, sourceFeatureId };
1126
+ store[refName] = { type: 'EDGE', positions, sourceUuid, sourceFeatureId, sourceTimestamp };
1124
1127
  }
1125
1128
  } else if (objType === SelectionFilter.VERTEX || objType === 'VERTEX') {
1126
1129
  const pos = new THREE.Vector3();
@@ -1128,7 +1131,7 @@ export class SchemaForm {
1128
1131
  if (typeof obj.getWorldPosition === 'function') obj.getWorldPosition(pos);
1129
1132
  else pos.set(obj.position?.x || 0, obj.position?.y || 0, obj.position?.z || 0);
1130
1133
  } catch (_) { }
1131
- 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 };
1132
1135
  }
1133
1136
  }
1134
1137
  } catch (_) { }
@@ -1275,60 +1278,87 @@ export class SchemaForm {
1275
1278
  _hoverReferenceSelectionItem(inputEl, def, name) {
1276
1279
  try {
1277
1280
  if (!inputEl) return;
1278
- const activeInput = SchemaForm.__activeRefInput || null;
1279
- if (activeInput !== inputEl) {
1280
- const wrap = inputEl.closest?.('.ref-active') || null;
1281
- if (!wrap) return;
1282
- }
1281
+ const isActive = (SchemaForm.__activeRefInput || null) === inputEl;
1283
1282
  const normalized = normalizeReferenceName(name);
1284
1283
  if (!normalized) return;
1285
1284
  const scene = this._getReferenceSelectionScene();
1286
1285
  if (!scene) return;
1287
1286
  try { console.log('[ReferenceSelection] Hover', { name: normalized }); } catch (_) { }
1288
- try { this._ensureReferencePreviewSnapshots(inputEl, def); } catch (_) { }
1289
- try { this._seedReferencePreviewCacheFromScene(inputEl, def, [normalized], scene); } catch (_) { }
1290
- try { this._syncActiveReferenceSelectionPreview(inputEl, def); } catch (_) { }
1291
-
1292
- let target = null;
1293
- try { target = scene.getObjectByName(normalized); } catch (_) { target = null; }
1294
- if (!target) {
1295
- const cache = this._getReferencePreviewCache(inputEl);
1296
- const entry = cache ? cache.get(normalized) : null;
1297
- target = entry?.object || entry || null;
1287
+ if (isActive) {
1288
+ try { this._ensureReferencePreviewSnapshots(inputEl, def); } catch (_) { }
1298
1289
  }
1299
- if (!target) {
1300
- 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 (_) { }
1301
1293
  }
1302
- if (!target) return;
1303
- 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;
1304
1299
  let candidate = null;
1305
1300
  try {
1306
- target.traverse((child) => {
1301
+ obj.traverse((child) => {
1307
1302
  if (!child || candidate) return;
1308
1303
  if (child.type === 'REF_PREVIEW_EDGE') { candidate = child; return; }
1309
1304
  if (child.material) candidate = child;
1310
1305
  });
1311
1306
  } catch (_) { }
1312
- 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 (_) { }
1313
1340
  }
1314
1341
  try {
1315
1342
  console.log('[ReferenceSelection] Hover target', {
1316
1343
  name: normalized,
1317
- target: target || null,
1318
- inScene: !!(target && target.parent),
1344
+ targetCount: targets.length,
1319
1345
  });
1320
1346
  } catch (_) { }
1321
1347
  inputEl.__refChipHoverActive = true;
1322
- try { SelectionFilter.setHoverObject(target, { ignoreFilter: true }); } catch (_) { }
1348
+ try { SelectionFilter.setHoverObjects(targets, { ignoreFilter: true }); } catch (_) { }
1323
1349
  } catch (_) { }
1324
1350
  }
1325
1351
 
1326
1352
  _clearReferenceSelectionHover(inputEl) {
1327
1353
  try {
1328
- if (!inputEl || SchemaForm.__activeRefInput !== inputEl) return;
1354
+ if (!inputEl) return;
1329
1355
  if (!inputEl.__refChipHoverActive) return;
1330
1356
  inputEl.__refChipHoverActive = false;
1331
1357
  SelectionFilter.clearHover();
1358
+ if (SchemaForm.__activeRefInput !== inputEl && inputEl.__refPreviewHoverGroup) {
1359
+ inputEl.__refPreviewHoverGroup = false;
1360
+ try { this._removeReferencePreviewGroup(inputEl); } catch (_) { }
1361
+ }
1332
1362
  } catch (_) { }
1333
1363
  }
1334
1364
 
@@ -65,6 +65,38 @@ export class LabelOverlay {
65
65
  }
66
66
  });
67
67
  }
68
+ if (!el.__wheelPassThrough) {
69
+ const onWheel = (e) => {
70
+ if (!el.classList.contains('constraint-label')) return;
71
+ const canvas = this.viewer?.renderer?.domElement;
72
+ if (!canvas) return;
73
+ let canceled = false;
74
+ try {
75
+ const forwarded = new WheelEvent(e.type, {
76
+ bubbles: true,
77
+ cancelable: true,
78
+ deltaX: e.deltaX,
79
+ deltaY: e.deltaY,
80
+ deltaZ: e.deltaZ,
81
+ deltaMode: e.deltaMode,
82
+ clientX: e.clientX,
83
+ clientY: e.clientY,
84
+ screenX: e.screenX,
85
+ screenY: e.screenY,
86
+ ctrlKey: e.ctrlKey,
87
+ shiftKey: e.shiftKey,
88
+ altKey: e.altKey,
89
+ metaKey: e.metaKey,
90
+ });
91
+ canceled = !canvas.dispatchEvent(forwarded);
92
+ } catch { }
93
+ if (canceled) {
94
+ try { e.preventDefault(); } catch { }
95
+ }
96
+ };
97
+ el.addEventListener('wheel', onWheel, { passive: false });
98
+ el.__wheelPassThrough = onWheel;
99
+ }
68
100
  if (this.onDblClick) el.addEventListener('dblclick', (e) => this.onDblClick(idx, ann, e));
69
101
  try { this._root.appendChild(el); this._labelMap.set(idx, el); } catch {}
70
102
  } else if (text != null) {
@@ -12,6 +12,7 @@ import { getPMIStyle, setPMIStyle, sanitizePMIStyle } from './pmiStyle.js';
12
12
  import { AnnotationHistory } from './AnnotationHistory.js';
13
13
  import { LabelOverlay } from './LabelOverlay.js';
14
14
  import { AnnotationCollectionWidget } from './AnnotationCollectionWidget.js';
15
+ import { SelectionFilter } from '../SelectionFilter.js';
15
16
  import { localStorage as LS } from '../../idbStorage.js';
16
17
 
17
18
  const cssEscape = (value) => {
@@ -89,6 +90,9 @@ export class PMIMode {
89
90
  const v = this.viewer;
90
91
  if (!v || !v.container) return;
91
92
 
93
+ // Clear any lingering reference-selection state before PMI interactions.
94
+ this.#clearActiveReferenceSelection();
95
+
92
96
  // Save and hide existing accordion sections instead of hiding the whole sidebar
93
97
  this.#hideOriginalSidebarSections();
94
98
 
@@ -172,6 +176,22 @@ export class PMIMode {
172
176
  try { this._annotationWidget?.collapseExpandedEntries?.({ clearOpenState: true }); } catch { /* ignore */ }
173
177
  }
174
178
 
179
+ #clearActiveReferenceSelection() {
180
+ try {
181
+ const active = document.querySelectorAll('[active-reference-selection="true"],[active-reference-selection=true]');
182
+ active.forEach((el) => {
183
+ try { el.style.filter = 'none'; } catch { }
184
+ try { el.removeAttribute('active-reference-selection'); } catch { }
185
+ try {
186
+ const wrap = el.closest('.ref-single-wrap, .ref-multi-wrap');
187
+ if (wrap) wrap.classList.remove('ref-active');
188
+ } catch { }
189
+ });
190
+ } catch { }
191
+ try { if (window.__BREP_activeRefInput) window.__BREP_activeRefInput = null; } catch { }
192
+ try { SelectionFilter.restoreAllowedSelectionTypes(); } catch { }
193
+ }
194
+
175
195
  applyViewTransformsSequential() {
176
196
  try {
177
197
  this.#applyViewTransforms();
@@ -599,6 +619,9 @@ export class PMIMode {
599
619
  this.#markAnnotationsDirty();
600
620
  },
601
621
  });
622
+ // Avoid auto-activating reference selection on expand; PMI scene clicks should
623
+ // stay as normal selections unless the user explicitly activates a ref field.
624
+ this._annotationWidget._autoFocusOnExpand = false;
602
625
  widgetWrap.appendChild(this._annotationWidget.uiElement);
603
626
 
604
627
  this._pmiAnnotationsSection = sec;
@@ -331,13 +331,19 @@ function hasHoleMetadata(target) {
331
331
  if (!target) return false;
332
332
  const queue = [target];
333
333
  const visited = new Set();
334
+ const hasOwnFaces = (obj) => Object.prototype.hasOwnProperty.call(obj, 'faces');
334
335
  while (queue.length) {
335
336
  const obj = queue.shift();
336
337
  if (!obj || visited.has(obj)) continue;
337
338
  visited.add(obj);
338
339
  if (readHoleMetadata(obj)) return true;
339
- if (Array.isArray(obj.faces)) {
340
+ if (hasOwnFaces(obj) && Array.isArray(obj.faces)) {
340
341
  for (const face of obj.faces) queue.push(face);
342
+ } else if (obj.type === 'SOLID' || obj.type === 'COMPONENT') {
343
+ const kids = Array.isArray(obj.children) ? obj.children : [];
344
+ for (const child of kids) {
345
+ if (child && child.type === 'FACE') queue.push(child);
346
+ }
341
347
  }
342
348
  if (obj.parent) queue.push(obj.parent);
343
349
  }
@@ -1,4 +1,5 @@
1
1
  import * as THREE from 'three';
2
+ import { computeFaceCenter, computeFaceNormal } from '../faceUtils.js';
2
3
 
3
4
  const FACE_TYPES = new Set(['FACE', 'PLANE']);
4
5
 
@@ -26,46 +27,12 @@ function _findSelectedFace(viewer) {
26
27
  return found;
27
28
  }
28
29
 
29
- function _computeFaceCenter(obj) {
30
- if (!obj) return null;
31
- try { obj.updateMatrixWorld?.(true); } catch {}
32
- try {
33
- const box = new THREE.Box3().setFromObject(obj);
34
- if (!box.isEmpty()) return box.getCenter(new THREE.Vector3());
35
- } catch {}
36
- try {
37
- const geom = obj.geometry;
38
- const bs = geom?.boundingSphere || (geom?.computeBoundingSphere && (geom.computeBoundingSphere(), geom.boundingSphere));
39
- if (bs) return obj.localToWorld(bs.center.clone());
40
- } catch {}
41
- try {
42
- return obj.getWorldPosition?.(new THREE.Vector3()) || null;
43
- } catch {}
44
- return null;
45
- }
46
-
47
- function _computeFaceNormal(obj) {
48
- if (!obj) return null;
49
- let n = null;
50
- if (typeof obj.getAverageNormal === 'function') {
51
- try { n = obj.getAverageNormal().clone(); } catch {}
52
- }
53
- if (!n || n.lengthSq() < 1e-10) {
54
- try {
55
- const q = obj.getWorldQuaternion?.(new THREE.Quaternion());
56
- if (q) n = new THREE.Vector3(0, 0, 1).applyQuaternion(q);
57
- } catch {}
58
- }
59
- if (!n || n.lengthSq() < 1e-10) return null;
60
- return n.normalize();
61
- }
62
-
63
30
  function _orientCameraToFace(viewer, face) {
64
31
  const cam = viewer?.camera;
65
32
  if (!viewer || !cam || !face) return false;
66
33
 
67
- const target = _computeFaceCenter(face);
68
- const normal = _computeFaceNormal(face);
34
+ const target = computeFaceCenter(face);
35
+ const normal = computeFaceNormal(face);
69
36
  if (!target || !normal) return false;
70
37
 
71
38
  const toCam = cam.position.clone().sub(target);
@@ -11,6 +11,7 @@ import { createFlatPatternButton } from './flatPatternButton.js';
11
11
  import { createAboutButton } from './aboutButton.js';
12
12
  import { createTestsButton } from './testsButton.js';
13
13
  import { createScriptRunnerButton } from './scriptRunnerButton.js';
14
+ import { createSelectionStateButton } from './selectionStateButton.js';
14
15
 
15
16
  export function registerDefaultToolbarButtons(viewer) {
16
17
  if (!viewer || typeof viewer.addToolbarButton !== 'function') return;
@@ -25,6 +26,7 @@ export function registerDefaultToolbarButtons(viewer) {
25
26
  createAboutButton,
26
27
  createTestsButton,
27
28
  createScriptRunnerButton,
29
+ createSelectionStateButton,
28
30
  createUndoButton,
29
31
  createRedoButton,
30
32
  ];