brep-io-kernel 1.0.19 → 1.0.21

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.
@@ -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') {
@@ -1287,6 +1333,15 @@ export class SchemaForm {
1287
1333
  }
1288
1334
 
1289
1335
  _activateReferenceSelection(inputEl, def) {
1336
+ // If switching between reference fields, fully stop the previous session so
1337
+ // selection filters restore correctly (prevents sticky FACE-only state).
1338
+ try {
1339
+ const prevActive = SchemaForm.__activeRefInput || null;
1340
+ if (prevActive && prevActive !== inputEl) {
1341
+ this._stopActiveReferenceSelection();
1342
+ }
1343
+ } catch (_) { }
1344
+
1290
1345
  // Clear any lingering scene selection so the new reference starts fresh
1291
1346
  try {
1292
1347
  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();
@@ -53,6 +53,7 @@ export class AnnotationCollectionWidget extends HistoryCollectionWidget {
53
53
  });
54
54
 
55
55
  this.pmimode = pmimode || null;
56
+ this._autoFocusOnExpand = true;
56
57
  }
57
58
 
58
59
  #applyEntryChange({ entry, details }) {
@@ -15,6 +15,43 @@ export class BaseAnnotation extends ListEntityBase {
15
15
  this.resultArtifacts = [];
16
16
  }
17
17
 
