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
@@ -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 }
@@ -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
 
@@ -168,6 +172,26 @@ export class PMIMode {
168
172
  try { if (v.controls) v.controls.enabled = true; } catch { }
169
173
  }
170
174
 
175
+ collapseExpandedDialogs() {
176
+ try { this._annotationWidget?.collapseExpandedEntries?.({ clearOpenState: true }); } catch { /* ignore */ }
177
+ }
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
+
171
195
  applyViewTransformsSequential() {
172
196
  try {
173
197
  this.#applyViewTransforms();
@@ -595,6 +619,9 @@ export class PMIMode {
595
619
  this.#markAnnotationsDirty();
596
620
  },
597
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;
598
625
  widgetWrap.appendChild(this._annotationWidget.uiElement);
599
626
 
600
627
  this._pmiAnnotationsSection = sec;
@@ -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,52 @@ 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
+ const hasOwnFaces = (obj) => Object.prototype.hasOwnProperty.call(obj, 'faces');
335
+ while (queue.length) {
336
+ const obj = queue.shift();
337
+ if (!obj || visited.has(obj)) continue;
338
+ visited.add(obj);
339
+ if (readHoleMetadata(obj)) return true;
340
+ if (hasOwnFaces(obj) && Array.isArray(obj.faces)) {
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
+ }
347
+ }
348
+ if (obj.parent) queue.push(obj.parent);
349
+ }
350
+ return false;
351
+ }
352
+
296
353
 
297
354
  function formatHoleCallout(desc, quantity = 1, options = {}) {
298
355
  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);
@@ -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
  ];