brep-io-kernel 1.0.0-ci.10
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/LICENSE.md +32 -0
- package/README.md +157 -0
- package/dist-kernel/brep-kernel.js +74699 -0
- package/package.json +58 -0
- package/src/BREP/AssemblyComponent.js +42 -0
- package/src/BREP/BREP.js +43 -0
- package/src/BREP/BetterSolid.js +805 -0
- package/src/BREP/Edge.js +103 -0
- package/src/BREP/Extrude.js +403 -0
- package/src/BREP/Face.js +187 -0
- package/src/BREP/MeshRepairer.js +634 -0
- package/src/BREP/OffsetShellSolid.js +614 -0
- package/src/BREP/PointCloudWrap.js +302 -0
- package/src/BREP/Revolve.js +345 -0
- package/src/BREP/SolidMethods/authoring.js +112 -0
- package/src/BREP/SolidMethods/booleanOps.js +230 -0
- package/src/BREP/SolidMethods/chamfer.js +122 -0
- package/src/BREP/SolidMethods/edgeResolution.js +25 -0
- package/src/BREP/SolidMethods/fillet.js +792 -0
- package/src/BREP/SolidMethods/index.js +72 -0
- package/src/BREP/SolidMethods/io.js +105 -0
- package/src/BREP/SolidMethods/lifecycle.js +103 -0
- package/src/BREP/SolidMethods/manifoldOps.js +375 -0
- package/src/BREP/SolidMethods/meshCleanup.js +2512 -0
- package/src/BREP/SolidMethods/meshQueries.js +264 -0
- package/src/BREP/SolidMethods/metadata.js +106 -0
- package/src/BREP/SolidMethods/metrics.js +51 -0
- package/src/BREP/SolidMethods/transforms.js +361 -0
- package/src/BREP/SolidMethods/visualize.js +508 -0
- package/src/BREP/SolidShared.js +26 -0
- package/src/BREP/Sweep.js +1596 -0
- package/src/BREP/Tube.js +857 -0
- package/src/BREP/Vertex.js +43 -0
- package/src/BREP/applyBooleanOperation.js +704 -0
- package/src/BREP/boundsUtils.js +48 -0
- package/src/BREP/chamfer.js +551 -0
- package/src/BREP/edgePolylineUtils.js +85 -0
- package/src/BREP/fillets/common.js +388 -0
- package/src/BREP/fillets/fillet.js +1422 -0
- package/src/BREP/fillets/filletGeometry.js +15 -0
- package/src/BREP/fillets/inset.js +389 -0
- package/src/BREP/fillets/offsetHelper.js +143 -0
- package/src/BREP/fillets/outset.js +88 -0
- package/src/BREP/helix.js +193 -0
- package/src/BREP/meshToBrep.js +234 -0
- package/src/BREP/primitives.js +279 -0
- package/src/BREP/setupManifold.js +71 -0
- package/src/BREP/threadGeometry.js +1120 -0
- package/src/BREP/triangleUtils.js +8 -0
- package/src/BREP/triangulate.js +608 -0
- package/src/FeatureRegistry.js +183 -0
- package/src/PartHistory.js +1132 -0
- package/src/UI/AccordionWidget.js +292 -0
- package/src/UI/CADmaterials.js +850 -0
- package/src/UI/EnvMonacoEditor.js +522 -0
- package/src/UI/FloatingWindow.js +396 -0
- package/src/UI/HistoryWidget.js +457 -0
- package/src/UI/MainToolbar.js +131 -0
- package/src/UI/ModelLibraryView.js +194 -0
- package/src/UI/OrthoCameraIdle.js +206 -0
- package/src/UI/PluginsWidget.js +280 -0
- package/src/UI/SceneListing.js +606 -0
- package/src/UI/SelectionFilter.js +629 -0
- package/src/UI/ViewCube.js +389 -0
- package/src/UI/assembly/AssemblyConstraintCollectionWidget.js +329 -0
- package/src/UI/assembly/AssemblyConstraintControlsWidget.js +282 -0
- package/src/UI/assembly/AssemblyConstraintsWidget.css +292 -0
- package/src/UI/assembly/AssemblyConstraintsWidget.js +1373 -0
- package/src/UI/assembly/constraintFaceUtils.js +115 -0
- package/src/UI/assembly/constraintHighlightUtils.js +70 -0
- package/src/UI/assembly/constraintLabelUtils.js +31 -0
- package/src/UI/assembly/constraintPointUtils.js +64 -0
- package/src/UI/assembly/constraintSelectionUtils.js +185 -0
- package/src/UI/assembly/constraintStatusUtils.js +142 -0
- package/src/UI/componentSelectorModal.js +240 -0
- package/src/UI/controls/CombinedTransformControls.js +386 -0
- package/src/UI/dialogs.js +351 -0
- package/src/UI/expressionsManager.js +100 -0
- package/src/UI/featureDialogWidgets/booleanField.js +25 -0
- package/src/UI/featureDialogWidgets/booleanOperationField.js +97 -0
- package/src/UI/featureDialogWidgets/buttonField.js +45 -0
- package/src/UI/featureDialogWidgets/componentSelectorField.js +102 -0
- package/src/UI/featureDialogWidgets/defaultField.js +23 -0
- package/src/UI/featureDialogWidgets/fileField.js +66 -0
- package/src/UI/featureDialogWidgets/index.js +34 -0
- package/src/UI/featureDialogWidgets/numberField.js +165 -0
- package/src/UI/featureDialogWidgets/optionsField.js +33 -0
- package/src/UI/featureDialogWidgets/referenceSelectionField.js +208 -0
- package/src/UI/featureDialogWidgets/stringField.js +24 -0
- package/src/UI/featureDialogWidgets/textareaField.js +28 -0
- package/src/UI/featureDialogWidgets/threadDesignationField.js +160 -0
- package/src/UI/featureDialogWidgets/transformField.js +252 -0
- package/src/UI/featureDialogWidgets/utils.js +43 -0
- package/src/UI/featureDialogWidgets/vec3Field.js +133 -0
- package/src/UI/featureDialogs.js +1414 -0
- package/src/UI/fileManagerWidget.js +615 -0
- package/src/UI/history/HistoryCollectionWidget.js +1294 -0
- package/src/UI/history/historyCollectionWidget.css.js +257 -0
- package/src/UI/history/historyDisplayInfo.js +133 -0
- package/src/UI/mobile.js +28 -0
- package/src/UI/objectDump.js +442 -0
- package/src/UI/pmi/AnnotationCollectionWidget.js +120 -0
- package/src/UI/pmi/AnnotationHistory.js +353 -0
- package/src/UI/pmi/AnnotationRegistry.js +90 -0
- package/src/UI/pmi/BaseAnnotation.js +269 -0
- package/src/UI/pmi/LabelOverlay.css +102 -0
- package/src/UI/pmi/LabelOverlay.js +191 -0
- package/src/UI/pmi/PMIMode.js +1550 -0
- package/src/UI/pmi/PMIViewsWidget.js +1098 -0
- package/src/UI/pmi/annUtils.js +729 -0
- package/src/UI/pmi/dimensions/AngleDimensionAnnotation.js +647 -0
- package/src/UI/pmi/dimensions/ExplodeBodyAnnotation.js +507 -0
- package/src/UI/pmi/dimensions/HoleCalloutAnnotation.js +462 -0
- package/src/UI/pmi/dimensions/LeaderAnnotation.js +403 -0
- package/src/UI/pmi/dimensions/LinearDimensionAnnotation.js +532 -0
- package/src/UI/pmi/dimensions/NoteAnnotation.js +110 -0
- package/src/UI/pmi/dimensions/RadialDimensionAnnotation.js +659 -0
- package/src/UI/pmi/pmiStyle.js +44 -0
- package/src/UI/sketcher/SketchMode3D.js +4095 -0
- package/src/UI/sketcher/dimensions.js +674 -0
- package/src/UI/sketcher/glyphs.js +236 -0
- package/src/UI/sketcher/highlights.js +60 -0
- package/src/UI/toolbarButtons/aboutButton.js +5 -0
- package/src/UI/toolbarButtons/exportButton.js +609 -0
- package/src/UI/toolbarButtons/flatPatternButton.js +307 -0
- package/src/UI/toolbarButtons/importButton.js +160 -0
- package/src/UI/toolbarButtons/inspectorToggleButton.js +12 -0
- package/src/UI/toolbarButtons/metadataButton.js +1063 -0
- package/src/UI/toolbarButtons/orientToFaceButton.js +114 -0
- package/src/UI/toolbarButtons/registerDefaultButtons.js +46 -0
- package/src/UI/toolbarButtons/saveButton.js +99 -0
- package/src/UI/toolbarButtons/scriptRunnerButton.js +302 -0
- package/src/UI/toolbarButtons/testsButton.js +26 -0
- package/src/UI/toolbarButtons/undoRedoButtons.js +25 -0
- package/src/UI/toolbarButtons/wireframeToggleButton.js +5 -0
- package/src/UI/toolbarButtons/zoomToFitButton.js +5 -0
- package/src/UI/triangleDebuggerWindow.js +945 -0
- package/src/UI/viewer.js +4228 -0
- package/src/assemblyConstraints/AssemblyConstraintHistory.js +1576 -0
- package/src/assemblyConstraints/AssemblyConstraintRegistry.js +120 -0
- package/src/assemblyConstraints/BaseAssemblyConstraint.js +66 -0
- package/src/assemblyConstraints/constraintExpressionUtils.js +35 -0
- package/src/assemblyConstraints/constraintUtils/parallelAlignment.js +676 -0
- package/src/assemblyConstraints/constraints/AngleConstraint.js +485 -0
- package/src/assemblyConstraints/constraints/CoincidentConstraint.js +194 -0
- package/src/assemblyConstraints/constraints/DistanceConstraint.js +616 -0
- package/src/assemblyConstraints/constraints/FixedConstraint.js +78 -0
- package/src/assemblyConstraints/constraints/ParallelConstraint.js +252 -0
- package/src/assemblyConstraints/constraints/TouchAlignConstraint.js +961 -0
- package/src/core/entities/HistoryCollectionBase.js +72 -0
- package/src/core/entities/ListEntityBase.js +109 -0
- package/src/core/entities/schemaProcesser.js +121 -0
- package/src/exporters/sheetMetalFlatPattern.js +659 -0
- package/src/exporters/sheetMetalUnfold.js +862 -0
- package/src/exporters/step.js +1135 -0
- package/src/exporters/threeMF.js +575 -0
- package/src/features/assemblyComponent/AssemblyComponentFeature.js +780 -0
- package/src/features/boolean/BooleanFeature.js +94 -0
- package/src/features/chamfer/ChamferFeature.js +116 -0
- package/src/features/datium/DatiumFeature.js +80 -0
- package/src/features/edgeFeatureUtils.js +41 -0
- package/src/features/extrude/ExtrudeFeature.js +143 -0
- package/src/features/fillet/FilletFeature.js +197 -0
- package/src/features/helix/HelixFeature.js +405 -0
- package/src/features/hole/HoleFeature.js +1050 -0
- package/src/features/hole/screwClearance.js +86 -0
- package/src/features/hole/threadDesignationCatalog.js +149 -0
- package/src/features/imageHeightSolid/ImageHeightmapSolidFeature.js +463 -0
- package/src/features/imageToFace/ImageToFaceFeature.js +727 -0
- package/src/features/imageToFace/imageEditor.js +1270 -0
- package/src/features/imageToFace/traceUtils.js +971 -0
- package/src/features/import3dModel/Import3dModelFeature.js +151 -0
- package/src/features/loft/LoftFeature.js +605 -0
- package/src/features/mirror/MirrorFeature.js +151 -0
- package/src/features/offsetFace/OffsetFaceFeature.js +370 -0
- package/src/features/offsetShell/OffsetShellFeature.js +89 -0
- package/src/features/overlapCleanup/OverlapCleanupFeature.js +85 -0
- package/src/features/pattern/PatternFeature.js +275 -0
- package/src/features/patternLinear/PatternLinearFeature.js +120 -0
- package/src/features/patternRadial/PatternRadialFeature.js +186 -0
- package/src/features/plane/PlaneFeature.js +154 -0
- package/src/features/primitiveCone/primitiveConeFeature.js +99 -0
- package/src/features/primitiveCube/primitiveCubeFeature.js +70 -0
- package/src/features/primitiveCylinder/primitiveCylinderFeature.js +91 -0
- package/src/features/primitivePyramid/primitivePyramidFeature.js +72 -0
- package/src/features/primitiveSphere/primitiveSphereFeature.js +62 -0
- package/src/features/primitiveTorus/primitiveTorusFeature.js +109 -0
- package/src/features/remesh/RemeshFeature.js +97 -0
- package/src/features/revolve/RevolveFeature.js +111 -0
- package/src/features/selectionUtils.js +118 -0
- package/src/features/sheetMetal/SheetMetalContourFlangeFeature.js +1656 -0
- package/src/features/sheetMetal/SheetMetalCutoutFeature.js +1056 -0
- package/src/features/sheetMetal/SheetMetalFlangeFeature.js +1568 -0
- package/src/features/sheetMetal/SheetMetalHemFeature.js +43 -0
- package/src/features/sheetMetal/SheetMetalObject.js +141 -0
- package/src/features/sheetMetal/SheetMetalTabFeature.js +176 -0
- package/src/features/sheetMetal/UNFOLD_NEUTRAL_REQUIREMENTS.md +153 -0
- package/src/features/sheetMetal/contour-flange-rebuild-spec.md +261 -0
- package/src/features/sheetMetal/profileUtils.js +25 -0
- package/src/features/sheetMetal/sheetMetalCleanup.js +9 -0
- package/src/features/sheetMetal/sheetMetalFaceTypes.js +146 -0
- package/src/features/sheetMetal/sheetMetalMetadata.js +165 -0
- package/src/features/sheetMetal/sheetMetalPipeline.js +169 -0
- package/src/features/sheetMetal/sheetMetalProfileUtils.js +216 -0
- package/src/features/sheetMetal/sheetMetalTabUtils.js +29 -0
- package/src/features/sheetMetal/sheetMetalTree.js +210 -0
- package/src/features/sketch/SketchFeature.js +955 -0
- package/src/features/sketch/sketchSolver2D/ConstraintEngine.js +800 -0
- package/src/features/sketch/sketchSolver2D/constraintDefinitions.js +704 -0
- package/src/features/sketch/sketchSolver2D/mathHelpersMod.js +307 -0
- package/src/features/spline/SplineEditorSession.js +988 -0
- package/src/features/spline/SplineFeature.js +1388 -0
- package/src/features/spline/splineUtils.js +218 -0
- package/src/features/sweep/SweepFeature.js +110 -0
- package/src/features/transform/TransformFeature.js +152 -0
- package/src/features/tube/TubeFeature.js +635 -0
- package/src/fs.proxy.js +625 -0
- package/src/idbStorage.js +254 -0
- package/src/index.js +12 -0
- package/src/main.js +15 -0
- package/src/metadataManager.js +64 -0
- package/src/path.proxy.js +277 -0
- package/src/plugins/ghLoader.worker.js +151 -0
- package/src/plugins/pluginManager.js +286 -0
- package/src/pmi/PMIViewsManager.js +134 -0
- package/src/services/componentLibrary.js +198 -0
- package/src/tests/ConsoleCapture.js +189 -0
- package/src/tests/S7-diagnostics-2025-12-23T18-37-23-570Z.json +630 -0
- package/src/tests/browserTests.js +597 -0
- package/src/tests/debugBoolean.js +225 -0
- package/src/tests/partFiles/badBoolean.json +957 -0
- package/src/tests/partFiles/extrudeTest.json +88 -0
- package/src/tests/partFiles/filletFail.json +58 -0
- package/src/tests/partFiles/import_TEst.part.part.json +646 -0
- package/src/tests/partFiles/sheetMetalHem.BREP.json +734 -0
- package/src/tests/test_boolean_subtract.js +27 -0
- package/src/tests/test_chamfer.js +17 -0
- package/src/tests/test_extrudeFace.js +24 -0
- package/src/tests/test_fillet.js +17 -0
- package/src/tests/test_fillet_nonClosed.js +45 -0
- package/src/tests/test_filletsMoreDifficult.js +46 -0
- package/src/tests/test_history_features_basic.js +149 -0
- package/src/tests/test_hole.js +282 -0
- package/src/tests/test_mirror.js +16 -0
- package/src/tests/test_offsetShellGrouping.js +85 -0
- package/src/tests/test_plane.js +4 -0
- package/src/tests/test_primitiveCone.js +11 -0
- package/src/tests/test_primitiveCube.js +7 -0
- package/src/tests/test_primitiveCylinder.js +8 -0
- package/src/tests/test_primitivePyramid.js +9 -0
- package/src/tests/test_primitiveSphere.js +17 -0
- package/src/tests/test_primitiveTorus.js +21 -0
- package/src/tests/test_pushFace.js +126 -0
- package/src/tests/test_sheetMetalContourFlange.js +125 -0
- package/src/tests/test_sheetMetal_features.js +80 -0
- package/src/tests/test_sketch_openLoop.js +45 -0
- package/src/tests/test_solidMetrics.js +58 -0
- package/src/tests/test_stlLoader.js +1889 -0
- package/src/tests/test_sweepFace.js +55 -0
- package/src/tests/test_tube.js +45 -0
- package/src/tests/test_tube_closedLoop.js +67 -0
- package/src/tests/tests.js +493 -0
- package/src/tools/assemblyConstraintDialogCapturePage.js +56 -0
- package/src/tools/dialogCapturePageFactory.js +227 -0
- package/src/tools/featureDialogCapturePage.js +47 -0
- package/src/tools/pmiAnnotationDialogCapturePage.js +60 -0
- package/src/utils/axisHelpers.js +99 -0
- package/src/utils/deepClone.js +69 -0
- package/src/utils/geometryTolerance.js +37 -0
- package/src/utils/normalizeTypeString.js +8 -0
- package/src/utils/xformMath.js +51 -0
|
@@ -0,0 +1,1050 @@
|
|
|
1
|
+
import { BREP } from '../../BREP/BREP.js';
|
|
2
|
+
import { ThreadGeometry, ThreadStandard } from '../../BREP/threadGeometry.js';
|
|
3
|
+
import { getClearanceDiameter } from './screwClearance.js';
|
|
4
|
+
|
|
5
|
+
const inputParamsSchema = {
|
|
6
|
+
id: {
|
|
7
|
+
type: 'string',
|
|
8
|
+
default_value: null,
|
|
9
|
+
hint: 'Unique identifier for the hole feature',
|
|
10
|
+
},
|
|
11
|
+
face: {
|
|
12
|
+
type: 'reference_selection',
|
|
13
|
+
label: 'Placement (sketch)',
|
|
14
|
+
selectionFilter: ['SKETCH'],
|
|
15
|
+
multiple: false,
|
|
16
|
+
minSelections: 1,
|
|
17
|
+
default_value: null,
|
|
18
|
+
hint: 'Select a sketch to place the hole',
|
|
19
|
+
},
|
|
20
|
+
holeType: {
|
|
21
|
+
type: 'options',
|
|
22
|
+
label: 'Hole type',
|
|
23
|
+
options: ['SIMPLE', 'COUNTERSINK', 'COUNTERBORE', 'THREADED'],
|
|
24
|
+
default_value: 'SIMPLE',
|
|
25
|
+
hint: 'Choose the hole style',
|
|
26
|
+
},
|
|
27
|
+
clearanceFit: {
|
|
28
|
+
type: 'options',
|
|
29
|
+
label: 'Screw clearance fit',
|
|
30
|
+
options: ['NONE', 'CLOSE', 'NORMAL', 'LOOSE'],
|
|
31
|
+
default_value: 'NONE',
|
|
32
|
+
hint: 'Use screw clearance data to size the hole (NONE keeps manual diameter)',
|
|
33
|
+
},
|
|
34
|
+
clearanceStandard: {
|
|
35
|
+
type: 'options',
|
|
36
|
+
label: 'Clearance standard',
|
|
37
|
+
options: ['ISO_METRIC', 'UNIFIED'],
|
|
38
|
+
default_value: 'ISO_METRIC',
|
|
39
|
+
hint: 'Standard for clearance hole lookup',
|
|
40
|
+
},
|
|
41
|
+
clearanceDesignation: {
|
|
42
|
+
type: 'thread_designation',
|
|
43
|
+
label: 'Clearance screw size / designation',
|
|
44
|
+
default_value: '',
|
|
45
|
+
hint: 'Size to use for clearance hole lookup (e.g. M6x1, 1/4-20UNC, #10-32UNF)',
|
|
46
|
+
standardField: 'clearanceStandard',
|
|
47
|
+
},
|
|
48
|
+
diameter: {
|
|
49
|
+
type: 'number',
|
|
50
|
+
label: 'Diameter',
|
|
51
|
+
default_value: 6,
|
|
52
|
+
min: 0,
|
|
53
|
+
step: 0.1,
|
|
54
|
+
hint: 'Straight hole diameter',
|
|
55
|
+
},
|
|
56
|
+
depth: {
|
|
57
|
+
type: 'number',
|
|
58
|
+
label: 'Depth',
|
|
59
|
+
default_value: 10,
|
|
60
|
+
min: 0,
|
|
61
|
+
step: 0.1,
|
|
62
|
+
hint: 'Straight portion depth (ignored when Through All)',
|
|
63
|
+
},
|
|
64
|
+
throughAll: {
|
|
65
|
+
type: 'boolean',
|
|
66
|
+
label: 'Through all',
|
|
67
|
+
default_value: false,
|
|
68
|
+
hint: 'Cut through the entire target thickness',
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
countersinkDiameter: {
|
|
72
|
+
type: 'number',
|
|
73
|
+
label: 'Countersink diameter',
|
|
74
|
+
default_value: 10,
|
|
75
|
+
min: 0,
|
|
76
|
+
step: 0.1,
|
|
77
|
+
hint: 'Major diameter of the countersink',
|
|
78
|
+
},
|
|
79
|
+
countersinkAngle: {
|
|
80
|
+
type: 'number',
|
|
81
|
+
label: 'Countersink angle (deg)',
|
|
82
|
+
default_value: 82,
|
|
83
|
+
min: 1,
|
|
84
|
+
max: 179,
|
|
85
|
+
step: 1,
|
|
86
|
+
hint: 'Included angle of the countersink',
|
|
87
|
+
},
|
|
88
|
+
counterboreDiameter: {
|
|
89
|
+
type: 'number',
|
|
90
|
+
label: 'Counterbore diameter',
|
|
91
|
+
default_value: 10,
|
|
92
|
+
min: 0,
|
|
93
|
+
step: 0.1,
|
|
94
|
+
hint: 'Major diameter of the counterbore',
|
|
95
|
+
},
|
|
96
|
+
counterboreDepth: {
|
|
97
|
+
type: 'number',
|
|
98
|
+
label: 'Counterbore depth',
|
|
99
|
+
default_value: 3,
|
|
100
|
+
min: 0,
|
|
101
|
+
step: 0.1,
|
|
102
|
+
hint: 'Depth of the counterbore recess',
|
|
103
|
+
},
|
|
104
|
+
threadStandard: {
|
|
105
|
+
type: 'options',
|
|
106
|
+
label: 'Thread standard',
|
|
107
|
+
options: ['NONE', 'ISO_METRIC', 'UNIFIED', 'TRAPEZOIDAL_METRIC', 'ACME', 'STUB_ACME', 'WHITWORTH', 'NPT'],
|
|
108
|
+
default_value: 'NONE',
|
|
109
|
+
hint: 'Thread specification family',
|
|
110
|
+
},
|
|
111
|
+
threadDesignation: {
|
|
112
|
+
type: 'thread_designation',
|
|
113
|
+
label: 'Thread size / designation',
|
|
114
|
+
default_value: '',
|
|
115
|
+
hint: 'Choose a preset for the selected thread standard or enter a custom size (e.g. M6x1, 1/4-20, Tr16x4, 1/4-18NPT, 10-32)',
|
|
116
|
+
},
|
|
117
|
+
threadMode: {
|
|
118
|
+
type: 'options',
|
|
119
|
+
label: 'Thread modeling',
|
|
120
|
+
options: ['SYMBOLIC', 'MODELED'],
|
|
121
|
+
default_value: 'SYMBOLIC',
|
|
122
|
+
hint: 'Symbolic is faster; modeled is helical geometry',
|
|
123
|
+
},
|
|
124
|
+
threadRadialOffset: {
|
|
125
|
+
type: 'number',
|
|
126
|
+
label: 'Thread radial offset',
|
|
127
|
+
default_value: 0,
|
|
128
|
+
step: 0.01,
|
|
129
|
+
hint: 'Optional clearance (+) or interference (-) applied to the thread profile',
|
|
130
|
+
},
|
|
131
|
+
threadSegmentsPerTurn: {
|
|
132
|
+
type: 'number',
|
|
133
|
+
label: 'Thread segments/turn',
|
|
134
|
+
default_value: 32,
|
|
135
|
+
min: 4,
|
|
136
|
+
step: 1,
|
|
137
|
+
hint: 'Resolution for modeled threads (ignored for symbolic)',
|
|
138
|
+
},
|
|
139
|
+
debugShowSolid: {
|
|
140
|
+
type: 'boolean',
|
|
141
|
+
label: 'Debug: show tool solid',
|
|
142
|
+
default_value: false,
|
|
143
|
+
hint: 'Visualize the cutting solid even if it is non-manifold',
|
|
144
|
+
},
|
|
145
|
+
boolean: {
|
|
146
|
+
type: 'boolean_operation',
|
|
147
|
+
label: 'Boolean',
|
|
148
|
+
default_value: { targets: [], operation: 'SUBTRACT' },
|
|
149
|
+
hint: 'Targets to cut; defaults to the selected body',
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const THREE = BREP.THREE;
|
|
154
|
+
|
|
155
|
+
function fallbackVector(v, def = new THREE.Vector3()) {
|
|
156
|
+
if (!v || typeof v.x !== 'number' || typeof v.y !== 'number' || typeof v.z !== 'number') return def.clone();
|
|
157
|
+
return new THREE.Vector3(v.x, v.y, v.z);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function buildBasisFromNormal(normal) {
|
|
161
|
+
const up = fallbackVector(normal, new THREE.Vector3(0, 1, 0)).clone();
|
|
162
|
+
if (up.lengthSq() < 1e-12) up.set(0, 1, 0);
|
|
163
|
+
up.normalize();
|
|
164
|
+
const ref = Math.abs(up.y) < 0.9 ? new THREE.Vector3(0, 1, 0) : new THREE.Vector3(1, 0, 0);
|
|
165
|
+
const x = new THREE.Vector3().crossVectors(ref, up);
|
|
166
|
+
if (x.lengthSq() < 1e-12) x.set(1, 0, 0);
|
|
167
|
+
x.normalize();
|
|
168
|
+
const z = new THREE.Vector3().crossVectors(up, x).normalize();
|
|
169
|
+
const mat = new THREE.Matrix4();
|
|
170
|
+
mat.makeBasis(x, up, z);
|
|
171
|
+
return mat;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function boxDiagonalLength(obj) {
|
|
175
|
+
try {
|
|
176
|
+
const box = new THREE.Box3().setFromObject(obj);
|
|
177
|
+
if (!box.isEmpty()) return box.getSize(new THREE.Vector3()).length();
|
|
178
|
+
} catch {
|
|
179
|
+
/* ignore */
|
|
180
|
+
}
|
|
181
|
+
return 0;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function parseNumberLike(value) {
|
|
185
|
+
if (value == null) return NaN;
|
|
186
|
+
const raw = String(value).trim();
|
|
187
|
+
if (!raw) return NaN;
|
|
188
|
+
const mixed = raw.match(/^([0-9]+)[-\s]+([0-9]+)\/([0-9]+)$/);
|
|
189
|
+
if (mixed) {
|
|
190
|
+
const whole = Number(mixed[1]);
|
|
191
|
+
const num = Number(mixed[2]);
|
|
192
|
+
const den = Number(mixed[3]);
|
|
193
|
+
if (Number.isFinite(whole) && Number.isFinite(num) && Number.isFinite(den) && den !== 0) {
|
|
194
|
+
return whole + num / den;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const frac = raw.match(/^([0-9]+)\/([0-9]+)$/);
|
|
198
|
+
if (frac) {
|
|
199
|
+
const num = Number(frac[1]);
|
|
200
|
+
const den = Number(frac[2]);
|
|
201
|
+
if (Number.isFinite(num) && Number.isFinite(den) && den !== 0) return num / den;
|
|
202
|
+
}
|
|
203
|
+
const n = Number(raw);
|
|
204
|
+
return Number.isFinite(n) ? n : NaN;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function unionSolids(solids) {
|
|
208
|
+
if (!Array.isArray(solids) || solids.length === 0) {
|
|
209
|
+
console.warn('[HoleFeature] unionSolids: no solids provided');
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
console.log('[HoleFeature] unionSolids: combining', solids.length, 'solids');
|
|
213
|
+
let current = solids[0];
|
|
214
|
+
if (!current) {
|
|
215
|
+
console.warn('[HoleFeature] unionSolids: first solid is null/undefined');
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
console.log('[HoleFeature] unionSolids: first solid type:', current?.constructor?.name);
|
|
219
|
+
for (let i = 1; i < solids.length; i++) {
|
|
220
|
+
const next = solids[i];
|
|
221
|
+
if (!next) continue;
|
|
222
|
+
try {
|
|
223
|
+
console.log('[HoleFeature] unionSolids: unioning with solid', i, 'type:', next?.constructor?.name);
|
|
224
|
+
current = current.union(next);
|
|
225
|
+
}
|
|
226
|
+
catch (error) { console.warn('[HoleFeature] Union failed:', error); }
|
|
227
|
+
}
|
|
228
|
+
console.log('[HoleFeature] unionSolids: final result type:', current?.constructor?.name);
|
|
229
|
+
return current;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function getWorldPosition(obj) {
|
|
233
|
+
if (!obj) return null;
|
|
234
|
+
if (obj.isVector3) return obj.clone();
|
|
235
|
+
const out = new THREE.Vector3();
|
|
236
|
+
if (typeof obj.getWorldPosition === 'function') {
|
|
237
|
+
try { return obj.getWorldPosition(out); } catch { }
|
|
238
|
+
}
|
|
239
|
+
if (obj.position && typeof obj.position === 'object') {
|
|
240
|
+
out.copy(obj.position);
|
|
241
|
+
try {
|
|
242
|
+
if (obj.matrixWorld && typeof obj.matrixWorld.isMatrix4 === 'boolean') {
|
|
243
|
+
out.applyMatrix4(obj.matrixWorld);
|
|
244
|
+
}
|
|
245
|
+
} catch {
|
|
246
|
+
/* ignore */
|
|
247
|
+
}
|
|
248
|
+
return out;
|
|
249
|
+
}
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function normalFromSketch(sketch) {
|
|
254
|
+
const fallback = new THREE.Vector3(0, 0, 1);
|
|
255
|
+
if (!sketch) return fallback;
|
|
256
|
+
|
|
257
|
+
// Prefer an explicit sketch basis if provided by the sketch feature.
|
|
258
|
+
const basis = sketch.userData?.sketchBasis;
|
|
259
|
+
if (basis && Array.isArray(basis.x) && Array.isArray(basis.y)) {
|
|
260
|
+
const bx = new THREE.Vector3().fromArray(basis.x);
|
|
261
|
+
const by = new THREE.Vector3().fromArray(basis.y);
|
|
262
|
+
const bz = Array.isArray(basis.z) ? new THREE.Vector3().fromArray(basis.z) : new THREE.Vector3().crossVectors(bx, by);
|
|
263
|
+
if (bz.lengthSq() > 1e-12) return bz.normalize();
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Fallback to world transform normal if available.
|
|
267
|
+
try {
|
|
268
|
+
const n = new THREE.Vector3(0, 0, 1);
|
|
269
|
+
const nm = new THREE.Matrix3();
|
|
270
|
+
nm.getNormalMatrix(sketch.matrixWorld || new THREE.Matrix4());
|
|
271
|
+
n.applyMatrix3(nm);
|
|
272
|
+
if (n.lengthSq() > 1e-12) return n.normalize();
|
|
273
|
+
} catch { /* ignore */ }
|
|
274
|
+
|
|
275
|
+
return new THREE.Vector3(0, 1, 0);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function centerFromObject(obj) {
|
|
279
|
+
try {
|
|
280
|
+
const box = new THREE.Box3().setFromObject(obj);
|
|
281
|
+
if (!box.isEmpty()) return box.getCenter(new THREE.Vector3());
|
|
282
|
+
} catch {
|
|
283
|
+
/* ignore */
|
|
284
|
+
}
|
|
285
|
+
return new THREE.Vector3();
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function collectSceneSolids(scene) {
|
|
289
|
+
const solids = [];
|
|
290
|
+
const pushIfSolid = (obj) => {
|
|
291
|
+
if (!obj || obj === scene) return;
|
|
292
|
+
const solidLike =
|
|
293
|
+
obj.userData?.isSolid
|
|
294
|
+
|| obj.isSolid
|
|
295
|
+
|| obj.type === 'Solid'
|
|
296
|
+
|| obj.constructor?.name === 'Solid'
|
|
297
|
+
|| typeof obj._manifoldize === 'function'
|
|
298
|
+
|| typeof obj.union === 'function';
|
|
299
|
+
if (solidLike) solids.push(obj);
|
|
300
|
+
};
|
|
301
|
+
if (!scene) return solids;
|
|
302
|
+
if (typeof scene.traverse === 'function') {
|
|
303
|
+
scene.traverse((obj) => pushIfSolid(obj));
|
|
304
|
+
} else if (Array.isArray(scene.children)) {
|
|
305
|
+
for (const obj of scene.children) pushIfSolid(obj);
|
|
306
|
+
}
|
|
307
|
+
return solids;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function chooseNearestSolid(solids, point) {
|
|
311
|
+
if (!Array.isArray(solids) || !solids.length || !point) return null;
|
|
312
|
+
let best = null;
|
|
313
|
+
let bestD2 = Infinity;
|
|
314
|
+
const tmpBox = new THREE.Box3();
|
|
315
|
+
const nearestToBox = new THREE.Vector3();
|
|
316
|
+
for (const s of solids) {
|
|
317
|
+
if (!s) continue;
|
|
318
|
+
try {
|
|
319
|
+
tmpBox.setFromObject(s);
|
|
320
|
+
const clamped = tmpBox.clampPoint(point, nearestToBox);
|
|
321
|
+
const d2 = clamped.distanceToSquared(point);
|
|
322
|
+
if (d2 < bestD2) {
|
|
323
|
+
bestD2 = d2;
|
|
324
|
+
best = s;
|
|
325
|
+
}
|
|
326
|
+
} catch {
|
|
327
|
+
/* ignore solids that fail bbox */
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return best;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function collectSketchVertices(sketch) {
|
|
334
|
+
const verts = [];
|
|
335
|
+
try {
|
|
336
|
+
if (!sketch || !Array.isArray(sketch.children)) return verts;
|
|
337
|
+
for (const child of sketch.children) {
|
|
338
|
+
if (!child) continue;
|
|
339
|
+
const sid = child?.userData?.sketchPointId;
|
|
340
|
+
const name = child?.name || '';
|
|
341
|
+
const isCenter = sid === 0 || sid === '0' || name === 'P0' || name.endsWith(':P0');
|
|
342
|
+
if (isCenter) continue;
|
|
343
|
+
const isVertexLike = child.type === 'Vertex' || child.isVertex || child.userData?.isVertex || child.userData?.type === 'VERTEX';
|
|
344
|
+
if (isVertexLike) verts.push(child);
|
|
345
|
+
}
|
|
346
|
+
} catch { /* ignore */ }
|
|
347
|
+
return verts;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function collectSketchVerticesByName(scene, sketchName) {
|
|
351
|
+
const verts = [];
|
|
352
|
+
if (!scene || !sketchName) return verts;
|
|
353
|
+
const prefix = `${sketchName}:P`;
|
|
354
|
+
const re = new RegExp(`^${escapeRegExp(prefix)}(\\d+)$`);
|
|
355
|
+
const walk = (obj) => {
|
|
356
|
+
if (!obj) return;
|
|
357
|
+
const nm = obj.name || '';
|
|
358
|
+
const m = nm.match(re);
|
|
359
|
+
if (m) {
|
|
360
|
+
const id = Number(m[1]);
|
|
361
|
+
if (id !== 0) verts.push(obj);
|
|
362
|
+
}
|
|
363
|
+
const children = Array.isArray(obj.children) ? obj.children : [];
|
|
364
|
+
for (const c of children) walk(c);
|
|
365
|
+
};
|
|
366
|
+
walk(scene);
|
|
367
|
+
return verts;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function escapeRegExp(str) {
|
|
371
|
+
return String(str).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function parseThreadGeometry({ standard, designation, isExternal }) {
|
|
375
|
+
const stdRaw = standard || 'NONE';
|
|
376
|
+
const std = String(stdRaw).toUpperCase();
|
|
377
|
+
const desig = String(designation || '').trim();
|
|
378
|
+
if (!desig || std === 'NONE') return null;
|
|
379
|
+
|
|
380
|
+
const clean = desig.replace(/\s+/g, '').toUpperCase();
|
|
381
|
+
const gaugeToInch = (n) => 0.06 + 0.013 * n; // approx formula for # screw sizes
|
|
382
|
+
const tryMetric = () => {
|
|
383
|
+
const normalized = clean.replace(/×/g, 'X').replace(/M(?=\d)/, 'M');
|
|
384
|
+
return ThreadGeometry.fromMetricDesignation(normalized, { isExternal });
|
|
385
|
+
};
|
|
386
|
+
const tryTr = () => {
|
|
387
|
+
const normalized = clean.replace(/×/g, 'X').replace(/^TR?/, 'TR');
|
|
388
|
+
return ThreadGeometry.fromTrapezoidalDesignation(normalized, { isExternal });
|
|
389
|
+
};
|
|
390
|
+
const parseDiaTpi = () => {
|
|
391
|
+
const m = clean.match(/^#?([0-9./]+)-([0-9.]+)([A-Z]+)?$/);
|
|
392
|
+
if (!m) return null;
|
|
393
|
+
const rawDia = m[1];
|
|
394
|
+
const tpi = Number(m[2]);
|
|
395
|
+
const series = m[3] ? m[3].toUpperCase() : null;
|
|
396
|
+
let dia = parseNumberLike(rawDia);
|
|
397
|
+
const intOnly = /^[0-9]+$/.test(rawDia);
|
|
398
|
+
if (intOnly && (!Number.isFinite(dia) || dia > 1.5)) {
|
|
399
|
+
const g = Number(rawDia);
|
|
400
|
+
if (Number.isFinite(g) && g >= 0 && g <= 14) dia = gaugeToInch(g);
|
|
401
|
+
}
|
|
402
|
+
if (!Number.isFinite(dia) || !Number.isFinite(tpi) || dia <= 0 || tpi <= 0) return null;
|
|
403
|
+
return { dia, tpi, series };
|
|
404
|
+
};
|
|
405
|
+
const tryUnified = () => {
|
|
406
|
+
const dt = parseDiaTpi();
|
|
407
|
+
if (!dt) return null;
|
|
408
|
+
const g = ThreadGeometry.fromUnified(dt.dia, dt.tpi, { isExternal });
|
|
409
|
+
if (dt.series) g.series = dt.series;
|
|
410
|
+
return g;
|
|
411
|
+
};
|
|
412
|
+
const tryAcme = () => {
|
|
413
|
+
const dt = parseDiaTpi();
|
|
414
|
+
if (!dt) return null;
|
|
415
|
+
return ThreadGeometry.fromAcme(dt.dia, dt.tpi, { isExternal });
|
|
416
|
+
};
|
|
417
|
+
const tryStubAcme = () => {
|
|
418
|
+
const dt = parseDiaTpi();
|
|
419
|
+
if (!dt) return null;
|
|
420
|
+
return ThreadGeometry.fromStubAcme(dt.dia, dt.tpi, { isExternal });
|
|
421
|
+
};
|
|
422
|
+
const tryWhitworth = () => {
|
|
423
|
+
const dt = parseDiaTpi();
|
|
424
|
+
if (!dt) return null;
|
|
425
|
+
return ThreadGeometry.fromWhitworth(dt.dia, dt.tpi, { isExternal });
|
|
426
|
+
};
|
|
427
|
+
const tryNpt = () => {
|
|
428
|
+
const m = clean.replace(/^NPT/i, '').replace(/NPT$/i, '');
|
|
429
|
+
const m1 = m.match(/^([0-9./]+)-([0-9.]+)$/);
|
|
430
|
+
if (!m1) return null;
|
|
431
|
+
const dia = parseNumberLike(m1[1]);
|
|
432
|
+
const tpi = Number(m1[2]);
|
|
433
|
+
if (!Number.isFinite(dia) || !Number.isFinite(tpi) || dia <= 0 || tpi <= 0) return null;
|
|
434
|
+
return ThreadGeometry.fromNPT(dia, tpi, { isExternal, taperDirection: 1 });
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
const tryMap = {
|
|
438
|
+
[ThreadStandard.ISO_METRIC]: tryMetric,
|
|
439
|
+
[ThreadStandard.TRAPEZOIDAL_METRIC]: tryTr,
|
|
440
|
+
[ThreadStandard.UNIFIED]: tryUnified,
|
|
441
|
+
[ThreadStandard.ACME]: tryAcme,
|
|
442
|
+
[ThreadStandard.STUB_ACME]: tryStubAcme,
|
|
443
|
+
[ThreadStandard.WHITWORTH]: tryWhitworth,
|
|
444
|
+
[ThreadStandard.NPT]: tryNpt,
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
const primary = tryMap[std];
|
|
448
|
+
if (primary) {
|
|
449
|
+
const g = primary();
|
|
450
|
+
if (g) return g;
|
|
451
|
+
console.warn('[HoleFeature] Thread parse failed for requested standard, attempting fallback:', std, desig);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Fallback inference by pattern
|
|
455
|
+
if (/^M\d+/i.test(clean)) {
|
|
456
|
+
const g = tryMetric();
|
|
457
|
+
if (g) return g;
|
|
458
|
+
}
|
|
459
|
+
if (/^TR/i.test(clean)) {
|
|
460
|
+
const g = tryTr();
|
|
461
|
+
if (g) return g;
|
|
462
|
+
}
|
|
463
|
+
if (/NPT/i.test(clean)) {
|
|
464
|
+
const g = tryNpt();
|
|
465
|
+
if (g) return g;
|
|
466
|
+
}
|
|
467
|
+
if (clean.includes('-')) {
|
|
468
|
+
const g = tryUnified() || tryAcme() || tryStubAcme() || tryWhitworth();
|
|
469
|
+
if (g) return g;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
throw new Error(`Unable to parse thread designation "${desig}" for standard ${stdRaw}.`);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function makeHoleTool({ holeType, radius, straightDepthTotal, sinkDia, sinkAngle, boreDia, boreDepth, res, featureID, omitStraight = false }) {
|
|
476
|
+
const solids = [];
|
|
477
|
+
const descriptors = [];
|
|
478
|
+
if (holeType === 'COUNTERSINK') {
|
|
479
|
+
const sinkRadius = Math.max(radius, sinkDia * 0.5);
|
|
480
|
+
const angleRad = sinkAngle * (Math.PI / 180);
|
|
481
|
+
const sinkHeight = (sinkRadius - radius) / Math.tan(angleRad * 0.5);
|
|
482
|
+
const coreDepth = Math.max(0, straightDepthTotal - sinkHeight);
|
|
483
|
+
if (sinkHeight > 0) {
|
|
484
|
+
solids.push(new BREP.Cone({
|
|
485
|
+
r1: radius,
|
|
486
|
+
r2: sinkRadius,
|
|
487
|
+
h: sinkHeight,
|
|
488
|
+
resolution: res,
|
|
489
|
+
name: featureID ? `${featureID}_CSK` : 'CSK',
|
|
490
|
+
}));
|
|
491
|
+
}
|
|
492
|
+
if (!omitStraight && coreDepth > 0) {
|
|
493
|
+
const cyl = new BREP.Cylinder({
|
|
494
|
+
radius,
|
|
495
|
+
height: coreDepth,
|
|
496
|
+
resolution: res,
|
|
497
|
+
name: featureID ? `${featureID}_Hole` : 'Hole',
|
|
498
|
+
});
|
|
499
|
+
cyl.bakeTRS({ position: [0, sinkHeight, 0], rotationEuler: [0, 0, 0], scale: [1, 1, 1] });
|
|
500
|
+
solids.push(cyl);
|
|
501
|
+
}
|
|
502
|
+
descriptors.push({
|
|
503
|
+
type: 'COUNTERSINK',
|
|
504
|
+
totalDepth: straightDepthTotal,
|
|
505
|
+
straightDepth: coreDepth,
|
|
506
|
+
countersinkHeight: sinkHeight,
|
|
507
|
+
countersinkDiameter: sinkRadius * 2,
|
|
508
|
+
diameter: radius * 2,
|
|
509
|
+
countersinkAngle: sinkAngle,
|
|
510
|
+
counterboreDepth: 0,
|
|
511
|
+
counterboreDiameter: 0,
|
|
512
|
+
});
|
|
513
|
+
} else if (holeType === 'COUNTERBORE') {
|
|
514
|
+
const coreDepth = Math.max(0, straightDepthTotal - boreDepth);
|
|
515
|
+
if (boreDepth > 0) {
|
|
516
|
+
solids.push(new BREP.Cylinder({
|
|
517
|
+
radius: Math.max(radius, boreDia * 0.5),
|
|
518
|
+
height: boreDepth,
|
|
519
|
+
resolution: res,
|
|
520
|
+
name: featureID ? `${featureID}_CBore` : 'CBore',
|
|
521
|
+
}));
|
|
522
|
+
}
|
|
523
|
+
if (!omitStraight && coreDepth > 0) {
|
|
524
|
+
const cyl = new BREP.Cylinder({
|
|
525
|
+
radius,
|
|
526
|
+
height: coreDepth,
|
|
527
|
+
resolution: res,
|
|
528
|
+
name: featureID ? `${featureID}_Hole` : 'Hole',
|
|
529
|
+
});
|
|
530
|
+
cyl.bakeTRS({ position: [0, boreDepth, 0], rotationEuler: [0, 0, 0], scale: [1, 1, 1] });
|
|
531
|
+
solids.push(cyl);
|
|
532
|
+
}
|
|
533
|
+
descriptors.push({
|
|
534
|
+
type: 'COUNTERBORE',
|
|
535
|
+
totalDepth: straightDepthTotal,
|
|
536
|
+
straightDepth: coreDepth,
|
|
537
|
+
countersinkHeight: 0,
|
|
538
|
+
countersinkDiameter: 0,
|
|
539
|
+
diameter: radius * 2,
|
|
540
|
+
countersinkAngle: 0,
|
|
541
|
+
counterboreDepth: boreDepth,
|
|
542
|
+
counterboreDiameter: Math.max(radius, boreDia * 0.5) * 2,
|
|
543
|
+
});
|
|
544
|
+
} else {
|
|
545
|
+
if (!omitStraight && straightDepthTotal > 0) {
|
|
546
|
+
solids.push(new BREP.Cylinder({
|
|
547
|
+
radius,
|
|
548
|
+
height: straightDepthTotal,
|
|
549
|
+
resolution: res,
|
|
550
|
+
name: featureID ? `${featureID}_Hole` : 'Hole',
|
|
551
|
+
}));
|
|
552
|
+
}
|
|
553
|
+
descriptors.push({
|
|
554
|
+
type: 'SIMPLE',
|
|
555
|
+
totalDepth: straightDepthTotal,
|
|
556
|
+
straightDepth: straightDepthTotal,
|
|
557
|
+
countersinkHeight: 0,
|
|
558
|
+
countersinkDiameter: 0,
|
|
559
|
+
diameter: radius * 2,
|
|
560
|
+
countersinkAngle: 0,
|
|
561
|
+
counterboreDepth: 0,
|
|
562
|
+
counterboreDiameter: 0,
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
return { solids, descriptors };
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
export class HoleFeature {
|
|
569
|
+
static shortName = 'H';
|
|
570
|
+
static longName = 'Hole';
|
|
571
|
+
static inputParamsSchema = inputParamsSchema;
|
|
572
|
+
|
|
573
|
+
constructor() {
|
|
574
|
+
this.inputParams = {};
|
|
575
|
+
this.persistentData = {};
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
uiFieldsTest(context) {
|
|
579
|
+
const params = this.inputParams || context?.params || {};
|
|
580
|
+
const t = String(params?.holeType || 'SIMPLE').toUpperCase();
|
|
581
|
+
const clearanceFit = String(params?.clearanceFit || 'NONE').toUpperCase();
|
|
582
|
+
const exclude = new Set();
|
|
583
|
+
|
|
584
|
+
const hide = (...keys) => { for (const key of keys) exclude.add(key); };
|
|
585
|
+
const hideCountersink = () => hide('countersinkDiameter', 'countersinkAngle');
|
|
586
|
+
const hideCounterbore = () => hide('counterboreDiameter', 'counterboreDepth');
|
|
587
|
+
const hideThread = () => hide('threadStandard', 'threadDesignation', 'threadMode', 'threadRadialOffset', 'threadSegmentsPerTurn');
|
|
588
|
+
|
|
589
|
+
if (t === 'THREADED') {
|
|
590
|
+
hideCountersink();
|
|
591
|
+
hideCounterbore();
|
|
592
|
+
hide('clearanceFit', 'clearanceStandard', 'clearanceDesignation', 'diameter');
|
|
593
|
+
} else {
|
|
594
|
+
hideThread();
|
|
595
|
+
if (t === 'COUNTERSINK') {
|
|
596
|
+
hideCounterbore();
|
|
597
|
+
} else if (t === 'COUNTERBORE') {
|
|
598
|
+
hideCountersink();
|
|
599
|
+
} else {
|
|
600
|
+
hideCountersink();
|
|
601
|
+
hideCounterbore();
|
|
602
|
+
}
|
|
603
|
+
if (clearanceFit === 'NONE') {
|
|
604
|
+
hide('clearanceStandard', 'clearanceDesignation');
|
|
605
|
+
} else {
|
|
606
|
+
hide('diameter');
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
return Array.from(exclude);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
async run(partHistory) {
|
|
614
|
+
const params = this.inputParams || {};
|
|
615
|
+
const featureID = params.featureID || params.id || null;
|
|
616
|
+
const selectionRaw = Array.isArray(params.face) ? params.face.filter(Boolean) : (params.face ? [params.face] : []);
|
|
617
|
+
const sketch = selectionRaw.find((o) => o && o.type === 'SKETCH') || null;
|
|
618
|
+
if (!sketch) throw new Error('HoleFeature requires a sketch selection; individual vertex picks are not supported.');
|
|
619
|
+
|
|
620
|
+
const pointObjs = [];
|
|
621
|
+
const sceneSolids = collectSceneSolids(partHistory?.scene);
|
|
622
|
+
let pointPositions = [];
|
|
623
|
+
|
|
624
|
+
// Use sketch-defined points (excluding the sketch origin) as hole centers.
|
|
625
|
+
const extraPts = collectSketchVertices(sketch);
|
|
626
|
+
if (extraPts.length) {
|
|
627
|
+
pointObjs.push(...extraPts);
|
|
628
|
+
pointPositions = pointObjs.map((o) => getWorldPosition(o)).filter(Boolean);
|
|
629
|
+
}
|
|
630
|
+
if (!pointPositions.length && partHistory?.scene && sketch?.name) {
|
|
631
|
+
const fallbackPts = collectSketchVerticesByName(partHistory.scene, sketch.name);
|
|
632
|
+
if (fallbackPts.length) {
|
|
633
|
+
pointObjs.push(...fallbackPts);
|
|
634
|
+
pointPositions = pointObjs.map((o) => getWorldPosition(o)).filter(Boolean);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
const hasPoints = pointPositions.length > 0;
|
|
639
|
+
const normal = normalFromSketch(sketch); // keep hole axis perpendicular to the sketch plane
|
|
640
|
+
const center = centerFromObject(sketch);
|
|
641
|
+
|
|
642
|
+
const holeType = String(params.holeType || 'SIMPLE').toUpperCase();
|
|
643
|
+
const clearanceFit = String(params.clearanceFit || 'NONE').toUpperCase();
|
|
644
|
+
const clearanceStandard = String(params.clearanceStandard || params.threadStandard || 'ISO_METRIC').toUpperCase();
|
|
645
|
+
const clearanceDesignation = String(params.clearanceDesignation || '').trim();
|
|
646
|
+
const diameterManual = Math.max(0, Number(params.diameter) || 0);
|
|
647
|
+
const straightDepthInput = Math.max(0, Number(params.depth) || 0);
|
|
648
|
+
const throughAll = params.throughAll === true;
|
|
649
|
+
const sinkDia = Math.max(0, Number(params.countersinkDiameter) || 0);
|
|
650
|
+
const sinkAngle = Math.max(1, Math.min(179, Number(params.countersinkAngle) || 82));
|
|
651
|
+
const boreDia = Math.max(0, Number(params.counterboreDiameter) || 0);
|
|
652
|
+
const boreDepth = Math.max(0, Number(params.counterboreDepth) || 0);
|
|
653
|
+
const threaded = String(params.holeType || '').toUpperCase() === 'THREADED';
|
|
654
|
+
const threadStandard = String(params.threadStandard || 'NONE').toUpperCase();
|
|
655
|
+
const threadDesignation = String(params.threadDesignation || params.threadSize || '').trim();
|
|
656
|
+
const threadMode = String(params.threadMode || 'SYMBOLIC').toUpperCase();
|
|
657
|
+
const threadRadialOffset = Number(params.threadRadialOffset ?? 0);
|
|
658
|
+
const threadSegmentsPerTurn = Math.max(4, Math.floor(Number(params.threadSegmentsPerTurn ?? (threadMode === 'MODELED' ? 32 : 12))));
|
|
659
|
+
let threadGeom = null;
|
|
660
|
+
if (threaded && threadStandard !== 'NONE' && threadDesignation) {
|
|
661
|
+
try {
|
|
662
|
+
threadGeom = parseThreadGeometry({
|
|
663
|
+
standard: threadStandard,
|
|
664
|
+
designation: threadDesignation,
|
|
665
|
+
isExternal: false,
|
|
666
|
+
});
|
|
667
|
+
} catch (err) {
|
|
668
|
+
console.warn('[HoleFeature] Thread parse failed:', err);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
const threadUnitScale = threadGeom && threadGeom.units === 'inch' ? 25.4 : 1;
|
|
672
|
+
let clearanceDia = null;
|
|
673
|
+
if (!threaded && clearanceFit !== 'NONE') {
|
|
674
|
+
const lookupDesig = clearanceDesignation || threadDesignation;
|
|
675
|
+
clearanceDia = getClearanceDiameter({
|
|
676
|
+
standard: clearanceStandard,
|
|
677
|
+
designation: lookupDesig,
|
|
678
|
+
fit: clearanceFit,
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
const effectiveDiameter = clearanceDia || diameterManual;
|
|
683
|
+
|
|
684
|
+
const radius = Math.max(
|
|
685
|
+
1e-4,
|
|
686
|
+
threadGeom ? (threadGeom.crestDiameter * threadUnitScale * 0.5 + threadRadialOffset) : effectiveDiameter * 0.5,
|
|
687
|
+
);
|
|
688
|
+
const debugShowSolid = params.debugShowSolid === true;
|
|
689
|
+
|
|
690
|
+
let booleanParam = params.boolean || { targets: [], operation: 'SUBTRACT' };
|
|
691
|
+
const rawTargets = Array.isArray(booleanParam.targets) ? booleanParam.targets : [];
|
|
692
|
+
const filteredTargets = rawTargets.filter((t) => sceneSolids.includes(t));
|
|
693
|
+
if (!filteredTargets.length) {
|
|
694
|
+
const sketchParent = (sketch && sceneSolids.includes(sketch.parent)) ? sketch.parent : null;
|
|
695
|
+
const firstParent = selectionRaw[0] && sceneSolids.includes(selectionRaw[0].parent)
|
|
696
|
+
? selectionRaw[0].parent
|
|
697
|
+
: null;
|
|
698
|
+
const candidate = sketchParent || firstParent || null;
|
|
699
|
+
if (candidate) {
|
|
700
|
+
booleanParam = { ...booleanParam, targets: [candidate], operation: booleanParam.operation || 'SUBTRACT' };
|
|
701
|
+
} else if (sceneSolids.length) {
|
|
702
|
+
const nearest = chooseNearestSolid(sceneSolids, center);
|
|
703
|
+
if (nearest) booleanParam = { ...booleanParam, targets: [nearest], operation: booleanParam.operation || 'SUBTRACT' };
|
|
704
|
+
}
|
|
705
|
+
} else {
|
|
706
|
+
booleanParam = { ...booleanParam, targets: filteredTargets };
|
|
707
|
+
}
|
|
708
|
+
if (booleanParam && typeof booleanParam.operation === 'string') {
|
|
709
|
+
booleanParam = { ...booleanParam, operation: String(booleanParam.operation).toUpperCase() };
|
|
710
|
+
}
|
|
711
|
+
const primaryTarget = (booleanParam.targets && booleanParam.targets[0])
|
|
712
|
+
|| (sketch && sceneSolids.includes(sketch.parent) ? sketch.parent : null)
|
|
713
|
+
|| chooseNearestSolid(sceneSolids, center)
|
|
714
|
+
|| null;
|
|
715
|
+
if (primaryTarget) {
|
|
716
|
+
// Choose the normal direction that points into the target solid.
|
|
717
|
+
try {
|
|
718
|
+
const box = new THREE.Box3().setFromObject(primaryTarget);
|
|
719
|
+
const toCenter = box.clampPoint(center, new THREE.Vector3()).sub(center);
|
|
720
|
+
if (toCenter.lengthSq() < 1e-12) {
|
|
721
|
+
toCenter.copy(box.getCenter(new THREE.Vector3()).sub(center));
|
|
722
|
+
}
|
|
723
|
+
if (toCenter.lengthSq() > 1e-10 && normal.dot(toCenter) < 0) {
|
|
724
|
+
normal.multiplyScalar(-1);
|
|
725
|
+
}
|
|
726
|
+
} catch {
|
|
727
|
+
/* ignore orientation flip issues */
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
const diag = primaryTarget ? boxDiagonalLength(primaryTarget) : boxDiagonalLength(chooseNearestSolid(sceneSolids, center));
|
|
731
|
+
const straightDepth = throughAll ? Math.max(straightDepthInput, diag * 1.5 || 50) : straightDepthInput;
|
|
732
|
+
|
|
733
|
+
const res = 48;
|
|
734
|
+
const backOffset = 1e-5; // small pullback to avoid coincident faces in booleans
|
|
735
|
+
const centers = hasPoints ? pointPositions : [center];
|
|
736
|
+
const sourceNames = hasPoints ? pointObjs.map((o) => o?.name || o?.uuid || null) : [null];
|
|
737
|
+
const tools = [];
|
|
738
|
+
const holeRecords = [];
|
|
739
|
+
const debugVisualizationObjects = []; // Store debug viz objects separately
|
|
740
|
+
centers.forEach((c, idx) => {
|
|
741
|
+
const pointName = sourceNames[idx] || null;
|
|
742
|
+
const holeFacePrefix = pointName || (featureID ? `${featureID}_${idx}` : `HOLE_${idx}`);
|
|
743
|
+
const { solids: toolSolids, descriptors } = makeHoleTool({
|
|
744
|
+
holeType,
|
|
745
|
+
radius,
|
|
746
|
+
straightDepthTotal: straightDepth,
|
|
747
|
+
sinkDia,
|
|
748
|
+
sinkAngle,
|
|
749
|
+
boreDia,
|
|
750
|
+
boreDepth,
|
|
751
|
+
res,
|
|
752
|
+
featureID: holeFacePrefix,
|
|
753
|
+
omitStraight: threaded,
|
|
754
|
+
});
|
|
755
|
+
// annotate faces with hole metadata before union so labels propagate
|
|
756
|
+
const descriptor = descriptors[0] || null;
|
|
757
|
+
const basePos = (c || center).clone();
|
|
758
|
+
const originPos = basePos.clone().addScaledVector(normal, -backOffset);
|
|
759
|
+
if (descriptor) {
|
|
760
|
+
if (holeType === 'THREADED') descriptor.type = 'THREADED';
|
|
761
|
+
descriptor.center = [originPos.x, originPos.y, originPos.z];
|
|
762
|
+
descriptor.normal = [normal.x, normal.y, normal.z];
|
|
763
|
+
descriptor.throughAll = throughAll;
|
|
764
|
+
descriptor.targetId = primaryTarget?.uuid || primaryTarget?.id || primaryTarget?.name || null;
|
|
765
|
+
descriptor.featureId = featureID || null;
|
|
766
|
+
descriptor.sourceName = sourceNames[idx] || null;
|
|
767
|
+
for (const solid of toolSolids) {
|
|
768
|
+
if (!solid || !solid.name) continue;
|
|
769
|
+
const sideName = `${solid.name}_S`;
|
|
770
|
+
try { solid.setFaceMetadata(sideName, { hole: { ...descriptor } }); } catch { }
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
if (threadGeom) {
|
|
775
|
+
try {
|
|
776
|
+
const axialBlend = 1e-4;
|
|
777
|
+
let threadLength = Math.max(
|
|
778
|
+
0,
|
|
779
|
+
descriptor?.straightDepth ?? descriptor?.totalDepth ?? straightDepth,
|
|
780
|
+
);
|
|
781
|
+
let threadStart = 0;
|
|
782
|
+
if (descriptor?.type === 'COUNTERSINK') threadStart = descriptor.countersinkHeight || 0;
|
|
783
|
+
else if (descriptor?.type === 'COUNTERBORE') threadStart = descriptor.counterboreDepth || 0;
|
|
784
|
+
threadStart = Math.max(0, threadStart - axialBlend);
|
|
785
|
+
threadLength = Math.max(0, threadLength + axialBlend);
|
|
786
|
+
if (threadLength > 0) {
|
|
787
|
+
console.log('[HoleFeature] Generating thread:', {
|
|
788
|
+
mode: threadMode,
|
|
789
|
+
length: threadLength,
|
|
790
|
+
threadStart,
|
|
791
|
+
majorDiameter: threadGeom.majorDiameter,
|
|
792
|
+
minorDiameter: threadGeom.minorDiameter,
|
|
793
|
+
crestRadius: threadGeom.crestRadius,
|
|
794
|
+
rootRadius: threadGeom.rootRadius,
|
|
795
|
+
pitch: threadGeom.pitch,
|
|
796
|
+
isExternal: threadGeom.isExternal,
|
|
797
|
+
radialOffset: threadRadialOffset,
|
|
798
|
+
segmentsPerTurn: threadSegmentsPerTurn,
|
|
799
|
+
});
|
|
800
|
+
// Scale the thread geometry to millimeters if needed
|
|
801
|
+
let threadGeomScaled = threadGeom;
|
|
802
|
+
if (threadUnitScale !== 1) {
|
|
803
|
+
console.log('[HoleFeature] Creating scaled thread geometry with scale factor', threadUnitScale);
|
|
804
|
+
// Create a new ThreadGeometry with scaled dimensions
|
|
805
|
+
threadGeomScaled = new ThreadGeometry({
|
|
806
|
+
standard: threadGeom.standard,
|
|
807
|
+
nominalDiameter: threadGeom.nominalDiameter * threadUnitScale,
|
|
808
|
+
pitch: threadGeom.pitch * threadUnitScale,
|
|
809
|
+
isExternal: threadGeom.isExternal,
|
|
810
|
+
starts: threadGeom.starts,
|
|
811
|
+
taperDirection: threadGeom.taperDirection,
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
// Extend one full pitch past both start and end to avoid flats for modeled threads only
|
|
815
|
+
const extraThreadLength = Math.max(0, threadGeomScaled.pitch || threadGeom.pitch || 0);
|
|
816
|
+
const threadStartEffective = threadMode === 'MODELED'
|
|
817
|
+
? threadStart - extraThreadLength
|
|
818
|
+
: threadStart;
|
|
819
|
+
const threadLengthEffective = threadMode === 'MODELED'
|
|
820
|
+
? Math.max(0, threadLength + extraThreadLength * 2)
|
|
821
|
+
: threadLength;
|
|
822
|
+
|
|
823
|
+
const threadSolid = threadGeomScaled.toSolid({
|
|
824
|
+
length: threadLengthEffective,
|
|
825
|
+
mode: threadMode === 'MODELED' ? 'modeled' : 'symbolic',
|
|
826
|
+
radialOffset: threadRadialOffset,
|
|
827
|
+
symbolicRadius: 'crest',
|
|
828
|
+
includeCore: false, // Core disabled - helical surface only for now
|
|
829
|
+
resolution: res,
|
|
830
|
+
segmentsPerTurn: threadSegmentsPerTurn,
|
|
831
|
+
name: `${holeFacePrefix}_THREAD`,
|
|
832
|
+
faceName: `${holeFacePrefix}_THREAD_FACE`,
|
|
833
|
+
axis: [0, 1, 0],
|
|
834
|
+
origin: [0, threadStartEffective, 0],
|
|
835
|
+
xDirection: [1, 0, 0],
|
|
836
|
+
});
|
|
837
|
+
console.log('[HoleFeature] Thread solid created:', {
|
|
838
|
+
type: threadSolid?.constructor?.name,
|
|
839
|
+
hasGeometry: !!threadSolid?.geometry,
|
|
840
|
+
vertexCount: threadSolid?.geometry?.attributes?.position?.count,
|
|
841
|
+
triangleCount: threadSolid?.triangles?.length,
|
|
842
|
+
faceCount: threadSolid?.faces?.size,
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
// Add a core solid at the minor diameter so the threaded hole removes material fully.
|
|
846
|
+
const minorRadiusAt = (z) => {
|
|
847
|
+
try {
|
|
848
|
+
const d = typeof threadGeomScaled.diametersAtZ === 'function'
|
|
849
|
+
? threadGeomScaled.diametersAtZ(z)
|
|
850
|
+
: null;
|
|
851
|
+
const minorDia = Number(d?.minor ?? threadGeomScaled.minorDiameter ?? threadGeomScaled.crestDiameter);
|
|
852
|
+
if (Number.isFinite(minorDia) && minorDia > 0) {
|
|
853
|
+
return Math.max(1e-4, minorDia * 0.5 + threadRadialOffset);
|
|
854
|
+
}
|
|
855
|
+
} catch { /* ignore */ }
|
|
856
|
+
return Math.max(1e-4, threadGeomScaled.crestRadius + threadRadialOffset);
|
|
857
|
+
};
|
|
858
|
+
const coreR0 = minorRadiusAt(0);
|
|
859
|
+
const coreR1 = minorRadiusAt(threadLengthEffective);
|
|
860
|
+
const coreName = `${holeFacePrefix}_THREAD_CORE`;
|
|
861
|
+
const coreHeight = threadLengthEffective;
|
|
862
|
+
if (coreHeight > 0) {
|
|
863
|
+
const coreSolid = threadGeomScaled.isTapered && Math.abs(coreR0 - coreR1) > 1e-6
|
|
864
|
+
? new BREP.Cone({
|
|
865
|
+
r1: coreR0,
|
|
866
|
+
r2: coreR1,
|
|
867
|
+
h: coreHeight,
|
|
868
|
+
resolution: res,
|
|
869
|
+
name: coreName,
|
|
870
|
+
})
|
|
871
|
+
: new BREP.Cylinder({
|
|
872
|
+
radius: coreR0,
|
|
873
|
+
height: coreHeight,
|
|
874
|
+
resolution: res,
|
|
875
|
+
name: coreName,
|
|
876
|
+
});
|
|
877
|
+
coreSolid.bakeTRS({
|
|
878
|
+
position: [0, threadStartEffective, 0],
|
|
879
|
+
rotationEuler: [0, 0, 0],
|
|
880
|
+
scale: [1, 1, 1],
|
|
881
|
+
});
|
|
882
|
+
if (descriptor) {
|
|
883
|
+
try { coreSolid.setFaceMetadata(`${coreName}_S`, { hole: { ...descriptor } }); } catch { /* best-effort */ }
|
|
884
|
+
}
|
|
885
|
+
toolSolids.push(coreSolid);
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
toolSolids.push(threadSolid);
|
|
889
|
+
|
|
890
|
+
if (debugShowSolid) {
|
|
891
|
+
try {
|
|
892
|
+
console.log('[HoleFeature] Creating profile cross-section visualization using primitives...');
|
|
893
|
+
|
|
894
|
+
// Get the profile points from the thread geometry
|
|
895
|
+
const crestR = threadGeomScaled.crestRadius;
|
|
896
|
+
const rootR = threadGeomScaled.rootRadius;
|
|
897
|
+
const pitch = threadGeomScaled.pitch;
|
|
898
|
+
const halfPitch = pitch / 2;
|
|
899
|
+
|
|
900
|
+
console.log('[HoleFeature] Profile dimensions:', {
|
|
901
|
+
crestR,
|
|
902
|
+
rootR,
|
|
903
|
+
pitch,
|
|
904
|
+
halfPitch,
|
|
905
|
+
depth: rootR - crestR,
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
// Create small spheres at each corner of the profile to visualize it
|
|
909
|
+
const markerSize = Math.max(0.5, pitch * 0.2);
|
|
910
|
+
const vizCenterZ = threadLengthEffective + 5; // Place it above the thread
|
|
911
|
+
|
|
912
|
+
// Profile corners in [R, Z] cylindrical coords (R=radial, Z=axial position in thread)
|
|
913
|
+
// We need to display this as a cross-section shape oriented perpendicular to hole axis
|
|
914
|
+
// Map: axial variation (Z in profile) -> X, radial distance (R) -> Y, depth -> Z
|
|
915
|
+
const profileCorners = [
|
|
916
|
+
[-halfPitch, crestR, vizCenterZ], // X=axial, Y=radial, Z=depth
|
|
917
|
+
[-halfPitch * 0.3, rootR, vizCenterZ],
|
|
918
|
+
[halfPitch * 0.3, rootR, vizCenterZ],
|
|
919
|
+
[halfPitch, crestR, vizCenterZ],
|
|
920
|
+
];
|
|
921
|
+
|
|
922
|
+
// Create a marker at each corner - store separately for debug viz
|
|
923
|
+
for (let i = 0; i < profileCorners.length; i++) {
|
|
924
|
+
const corner = profileCorners[i];
|
|
925
|
+
const marker = new BREP.Sphere({
|
|
926
|
+
radius: markerSize,
|
|
927
|
+
resolution: 16,
|
|
928
|
+
name: `PROFILE_MARKER_${featureID}_${idx}_${i}`,
|
|
929
|
+
});
|
|
930
|
+
marker.bakeTRS({
|
|
931
|
+
position: corner,
|
|
932
|
+
rotationEuler: [0, 0, 0],
|
|
933
|
+
scale: [1, 1, 1],
|
|
934
|
+
});
|
|
935
|
+
debugVisualizationObjects.push(marker);
|
|
936
|
+
console.log(`[HoleFeature] Added profile marker ${i} at`, corner);
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// Also create connecting cylinders to show the edges
|
|
940
|
+
for (let i = 0; i < profileCorners.length; i++) {
|
|
941
|
+
const p1 = profileCorners[i];
|
|
942
|
+
const p2 = profileCorners[(i + 1) % profileCorners.length];
|
|
943
|
+
const dx = p2[0] - p1[0];
|
|
944
|
+
const dy = p2[1] - p1[1];
|
|
945
|
+
const dz = p2[2] - p1[2];
|
|
946
|
+
const length = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
947
|
+
|
|
948
|
+
if (length > 0.01) {
|
|
949
|
+
const edge = new BREP.Cylinder({
|
|
950
|
+
radius: markerSize * 0.3,
|
|
951
|
+
height: length,
|
|
952
|
+
resolution: 12,
|
|
953
|
+
name: `PROFILE_EDGE_${featureID}_${idx}_${i}`,
|
|
954
|
+
});
|
|
955
|
+
|
|
956
|
+
// Position and orient the cylinder to connect the points
|
|
957
|
+
const midpoint = [(p1[0] + p2[0]) / 2, (p1[1] + p2[1]) / 2, (p1[2] + p2[2]) / 2];
|
|
958
|
+
const angleY = Math.atan2(dx, dy);
|
|
959
|
+
const angleX = Math.atan2(Math.sqrt(dx * dx + dz * dz), dy);
|
|
960
|
+
|
|
961
|
+
edge.bakeTRS({
|
|
962
|
+
position: midpoint,
|
|
963
|
+
rotationEuler: [angleX * 180 / Math.PI, 0, angleY * 180 / Math.PI],
|
|
964
|
+
scale: [1, 1, 1],
|
|
965
|
+
});
|
|
966
|
+
debugVisualizationObjects.push(edge);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
} catch (err) {
|
|
971
|
+
console.warn('[HoleFeature] Profile visualization creation failed:', err);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
if (descriptor) {
|
|
975
|
+
descriptor.thread = {
|
|
976
|
+
standard: threadStandard,
|
|
977
|
+
designation: threadDesignation,
|
|
978
|
+
series: threadGeom?.series || null,
|
|
979
|
+
mode: threadMode,
|
|
980
|
+
radialOffset: threadRadialOffset,
|
|
981
|
+
length: threadLengthEffective,
|
|
982
|
+
startOffset: threadStartEffective,
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
} catch (threadErr) {
|
|
987
|
+
console.warn('[HoleFeature] Thread generation failed:', threadErr);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
if (descriptor) {
|
|
992
|
+
holeRecords.push({ ...descriptor });
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
console.log('[HoleFeature] Unioning', toolSolids.length, 'solids for hole tool');
|
|
996
|
+
console.log('[HoleFeature] Unioning', toolSolids.length, 'solids for hole tool');
|
|
997
|
+
const tool = unionSolids(toolSolids);
|
|
998
|
+
if (!tool) return;
|
|
999
|
+
if (debugShowSolid) {
|
|
1000
|
+
try {
|
|
1001
|
+
tool.visualize();
|
|
1002
|
+
} catch (err) {
|
|
1003
|
+
console.warn('[HoleFeature] Debug visualize failed:', err);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
const basis = buildBasisFromNormal(normal);
|
|
1007
|
+
basis.setPosition(originPos);
|
|
1008
|
+
try { tool.bakeTransform(basis); }
|
|
1009
|
+
catch (error) { console.warn('[HoleFeature] Failed to transform tool:', error); }
|
|
1010
|
+
// add centerline for PMI/visualization
|
|
1011
|
+
const totalDepth = descriptor?.totalDepth || straightDepth || 1;
|
|
1012
|
+
const start = originPos;
|
|
1013
|
+
const end = start.clone().add(normal.clone().multiplyScalar(totalDepth));
|
|
1014
|
+
try {
|
|
1015
|
+
tool.addCenterline([start.x, start.y, start.z], [end.x, end.y, end.z], featureID ? `${featureID}_AXIS_${idx}` : `HOLE_AXIS_${idx}`, { materialKey: 'OVERLAY' });
|
|
1016
|
+
} catch { /* best-effort */ }
|
|
1017
|
+
tools.push(tool);
|
|
1018
|
+
});
|
|
1019
|
+
|
|
1020
|
+
if (!tools.length) throw new Error('HoleFeature could not build cutting tool geometry.');
|
|
1021
|
+
const combinedTool = tools.length === 1 ? tools[0] : unionSolids(tools);
|
|
1022
|
+
|
|
1023
|
+
const effects = await BREP.applyBooleanOperation(partHistory || {}, combinedTool, booleanParam, featureID);
|
|
1024
|
+
try { this.persistentData.holes = holeRecords; } catch { }
|
|
1025
|
+
|
|
1026
|
+
// Add debug visualization objects to the effects if they exist
|
|
1027
|
+
if (debugShowSolid && debugVisualizationObjects.length > 0) {
|
|
1028
|
+
console.log('[HoleFeature] Adding', debugVisualizationObjects.length, 'debug visualization objects to scene');
|
|
1029
|
+
if (!effects.additions) effects.additions = [];
|
|
1030
|
+
|
|
1031
|
+
// Convert each solid to a mesh and add to additions
|
|
1032
|
+
for (const vizObj of debugVisualizationObjects) {
|
|
1033
|
+
try {
|
|
1034
|
+
// Convert to mesh and add type/name metadata
|
|
1035
|
+
const mesh = vizObj.toMesh ? vizObj.toMesh() : vizObj;
|
|
1036
|
+
mesh.type = 'DEBUG_VIZ';
|
|
1037
|
+
mesh.name = vizObj.name || 'DEBUG_VIZ';
|
|
1038
|
+
mesh.userData = mesh.userData || {};
|
|
1039
|
+
mesh.userData.isDebugVisualization = true;
|
|
1040
|
+
effects.additions.push(mesh);
|
|
1041
|
+
console.log('[HoleFeature] Added debug viz:', mesh.name, 'to scene');
|
|
1042
|
+
} catch (err) {
|
|
1043
|
+
console.warn('[HoleFeature] Failed to convert debug viz to mesh:', err);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
return effects;
|
|
1049
|
+
}
|
|
1050
|
+
}
|