18
+ static _normalizeSelectionItems(selectedItems) {
19
+ return Array.isArray(selectedItems) ? selectedItems : [];
20
+ }
21
+
22
+ static _normalizeSelectionType(type) {
23
+ return String(type || '').toUpperCase();
24
+ }
25
+
26
+ static _isSelectionType(item, allowed) {
27
+ if (!allowed || !allowed.size) return true;
28
+ return allowed.has(BaseAnnotation._normalizeSelectionType(item?.type));
29
+ }
30
+
31
+ static _selectionRefName(item) {
32
+ return item?.name
33
+ || item?.userData?.faceName
34
+ || item?.userData?.edgeName
35
+ || item?.userData?.vertexName
36
+ || item?.userData?.solidName
37
+ || item?.userData?.name
38
+ || null;
39
+ }
40
+
41
+ static _collectSelectionRefs(selectedItems, types = null) {
42
+ const items = BaseAnnotation._normalizeSelectionItems(selectedItems);
43
+ const allowed = Array.isArray(types)
44
+ ? new Set(types.map((t) => BaseAnnotation._normalizeSelectionType(t)))
45
+ : null;
46
+ const refs = [];
47
+ for (const item of items) {
48
+ if (!BaseAnnotation._isSelectionType(item, allowed)) continue;
49
+ const ref = BaseAnnotation._selectionRefName(item);
50
+ if (ref) refs.push(ref);
51
+ }
52
+ return refs;
53
+ }
54
+
18
55
  async run(renderingContext) {
19
56
  // Base implementation - subclasses should override
20
57
  // renderingContext contains: { pmimode, group, idx, ctx }
@@ -168,6 +168,10 @@ export class PMIMode {
168
168
  try { if (v.controls) v.controls.enabled = true; } catch { }
169
169
  }
170
170
 
171
+ collapseExpandedDialogs() {
172
+ try { this._annotationWidget?.collapseExpandedEntries?.({ clearOpenState: true }); } catch { /* ignore */ }
173
+ }
174
+
171
175
  applyViewTransformsSequential() {
172
176
  try {
173
177
  this.#applyViewTransforms();
@@ -71,6 +71,11 @@ export class AngleDimensionAnnotation extends BaseAnnotation {
71
71
  static longName = 'Angle Dimension';
72
72
  static title = 'Angle';
73
73
  static inputParamsSchema = inputParamsSchema;
74
+ static showContexButton(selectedItems) {
75
+ const refs = BaseAnnotation._collectSelectionRefs(selectedItems, ['FACE', 'EDGE']);
76
+ if (refs.length < 2) return false;
77
+ return { params: { targets: refs.slice(0, 2) } };
78
+ }
74
79
 
75
80
  constructor(opts = {}) {
76
81
  super(opts);
@@ -41,6 +41,11 @@ export class ExplodeBodyAnnotation extends BaseAnnotation {
41
41
  static title = 'Explode Body';
42
42
  static inputParamsSchema = inputParamsSchema;
43
43
  static aliases = ['viewTransform'];
44
+ static showContexButton(selectedItems) {
45
+ const refs = BaseAnnotation._collectSelectionRefs(selectedItems, ['SOLID']);
46
+ if (!refs.length) return false;
47
+ return { params: { targets: refs } };
48
+ }
44
49
 
45
50
  constructor(opts = {}) {
46
51
  super(opts);
@@ -71,6 +71,17 @@ export class HoleCalloutAnnotation extends BaseAnnotation {
71
71
  static longName = 'Hole Callout';
72
72
  static title = 'Hole Callout';
73
73
  static inputParamsSchema = inputParamsSchema;
74
+ static showContexButton(selectedItems) {
75
+ const items = BaseAnnotation._normalizeSelectionItems(selectedItems);
76
+ const allowed = new Set(['VERTEX', 'EDGE', 'FACE']);
77
+ for (const item of items) {
78
+ if (!BaseAnnotation._isSelectionType(item, allowed)) continue;
79
+ if (!hasHoleMetadata(item)) continue;
80
+ const ref = BaseAnnotation._selectionRefName(item);
81
+ if (ref) return { params: { target: ref } };
82
+ }
83
+ return false;
84
+ }
74
85
 
75
86
  constructor(opts = {}) {
76
87
  super(opts);
@@ -293,6 +304,46 @@ function findHoleDescriptor(partHistory, targetObj, fallbackPoint, targetName =
293
304
  return descriptors[0];
294
305
  }
295
306
 
307
+ function readHoleMetadata(obj) {
308
+ if (!obj) return null;
309
+ const ud = obj.userData || null;
310
+ if (ud?.hole) return ud.hole;
311
+ if (ud?.metadata?.hole) return ud.metadata.hole;
312
+ if (typeof obj.getMetadata === 'function') {
313
+ try {
314
+ const meta = obj.getMetadata();
315
+ if (meta?.hole) return meta.hole;
316
+ if (meta?.metadata?.hole) return meta.metadata.hole;
317
+ } catch { /* ignore */ }
318
+ }
319
+ const faceName = obj?.name || ud?.faceName || null;
320
+ const parentSolid = obj?.parentSolid || ud?.parentSolid || null;
321
+ if (faceName && parentSolid && typeof parentSolid.getFaceMetadata === 'function') {
322
+ try {
323
+ const meta = parentSolid.getFaceMetadata(faceName);
324
+ if (meta?.hole) return meta.hole;
325
+ } catch { /* ignore */ }
326
+ }
327
+ return null;
328
+ }
329
+
330
+ function hasHoleMetadata(target) {
331
+ if (!target) return false;
332
+ const queue = [target];
333
+ const visited = new Set();
334
+ while (queue.length) {
335
+ const obj = queue.shift();
336
+ if (!obj || visited.has(obj)) continue;
337
+ visited.add(obj);
338
+ if (readHoleMetadata(obj)) return true;
339
+ if (Array.isArray(obj.faces)) {
340
+ for (const face of obj.faces) queue.push(face);
341
+ }
342
+ if (obj.parent) queue.push(obj.parent);
343
+ }
344
+ return false;
345
+ }
346
+
296
347
 
297
348
  function formatHoleCallout(desc, quantity = 1, options = {}) {
298
349
  if (!desc) return '';
@@ -65,6 +65,11 @@ export class LeaderAnnotation extends BaseAnnotation {
65
65
  static longName = 'Leader';
66
66
  static title = 'Leader';
67
67
  static inputParamsSchema = inputParamsSchema;
68
+ static showContexButton(selectedItems) {
69
+ const refs = BaseAnnotation._collectSelectionRefs(selectedItems, ['VERTEX']);
70
+ if (!refs.length) return false;
71
+ return { params: { target: refs } };
72
+ }
68
73
 
69
74
  constructor(opts = {}) {
70
75
  super(opts);
@@ -9,22 +9,7 @@ const inputParamsSchema = {
9
9
  label: 'ID',
10
10
  hint: 'unique identifier for the linear dimension',
11
11
  },
12
- decimals: {
13
- type: 'number',
14
- default_value: 3,
15
- defaultResolver: ({ pmimode }) => {
16
- const dec = Number.isFinite(pmimode?._opts?.dimDecimals)
17
- ? (pmimode._opts.dimDecimals | 0)
18
- : undefined;
19
- if (!Number.isFinite(dec)) return undefined;
20
- return Math.max(0, Math.min(8, dec));
21
- },
22
- label: 'Decimals',
23
- hint: 'Number of decimal places to display',
24
- min: 0,
25
- max: 8,
26
- step: 1,
27
- },
12
+
28
13
  targets: {
29
14
  type: 'reference_selection',
30
15
  selectionFilter: ['VERTEX', 'EDGE'],
@@ -67,6 +52,22 @@ const inputParamsSchema = {
67
52
  label: 'Reference',
68
53
  hint: 'Mark as reference dimension (parentheses)',
69
54
  },
55
+ decimals: {
56
+ type: 'number',
57
+ default_value: 3,
58
+ defaultResolver: ({ pmimode }) => {
59
+ const dec = Number.isFinite(pmimode?._opts?.dimDecimals)
60
+ ? (pmimode._opts.dimDecimals | 0)
61
+ : undefined;
62
+ if (!Number.isFinite(dec)) return undefined;
63
+ return Math.max(0, Math.min(8, dec));
64
+ },
65
+ label: 'Decimals',
66
+ hint: 'Number of decimal places to display',
67
+ min: 0,
68
+ max: 8,
69
+ step: 1,
70
+ },
70
71
  };
71
72
 
72
73
  export class LinearDimensionAnnotation extends BaseAnnotation {
@@ -76,6 +77,11 @@ export class LinearDimensionAnnotation extends BaseAnnotation {
76
77
  static longName = 'Linear Dimension';
77
78
  static title = 'Linear';
78
79
  static inputParamsSchema = inputParamsSchema;
80
+ static showContexButton(selectedItems) {
81
+ const refs = BaseAnnotation._collectSelectionRefs(selectedItems, ['VERTEX', 'EDGE']);
82
+ if (!refs.length) return false;
83
+ return { params: { targets: refs.slice(0, 2) } };
84
+ }
79
85
 
80
86
  constructor(opts = {}) {
81
87
  super(opts);
@@ -4,6 +4,7 @@
4
4
  import * as THREE from 'three';
5
5
  import { BaseAnnotation } from '../BaseAnnotation.js';
6
6
  import { getPMIStyle } from '../pmiStyle.js';
7
+ import { objectRepresentativePoint } from '../annUtils.js';
7
8
 
8
9
  const inputParamsSchema = {
9
10
  id: {
@@ -34,6 +35,14 @@ export class NoteAnnotation extends BaseAnnotation {
34
35
  static longName = 'Note';
35
36
  static title = 'Note';
36
37
  static inputParamsSchema = inputParamsSchema;
38
+ static showContexButton(selectedItems) {
39
+ const items = BaseAnnotation._normalizeSelectionItems(selectedItems);
40
+ if (!items.length) return false;
41
+ const anchor = items[0];
42
+ const point = objectRepresentativePoint(null, anchor);
43
+ if (!point) return false;
44
+ return { params: { position: { x: point.x, y: point.y, z: point.z } } };
45
+ }
37
46
 
38
47
  constructor(opts = {}) {
39
48
  super(opts);
@@ -9,22 +9,6 @@ const inputParamsSchema = {
9
9
  label: 'ID',
10
10
  hint: 'unique identifier for the radial dimension',
11
11
  },
12
- decimals: {
13
- type: 'number',
14
- default_value: 3,
15
- defaultResolver: ({ pmimode }) => {
16
- const dec = Number.isFinite(pmimode?._opts?.dimDecimals)
17
- ? (pmimode._opts.dimDecimals | 0)
18
- : undefined;
19
- if (!Number.isFinite(dec)) return undefined;
20
- return Math.max(0, Math.min(8, dec));
21
- },
22
- label: 'Decimals',
23
- hint: 'Number of decimal places to display',
24
- min: 0,
25
- max: 8,
26
- step: 1,
27
- },
28
12
  cylindricalFaceRef: {
29
13
  type: 'reference_selection',
30
14
  selectionFilter: ['FACE'],
@@ -68,6 +52,22 @@ const inputParamsSchema = {
68
52
  label: 'Reference',
69
53
  hint: 'Mark as reference dimension (parentheses)',
70
54
  },
55
+ decimals: {
56
+ type: 'number',
57
+ default_value: 3,
58
+ defaultResolver: ({ pmimode }) => {
59
+ const dec = Number.isFinite(pmimode?._opts?.dimDecimals)
60
+ ? (pmimode._opts.dimDecimals | 0)
61
+ : undefined;
62
+ if (!Number.isFinite(dec)) return undefined;
63
+ return Math.max(0, Math.min(8, dec));
64
+ },
65
+ label: 'Decimals',
66
+ hint: 'Number of decimal places to display',
67
+ min: 0,
68
+ max: 8,
69
+ step: 1,
70
+ },
71
71
  };
72
72
 
73
73
  export class RadialDimensionAnnotation extends BaseAnnotation {
@@ -77,6 +77,17 @@ export class RadialDimensionAnnotation extends BaseAnnotation {
77
77
  static longName = 'Radial Dimension';
78
78
  static title = 'Radial';
79
79
  static inputParamsSchema = inputParamsSchema;
80
+ static showContexButton(selectedItems) {
81
+ const items = BaseAnnotation._normalizeSelectionItems(selectedItems);
82
+ const allowed = new Set(['FACE']);
83
+ for (const item of items) {
84
+ if (!BaseAnnotation._isSelectionType(item, allowed)) continue;
85
+ if (!hasCylindricalMetadata(item)) continue;
86
+ const ref = BaseAnnotation._selectionRefName(item);
87
+ if (ref) return { params: { cylindricalFaceRef: ref } };
88
+ }
89
+ return false;
90
+ }
80
91
 
81
92
  constructor(opts = {}) {
82
93
  super(opts);
@@ -345,6 +356,60 @@ function computeRadialPoints(pmimode, ann, ctx) {
345
356
  }
346
357
  }
347
358
 
359
+ function isCylindricalMetadata(meta) {
360
+ if (!meta || typeof meta !== 'object') return false;
361
+ const type = String(meta.type || '').toLowerCase();
362
+ if (type === 'cylindrical') {
363
+ const radius = Number(meta.radius);
364
+ return Number.isFinite(radius) && radius > 0;
365
+ }
366
+ if (type === 'conical') {
367
+ const r0 = Number(meta.radiusBottom);
368
+ const r1 = Number(meta.radiusTop);
369
+ if (!Number.isFinite(r0) || !Number.isFinite(r1)) return false;
370
+ if (Math.abs(r0 - r1) > 1e-6) return false;
371
+ return Math.max(r0, r1) > 0;
372
+ }
373
+ return false;
374
+ }
375
+
376
+ function readCylindricalMetadata(obj) {
377
+ if (!obj) return null;
378
+ const ud = obj.userData || null;
379
+ if (ud?.metadata && isCylindricalMetadata(ud.metadata)) return ud.metadata;
380
+ if (typeof obj.getMetadata === 'function') {
381
+ try {
382
+ const meta = obj.getMetadata();
383
+ if (isCylindricalMetadata(meta)) return meta;
384
+ } catch { /* ignore */ }
385
+ }
386
+ const faceName = obj?.name || ud?.faceName || null;
387
+ let owner = obj?.parentSolid || ud?.parentSolid || obj?.parent || null;
388
+ while (owner) {
389
+ if (faceName && typeof owner.getFaceMetadata === 'function') {
390
+ try {
391
+ const meta = owner.getFaceMetadata(faceName);
392
+ if (isCylindricalMetadata(meta)) return meta;
393
+ } catch { /* ignore */ }
394
+ break;
395
+ }
396
+ owner = owner.parent || null;
397
+ }
398
+ return null;
399
+ }
400
+
401
+ function hasCylindricalMetadata(target) {
402
+ if (!target) return false;
403
+ if (readCylindricalMetadata(target)) return true;
404
+ if (Array.isArray(target.faces)) {
405
+ for (const face of target.faces) {
406
+ if (readCylindricalMetadata(face)) return true;
407
+ }
408
+ }
409
+ if (target.parent && readCylindricalMetadata(target.parent)) return true;
410
+ return false;
411
+ }
412
+
348
413
  function measureRadialValue(pmimode, ann) {
349
414
  try {
350
415
  const data = computeRadialPoints(pmimode, ann);
package/src/UI/viewer.js CHANGED
@@ -533,6 +533,7 @@ export class Viewer {
533
533
  this._attachRendererEvents(el);
534
534
 
535
535
  SelectionFilter.viewer = this;
536
+ try { SelectionFilter._ensureSelectionFilterIndicator?.(this); } catch (_) { }
536
537
  // Use capture on pointerup to ensure we end interactions even if pointerup fires off-element
537
538
  window.addEventListener('pointerup', this._onPointerUp, { passive: false, capture: true });
538
539
  document.addEventListener('dblclick', this._onGlobalDoubleClick, { passive: false, capture: true });
@@ -1476,8 +1477,15 @@ export class Viewer {
1476
1477
  // ----------------------------------------
1477
1478
  // PMI Edit Mode API
1478
1479
  // ----------------------------------------
1480
+ _collapseExpandedDialogsForModeSwitch() {
1481
+ try { this.historyWidget?.collapseExpandedEntries?.({ clearOpenState: true, notify: false }); } catch { }
1482
+ try { this.assemblyConstraintsWidget?.collapseExpandedDialogs?.(); } catch { }
1483
+ try { this._pmiMode?.collapseExpandedDialogs?.(); } catch { }
1484
+ }
1485
+
1479
1486
  startPMIMode(viewEntry, viewIndex, widget = this.pmiViewsWidget) {
1480
1487
  const alreadyActive = !!this._pmiMode;
1488
+ try { this._collapseExpandedDialogsForModeSwitch(); } catch { }
1481
1489
  if (!alreadyActive) {
1482
1490
  try { this.assemblyConstraintsWidget?.onPMIModeEnter?.(); } catch { }
1483
1491
  }
@@ -1506,6 +1514,9 @@ export class Viewer {
1506
1514
 
1507
1515
  endPMIMode() {
1508
1516
  const hadMode = !!this._pmiMode;
1517
+ if (hadMode) {
1518
+ try { this._collapseExpandedDialogsForModeSwitch(); } catch { }
1519
+ }
1509
1520
  try { if (this._pmiMode) this._pmiMode.dispose(); } catch { }
1510
1521
  this._pmiMode = null;
1511
1522
  if (hadMode) {
@@ -2365,24 +2376,34 @@ export class Viewer {
2365
2376
 
2366
2377
  // Prefer the intersected object if it is clickable
2367
2378
  let obj = intersection.object;
2379
+ if (obj && obj.type === 'POINTS' && obj.parent && String(obj.parent.type || '').toUpperCase() === SelectionFilter.VERTEX) {
2380
+ obj = obj.parent;
2381
+ }
2368
2382
 
2369
2383
  // If the object (or its ancestors) doesn't expose onClick, climb to one that does
2370
2384
  let target = obj;
2371
2385
  while (target && typeof target.onClick !== 'function' && target.visible) target = target.parent;
2386
+ if (!target) target = obj;
2372
2387
  if (!target) return null;
2373
2388
 
2374
2389
  // Respect selection filter: ensure target is a permitted type, or ALL
2375
2390
  if (typeof isAllowed === 'function') {
2376
2391
  // Allow selecting already-selected items regardless (toggle off), consistent with SceneListing
2377
2392
  if (!isAllowed(target.type) && !target.selected) {
2378
- // Try to find a closer ancestor/descendant of allowed type that is clickable
2393
+ // Try to find a closer ancestor of allowed type
2379
2394
  // Ascend first (e.g., FACE hit while EDGE is active should try parent SOLID only if allowed)
2380
2395
  let t = target.parent;
2381
- while (t && typeof t.onClick === 'function' && !isAllowed(t.type)) t = t.parent;
2382
- if (t && typeof t.onClick === 'function' && isAllowed(t.type)) target = t;
2396
+ while (t && !isAllowed(t.type)) t = t.parent;
2397
+ if (t && isAllowed(t.type)) target = t;
2383
2398
  else return null;
2384
2399
  }
2385
2400
  }
2401
+ if (target && typeof target.onClick !== 'function') {
2402
+ try {
2403
+ const deep = target.type === SelectionFilter.SOLID || target.type === SelectionFilter.COMPONENT;
2404
+ SelectionFilter.ensureSelectionHandlers?.(target, { deep });
2405
+ } catch { }
2406
+ }
2386
2407
  return target;
2387
2408
  }
2388
2409