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.
- package/README.md +4 -1
- package/dist-kernel/brep-kernel.js +10858 -9938
- package/package.json +3 -2
- package/src/BREP/Edge.js +2 -0
- package/src/BREP/Face.js +2 -0
- package/src/BREP/SolidMethods/visualize.js +372 -365
- package/src/BREP/Vertex.js +2 -17
- package/src/PartHistory.js +4 -25
- package/src/SketchSolver2D.js +3 -0
- package/src/UI/AccordionWidget.js +1 -1
- package/src/UI/EnvMonacoEditor.js +0 -3
- package/src/UI/HistoryWidget.js +12 -4
- package/src/UI/SceneListing.js +45 -7
- package/src/UI/SelectionFilter.js +903 -438
- package/src/UI/SelectionState.js +464 -0
- package/src/UI/assembly/AssemblyConstraintCollectionWidget.js +41 -1
- package/src/UI/assembly/AssemblyConstraintsWidget.js +21 -3
- package/src/UI/assembly/constraintSelectionUtils.js +3 -182
- package/src/UI/{assembly/constraintFaceUtils.js → faceUtils.js} +30 -5
- package/src/UI/featureDialogs.js +154 -69
- package/src/UI/history/HistoryCollectionWidget.js +65 -0
- package/src/UI/pmi/AnnotationCollectionWidget.js +1 -0
- package/src/UI/pmi/BaseAnnotation.js +37 -0
- package/src/UI/pmi/LabelOverlay.js +32 -0
- package/src/UI/pmi/PMIMode.js +27 -0
- package/src/UI/pmi/dimensions/AngleDimensionAnnotation.js +5 -0
- package/src/UI/pmi/dimensions/ExplodeBodyAnnotation.js +5 -0
- package/src/UI/pmi/dimensions/HoleCalloutAnnotation.js +57 -0
- package/src/UI/pmi/dimensions/LeaderAnnotation.js +5 -0
- package/src/UI/pmi/dimensions/LinearDimensionAnnotation.js +22 -16
- package/src/UI/pmi/dimensions/NoteAnnotation.js +9 -0
- package/src/UI/pmi/dimensions/RadialDimensionAnnotation.js +81 -16
- package/src/UI/toolbarButtons/orientToFaceButton.js +3 -36
- package/src/UI/toolbarButtons/registerDefaultButtons.js +2 -0
- package/src/UI/toolbarButtons/selectionStateButton.js +206 -0
- package/src/UI/viewer.js +34 -13
- package/src/assemblyConstraints/AssemblyConstraintHistory.js +18 -42
- package/src/assemblyConstraints/constraints/AngleConstraint.js +1 -0
- package/src/assemblyConstraints/constraints/DistanceConstraint.js +1 -0
- package/src/features/selectionUtils.js +21 -5
- package/src/features/sketch/SketchFeature.js +2 -2
- package/src/features/sketch/sketchSolver2D/constraintDefinitions.js +3 -2
- package/src/utils/selectionResolver.js +258 -0
|
@@ -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) {
|
package/src/UI/pmi/PMIMode.js
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
68
|
-
const normal =
|
|
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
|
];
|