brep-io-kernel 1.0.0
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 +144 -0
- package/dist-kernel/brep-kernel.js +74699 -0
- package/dist-kernel/help/CONTRIBUTING.html +248 -0
- package/dist-kernel/help/LICENSE.html +248 -0
- package/dist-kernel/help/MODELING.png +0 -0
- package/dist-kernel/help/PMI.png +0 -0
- package/dist-kernel/help/SKETCH.png +0 -0
- package/dist-kernel/help/assembly-constraints__Coincident_Constraint_dialog.png +0 -0
- package/dist-kernel/help/assembly-constraints___Angle_Constraint_dialog.png +0 -0
- package/dist-kernel/help/assembly-constraints___Distance_Constraint_dialog.png +0 -0
- package/dist-kernel/help/assembly-constraints___Fixed_Constraint_dialog.png +0 -0
- package/dist-kernel/help/assembly-constraints___Parallel_Constraint_dialog.png +0 -0
- package/dist-kernel/help/assembly-constraints___Touch_Align_Constraint_dialog.png +0 -0
- package/dist-kernel/help/assembly-constraints__angle-constraint.html +248 -0
- package/dist-kernel/help/assembly-constraints__coincident-constraint.html +248 -0
- package/dist-kernel/help/assembly-constraints__distance-constraint.html +248 -0
- package/dist-kernel/help/assembly-constraints__fixed-constraint.html +248 -0
- package/dist-kernel/help/assembly-constraints__parallel-constraint.html +248 -0
- package/dist-kernel/help/assembly-constraints__solver.html +248 -0
- package/dist-kernel/help/assembly-constraints__touch-align-constraint.html +248 -0
- package/dist-kernel/help/brep-api.html +263 -0
- package/dist-kernel/help/brep-kernel.html +258 -0
- package/dist-kernel/help/brep-model.html +248 -0
- package/dist-kernel/help/cylindrical-face-radius-embedding.html +290 -0
- package/dist-kernel/help/dialog-screenshots.html +248 -0
- package/dist-kernel/help/extruded-sketch-radius-embedding.html +336 -0
- package/dist-kernel/help/features__Assembly_Component_dialog.png +0 -0
- package/dist-kernel/help/features__Boolean_dialog.png +0 -0
- package/dist-kernel/help/features__Chamfer_dialog.png +0 -0
- package/dist-kernel/help/features__Datium_dialog.png +0 -0
- package/dist-kernel/help/features__Extrude_dialog.png +0 -0
- package/dist-kernel/help/features__Fillet_dialog.png +0 -0
- package/dist-kernel/help/features__Helix_dialog.png +0 -0
- package/dist-kernel/help/features__Hole_dialog.png +0 -0
- package/dist-kernel/help/features__Image_Heightmap_Solid_dialog.png +0 -0
- package/dist-kernel/help/features__Image_to_Face_dialog.png +0 -0
- package/dist-kernel/help/features__Import_3D_Model_dialog.png +0 -0
- package/dist-kernel/help/features__Loft_dialog.png +0 -0
- package/dist-kernel/help/features__Mirror_dialog.png +0 -0
- package/dist-kernel/help/features__Offset_Shell_dialog.png +0 -0
- package/dist-kernel/help/features__Overlap_Cleanup_dialog.png +0 -0
- package/dist-kernel/help/features__Pattern_Linear_dialog.png +0 -0
- package/dist-kernel/help/features__Pattern_Radial_dialog.png +0 -0
- package/dist-kernel/help/features__Pattern_dialog.png +0 -0
- package/dist-kernel/help/features__Plane_dialog.png +0 -0
- package/dist-kernel/help/features__Primitive_Cone_dialog.png +0 -0
- package/dist-kernel/help/features__Primitive_Cube_dialog.png +0 -0
- package/dist-kernel/help/features__Primitive_Cylinder_dialog.png +0 -0
- package/dist-kernel/help/features__Primitive_Pyramid_dialog.png +0 -0
- package/dist-kernel/help/features__Primitive_Sphere_dialog.png +0 -0
- package/dist-kernel/help/features__Primitive_Torus_dialog.png +0 -0
- package/dist-kernel/help/features__Remesh_dialog.png +0 -0
- package/dist-kernel/help/features__Revolve_dialog.png +0 -0
- package/dist-kernel/help/features__Sheet_Metal_Contour_Flange_dialog.png +0 -0
- package/dist-kernel/help/features__Sheet_Metal_Cutout_dialog.png +0 -0
- package/dist-kernel/help/features__Sheet_Metal_Flange_dialog.png +0 -0
- package/dist-kernel/help/features__Sheet_Metal_Tab_dialog.png +0 -0
- package/dist-kernel/help/features__Sketch_dialog.png +0 -0
- package/dist-kernel/help/features__Spline_dialog.png +0 -0
- package/dist-kernel/help/features__Sweep_dialog.png +0 -0
- package/dist-kernel/help/features__Transform_dialog.png +0 -0
- package/dist-kernel/help/features__Tube_dialog.png +0 -0
- package/dist-kernel/help/features__assembly-component.html +248 -0
- package/dist-kernel/help/features__boolean.html +248 -0
- package/dist-kernel/help/features__chamfer.html +248 -0
- package/dist-kernel/help/features__datium.html +248 -0
- package/dist-kernel/help/features__datum.html +248 -0
- package/dist-kernel/help/features__extrude.html +248 -0
- package/dist-kernel/help/features__fillet.html +248 -0
- package/dist-kernel/help/features__helix.html +248 -0
- package/dist-kernel/help/features__hole.html +248 -0
- package/dist-kernel/help/features__image-heightmap-solid.html +248 -0
- package/dist-kernel/help/features__image-to-face-2D_dialog.png +0 -0
- package/dist-kernel/help/features__image-to-face-3D_dialog.png +0 -0
- package/dist-kernel/help/features__image-to-face.html +248 -0
- package/dist-kernel/help/features__import-3d-model.html +248 -0
- package/dist-kernel/help/features__index.html +248 -0
- package/dist-kernel/help/features__loft.html +248 -0
- package/dist-kernel/help/features__mirror.html +248 -0
- package/dist-kernel/help/features__offset-shell.html +248 -0
- package/dist-kernel/help/features__pattern-linear.html +248 -0
- package/dist-kernel/help/features__pattern-radial.html +248 -0
- package/dist-kernel/help/features__pattern.html +248 -0
- package/dist-kernel/help/features__plane.html +248 -0
- package/dist-kernel/help/features__primitive-cone.html +248 -0
- package/dist-kernel/help/features__primitive-cube.html +248 -0
- package/dist-kernel/help/features__primitive-cylinder.html +248 -0
- package/dist-kernel/help/features__primitive-pyramid.html +248 -0
- package/dist-kernel/help/features__primitive-sphere.html +248 -0
- package/dist-kernel/help/features__primitive-torus.html +248 -0
- package/dist-kernel/help/features__remesh.html +248 -0
- package/dist-kernel/help/features__revolve.html +248 -0
- package/dist-kernel/help/features__sheet-metal-contour-flange.html +248 -0
- package/dist-kernel/help/features__sheet-metal-flange.html +248 -0
- package/dist-kernel/help/features__sheet-metal-tab.html +248 -0
- package/dist-kernel/help/features__sketch.html +248 -0
- package/dist-kernel/help/features__spline.html +248 -0
- package/dist-kernel/help/features__sweep.html +248 -0
- package/dist-kernel/help/features__transform.html +248 -0
- package/dist-kernel/help/features__tube.html +248 -0
- package/dist-kernel/help/file-formats.html +248 -0
- package/dist-kernel/help/getting-started.html +248 -0
- package/dist-kernel/help/highlights.html +248 -0
- package/dist-kernel/help/history-systems.html +248 -0
- package/dist-kernel/help/how-it-works.html +248 -0
- package/dist-kernel/help/index.html +862 -0
- package/dist-kernel/help/input-params-schema.html +363 -0
- package/dist-kernel/help/inspector-improvements.html +248 -0
- package/dist-kernel/help/inspector.html +248 -0
- package/dist-kernel/help/modes__modeling.html +248 -0
- package/dist-kernel/help/modes__pmi.html +248 -0
- package/dist-kernel/help/modes__sketch.html +248 -0
- package/dist-kernel/help/plugins.html +248 -0
- package/dist-kernel/help/pmi-annotations__Angle_Dimension_dialog.png +0 -0
- package/dist-kernel/help/pmi-annotations__Explode_Body_dialog.png +0 -0
- package/dist-kernel/help/pmi-annotations__Hole_Callout_dialog.png +0 -0
- package/dist-kernel/help/pmi-annotations__Leader_dialog.png +0 -0
- package/dist-kernel/help/pmi-annotations__Linear_Dimension_dialog.png +0 -0
- package/dist-kernel/help/pmi-annotations__Note_dialog.png +0 -0
- package/dist-kernel/help/pmi-annotations__Radial_Dimension_dialog.png +0 -0
- package/dist-kernel/help/pmi-annotations__angle-dimension.html +248 -0
- package/dist-kernel/help/pmi-annotations__explode-body.html +248 -0
- package/dist-kernel/help/pmi-annotations__hole-callout.html +248 -0
- package/dist-kernel/help/pmi-annotations__index.html +248 -0
- package/dist-kernel/help/pmi-annotations__leader.html +248 -0
- package/dist-kernel/help/pmi-annotations__linear-dimension.html +248 -0
- package/dist-kernel/help/pmi-annotations__note.html +248 -0
- package/dist-kernel/help/pmi-annotations__radial-dimension.html +248 -0
- package/dist-kernel/help/search-index.json +464 -0
- package/dist-kernel/help/simplified-radial-dimensions.html +298 -0
- package/dist-kernel/help/solid-methods.html +359 -0
- package/dist-kernel/help/table-of-contents.html +330 -0
- package/dist-kernel/help/ui-overview.html +248 -0
- package/dist-kernel/help/whats-new.html +248 -0
- package/package.json +54 -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,1656 @@
|
|
|
1
|
+
import { BREP } from "../../BREP/BREP.js";
|
|
2
|
+
import { selectionHasSketch } from "../selectionUtils.js";
|
|
3
|
+
import {
|
|
4
|
+
normalizeThickness,
|
|
5
|
+
normalizeBendRadius,
|
|
6
|
+
normalizeNeutralFactor,
|
|
7
|
+
applySheetMetalMetadata,
|
|
8
|
+
} from "./sheetMetalMetadata.js";
|
|
9
|
+
import { setSheetMetalFaceTypeMetadata, SHEET_METAL_FACE_TYPES, propagateSheetMetalFaceTypesToEdges } from "./sheetMetalFaceTypes.js";
|
|
10
|
+
import { cleanupSheetMetalOppositeEdgeFaces } from "./sheetMetalCleanup.js";
|
|
11
|
+
import { SheetMetalObject } from "./SheetMetalObject.js";
|
|
12
|
+
import { cloneSheetMetalTree, createSheetMetalContourFlangeNode, createSheetMetalTree } from "./sheetMetalTree.js";
|
|
13
|
+
|
|
14
|
+
const THREE = BREP.THREE;
|
|
15
|
+
|
|
16
|
+
const inputParamsSchema = {
|
|
17
|
+
id: {
|
|
18
|
+
type: "string",
|
|
19
|
+
default_value: null,
|
|
20
|
+
hint: "Unique identifier for the contour flange feature",
|
|
21
|
+
},
|
|
22
|
+
path: {
|
|
23
|
+
type: "reference_selection",
|
|
24
|
+
selectionFilter: ["SKETCH", "EDGE"],
|
|
25
|
+
multiple: true,
|
|
26
|
+
default_value: null,
|
|
27
|
+
hint: "Open sketch (or connected edges) defining the flange centerline.",
|
|
28
|
+
},
|
|
29
|
+
distance: {
|
|
30
|
+
type: "number",
|
|
31
|
+
default_value: 20,
|
|
32
|
+
min: 0,
|
|
33
|
+
hint: "How far the sheet extends from the selected path (strip width).",
|
|
34
|
+
},
|
|
35
|
+
thickness: {
|
|
36
|
+
type: "number",
|
|
37
|
+
default_value: 2,
|
|
38
|
+
min: 0,
|
|
39
|
+
hint: "Sheet metal thickness (extruded normal to the sketch plane).",
|
|
40
|
+
},
|
|
41
|
+
reverseSheetSide: {
|
|
42
|
+
type: "boolean",
|
|
43
|
+
default_value: false,
|
|
44
|
+
hint: "Flip the sheet offset to the opposite side of the sketch.",
|
|
45
|
+
},
|
|
46
|
+
bendRadius: {
|
|
47
|
+
type: "number",
|
|
48
|
+
default_value: 2,
|
|
49
|
+
min: 0,
|
|
50
|
+
hint: "Default inside bend radius inserted wherever two lines meet.",
|
|
51
|
+
},
|
|
52
|
+
neutralFactor: {
|
|
53
|
+
type: "number",
|
|
54
|
+
default_value: 0.5,
|
|
55
|
+
min: 0,
|
|
56
|
+
max: 1,
|
|
57
|
+
step: 0.01,
|
|
58
|
+
hint: "Neutral factor used for flat pattern bend allowance (0-1).",
|
|
59
|
+
},
|
|
60
|
+
consumePathSketch: {
|
|
61
|
+
type: "boolean",
|
|
62
|
+
default_value: true,
|
|
63
|
+
hint: "Remove the referenced sketch after creating the flange. Turn off to keep it in the scene.",
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export class SheetMetalContourFlangeFeature {
|
|
68
|
+
static shortName = "SM.CF";
|
|
69
|
+
static longName = "Sheet Metal Contour Flange";
|
|
70
|
+
static inputParamsSchema = inputParamsSchema;
|
|
71
|
+
|
|
72
|
+
constructor() {
|
|
73
|
+
this.inputParams = {};
|
|
74
|
+
this.persistentData = {};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
uiFieldsTest(context) {
|
|
78
|
+
const params = this.inputParams || context?.params || {};
|
|
79
|
+
const partHistory = context?.history || null;
|
|
80
|
+
const pathRef = params.path ?? params.profile;
|
|
81
|
+
return selectionHasSketch(pathRef, partHistory) ? [] : ["consumePathSketch"];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async run(partHistory) {
|
|
85
|
+
const pathRef = this.inputParams?.path ?? this.inputParams?.profile;
|
|
86
|
+
const { edges, sketches } = resolvePathSelection(pathRef, partHistory);
|
|
87
|
+
if (!edges.length) {
|
|
88
|
+
throw new Error("Contour Flange requires selecting a SKETCH or one or more connected EDGEs.");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const { magnitude: thicknessAbs, signed: signedThickness } = normalizeThickness(
|
|
92
|
+
this.inputParams?.thickness ?? 1,
|
|
93
|
+
);
|
|
94
|
+
const bendRadius = normalizeBendRadius(this.inputParams?.bendRadius ?? 0);
|
|
95
|
+
const neutralFactor = normalizeNeutralFactor(this.inputParams?.neutralFactor ?? 0.5);
|
|
96
|
+
const rawDistance = Number(this.inputParams?.distance ?? 0);
|
|
97
|
+
if (!Number.isFinite(rawDistance) || rawDistance === 0) {
|
|
98
|
+
throw new Error("Contour Flange distance must be a non-zero number.");
|
|
99
|
+
}
|
|
100
|
+
const distance = Math.abs(rawDistance);
|
|
101
|
+
|
|
102
|
+
const { sheetSide, reverseSheetSide } = resolveSheetSideOption(this.inputParams);
|
|
103
|
+
|
|
104
|
+
const sheetMetal = new SheetMetalObject({
|
|
105
|
+
tree: createSheetMetalTree(),
|
|
106
|
+
kFactor: neutralFactor,
|
|
107
|
+
thickness: thicknessAbs,
|
|
108
|
+
bendRadius,
|
|
109
|
+
});
|
|
110
|
+
const contourNode = createSheetMetalContourFlangeNode({
|
|
111
|
+
featureID: this.inputParams?.featureID || null,
|
|
112
|
+
pathRefs: pathRef,
|
|
113
|
+
distance: rawDistance,
|
|
114
|
+
thickness: thicknessAbs,
|
|
115
|
+
signedThickness,
|
|
116
|
+
signedDistance: rawDistance,
|
|
117
|
+
reverseSheetSide,
|
|
118
|
+
sheetSide,
|
|
119
|
+
bendRadius,
|
|
120
|
+
neutralFactor,
|
|
121
|
+
consumePathSketch: this.inputParams?.consumePathSketch !== false,
|
|
122
|
+
});
|
|
123
|
+
sheetMetal.appendNode(contourNode);
|
|
124
|
+
await sheetMetal.generate({
|
|
125
|
+
partHistory,
|
|
126
|
+
metadataManager: partHistory?.metadataManager,
|
|
127
|
+
mode: "solid",
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const consumeSketch = this.inputParams?.consumePathSketch !== false;
|
|
131
|
+
const removed = consumeSketch ? sketches : [];
|
|
132
|
+
try {
|
|
133
|
+
for (const obj of removed) {
|
|
134
|
+
if (obj) obj.__removeFlag = true;
|
|
135
|
+
}
|
|
136
|
+
} catch { /* best effort */ }
|
|
137
|
+
|
|
138
|
+
const added = [sheetMetal];
|
|
139
|
+
cleanupSheetMetalOppositeEdgeFaces(added);
|
|
140
|
+
propagateSheetMetalFaceTypesToEdges(added);
|
|
141
|
+
applySheetMetalMetadata(added, partHistory?.metadataManager, {
|
|
142
|
+
featureID: this.inputParams?.featureID || null,
|
|
143
|
+
thickness: thicknessAbs,
|
|
144
|
+
bendRadius,
|
|
145
|
+
neutralFactor,
|
|
146
|
+
baseType: "CONTOUR_FLANGE",
|
|
147
|
+
extra: {
|
|
148
|
+
signedThickness,
|
|
149
|
+
sheetSide,
|
|
150
|
+
reverseSheetSide,
|
|
151
|
+
signedDistance: rawDistance,
|
|
152
|
+
distance,
|
|
153
|
+
pathRefCount: Array.isArray(contourNode?.params?.pathRefs)
|
|
154
|
+
? contourNode.params.pathRefs.length
|
|
155
|
+
: null,
|
|
156
|
+
},
|
|
157
|
+
forceBaseOverwrite: true,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
for (const solid of added) {
|
|
161
|
+
if (!solid) continue;
|
|
162
|
+
solid.userData = solid.userData || {};
|
|
163
|
+
solid.userData.sheetMetalTree = cloneSheetMetalTree(sheetMetal.tree);
|
|
164
|
+
solid.userData.sheetMetalKFactor = neutralFactor;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
this.persistentData = this.persistentData || {};
|
|
168
|
+
this.persistentData.sheetMetal = {
|
|
169
|
+
baseType: "CONTOUR_FLANGE",
|
|
170
|
+
thickness: thicknessAbs,
|
|
171
|
+
bendRadius,
|
|
172
|
+
neutralFactor,
|
|
173
|
+
signedThickness,
|
|
174
|
+
sheetSide,
|
|
175
|
+
reverseSheetSide,
|
|
176
|
+
signedDistance: rawDistance,
|
|
177
|
+
distance,
|
|
178
|
+
pathRefCount: Array.isArray(contourNode?.params?.pathRefs)
|
|
179
|
+
? contourNode.params.pathRefs.length
|
|
180
|
+
: null,
|
|
181
|
+
tree: sheetMetal.tree,
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
return { added, removed };
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function buildContourFlangeSolidFromParams(params, partHistory) {
|
|
189
|
+
const { edges, sketches, basisHint, pathOverride } = resolvePathSelection(
|
|
190
|
+
params?.path ?? params?.profile,
|
|
191
|
+
partHistory,
|
|
192
|
+
);
|
|
193
|
+
if (!edges.length) {
|
|
194
|
+
throw new Error("Contour Flange requires selecting a SKETCH or one or more connected EDGEs.");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const { magnitude: thicknessAbs, signed: signedThickness } = normalizeThickness(
|
|
198
|
+
params?.thickness ?? 1,
|
|
199
|
+
);
|
|
200
|
+
const bendRadius = normalizeBendRadius(params?.bendRadius ?? 0);
|
|
201
|
+
const neutralFactor = normalizeNeutralFactor(params?.neutralFactor ?? 0.5);
|
|
202
|
+
const rawDistance = Number(params?.distance ?? 0);
|
|
203
|
+
if (!Number.isFinite(rawDistance) || rawDistance === 0) {
|
|
204
|
+
throw new Error("Contour Flange distance must be a non-zero number.");
|
|
205
|
+
}
|
|
206
|
+
const distance = Math.abs(rawDistance);
|
|
207
|
+
const extrudeDirectionSign = rawDistance >= 0 ? 1 : -1;
|
|
208
|
+
|
|
209
|
+
const { sheetSide, reverseSheetSide } = resolveSheetSideOption(params);
|
|
210
|
+
|
|
211
|
+
const pathData = buildPathPoints(edges);
|
|
212
|
+
let rawPath = pathData.points;
|
|
213
|
+
let pathSegmentNames = pathData.segmentNames;
|
|
214
|
+
if ((!rawPath || rawPath.length < 2) && Array.isArray(pathOverride) && pathOverride.length >= 2) {
|
|
215
|
+
rawPath = pathOverride.map((pt) => (pt instanceof THREE.Vector3)
|
|
216
|
+
? pt.clone()
|
|
217
|
+
: new THREE.Vector3(pt.x ?? pt[0] ?? 0, pt.y ?? pt[1] ?? 0, pt.z ?? pt[2] ?? 0));
|
|
218
|
+
pathSegmentNames = buildDefaultSegmentNames(rawPath.length - 1);
|
|
219
|
+
}
|
|
220
|
+
if (!rawPath || rawPath.length < 2) {
|
|
221
|
+
throw new Error("Contour Flange path must contain at least two points.");
|
|
222
|
+
}
|
|
223
|
+
pathSegmentNames = normalizeSegmentNameCount(pathSegmentNames, rawPath.length - 1);
|
|
224
|
+
|
|
225
|
+
const planeBasis = computePlaneBasis(rawPath, basisHint);
|
|
226
|
+
const filletResult = bendRadius > 0
|
|
227
|
+
? filletPolyline(rawPath, bendRadius, planeBasis, pathSegmentNames, sheetSide, thicknessAbs)
|
|
228
|
+
: { points: rawPath.map((pt) => pt.clone()), points2D: null, tangents2D: null, segmentNames: pathSegmentNames.slice(), arcs: [] };
|
|
229
|
+
const filletedPath = filletResult.points;
|
|
230
|
+
const filletedPath2D = Array.isArray(filletResult.points2D) ? filletResult.points2D : null;
|
|
231
|
+
const filletedTangents2D = Array.isArray(filletResult.tangents2D) ? filletResult.tangents2D : null;
|
|
232
|
+
const filletedArcs = Array.isArray(filletResult.arcs) ? filletResult.arcs : [];
|
|
233
|
+
const filletedSegmentNames = normalizeSegmentNameCount(
|
|
234
|
+
filletResult.segmentNames,
|
|
235
|
+
filletedPath.length - 1,
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
if (filletedPath.length < 2) {
|
|
239
|
+
throw new Error("Contour Flange requires at least two path points after filleting.");
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const flangeFaces = buildContourFlangeStripFaces({
|
|
243
|
+
featureID: params?.featureID,
|
|
244
|
+
pathPoints: filletedPath,
|
|
245
|
+
pathSegmentNames: filletedSegmentNames,
|
|
246
|
+
planeBasis,
|
|
247
|
+
thickness: thicknessAbs,
|
|
248
|
+
sheetSide,
|
|
249
|
+
path2DOverride: filletedPath2D,
|
|
250
|
+
pathTangents2D: filletedTangents2D,
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
const converters = createPlaneBasisConverters(planeBasis);
|
|
254
|
+
const extrudeVector = planeBasis.planeNormal.clone().normalize().multiplyScalar(distance * extrudeDirectionSign);
|
|
255
|
+
const sweeps = flangeFaces.map((face) => {
|
|
256
|
+
const sweep = new BREP.Sweep({
|
|
257
|
+
face,
|
|
258
|
+
distance: extrudeVector,
|
|
259
|
+
mode: "translate",
|
|
260
|
+
name: params?.featureID,
|
|
261
|
+
omitBaseCap: false,
|
|
262
|
+
});
|
|
263
|
+
sweep.visualize();
|
|
264
|
+
tagContourFlangeFaceTypes(sweep);
|
|
265
|
+
// Add cylinder metadata and centerlines for bend arcs (if any).
|
|
266
|
+
const axisDir = extrudeVector.clone().normalize();
|
|
267
|
+
addCylMetadataToSideFaces(
|
|
268
|
+
sweep,
|
|
269
|
+
filletedArcs,
|
|
270
|
+
converters,
|
|
271
|
+
extrudeVector,
|
|
272
|
+
axisDir,
|
|
273
|
+
thicknessAbs,
|
|
274
|
+
bendRadius,
|
|
275
|
+
sheetSide,
|
|
276
|
+
);
|
|
277
|
+
return sweep;
|
|
278
|
+
});
|
|
279
|
+
if (!sweeps.length) {
|
|
280
|
+
throw new Error("Contour flange failed to generate any extrusions from the selected path.");
|
|
281
|
+
}
|
|
282
|
+
let combinedSweep = sweeps[0];
|
|
283
|
+
for (let i = 1; i < sweeps.length; i++) {
|
|
284
|
+
try {
|
|
285
|
+
combinedSweep = combinedSweep.union(sweeps[i]);
|
|
286
|
+
} catch {
|
|
287
|
+
combinedSweep = sweeps[i];
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (params?.featureID && combinedSweep) {
|
|
292
|
+
try { combinedSweep.name = params.featureID; } catch { /* best effort */ }
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return {
|
|
296
|
+
solid: combinedSweep,
|
|
297
|
+
sketches,
|
|
298
|
+
meta: {
|
|
299
|
+
thicknessAbs,
|
|
300
|
+
signedThickness,
|
|
301
|
+
bendRadius,
|
|
302
|
+
neutralFactor,
|
|
303
|
+
sheetSide,
|
|
304
|
+
reverseSheetSide,
|
|
305
|
+
rawDistance,
|
|
306
|
+
distance,
|
|
307
|
+
pathPointCount: filletedPath.length,
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function resolvePathSelection(pathRefs, partHistory) {
|
|
313
|
+
const refs = Array.isArray(pathRefs) ? pathRefs : (pathRefs ? [pathRefs] : []);
|
|
314
|
+
const edges = [];
|
|
315
|
+
const sketches = new Set();
|
|
316
|
+
const basisCandidates = [];
|
|
317
|
+
let overridePath = null;
|
|
318
|
+
|
|
319
|
+
for (const ref of refs) {
|
|
320
|
+
let obj = ref;
|
|
321
|
+
if (typeof obj === "string" && partHistory?.scene?.getObjectByName) {
|
|
322
|
+
obj = partHistory.scene.getObjectByName(obj);
|
|
323
|
+
}
|
|
324
|
+
if (!obj) continue;
|
|
325
|
+
if (obj.type === "EDGE") {
|
|
326
|
+
edges.push(obj);
|
|
327
|
+
const edgeBasis = extractBasisFromEdge(obj, partHistory);
|
|
328
|
+
if (edgeBasis) basisCandidates.push(edgeBasis);
|
|
329
|
+
} else if (obj.type === "SKETCH") {
|
|
330
|
+
sketches.add(obj);
|
|
331
|
+
const sketchBasis = extractBasisFromSketch(obj, partHistory);
|
|
332
|
+
if (sketchBasis) basisCandidates.push(sketchBasis);
|
|
333
|
+
if (!overridePath) {
|
|
334
|
+
const diagPath = buildPathFromSketchDiagnostics(obj, partHistory, sketchBasis);
|
|
335
|
+
if (diagPath && diagPath.points.length >= 2) {
|
|
336
|
+
overridePath = diagPath.points;
|
|
337
|
+
if (diagPath.basis) basisCandidates.push(diagPath.basis);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
const stack = Array.isArray(obj.children) ? obj.children.slice() : [];
|
|
341
|
+
for (const child of stack) {
|
|
342
|
+
if (child?.type === "EDGE") {
|
|
343
|
+
edges.push(child);
|
|
344
|
+
const childBasis = extractBasisFromEdge(child, partHistory);
|
|
345
|
+
if (childBasis) basisCandidates.push(childBasis);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return {
|
|
352
|
+
edges,
|
|
353
|
+
sketches: Array.from(sketches),
|
|
354
|
+
basisHint: basisCandidates.find(Boolean) || null,
|
|
355
|
+
pathOverride: overridePath,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function extractBasisFromEdge(edge, partHistory) {
|
|
360
|
+
if (!edge) return null;
|
|
361
|
+
const direct = convertStoredBasis(edge.userData?.sheetMetalBasis || edge.userData?.basis);
|
|
362
|
+
if (direct) return direct;
|
|
363
|
+
const sketchId = edge.userData?.sketchFeatureId || edge.userData?.sketchId;
|
|
364
|
+
const basis = findSketchBasis(sketchId, partHistory);
|
|
365
|
+
return convertStoredBasis(basis);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function extractBasisFromSketch(sketchObj, partHistory) {
|
|
369
|
+
if (!sketchObj) return null;
|
|
370
|
+
const direct = convertStoredBasis(sketchObj.userData?.sheetMetalBasis || sketchObj.userData?.basis);
|
|
371
|
+
if (direct) return direct;
|
|
372
|
+
const sketchId = sketchObj.name || sketchObj.userData?.sketchFeatureId;
|
|
373
|
+
const basis = findSketchBasis(sketchId, partHistory);
|
|
374
|
+
return convertStoredBasis(basis);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function findSketchBasis(featureId, partHistory) {
|
|
378
|
+
const normalized = normalizeId(featureId);
|
|
379
|
+
if (!normalized || !partHistory) return null;
|
|
380
|
+
const entry = findSketchFeatureEntry(partHistory, normalized);
|
|
381
|
+
return entry?.persistentData?.basis || null;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function normalizeId(value) {
|
|
385
|
+
if (value == null) return null;
|
|
386
|
+
try {
|
|
387
|
+
const str = String(value).trim();
|
|
388
|
+
return str.length ? str : null;
|
|
389
|
+
} catch {
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function findSketchFeatureEntry(partHistory, normalizedId) {
|
|
395
|
+
const list = Array.isArray(partHistory?.features) ? partHistory.features : [];
|
|
396
|
+
for (const entry of list) {
|
|
397
|
+
const entryId = normalizeId(entry?.inputParams?.id ?? entry?.id ?? entry?.inputParams?.featureID);
|
|
398
|
+
if (entryId && entryId === normalizedId) return entry;
|
|
399
|
+
}
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function buildPathFromSketchDiagnostics(sketchObj, partHistory, basisOverride = null) {
|
|
404
|
+
if (!sketchObj || !partHistory) return null;
|
|
405
|
+
const sketchId = normalizeId(sketchObj.name || sketchObj.userData?.sketchFeatureId);
|
|
406
|
+
if (!sketchId) return null;
|
|
407
|
+
const featureEntry = findSketchFeatureEntry(partHistory, sketchId);
|
|
408
|
+
if (!featureEntry) return null;
|
|
409
|
+
const diag = featureEntry?.persistentData?.lastProfileDiagnostics;
|
|
410
|
+
const openChains = Array.isArray(diag?.openChains2D) ? diag.openChains2D : [];
|
|
411
|
+
if (!openChains.length) return null;
|
|
412
|
+
let selected = null;
|
|
413
|
+
for (const chain of openChains) {
|
|
414
|
+
if (!Array.isArray(chain) || chain.length < 2) continue;
|
|
415
|
+
if (!selected || chain.length > selected.length) selected = chain;
|
|
416
|
+
}
|
|
417
|
+
if (!selected) return null;
|
|
418
|
+
const basis = convertStoredBasis(basisOverride || featureEntry?.persistentData?.basis);
|
|
419
|
+
if (!basis) return null;
|
|
420
|
+
const worldPts = selected.map((point) => projectSketchUVToWorld(point, basis));
|
|
421
|
+
return { points: worldPts, basis };
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function convertStoredBasis(raw) {
|
|
425
|
+
if (!raw || typeof raw !== "object") return null;
|
|
426
|
+
const origin = vectorFromArray(raw.origin) || new THREE.Vector3(0, 0, 0);
|
|
427
|
+
const xAxisRaw = vectorFromArray(raw.x || raw.xAxis) || new THREE.Vector3(1, 0, 0);
|
|
428
|
+
const zAxisRaw = vectorFromArray(raw.z || raw.zAxis || raw.planeNormal) || new THREE.Vector3(0, 0, 1);
|
|
429
|
+
if (xAxisRaw.lengthSq() < 1e-10 || zAxisRaw.lengthSq() < 1e-10) return null;
|
|
430
|
+
const zAxis = zAxisRaw.clone().normalize();
|
|
431
|
+
const xAxis = xAxisRaw.clone().sub(zAxis.clone().multiplyScalar(xAxisRaw.dot(zAxis))).normalize();
|
|
432
|
+
let yAxis = vectorFromArray(raw.y || raw.yAxis);
|
|
433
|
+
if (yAxis && yAxis.lengthSq() > 1e-10) {
|
|
434
|
+
yAxis = yAxis.clone().normalize();
|
|
435
|
+
} else {
|
|
436
|
+
yAxis = new THREE.Vector3().crossVectors(zAxis, xAxis).normalize();
|
|
437
|
+
}
|
|
438
|
+
return {
|
|
439
|
+
origin: origin.clone(),
|
|
440
|
+
xAxis,
|
|
441
|
+
yAxis,
|
|
442
|
+
planeNormal: zAxis,
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function vectorFromArray(raw) {
|
|
447
|
+
if (raw instanceof THREE.Vector3) return raw.clone();
|
|
448
|
+
if (Array.isArray(raw) && raw.length >= 3) {
|
|
449
|
+
const x = Number(raw[0]) || 0;
|
|
450
|
+
const y = Number(raw[1]) || 0;
|
|
451
|
+
const z = Number(raw[2]) || 0;
|
|
452
|
+
return new THREE.Vector3(x, y, z);
|
|
453
|
+
}
|
|
454
|
+
return null;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function projectSketchUVToWorld(point, basis) {
|
|
458
|
+
if (!basis) return new THREE.Vector3();
|
|
459
|
+
const u = Array.isArray(point) ? Number(point[0]) || 0 : Number(point?.u || 0);
|
|
460
|
+
const v = Array.isArray(point) ? Number(point[1]) || 0 : Number(point?.v || 0);
|
|
461
|
+
return basis.origin.clone()
|
|
462
|
+
.add(basis.xAxis.clone().multiplyScalar(u))
|
|
463
|
+
.add(basis.yAxis.clone().multiplyScalar(v));
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function buildPathPoints(edges) {
|
|
467
|
+
if (!Array.isArray(edges) || !edges.length) {
|
|
468
|
+
return { points: [], segmentNames: [] };
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const tmp = new THREE.Vector3();
|
|
472
|
+
const toWorld = (edge, pt) => {
|
|
473
|
+
tmp.set(pt[0], pt[1], pt[2]);
|
|
474
|
+
if (edge && typeof edge.updateWorldMatrix === "function") {
|
|
475
|
+
edge.updateWorldMatrix(true, true);
|
|
476
|
+
}
|
|
477
|
+
return tmp.clone().applyMatrix4(edge.matrixWorld || new THREE.Matrix4());
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
const segments = [];
|
|
481
|
+
let edgeCounter = 0;
|
|
482
|
+
for (const edge of edges) {
|
|
483
|
+
const pts = Array.isArray(edge?.userData?.polylineLocal)
|
|
484
|
+
? edge.userData.polylineLocal
|
|
485
|
+
: null;
|
|
486
|
+
let worldPts = null;
|
|
487
|
+
if (pts && pts.length >= 2) {
|
|
488
|
+
worldPts = pts.map((p) => toWorld(edge, p));
|
|
489
|
+
} else {
|
|
490
|
+
const pos = edge?.geometry?.getAttribute?.("position");
|
|
491
|
+
if (pos && pos.itemSize === 3 && pos.count >= 2) {
|
|
492
|
+
worldPts = [];
|
|
493
|
+
for (let i = 0; i < pos.count; i++) {
|
|
494
|
+
tmp.set(pos.getX(i), pos.getY(i), pos.getZ(i));
|
|
495
|
+
tmp.applyMatrix4(edge.matrixWorld || new THREE.Matrix4());
|
|
496
|
+
worldPts.push(tmp.clone());
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
if (!worldPts || worldPts.length < 2) continue;
|
|
501
|
+
const flat = worldPts.map((v) => [v.x, v.y, v.z]);
|
|
502
|
+
segments.push({
|
|
503
|
+
pts: flat,
|
|
504
|
+
startKey: `${flat[0][0].toFixed(6)},${flat[0][1].toFixed(6)},${flat[0][2].toFixed(6)}`,
|
|
505
|
+
endKey: `${flat[flat.length - 1][0].toFixed(6)},${flat[flat.length - 1][1].toFixed(6)},${flat[flat.length - 1][2].toFixed(6)}`,
|
|
506
|
+
name: deriveEdgeBaseName(edge, edgeCounter++),
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (!segments.length) {
|
|
511
|
+
return { points: [], segmentNames: [] };
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const used = new Array(segments.length).fill(false);
|
|
515
|
+
let bestPoints = [];
|
|
516
|
+
let bestSegmentMeta = [];
|
|
517
|
+
const key = (p) => `${p[0].toFixed(6)},${p[1].toFixed(6)},${p[2].toFixed(6)}`;
|
|
518
|
+
|
|
519
|
+
for (let i = 0; i < segments.length; i++) {
|
|
520
|
+
if (used[i]) continue;
|
|
521
|
+
const base = segments[i];
|
|
522
|
+
const chain = base.pts.slice();
|
|
523
|
+
const chainSegments = [{ name: base.name, pts: base.pts.slice() }];
|
|
524
|
+
used[i] = true;
|
|
525
|
+
let grew = true;
|
|
526
|
+
while (grew) {
|
|
527
|
+
grew = false;
|
|
528
|
+
for (let j = 0; j < segments.length; j++) {
|
|
529
|
+
if (used[j]) continue;
|
|
530
|
+
const seg = segments[j];
|
|
531
|
+
const head = chain[0];
|
|
532
|
+
const tail = chain[chain.length - 1];
|
|
533
|
+
const headKey = key(head);
|
|
534
|
+
const tailKey = key(tail);
|
|
535
|
+
if (seg.startKey === tailKey) {
|
|
536
|
+
chain.push(...seg.pts.slice(1));
|
|
537
|
+
chainSegments.push({ name: seg.name, pts: seg.pts.slice() });
|
|
538
|
+
used[j] = true;
|
|
539
|
+
grew = true;
|
|
540
|
+
} else if (seg.endKey === tailKey) {
|
|
541
|
+
const ptsRev = seg.pts.slice().reverse();
|
|
542
|
+
chain.push(...ptsRev.slice(1));
|
|
543
|
+
chainSegments.push({ name: seg.name, pts: ptsRev });
|
|
544
|
+
used[j] = true;
|
|
545
|
+
grew = true;
|
|
546
|
+
} else if (seg.endKey === headKey) {
|
|
547
|
+
const pts = seg.pts.slice();
|
|
548
|
+
chain.unshift(...pts.slice(0, pts.length - 1));
|
|
549
|
+
chainSegments.unshift({ name: seg.name, pts: pts });
|
|
550
|
+
used[j] = true;
|
|
551
|
+
grew = true;
|
|
552
|
+
} else if (seg.startKey === headKey) {
|
|
553
|
+
const ptsRev = seg.pts.slice().reverse();
|
|
554
|
+
chain.unshift(...ptsRev.slice(0, ptsRev.length - 1));
|
|
555
|
+
chainSegments.unshift({ name: seg.name, pts: ptsRev });
|
|
556
|
+
used[j] = true;
|
|
557
|
+
grew = true;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
if (chain.length > bestPoints.length) {
|
|
562
|
+
bestPoints = chain.slice();
|
|
563
|
+
bestSegmentMeta = chainSegments.map((entry) => ({ name: entry.name, pts: entry.pts.map((p) => p.slice()) }));
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if (!bestPoints.length) {
|
|
568
|
+
return { points: [], segmentNames: [] };
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const finalPoints = [];
|
|
572
|
+
const segmentNames = [];
|
|
573
|
+
for (const seg of bestSegmentMeta) {
|
|
574
|
+
const pts = seg.pts;
|
|
575
|
+
for (let i = 0; i < pts.length; i++) {
|
|
576
|
+
const p = pts[i];
|
|
577
|
+
if (!finalPoints.length) {
|
|
578
|
+
finalPoints.push(p);
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
const prev = finalPoints[finalPoints.length - 1];
|
|
582
|
+
if (prev[0] === p[0] && prev[1] === p[1] && prev[2] === p[2]) {
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
finalPoints.push(p);
|
|
586
|
+
segmentNames[finalPoints.length - 2] = seg.name || `SEGMENT_${segmentNames.length}`;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
return {
|
|
591
|
+
points: finalPoints.map((p) => new THREE.Vector3(p[0], p[1], p[2])),
|
|
592
|
+
segmentNames,
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function computePlaneBasis(points, hintBasis = null) {
|
|
597
|
+
if (hintBasis) {
|
|
598
|
+
const origin = hintBasis.origin instanceof THREE.Vector3
|
|
599
|
+
? hintBasis.origin.clone()
|
|
600
|
+
: Array.isArray(hintBasis.origin)
|
|
601
|
+
? new THREE.Vector3().fromArray(hintBasis.origin)
|
|
602
|
+
: new THREE.Vector3(0, 0, 0);
|
|
603
|
+
const zRaw = hintBasis.planeNormal || hintBasis.zAxis || hintBasis.z;
|
|
604
|
+
const xRaw = hintBasis.xAxis || hintBasis.x;
|
|
605
|
+
const yRaw = hintBasis.yAxis || hintBasis.y;
|
|
606
|
+
const z = zRaw instanceof THREE.Vector3 ? zRaw.clone()
|
|
607
|
+
: Array.isArray(zRaw) ? new THREE.Vector3().fromArray(zRaw)
|
|
608
|
+
: null;
|
|
609
|
+
const x = xRaw instanceof THREE.Vector3 ? xRaw.clone()
|
|
610
|
+
: Array.isArray(xRaw) ? new THREE.Vector3().fromArray(xRaw)
|
|
611
|
+
: null;
|
|
612
|
+
const y = yRaw instanceof THREE.Vector3 ? yRaw.clone()
|
|
613
|
+
: Array.isArray(yRaw) ? new THREE.Vector3().fromArray(yRaw)
|
|
614
|
+
: null;
|
|
615
|
+
if (z && z.lengthSq() > 1e-8 && x && x.lengthSq() > 1e-8) {
|
|
616
|
+
const zn = z.clone().normalize();
|
|
617
|
+
const xn = x.clone().normalize();
|
|
618
|
+
const yn = (y && y.lengthSq() > 1e-8)
|
|
619
|
+
? y.clone().normalize()
|
|
620
|
+
: new THREE.Vector3().crossVectors(zn, xn).normalize();
|
|
621
|
+
return { origin, planeNormal: zn, xAxis: xn, yAxis: yn };
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
if (!Array.isArray(points) || points.length < 2) {
|
|
626
|
+
const origin = new THREE.Vector3(0, 0, 0);
|
|
627
|
+
return {
|
|
628
|
+
origin,
|
|
629
|
+
planeNormal: new THREE.Vector3(0, 0, 1),
|
|
630
|
+
xAxis: new THREE.Vector3(1, 0, 0),
|
|
631
|
+
yAxis: new THREE.Vector3(0, 1, 0),
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
const origin = points[0].clone();
|
|
635
|
+
const xAxis = points[1].clone().sub(points[0]);
|
|
636
|
+
if (xAxis.lengthSq() < 1e-8 && points.length > 2) {
|
|
637
|
+
xAxis.copy(points[2]).sub(points[1]);
|
|
638
|
+
}
|
|
639
|
+
if (xAxis.lengthSq() < 1e-8) xAxis.set(1, 0, 0);
|
|
640
|
+
xAxis.normalize();
|
|
641
|
+
|
|
642
|
+
let planeNormal = new THREE.Vector3(0, 0, 1);
|
|
643
|
+
for (let i = 0; i < points.length - 2; i++) {
|
|
644
|
+
const v0 = points[i + 1].clone().sub(points[i]);
|
|
645
|
+
const v1 = points[i + 2].clone().sub(points[i + 1]);
|
|
646
|
+
const n = v0.clone().cross(v1);
|
|
647
|
+
if (n.lengthSq() > 1e-8) {
|
|
648
|
+
planeNormal = n.normalize();
|
|
649
|
+
break;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
if (planeNormal.lengthSq() < 1e-8) planeNormal.set(0, 0, 1);
|
|
653
|
+
|
|
654
|
+
// Create proper right-handed coordinate system: Y = Z × X
|
|
655
|
+
const yAxis = new THREE.Vector3().crossVectors(planeNormal, xAxis).normalize();
|
|
656
|
+
if (yAxis.lengthSq() < 1e-8) {
|
|
657
|
+
// If xAxis is parallel to planeNormal, choose a different approach
|
|
658
|
+
const tempX = Math.abs(planeNormal.dot(new THREE.Vector3(1, 0, 0))) < 0.9
|
|
659
|
+
? new THREE.Vector3(1, 0, 0)
|
|
660
|
+
: new THREE.Vector3(0, 1, 0);
|
|
661
|
+
const xAxis2 = new THREE.Vector3().crossVectors(planeNormal, tempX).normalize();
|
|
662
|
+
const yAxis2 = new THREE.Vector3().crossVectors(planeNormal, xAxis2).normalize();
|
|
663
|
+
return { origin, planeNormal, xAxis: xAxis2, yAxis: yAxis2 };
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// Ensure xAxis is orthogonal to planeNormal (project and normalize)
|
|
667
|
+
const xAxisCorrected = xAxis.clone().sub(planeNormal.clone().multiplyScalar(xAxis.dot(planeNormal))).normalize();
|
|
668
|
+
|
|
669
|
+
return { origin, planeNormal, xAxis: xAxisCorrected, yAxis };
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function filletPolyline(points, radius, basis, segmentNames = [], sheetSide = "left", thickness = 0) {
|
|
673
|
+
if (!Array.isArray(points) || points.length < 2) {
|
|
674
|
+
return {
|
|
675
|
+
points: Array.isArray(points) ? points.map((pt) => pt.clone()) : [],
|
|
676
|
+
segmentNames: [],
|
|
677
|
+
points2D: null,
|
|
678
|
+
tangents2D: null,
|
|
679
|
+
arcs: [],
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
const { origin, xAxis, yAxis, planeNormal } = basis;
|
|
684
|
+
const to2D = (vec) => {
|
|
685
|
+
const rel = vec.clone().sub(origin);
|
|
686
|
+
return {
|
|
687
|
+
u: rel.dot(xAxis),
|
|
688
|
+
v: rel.dot(yAxis),
|
|
689
|
+
w: rel.dot(planeNormal),
|
|
690
|
+
};
|
|
691
|
+
};
|
|
692
|
+
const to3D = (coord) => {
|
|
693
|
+
return origin.clone()
|
|
694
|
+
.add(xAxis.clone().multiplyScalar(coord.u))
|
|
695
|
+
.add(yAxis.clone().multiplyScalar(coord.v))
|
|
696
|
+
.add(planeNormal.clone().multiplyScalar(coord.w || 0));
|
|
697
|
+
};
|
|
698
|
+
|
|
699
|
+
if (!radius || radius <= 1e-8 || points.length < 3) {
|
|
700
|
+
const coordsSimple = points.map((pt) => to2D(pt));
|
|
701
|
+
return {
|
|
702
|
+
points: points.map((pt) => pt.clone()),
|
|
703
|
+
segmentNames: normalizeSegmentNameCount(segmentNames, points.length - 1),
|
|
704
|
+
points2D: coordsSimple,
|
|
705
|
+
tangents2D: null,
|
|
706
|
+
arcs: [],
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
const coords = points.map((pt) => to2D(pt));
|
|
711
|
+
const baseNames = normalizeSegmentNameCount(segmentNames, coords.length - 1);
|
|
712
|
+
const segCount = coords.length - 1;
|
|
713
|
+
const segmentStart = new Array(segCount);
|
|
714
|
+
const segmentEnd = new Array(segCount);
|
|
715
|
+
const arcCenters = new Array(coords.length).fill(null);
|
|
716
|
+
const arcSweepDirs = new Array(coords.length).fill(0);
|
|
717
|
+
const arcInfo = [];
|
|
718
|
+
for (let i = 0; i < segCount; i++) {
|
|
719
|
+
segmentStart[i] = { ...coords[i] };
|
|
720
|
+
segmentEnd[i] = { ...coords[i + 1] };
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
const arcSamples = new Array(coords.length).fill(null);
|
|
724
|
+
const arcNames = new Array(coords.length).fill(null);
|
|
725
|
+
|
|
726
|
+
const len = (a, b) => Math.hypot(b.u - a.u, b.v - a.v);
|
|
727
|
+
const norm = (a, b) => {
|
|
728
|
+
const d = len(a, b) || 1;
|
|
729
|
+
return { x: (b.u - a.u) / d, y: (b.v - a.v) / d };
|
|
730
|
+
};
|
|
731
|
+
|
|
732
|
+
for (let i = 1; i < coords.length - 1; i++) {
|
|
733
|
+
const prev = coords[i - 1];
|
|
734
|
+
const curr = coords[i];
|
|
735
|
+
const next = coords[i + 1];
|
|
736
|
+
const dirPrev = norm(prev, curr);
|
|
737
|
+
const dirNext = norm(curr, next);
|
|
738
|
+
const turn = dirPrev.x * dirNext.y - dirPrev.y * dirNext.x;
|
|
739
|
+
const dot = (-dirPrev.x) * dirNext.x + (-dirPrev.y) * dirNext.y;
|
|
740
|
+
const angle = Math.acos(Math.max(-1, Math.min(1, dot)));
|
|
741
|
+
if (!Number.isFinite(angle) || angle < (5 * Math.PI / 180)) {
|
|
742
|
+
continue;
|
|
743
|
+
}
|
|
744
|
+
let offset = radius / Math.tan(angle / 2);
|
|
745
|
+
const lenPrev = len(prev, curr);
|
|
746
|
+
const lenNext = len(curr, next);
|
|
747
|
+
offset = Math.min(offset, lenPrev * 0.9, lenNext * 0.9);
|
|
748
|
+
if (!Number.isFinite(offset) || offset <= 0) {
|
|
749
|
+
continue;
|
|
750
|
+
}
|
|
751
|
+
const insideSign = Math.sign(turn) || 1;
|
|
752
|
+
// Determine which side is "inside" the bend (where the sheet material is)
|
|
753
|
+
// For sheet metal: the bend creates two arcs
|
|
754
|
+
// - Inner arc (smaller radius) = bendRadius
|
|
755
|
+
// - Outer arc (larger radius) = bendRadius + thickness
|
|
756
|
+
|
|
757
|
+
// The "inside" of the bend is determined by the turn direction
|
|
758
|
+
const normalPrev = insideSign > 0
|
|
759
|
+
? { x: -dirPrev.y, y: dirPrev.x }
|
|
760
|
+
: { x: dirPrev.y, y: -dirPrev.x };
|
|
761
|
+
const normalNext = insideSign > 0
|
|
762
|
+
? { x: -dirNext.y, y: dirNext.x }
|
|
763
|
+
: { x: dirNext.y, y: -dirNext.x };
|
|
764
|
+
|
|
765
|
+
// Calculate the bisector direction (average of the two normals)
|
|
766
|
+
const bisectorX = normalPrev.x + normalNext.x;
|
|
767
|
+
const bisectorY = normalPrev.y + normalNext.y;
|
|
768
|
+
const bisectorLen = Math.hypot(bisectorX, bisectorY);
|
|
769
|
+
|
|
770
|
+
if (bisectorLen < 1e-10) continue; // Skip if normals cancel out (180° turn)
|
|
771
|
+
|
|
772
|
+
const bisectorNormX = bisectorX / bisectorLen;
|
|
773
|
+
const bisectorNormY = bisectorY / bisectorLen;
|
|
774
|
+
|
|
775
|
+
// For sheet metal bends, we need to consider which side the sheet is on
|
|
776
|
+
// sheetSide "left" means sheet material is on the left looking along the path
|
|
777
|
+
// sheetSide "right" means sheet material is on the right
|
|
778
|
+
|
|
779
|
+
// Determine if the bend is on the same side as the sheet
|
|
780
|
+
const bendOnSheetSide = (sheetSide === "left" && insideSign > 0) || (sheetSide === "right" && insideSign < 0);
|
|
781
|
+
|
|
782
|
+
// If bend is on sheet side: path follows outer radius (bendRadius + thickness)
|
|
783
|
+
// If bend is away from sheet: path follows inner radius (bendRadius)
|
|
784
|
+
const pathRadius = bendOnSheetSide ? radius + thickness : radius;
|
|
785
|
+
|
|
786
|
+
// The distance from vertex to center along bisector: radius / sin(angle/2)
|
|
787
|
+
const halfAngleSin = Math.sin(angle / 2);
|
|
788
|
+
const centerDist = halfAngleSin > 1e-10 ? pathRadius / halfAngleSin : pathRadius;
|
|
789
|
+
|
|
790
|
+
const center = {
|
|
791
|
+
u: curr.u + bisectorNormX * centerDist,
|
|
792
|
+
v: curr.v + bisectorNormY * centerDist,
|
|
793
|
+
w: curr.w,
|
|
794
|
+
};
|
|
795
|
+
|
|
796
|
+
// Calculate new tangent points: project center onto each line to find where
|
|
797
|
+
// a circle of pathRadius from center touches the incoming/outgoing segments
|
|
798
|
+
const centerRelToCurr = { u: center.u - curr.u, v: center.v - curr.v };
|
|
799
|
+
|
|
800
|
+
// For incoming segment: project center displacement onto line direction
|
|
801
|
+
const projPrev = centerRelToCurr.u * dirPrev.x + centerRelToCurr.v * dirPrev.y;
|
|
802
|
+
const perpDistPrevSq = centerRelToCurr.u * centerRelToCurr.u + centerRelToCurr.v * centerRelToCurr.v - projPrev * projPrev;
|
|
803
|
+
const tangentDistPrev = -projPrev + Math.sqrt(Math.max(0, pathRadius * pathRadius - perpDistPrevSq));
|
|
804
|
+
|
|
805
|
+
// For outgoing segment
|
|
806
|
+
const projNext = centerRelToCurr.u * dirNext.x + centerRelToCurr.v * dirNext.y;
|
|
807
|
+
const perpDistNextSq = centerRelToCurr.u * centerRelToCurr.u + centerRelToCurr.v * centerRelToCurr.v - projNext * projNext;
|
|
808
|
+
const tangentDistNext = projNext + Math.sqrt(Math.max(0, pathRadius * pathRadius - perpDistNextSq));
|
|
809
|
+
|
|
810
|
+
// Clamp to segment lengths
|
|
811
|
+
const clampedOffsetPrev = Math.min(tangentDistPrev, lenPrev * 0.9);
|
|
812
|
+
const clampedOffsetNext = Math.min(tangentDistNext, lenNext * 0.9);
|
|
813
|
+
|
|
814
|
+
const arcStart = {
|
|
815
|
+
u: curr.u - dirPrev.x * clampedOffsetPrev,
|
|
816
|
+
v: curr.v - dirPrev.y * clampedOffsetPrev,
|
|
817
|
+
w: curr.w,
|
|
818
|
+
};
|
|
819
|
+
|
|
820
|
+
const arcEnd = {
|
|
821
|
+
u: curr.u + dirNext.x * clampedOffsetNext,
|
|
822
|
+
v: curr.v + dirNext.y * clampedOffsetNext,
|
|
823
|
+
w: curr.w,
|
|
824
|
+
};
|
|
825
|
+
|
|
826
|
+
// Calculate the actual radius from center to the tangent points
|
|
827
|
+
const actualRadiusStart = Math.hypot(arcStart.u - center.u, arcStart.v - center.v);
|
|
828
|
+
const actualRadiusEnd = Math.hypot(arcEnd.u - center.u, arcEnd.v - center.v);
|
|
829
|
+
const actualArcRadius = (actualRadiusStart + actualRadiusEnd) * 0.5;
|
|
830
|
+
|
|
831
|
+
const startAng = Math.atan2(arcStart.v - center.v, arcStart.u - center.u);
|
|
832
|
+
const endAng = Math.atan2(arcEnd.v - center.v, arcEnd.u - center.u);
|
|
833
|
+
const sweepDir = insideSign > 0 ? 1 : -1;
|
|
834
|
+
let delta = endAng - startAng;
|
|
835
|
+
if (sweepDir > 0 && delta <= 0) delta += Math.PI * 2;
|
|
836
|
+
if (sweepDir < 0 && delta >= 0) delta -= Math.PI * 2;
|
|
837
|
+
const steps = Math.max(1, Math.ceil(Math.abs(delta) / (Math.PI / 18)));
|
|
838
|
+
|
|
839
|
+
segmentEnd[i - 1] = arcStart;
|
|
840
|
+
segmentStart[i] = arcEnd;
|
|
841
|
+
const arcPts = [arcStart];
|
|
842
|
+
for (let step = 1; step < steps; step++) {
|
|
843
|
+
const ang = startAng + sweepDir * (Math.abs(delta) * (step / steps));
|
|
844
|
+
arcPts.push({
|
|
845
|
+
u: center.u + Math.cos(ang) * actualArcRadius,
|
|
846
|
+
v: center.v + Math.sin(ang) * actualArcRadius,
|
|
847
|
+
w: curr.w,
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
arcPts.push(arcEnd);
|
|
851
|
+
arcSamples[i] = arcPts;
|
|
852
|
+
arcNames[i] = buildFilletEdgeName(baseNames[i - 1], baseNames[i]);
|
|
853
|
+
arcInfo.push({
|
|
854
|
+
name: arcNames[i] || buildFilletEdgeName(baseNames[i - 1], baseNames[i]),
|
|
855
|
+
center,
|
|
856
|
+
radius,
|
|
857
|
+
});
|
|
858
|
+
arcCenters[i] = center;
|
|
859
|
+
arcSweepDirs[i] = sweepDir;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
const segmentDirections = new Array(segCount);
|
|
863
|
+
const dirBetween = (a, b) => {
|
|
864
|
+
const dx = (b?.u ?? 0) - (a?.u ?? 0);
|
|
865
|
+
const dy = (b?.v ?? 0) - (a?.v ?? 0);
|
|
866
|
+
const l = Math.hypot(dx, dy);
|
|
867
|
+
if (l < 1e-12) return null;
|
|
868
|
+
return { x: dx / l, y: dy / l };
|
|
869
|
+
};
|
|
870
|
+
for (let i = 0; i < segCount; i++) {
|
|
871
|
+
segmentDirections[i] = dirBetween(segmentStart[i], segmentEnd[i]);
|
|
872
|
+
}
|
|
873
|
+
let fallbackDir = null;
|
|
874
|
+
for (const dir of segmentDirections) {
|
|
875
|
+
if (dir) {
|
|
876
|
+
fallbackDir = dir;
|
|
877
|
+
break;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
if (!fallbackDir) fallbackDir = { x: 1, y: 0 };
|
|
881
|
+
const safeDir = (dir) => {
|
|
882
|
+
if (dir && Number.isFinite(dir.x) && Number.isFinite(dir.y) && Math.hypot(dir.x, dir.y) > 1e-12) {
|
|
883
|
+
return dir;
|
|
884
|
+
}
|
|
885
|
+
return fallbackDir;
|
|
886
|
+
};
|
|
887
|
+
|
|
888
|
+
const outCoords = [];
|
|
889
|
+
const outNames = [];
|
|
890
|
+
const outTangents = [];
|
|
891
|
+
const coordsEqual = (a, b) => !a || !b
|
|
892
|
+
? false
|
|
893
|
+
: (Math.abs(a.u - b.u) < 1e-9 && Math.abs(a.v - b.v) < 1e-9 && Math.abs((a.w || 0) - (b.w || 0)) < 1e-9);
|
|
894
|
+
const normalizeTan = (tan) => {
|
|
895
|
+
if (!tan || !Number.isFinite(tan.x) || !Number.isFinite(tan.y)) return null;
|
|
896
|
+
const l = Math.hypot(tan.x, tan.y);
|
|
897
|
+
if (l < 1e-12) return null;
|
|
898
|
+
return { x: tan.x / l, y: tan.y / l };
|
|
899
|
+
};
|
|
900
|
+
const pushCoord = (coord, segName, tangent = null) => {
|
|
901
|
+
if (!coord) return false;
|
|
902
|
+
const copy = { u: coord.u, v: coord.v, w: coord.w };
|
|
903
|
+
const tanNorm = normalizeTan(tangent);
|
|
904
|
+
if (!outCoords.length) {
|
|
905
|
+
outCoords.push(copy);
|
|
906
|
+
outTangents.push(tanNorm);
|
|
907
|
+
return true;
|
|
908
|
+
}
|
|
909
|
+
const last = outCoords[outCoords.length - 1];
|
|
910
|
+
if (coordsEqual(last, copy)) {
|
|
911
|
+
if (!outTangents[outTangents.length - 1] && tanNorm) {
|
|
912
|
+
outTangents[outTangents.length - 1] = tanNorm;
|
|
913
|
+
}
|
|
914
|
+
return false;
|
|
915
|
+
}
|
|
916
|
+
outCoords.push(copy);
|
|
917
|
+
outTangents.push(tanNorm);
|
|
918
|
+
if (segName) {
|
|
919
|
+
outNames[outCoords.length - 2] = segName;
|
|
920
|
+
}
|
|
921
|
+
return true;
|
|
922
|
+
};
|
|
923
|
+
|
|
924
|
+
const arcTangent = (center, pt, sweepDir) => {
|
|
925
|
+
if (!center || !pt) return null;
|
|
926
|
+
const dx = pt.u - center.u;
|
|
927
|
+
const dy = pt.v - center.v;
|
|
928
|
+
const l = Math.hypot(dx, dy);
|
|
929
|
+
if (l < 1e-12) return null;
|
|
930
|
+
return sweepDir > 0
|
|
931
|
+
? { x: -dy / l, y: dx / l }
|
|
932
|
+
: { x: dy / l, y: -dx / l };
|
|
933
|
+
};
|
|
934
|
+
|
|
935
|
+
for (let seg = 0; seg < segCount; seg++) {
|
|
936
|
+
const segTangent = safeDir(segmentDirections[seg]);
|
|
937
|
+
pushCoord(segmentStart[seg], null, segTangent);
|
|
938
|
+
const segName = baseNames[seg] || `SEG_${seg}`;
|
|
939
|
+
pushCoord(segmentEnd[seg], segName, segTangent);
|
|
940
|
+
const arc = arcSamples[seg + 1];
|
|
941
|
+
if (arc && arc.length >= 2) {
|
|
942
|
+
const arcName = arcNames[seg + 1] || buildFilletEdgeName(baseNames[seg], baseNames[seg + 1]);
|
|
943
|
+
const center = arcCenters[seg + 1];
|
|
944
|
+
const sweepDir = arcSweepDirs[seg + 1] || 1;
|
|
945
|
+
for (let j = 1; j < arc.length; j++) {
|
|
946
|
+
const pt = arc[j];
|
|
947
|
+
const tan = arcTangent(center, pt, sweepDir) || segTangent;
|
|
948
|
+
pushCoord(pt, arcName, tan);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
const outPoints = outCoords.map((coord) => to3D(coord));
|
|
954
|
+
return {
|
|
955
|
+
points: outPoints,
|
|
956
|
+
segmentNames: normalizeSegmentNameCount(outNames, outPoints.length - 1),
|
|
957
|
+
points2D: outCoords.map((coord) => ({ ...coord })),
|
|
958
|
+
tangents2D: outTangents.map((tan) => (tan ? { x: tan.x, y: tan.y } : null)),
|
|
959
|
+
arcs: arcInfo,
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
function buildContourFlangeStripFaces({
|
|
964
|
+
featureID,
|
|
965
|
+
pathPoints,
|
|
966
|
+
pathSegmentNames,
|
|
967
|
+
planeBasis,
|
|
968
|
+
thickness,
|
|
969
|
+
sheetSide,
|
|
970
|
+
path2DOverride = null,
|
|
971
|
+
pathTangents2D = null,
|
|
972
|
+
}) {
|
|
973
|
+
if (!Array.isArray(pathPoints) || pathPoints.length < 2) {
|
|
974
|
+
throw new Error("Contour flange strip requires at least two path points.");
|
|
975
|
+
}
|
|
976
|
+
if (!(planeBasis?.origin && planeBasis?.xAxis && planeBasis?.yAxis && planeBasis?.planeNormal)) {
|
|
977
|
+
throw new Error("Contour flange could not resolve a sketch plane basis.");
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
const converters = createPlaneBasisConverters(planeBasis);
|
|
981
|
+
let path2D = Array.isArray(path2DOverride) && path2DOverride.length === pathPoints.length
|
|
982
|
+
? path2DOverride.map((coord) => ({ u: coord.u, v: coord.v, w: coord.w }))
|
|
983
|
+
: pathPoints.map((pt) => converters.to2D(pt));
|
|
984
|
+
const tangentHints = Array.isArray(pathTangents2D) && pathTangents2D.length === path2D.length
|
|
985
|
+
? pathTangents2D.map((tan) => {
|
|
986
|
+
if (!tan) return null;
|
|
987
|
+
const x = Number(tan.x ?? tan.u ?? tan[0]);
|
|
988
|
+
const y = Number(tan.y ?? tan.v ?? tan[1]);
|
|
989
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) return null;
|
|
990
|
+
const len = Math.hypot(x, y);
|
|
991
|
+
if (len < 1e-12) return null;
|
|
992
|
+
return { x: x / len, y: y / len };
|
|
993
|
+
})
|
|
994
|
+
: null;
|
|
995
|
+
const pathNames = normalizeSegmentNameCount(pathSegmentNames, path2D.length - 1);
|
|
996
|
+
let offset2D = offsetPolyline2D(path2D, thickness, sheetSide, tangentHints);
|
|
997
|
+
if (!offset2D || offset2D.length !== path2D.length) {
|
|
998
|
+
throw new Error("Contour flange failed to compute the offset path.");
|
|
999
|
+
}
|
|
1000
|
+
const areaTolerance = Math.max(thickness * thickness * 1e-6, 1e-9);
|
|
1001
|
+
const pathGroups = groupSegmentsByName(pathNames);
|
|
1002
|
+
if (!pathGroups.length && path2D.length >= 2) {
|
|
1003
|
+
pathGroups.push({ name: "SEG_0", startIndex: 0, endIndex: path2D.length - 2 });
|
|
1004
|
+
}
|
|
1005
|
+
const faces = [];
|
|
1006
|
+
let segmentCounter = 0;
|
|
1007
|
+
for (const group of pathGroups) {
|
|
1008
|
+
const face = buildSegmentFace({
|
|
1009
|
+
featureID,
|
|
1010
|
+
converters,
|
|
1011
|
+
path2D,
|
|
1012
|
+
offset2D,
|
|
1013
|
+
startIndex: group.startIndex,
|
|
1014
|
+
endIndex: group.endIndex,
|
|
1015
|
+
segmentName: group.name,
|
|
1016
|
+
areaTolerance,
|
|
1017
|
+
segmentIndex: segmentCounter++,
|
|
1018
|
+
});
|
|
1019
|
+
if (face) faces.push(face);
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
if (!faces.length) {
|
|
1023
|
+
throw new Error("Contour flange failed to create planar strip regions from the selected path.");
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
return faces;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
function groupSegmentsByName(names) {
|
|
1030
|
+
const result = [];
|
|
1031
|
+
if (!Array.isArray(names) || !names.length) return result;
|
|
1032
|
+
let current = names[0];
|
|
1033
|
+
let startIndex = 0;
|
|
1034
|
+
for (let i = 1; i <= names.length; i++) {
|
|
1035
|
+
const next = names[i];
|
|
1036
|
+
if (next !== current) {
|
|
1037
|
+
result.push({ name: current, startIndex, endIndex: i - 1 });
|
|
1038
|
+
startIndex = i;
|
|
1039
|
+
current = next;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
return result;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
function buildSegmentFace({
|
|
1046
|
+
featureID,
|
|
1047
|
+
converters,
|
|
1048
|
+
path2D,
|
|
1049
|
+
offset2D,
|
|
1050
|
+
startIndex,
|
|
1051
|
+
endIndex,
|
|
1052
|
+
segmentName,
|
|
1053
|
+
areaTolerance,
|
|
1054
|
+
segmentIndex,
|
|
1055
|
+
}) {
|
|
1056
|
+
const pathSlice = [];
|
|
1057
|
+
const offsetSlice = [];
|
|
1058
|
+
for (let i = startIndex; i <= endIndex + 1 && i < path2D.length; i++) {
|
|
1059
|
+
pathSlice.push({ ...path2D[i] });
|
|
1060
|
+
}
|
|
1061
|
+
for (let i = startIndex; i <= endIndex + 1 && i < offset2D.length; i++) {
|
|
1062
|
+
offsetSlice.push({ ...offset2D[i] });
|
|
1063
|
+
}
|
|
1064
|
+
if (pathSlice.length < 2 || offsetSlice.length < 2) return null;
|
|
1065
|
+
|
|
1066
|
+
const polygon = buildSegmentPolygon(pathSlice, offsetSlice);
|
|
1067
|
+
const loopCoords = dedupePolygonCoords(polygon);
|
|
1068
|
+
if (loopCoords.length < 3) return null;
|
|
1069
|
+
|
|
1070
|
+
let area = polygonArea2D(loopCoords);
|
|
1071
|
+
if (!Number.isFinite(area) || Math.abs(area) < areaTolerance) return null;
|
|
1072
|
+
if (area < 0) loopCoords.reverse();
|
|
1073
|
+
|
|
1074
|
+
const triangles = triangulatePolygon(loopCoords);
|
|
1075
|
+
if (!triangles.length) return null;
|
|
1076
|
+
|
|
1077
|
+
const worldPts = loopCoords.map((coord) => converters.to3D(coord));
|
|
1078
|
+
const positionArray = new Float32Array(worldPts.length * 3);
|
|
1079
|
+
for (let i = 0; i < worldPts.length; i++) {
|
|
1080
|
+
positionArray[i * 3 + 0] = worldPts[i].x;
|
|
1081
|
+
positionArray[i * 3 + 1] = worldPts[i].y;
|
|
1082
|
+
positionArray[i * 3 + 2] = worldPts[i].z;
|
|
1083
|
+
}
|
|
1084
|
+
const indexArray = new Uint32Array(triangles.length * 3);
|
|
1085
|
+
for (let i = 0; i < triangles.length; i++) {
|
|
1086
|
+
const tri = triangles[i];
|
|
1087
|
+
indexArray[i * 3 + 0] = tri[0];
|
|
1088
|
+
indexArray[i * 3 + 1] = tri[1];
|
|
1089
|
+
indexArray[i * 3 + 2] = tri[2];
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
const geometry = new THREE.BufferGeometry();
|
|
1093
|
+
geometry.setAttribute("position", new THREE.BufferAttribute(positionArray, 3));
|
|
1094
|
+
geometry.setIndex(new THREE.BufferAttribute(indexArray, 1));
|
|
1095
|
+
|
|
1096
|
+
const baseName = segmentName || `SEG_${segmentIndex}`;
|
|
1097
|
+
const face = new BREP.Face(geometry);
|
|
1098
|
+
face.name = featureID ? `${featureID}:${baseName}` : `SM.CF_${baseName}`;
|
|
1099
|
+
|
|
1100
|
+
const loopWorld = worldPts.map((pt) => [pt.x, pt.y, pt.z]);
|
|
1101
|
+
if (loopWorld.length >= 2) {
|
|
1102
|
+
const first = loopWorld[0];
|
|
1103
|
+
const last = loopWorld[loopWorld.length - 1];
|
|
1104
|
+
if (first[0] === last[0] && first[1] === last[1] && first[2] === last[2]) {
|
|
1105
|
+
loopWorld.pop();
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
face.userData = face.userData || {};
|
|
1109
|
+
face.userData.boundaryLoopsWorld = [{ pts: loopWorld, isHole: false }];
|
|
1110
|
+
|
|
1111
|
+
const pathEdgePts = pathSlice.map((coord) => converters.to3D(coord));
|
|
1112
|
+
const offsetEdgePts = offsetSlice.map((coord) => converters.to3D(coord));
|
|
1113
|
+
const pathEdge = createPseudoEdge(baseName, pathEdgePts);
|
|
1114
|
+
const offsetEdge = createPseudoEdge(`${baseName}_OFFSET`, offsetEdgePts);
|
|
1115
|
+
const startClosure = createPseudoEdge(`${baseName}_START_CAP`, [pathEdgePts[0], offsetEdgePts[0]]);
|
|
1116
|
+
const endClosure = createPseudoEdge(
|
|
1117
|
+
`${baseName}_END_CAP`,
|
|
1118
|
+
[pathEdgePts[pathEdgePts.length - 1], offsetEdgePts[offsetEdgePts.length - 1]],
|
|
1119
|
+
);
|
|
1120
|
+
const edges = [pathEdge, offsetEdge, startClosure, endClosure].filter(Boolean);
|
|
1121
|
+
face.edges = edges;
|
|
1122
|
+
|
|
1123
|
+
const baseEdges = pathEdge ? [pathEdge.name] : [];
|
|
1124
|
+
const offsetEdges = offsetEdge ? [offsetEdge.name] : [];
|
|
1125
|
+
const closureEdges = [startClosure?.name, endClosure?.name].filter(Boolean);
|
|
1126
|
+
face.userData.sheetMetalEdgeGroups = {
|
|
1127
|
+
baseEdges,
|
|
1128
|
+
offsetEdges,
|
|
1129
|
+
closureEdges,
|
|
1130
|
+
};
|
|
1131
|
+
return face;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
function buildSegmentPolygon(pathSlice, offsetSlice) {
|
|
1135
|
+
const polygon = [];
|
|
1136
|
+
for (const coord of pathSlice) {
|
|
1137
|
+
polygon.push({ ...coord });
|
|
1138
|
+
}
|
|
1139
|
+
for (let i = offsetSlice.length - 1; i >= 0; i--) {
|
|
1140
|
+
polygon.push({ ...offsetSlice[i] });
|
|
1141
|
+
}
|
|
1142
|
+
return polygon;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
function dedupePolygonCoords(coords) {
|
|
1146
|
+
const out = [];
|
|
1147
|
+
const push = (coord) => {
|
|
1148
|
+
if (!coord) return;
|
|
1149
|
+
if (!out.length || !coordsAlmostEqual(out[out.length - 1], coord)) {
|
|
1150
|
+
out.push({ u: coord.u, v: coord.v, w: coord.w });
|
|
1151
|
+
}
|
|
1152
|
+
};
|
|
1153
|
+
for (const coord of coords) push(coord);
|
|
1154
|
+
if (out.length >= 2 && coordsAlmostEqual(out[0], out[out.length - 1])) {
|
|
1155
|
+
out.pop();
|
|
1156
|
+
}
|
|
1157
|
+
return out;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
function coordsAlmostEqual(a, b, eps = 1e-9) {
|
|
1161
|
+
if (!a || !b) return false;
|
|
1162
|
+
return (
|
|
1163
|
+
Math.abs(a.u - b.u) < eps
|
|
1164
|
+
&& Math.abs(a.v - b.v) < eps
|
|
1165
|
+
&& Math.abs((a.w || 0) - (b.w || 0)) < eps
|
|
1166
|
+
);
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
function triangulatePolygon(points) {
|
|
1170
|
+
if (!Array.isArray(points) || points.length < 3) return [];
|
|
1171
|
+
const contour = points.map((coord) => new THREE.Vector2(coord.u, coord.v));
|
|
1172
|
+
let tris = THREE.ShapeUtils.triangulateShape(contour, []);
|
|
1173
|
+
if (!Array.isArray(tris) || !tris.length) {
|
|
1174
|
+
tris = [];
|
|
1175
|
+
for (let i = 1; i < points.length - 1; i++) {
|
|
1176
|
+
tris.push([0, i, i + 1]);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
return tris;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
let pseudoEdgeCounter = 0;
|
|
1183
|
+
function createPseudoEdge(name, points) {
|
|
1184
|
+
if (!Array.isArray(points) || points.length < 2) return null;
|
|
1185
|
+
const sanitized = points.map((pt) => [pt.x, pt.y, pt.z]);
|
|
1186
|
+
return {
|
|
1187
|
+
type: "EDGE",
|
|
1188
|
+
name: name || `SMCF_EDGE_${pseudoEdgeCounter++}`,
|
|
1189
|
+
userData: { polylineLocal: sanitized },
|
|
1190
|
+
matrixWorld: new THREE.Matrix4(),
|
|
1191
|
+
closedLoop: false,
|
|
1192
|
+
updateWorldMatrix: () => { },
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
function createPlaneBasisConverters(basis) {
|
|
1197
|
+
const origin = basis.origin instanceof THREE.Vector3
|
|
1198
|
+
? basis.origin.clone()
|
|
1199
|
+
: Array.isArray(basis.origin)
|
|
1200
|
+
? new THREE.Vector3().fromArray(basis.origin)
|
|
1201
|
+
: new THREE.Vector3(0, 0, 0);
|
|
1202
|
+
const xAxis = basis.xAxis instanceof THREE.Vector3
|
|
1203
|
+
? basis.xAxis.clone()
|
|
1204
|
+
: Array.isArray(basis.xAxis)
|
|
1205
|
+
? new THREE.Vector3().fromArray(basis.xAxis)
|
|
1206
|
+
: new THREE.Vector3(1, 0, 0);
|
|
1207
|
+
const yAxis = basis.yAxis instanceof THREE.Vector3
|
|
1208
|
+
? basis.yAxis.clone()
|
|
1209
|
+
: Array.isArray(basis.yAxis)
|
|
1210
|
+
? new THREE.Vector3().fromArray(basis.yAxis)
|
|
1211
|
+
: new THREE.Vector3(0, 1, 0);
|
|
1212
|
+
const planeNormal = basis.planeNormal instanceof THREE.Vector3
|
|
1213
|
+
? basis.planeNormal.clone()
|
|
1214
|
+
: Array.isArray(basis.planeNormal)
|
|
1215
|
+
? new THREE.Vector3().fromArray(basis.planeNormal)
|
|
1216
|
+
: new THREE.Vector3(0, 0, 1);
|
|
1217
|
+
return {
|
|
1218
|
+
origin,
|
|
1219
|
+
planeNormal,
|
|
1220
|
+
to2D(vec) {
|
|
1221
|
+
const rel = vec.clone().sub(origin);
|
|
1222
|
+
return {
|
|
1223
|
+
u: rel.dot(xAxis),
|
|
1224
|
+
v: rel.dot(yAxis),
|
|
1225
|
+
w: rel.dot(planeNormal),
|
|
1226
|
+
};
|
|
1227
|
+
},
|
|
1228
|
+
to3D(coord) {
|
|
1229
|
+
return origin.clone()
|
|
1230
|
+
.add(xAxis.clone().multiplyScalar(coord.u))
|
|
1231
|
+
.add(yAxis.clone().multiplyScalar(coord.v))
|
|
1232
|
+
.add(planeNormal.clone().multiplyScalar(coord.w || 0));
|
|
1233
|
+
},
|
|
1234
|
+
};
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
function offsetPolyline2D(path2D, thickness, sheetSide, tangentHints = null) {
|
|
1238
|
+
if (!Array.isArray(path2D) || path2D.length < 2) return [];
|
|
1239
|
+
const EPS = 1e-10;
|
|
1240
|
+
const normalizedTangents = Array.isArray(tangentHints) && tangentHints.length === path2D.length
|
|
1241
|
+
? tangentHints.map((tan) => {
|
|
1242
|
+
if (!tan) return null;
|
|
1243
|
+
const x = Number(tan.x ?? tan.u ?? tan[0]);
|
|
1244
|
+
const y = Number(tan.y ?? tan.v ?? tan[1]);
|
|
1245
|
+
const len = Math.hypot(x, y);
|
|
1246
|
+
if (!Number.isFinite(x) || !Number.isFinite(y) || len < EPS) return null;
|
|
1247
|
+
return { x: x / len, y: y / len };
|
|
1248
|
+
})
|
|
1249
|
+
: null;
|
|
1250
|
+
const dirs = [];
|
|
1251
|
+
for (let i = 0; i < path2D.length - 1; i++) {
|
|
1252
|
+
const dx = path2D[i + 1].u - path2D[i].u;
|
|
1253
|
+
const dy = path2D[i + 1].v - path2D[i].v;
|
|
1254
|
+
const len = Math.hypot(dx, dy);
|
|
1255
|
+
dirs.push(len > EPS ? { x: dx / len, y: dy / len } : null);
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
let fallback = null;
|
|
1259
|
+
for (const dir of dirs) {
|
|
1260
|
+
if (dir) {
|
|
1261
|
+
fallback = dir;
|
|
1262
|
+
break;
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
if (!fallback && normalizedTangents) {
|
|
1266
|
+
for (const tan of normalizedTangents) {
|
|
1267
|
+
if (tan) {
|
|
1268
|
+
fallback = tan;
|
|
1269
|
+
break;
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
if (!fallback) fallback = { x: 1, y: 0 };
|
|
1274
|
+
|
|
1275
|
+
const sideSign = sheetSide === "right" ? -1 : 1;
|
|
1276
|
+
const safeDir = (dir) => (dir && Number.isFinite(dir.x) && Number.isFinite(dir.y)
|
|
1277
|
+
? dir
|
|
1278
|
+
: fallback);
|
|
1279
|
+
const rotateDir = (dir) => {
|
|
1280
|
+
const base = safeDir(dir);
|
|
1281
|
+
return { x: -base.y * sideSign, y: base.x * sideSign };
|
|
1282
|
+
};
|
|
1283
|
+
const offsetPoint = (pt, perp) => ({
|
|
1284
|
+
u: pt.u + perp.x * thickness,
|
|
1285
|
+
v: pt.v + perp.y * thickness,
|
|
1286
|
+
w: pt.w,
|
|
1287
|
+
});
|
|
1288
|
+
const offsets = new Array(path2D.length);
|
|
1289
|
+
const tangentOffset = (index) => {
|
|
1290
|
+
if (!normalizedTangents) return null;
|
|
1291
|
+
const tan = normalizedTangents[index];
|
|
1292
|
+
if (!tan) return null;
|
|
1293
|
+
return offsetPoint(path2D[index], rotateDir(tan));
|
|
1294
|
+
};
|
|
1295
|
+
const getPrevDir = (vertexIndex) => {
|
|
1296
|
+
for (let seg = vertexIndex - 1; seg >= 0; seg--) {
|
|
1297
|
+
if (dirs[seg]) return dirs[seg];
|
|
1298
|
+
}
|
|
1299
|
+
return normalizedTangents?.[vertexIndex] || fallback;
|
|
1300
|
+
};
|
|
1301
|
+
const getNextDir = (vertexIndex) => {
|
|
1302
|
+
for (let seg = vertexIndex; seg < dirs.length; seg++) {
|
|
1303
|
+
if (dirs[seg]) return dirs[seg];
|
|
1304
|
+
}
|
|
1305
|
+
return normalizedTangents?.[vertexIndex] || fallback;
|
|
1306
|
+
};
|
|
1307
|
+
|
|
1308
|
+
offsets[0] = tangentOffset(0) || offsetPoint(path2D[0], rotateDir(getNextDir(0)));
|
|
1309
|
+
for (let i = 1; i < path2D.length - 1; i++) {
|
|
1310
|
+
const directOffset = tangentOffset(i);
|
|
1311
|
+
if (directOffset) {
|
|
1312
|
+
offsets[i] = directOffset;
|
|
1313
|
+
continue;
|
|
1314
|
+
}
|
|
1315
|
+
const prevDir = safeDir(getPrevDir(i));
|
|
1316
|
+
const nextDir = safeDir(getNextDir(i));
|
|
1317
|
+
const perpPrev = rotateDir(prevDir);
|
|
1318
|
+
const perpNext = rotateDir(nextDir);
|
|
1319
|
+
const a = offsetPoint(path2D[i], perpPrev);
|
|
1320
|
+
const b = offsetPoint(path2D[i], perpNext);
|
|
1321
|
+
const hit = intersectLines2D(a, prevDir, b, nextDir);
|
|
1322
|
+
if (hit) {
|
|
1323
|
+
offsets[i] = { u: hit.u, v: hit.v, w: path2D[i].w };
|
|
1324
|
+
} else {
|
|
1325
|
+
offsets[i] = {
|
|
1326
|
+
u: 0.5 * (a.u + b.u),
|
|
1327
|
+
v: 0.5 * (a.v + b.v),
|
|
1328
|
+
w: path2D[i].w,
|
|
1329
|
+
};
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
offsets[path2D.length - 1] = tangentOffset(path2D.length - 1)
|
|
1333
|
+
|| offsetPoint(path2D[path2D.length - 1], rotateDir(getPrevDir(path2D.length - 1)));
|
|
1334
|
+
return offsets;
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
function intersectLines2D(pointA, dirA, pointB, dirB) {
|
|
1338
|
+
const dAx = dirA?.x ?? 0;
|
|
1339
|
+
const dAy = dirA?.y ?? 0;
|
|
1340
|
+
const dBx = dirB?.x ?? 0;
|
|
1341
|
+
const dBy = dirB?.y ?? 0;
|
|
1342
|
+
const denom = dAx * dBy - dAy * dBx;
|
|
1343
|
+
if (!Number.isFinite(denom) || Math.abs(denom) < 1e-12) {
|
|
1344
|
+
return null;
|
|
1345
|
+
}
|
|
1346
|
+
const diffX = pointB.u - pointA.u;
|
|
1347
|
+
const diffY = pointB.v - pointA.v;
|
|
1348
|
+
const t = (diffX * dBy - diffY * dBx) / denom;
|
|
1349
|
+
return {
|
|
1350
|
+
u: pointA.u + dAx * t,
|
|
1351
|
+
v: pointA.v + dAy * t,
|
|
1352
|
+
w: pointA.w,
|
|
1353
|
+
};
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
function polygonArea2D(points) {
|
|
1357
|
+
if (!Array.isArray(points) || points.length < 3) return 0;
|
|
1358
|
+
let area = 0;
|
|
1359
|
+
for (let i = 0; i < points.length; i++) {
|
|
1360
|
+
const a = points[i];
|
|
1361
|
+
const b = points[(i + 1) % points.length];
|
|
1362
|
+
area += (a.u * b.v) - (a.v * b.u);
|
|
1363
|
+
}
|
|
1364
|
+
return area / 2;
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
function resolveSheetSideOption(inputParams) {
|
|
1368
|
+
const legacyValue = inputParams?.sheetSide;
|
|
1369
|
+
const reverseRaw = inputParams?.reverseSheetSide;
|
|
1370
|
+
let sheetSide = "left";
|
|
1371
|
+
if (typeof reverseRaw === "boolean") {
|
|
1372
|
+
sheetSide = reverseRaw ? "right" : "left";
|
|
1373
|
+
} else if (legacyValue != null) {
|
|
1374
|
+
sheetSide = (String(legacyValue).toLowerCase() === "right") ? "right" : "left";
|
|
1375
|
+
}
|
|
1376
|
+
const reverseSheetSide = sheetSide === "right";
|
|
1377
|
+
if (inputParams && typeof reverseRaw !== "boolean") {
|
|
1378
|
+
inputParams.reverseSheetSide = reverseSheetSide;
|
|
1379
|
+
}
|
|
1380
|
+
return { sheetSide, reverseSheetSide };
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
function normalizeSegmentNameCount(names, expectedLength) {
|
|
1384
|
+
const count = Math.max(0, Number(expectedLength) || 0);
|
|
1385
|
+
const result = new Array(count);
|
|
1386
|
+
for (let i = 0; i < count; i++) {
|
|
1387
|
+
const raw = Array.isArray(names) ? names[i] : null;
|
|
1388
|
+
if (typeof raw === "string" && raw.trim().length) {
|
|
1389
|
+
result[i] = raw.trim();
|
|
1390
|
+
} else {
|
|
1391
|
+
result[i] = `SEG_${i}`;
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
return result;
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
function buildDefaultSegmentNames(count) {
|
|
1398
|
+
const total = Math.max(0, Number(count) || 0);
|
|
1399
|
+
return Array.from({ length: total }, (_, i) => `SEG_${i}`);
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
function buildFilletEdgeName(nameA, nameB) {
|
|
1403
|
+
const left = (typeof nameA === "string" && nameA.trim().length) ? nameA.trim() : null;
|
|
1404
|
+
const right = (typeof nameB === "string" && nameB.trim().length) ? nameB.trim() : null;
|
|
1405
|
+
if (left && right && left !== right) return `${left}__${right}`;
|
|
1406
|
+
if (left && right) return `${left}_ARC`;
|
|
1407
|
+
if (left) return `${left}__SMCF`;
|
|
1408
|
+
if (right) return `SMCF__${right}`;
|
|
1409
|
+
return "SMCF_FILLET";
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
function deriveEdgeBaseName(edge, fallbackIndex) {
|
|
1413
|
+
const fallback = `EDGE_${fallbackIndex}`;
|
|
1414
|
+
if (!edge) return fallback;
|
|
1415
|
+
const direct = typeof edge.name === "string" && edge.name.trim().length ? edge.name.trim() : null;
|
|
1416
|
+
if (direct) return direct;
|
|
1417
|
+
const skId = edge.userData?.sketchGeometryId ?? edge.userData?.sketchGeomId ?? edge.userData?.id;
|
|
1418
|
+
if (skId != null) return `SKETCH_EDGE_${skId}`;
|
|
1419
|
+
const uid = edge.uuid ? String(edge.uuid).slice(0, 8) : null;
|
|
1420
|
+
if (uid) return `EDGE_${uid}`;
|
|
1421
|
+
return fallback;
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
function tagContourFlangeFaceTypes(sweep) {
|
|
1425
|
+
if (!sweep || typeof sweep.getFaceNames !== "function") return;
|
|
1426
|
+
const names = sweep.getFaceNames();
|
|
1427
|
+
const thicknessFaces = names.filter((name) =>
|
|
1428
|
+
name.endsWith("_START")
|
|
1429
|
+
|| name.endsWith("_END")
|
|
1430
|
+
|| name.includes("_CAP_SW"),
|
|
1431
|
+
);
|
|
1432
|
+
const bFaces = names.filter((name) => name.includes("_OFFSET_SW"));
|
|
1433
|
+
const aFaces = names.filter((name) =>
|
|
1434
|
+
name.endsWith("_SW")
|
|
1435
|
+
&& !name.includes("_OFFSET_SW")
|
|
1436
|
+
&& !name.includes("_CAP_SW"),
|
|
1437
|
+
);
|
|
1438
|
+
setSheetMetalFaceTypeMetadata(sweep, aFaces, SHEET_METAL_FACE_TYPES.A);
|
|
1439
|
+
setSheetMetalFaceTypeMetadata(sweep, bFaces, SHEET_METAL_FACE_TYPES.B);
|
|
1440
|
+
setSheetMetalFaceTypeMetadata(sweep, thicknessFaces, SHEET_METAL_FACE_TYPES.THICKNESS);
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
function addCylMetadataToSideFaces(
|
|
1444
|
+
sweep,
|
|
1445
|
+
arcs,
|
|
1446
|
+
converters,
|
|
1447
|
+
extrudeVector,
|
|
1448
|
+
axisDir,
|
|
1449
|
+
thickness = 0,
|
|
1450
|
+
bendRadius = null,
|
|
1451
|
+
sheetSide = "left",
|
|
1452
|
+
) {
|
|
1453
|
+
if (!sweep || !Array.isArray(sweep.faces)) return;
|
|
1454
|
+
const arcList = Array.isArray(arcs) ? arcs.filter((a) => a && a.center && Number.isFinite(a.radius)) : [];
|
|
1455
|
+
if (!arcList.length) return;
|
|
1456
|
+
|
|
1457
|
+
const height = extrudeVector.length();
|
|
1458
|
+
const featureTag = sweep.params?.name ? `${sweep.params.name}:` : "";
|
|
1459
|
+
const v = new THREE.Vector3();
|
|
1460
|
+
const eps = 1e-9;
|
|
1461
|
+
const relStdTol = 0.02;
|
|
1462
|
+
const absStdTol = Math.max(1e-5, Math.abs(thickness || 0) * 0.02, Math.abs(bendRadius || 0) * 0.02);
|
|
1463
|
+
|
|
1464
|
+
const arc3D = arcList.map((arc) => {
|
|
1465
|
+
const startCenter = converters.to3D(arc.center);
|
|
1466
|
+
const endCenter = startCenter.clone().add(extrudeVector);
|
|
1467
|
+
return {
|
|
1468
|
+
name: arc.name || "BEND",
|
|
1469
|
+
radius: arc.radius,
|
|
1470
|
+
axisStart: startCenter,
|
|
1471
|
+
axisEnd: endCenter,
|
|
1472
|
+
};
|
|
1473
|
+
});
|
|
1474
|
+
|
|
1475
|
+
const arc3DByName = new Map();
|
|
1476
|
+
for (const arc of arc3D) {
|
|
1477
|
+
const name = typeof arc.name === "string" ? arc.name.trim() : "";
|
|
1478
|
+
if (!name) continue;
|
|
1479
|
+
let list = arc3DByName.get(name);
|
|
1480
|
+
if (!list) { list = []; arc3DByName.set(name, list); }
|
|
1481
|
+
list.push(arc);
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
const isFacePlanar = (face) => {
|
|
1485
|
+
const geom = face?.geometry;
|
|
1486
|
+
const pos = geom?.getAttribute?.("position");
|
|
1487
|
+
if (!pos || pos.count < 3) return null;
|
|
1488
|
+
const idx = geom.getIndex?.() || null;
|
|
1489
|
+
const triCount = idx ? (idx.count / 3) | 0 : (pos.count / 3) | 0;
|
|
1490
|
+
if (!triCount) return null;
|
|
1491
|
+
|
|
1492
|
+
const a = new THREE.Vector3();
|
|
1493
|
+
const b = new THREE.Vector3();
|
|
1494
|
+
const c = new THREE.Vector3();
|
|
1495
|
+
const m = face.matrixWorld || null;
|
|
1496
|
+
const getPos = (out, i) => {
|
|
1497
|
+
out.set(pos.getX(i), pos.getY(i), pos.getZ(i));
|
|
1498
|
+
if (m) out.applyMatrix4(m);
|
|
1499
|
+
return out;
|
|
1500
|
+
};
|
|
1501
|
+
|
|
1502
|
+
let nx = 0;
|
|
1503
|
+
let ny = 0;
|
|
1504
|
+
let nz = 0;
|
|
1505
|
+
let areaSum = 0;
|
|
1506
|
+
for (let t = 0; t < triCount; t++) {
|
|
1507
|
+
const base = t * 3;
|
|
1508
|
+
const i0 = idx ? idx.getX(base) >>> 0 : base;
|
|
1509
|
+
const i1 = idx ? idx.getX(base + 1) >>> 0 : base + 1;
|
|
1510
|
+
const i2 = idx ? idx.getX(base + 2) >>> 0 : base + 2;
|
|
1511
|
+
getPos(a, i0);
|
|
1512
|
+
getPos(b, i1);
|
|
1513
|
+
getPos(c, i2);
|
|
1514
|
+
const abx = b.x - a.x;
|
|
1515
|
+
const aby = b.y - a.y;
|
|
1516
|
+
const abz = b.z - a.z;
|
|
1517
|
+
const acx = c.x - a.x;
|
|
1518
|
+
const acy = c.y - a.y;
|
|
1519
|
+
const acz = c.z - a.z;
|
|
1520
|
+
const tx = aby * acz - abz * acy;
|
|
1521
|
+
const ty = abz * acx - abx * acz;
|
|
1522
|
+
const tz = abx * acy - aby * acx;
|
|
1523
|
+
const area = Math.hypot(tx, ty, tz);
|
|
1524
|
+
if (area <= 1e-12) continue;
|
|
1525
|
+
nx += tx;
|
|
1526
|
+
ny += ty;
|
|
1527
|
+
nz += tz;
|
|
1528
|
+
areaSum += area;
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
const len = Math.hypot(nx, ny, nz);
|
|
1532
|
+
if (!(len > 1e-12) || !(areaSum > 1e-12)) return null;
|
|
1533
|
+
const ax = nx / len;
|
|
1534
|
+
const ay = ny / len;
|
|
1535
|
+
const az = nz / len;
|
|
1536
|
+
let maxAngle = 0;
|
|
1537
|
+
for (let t = 0; t < triCount; t++) {
|
|
1538
|
+
const base = t * 3;
|
|
1539
|
+
const i0 = idx ? idx.getX(base) >>> 0 : base;
|
|
1540
|
+
const i1 = idx ? idx.getX(base + 1) >>> 0 : base + 1;
|
|
1541
|
+
const i2 = idx ? idx.getX(base + 2) >>> 0 : base + 2;
|
|
1542
|
+
getPos(a, i0);
|
|
1543
|
+
getPos(b, i1);
|
|
1544
|
+
getPos(c, i2);
|
|
1545
|
+
const abx = b.x - a.x;
|
|
1546
|
+
const aby = b.y - a.y;
|
|
1547
|
+
const abz = b.z - a.z;
|
|
1548
|
+
const acx = c.x - a.x;
|
|
1549
|
+
const acy = c.y - a.y;
|
|
1550
|
+
const acz = c.z - a.z;
|
|
1551
|
+
const tx = aby * acz - abz * acy;
|
|
1552
|
+
const ty = abz * acx - abx * acz;
|
|
1553
|
+
const tz = abx * acy - aby * acx;
|
|
1554
|
+
const nLen = Math.hypot(tx, ty, tz);
|
|
1555
|
+
if (nLen <= 1e-12) continue;
|
|
1556
|
+
const dot = Math.abs((tx / nLen) * ax + (ty / nLen) * ay + (tz / nLen) * az);
|
|
1557
|
+
const clamped = Math.max(-1, Math.min(1, dot));
|
|
1558
|
+
const angle = Math.acos(clamped);
|
|
1559
|
+
if (angle > maxAngle) maxAngle = angle;
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
const planarRatio = len / areaSum;
|
|
1563
|
+
return planarRatio >= 0.9995 && maxAngle <= (Math.PI / 180);
|
|
1564
|
+
};
|
|
1565
|
+
|
|
1566
|
+
const findArcCandidates = (sourceName) => {
|
|
1567
|
+
if (!sourceName) return arc3D;
|
|
1568
|
+
const trimmed = String(sourceName).trim();
|
|
1569
|
+
if (!trimmed) return arc3D;
|
|
1570
|
+
const direct = arc3DByName.get(trimmed);
|
|
1571
|
+
if (direct && direct.length) return direct;
|
|
1572
|
+
const tail = trimmed.includes(":") ? trimmed.split(":").slice(-1)[0] : trimmed;
|
|
1573
|
+
if (tail !== trimmed) {
|
|
1574
|
+
const directTail = arc3DByName.get(tail);
|
|
1575
|
+
if (directTail && directTail.length) return directTail;
|
|
1576
|
+
}
|
|
1577
|
+
return [];
|
|
1578
|
+
};
|
|
1579
|
+
|
|
1580
|
+
// Centerlines per arc
|
|
1581
|
+
for (const arc of arc3D) {
|
|
1582
|
+
const clName = `${featureTag}${arc.name}_AXIS`;
|
|
1583
|
+
try { sweep.addCenterline(arc.axisStart, arc.axisEnd, clName, { polylineWorld: true }); } catch { /* optional */ }
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
// Attach metadata by geometric fit to each side face
|
|
1587
|
+
for (const face of sweep.faces) {
|
|
1588
|
+
try {
|
|
1589
|
+
const meta = face.getMetadata?.() || {};
|
|
1590
|
+
const faceType = meta?.faceType;
|
|
1591
|
+
if (faceType && faceType !== "SIDEWALL") continue;
|
|
1592
|
+
if (isFacePlanar(face)) continue;
|
|
1593
|
+
const pos = face.geometry?.getAttribute?.("position");
|
|
1594
|
+
if (!pos || pos.itemSize !== 3 || pos.count < 3) continue;
|
|
1595
|
+
const verts = [];
|
|
1596
|
+
for (let i = 0; i < pos.count; i++) {
|
|
1597
|
+
v.set(pos.getX(i), pos.getY(i), pos.getZ(i)).applyMatrix4(face.matrixWorld);
|
|
1598
|
+
verts.push(v.clone());
|
|
1599
|
+
}
|
|
1600
|
+
const sourceEdgeName = meta?.sourceEdgeName || face?.name || null;
|
|
1601
|
+
let candidates = findArcCandidates(sourceEdgeName);
|
|
1602
|
+
if (!candidates.length) candidates = arc3D;
|
|
1603
|
+
let best = null;
|
|
1604
|
+
for (const arc of candidates) {
|
|
1605
|
+
const axisVec = arc.axisEnd.clone().sub(arc.axisStart);
|
|
1606
|
+
const axisLen = axisVec.length();
|
|
1607
|
+
if (axisLen < 1e-9) continue;
|
|
1608
|
+
const axisN = axisVec.clone().normalize();
|
|
1609
|
+
const origin = arc.axisStart;
|
|
1610
|
+
let sumDist = 0;
|
|
1611
|
+
let sumSqDist = 0;
|
|
1612
|
+
let minT = Infinity;
|
|
1613
|
+
let maxT = -Infinity;
|
|
1614
|
+
for (const p of verts) {
|
|
1615
|
+
const t = p.clone().sub(origin).dot(axisN);
|
|
1616
|
+
if (t < minT) minT = t;
|
|
1617
|
+
if (t > maxT) maxT = t;
|
|
1618
|
+
const proj = origin.clone().add(axisN.clone().multiplyScalar(t));
|
|
1619
|
+
const d = p.distanceTo(proj);
|
|
1620
|
+
sumDist += d;
|
|
1621
|
+
sumSqDist += d * d;
|
|
1622
|
+
}
|
|
1623
|
+
const meanRadius = sumDist / verts.length;
|
|
1624
|
+
if (!(meanRadius > eps)) continue;
|
|
1625
|
+
const variance = Math.max(0, sumSqDist / verts.length - meanRadius * meanRadius);
|
|
1626
|
+
const std = Math.sqrt(variance);
|
|
1627
|
+
const relStd = std / meanRadius;
|
|
1628
|
+
if (std > absStdTol && relStd > relStdTol) continue;
|
|
1629
|
+
if (!best || relStd < best.relStd) {
|
|
1630
|
+
best = {
|
|
1631
|
+
arc,
|
|
1632
|
+
relStd,
|
|
1633
|
+
radius: meanRadius,
|
|
1634
|
+
axisN,
|
|
1635
|
+
center: origin.clone().add(axisN.clone().multiplyScalar((minT + maxT) * 0.5)),
|
|
1636
|
+
height: maxT - minT,
|
|
1637
|
+
};
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
if (best) {
|
|
1641
|
+
const arc = best.arc;
|
|
1642
|
+
const axisN = best.axisN || arc.axisEnd.clone().sub(arc.axisStart).normalize();
|
|
1643
|
+
const snappedR = best.radius;
|
|
1644
|
+
const adjustedCenter = best.center.clone();
|
|
1645
|
+
sweep.setFaceMetadata(face.name, {
|
|
1646
|
+
type: "cylindrical",
|
|
1647
|
+
radius: snappedR, // enforce stable bend radius independent of sheet side
|
|
1648
|
+
height: best.height || height,
|
|
1649
|
+
axis: [axisN.x, axisN.y, axisN.z],
|
|
1650
|
+
center: [adjustedCenter.x, adjustedCenter.y, adjustedCenter.z],
|
|
1651
|
+
pmiRadiusOverride: snappedR,
|
|
1652
|
+
});
|
|
1653
|
+
}
|
|
1654
|
+
} catch { /* ignore */ }
|
|
1655
|
+
}
|
|
1656
|
+
}
|