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,1596 @@
|
|
|
1
|
+
import { Solid } from './BetterSolid.js';
|
|
2
|
+
import { getEdgePolylineWorld } from './edgePolylineUtils.js';
|
|
3
|
+
import { computeBoundsFromVertices } from './boundsUtils.js';
|
|
4
|
+
import * as THREE from 'three';
|
|
5
|
+
const DEBUG = false;
|
|
6
|
+
|
|
7
|
+
// Debug helper for sweep/pathAlign. Enable by setting window.BREP_DEBUG_SWEEP = 1
|
|
8
|
+
// or adding '?sweepDebug=1' to the URL. Keeps logs grouped and throttled.
|
|
9
|
+
function sweepDebugEnabled() {
|
|
10
|
+
try {
|
|
11
|
+
// Enabled by default; allow explicit opt-out
|
|
12
|
+
if (DEBUG) {
|
|
13
|
+
if (typeof window !== 'undefined') {
|
|
14
|
+
if (window.BREP_DEBUG_SWEEP === 0 || window.BREP_DEBUG_SWEEP === false) return false;
|
|
15
|
+
const q = (window.location && window.location.search) || '';
|
|
16
|
+
if (/[?&]sweepDebug=0/.test(q)) return false;
|
|
17
|
+
}
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
if (typeof window === 'undefined') return false;
|
|
21
|
+
if (window.BREP_DEBUG_SWEEP) return true;
|
|
22
|
+
const q = (window.location && window.location.search) || '';
|
|
23
|
+
return /[?&]sweepDebug=1/.test(q);
|
|
24
|
+
} catch (_) { return DEBUG; }
|
|
25
|
+
}
|
|
26
|
+
function dlog(group, msg, obj) {
|
|
27
|
+
if (!sweepDebugEnabled()) return;
|
|
28
|
+
try {
|
|
29
|
+
if (group) console.log(`[SweepDBG] ${group}: ${msg}`, obj || '');
|
|
30
|
+
else console.log(`[SweepDBG] ${msg}`, obj || '');
|
|
31
|
+
} catch (_) {}
|
|
32
|
+
}
|
|
33
|
+
function djson(tag, obj) {
|
|
34
|
+
if (!sweepDebugEnabled()) return;
|
|
35
|
+
try {
|
|
36
|
+
console.log(`[SweepDBG-JSON] ${tag} ` + JSON.stringify(obj));
|
|
37
|
+
} catch (e) {
|
|
38
|
+
try { console.log(`[SweepDBG-JSON] ${tag} (stringify failed)`, obj); } catch(_) {}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const _round = (n)=> Math.abs(n) < 1e-12 ? 0 : Number(n.toFixed(6));
|
|
42
|
+
const _v3 = (v)=> (v && typeof v.x === 'number') ? [_round(v.x), _round(v.y), _round(v.z)] : v;
|
|
43
|
+
|
|
44
|
+
export class FacesSolid extends Solid {
|
|
45
|
+
/**
|
|
46
|
+
* @param {object} [opts]
|
|
47
|
+
* @param {string} [opts.name='FromFaces'] Name of the solid
|
|
48
|
+
*/
|
|
49
|
+
constructor({ name = 'FromFaces' } = {}) {
|
|
50
|
+
super();
|
|
51
|
+
this.name = name;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Reads this Group's descendant meshes, packs geometry arrays, and seeds
|
|
56
|
+
* per-triangle labels and face name mapping based on each mesh's name.
|
|
57
|
+
* After calling, this Solid can visualize and participate in booleans.
|
|
58
|
+
* Returns `this` for chaining.
|
|
59
|
+
*/
|
|
60
|
+
manifoldFromFaces() {
|
|
61
|
+
// Ensure world transforms are up to date
|
|
62
|
+
if (DEBUG) console.log(`[FacesSolid] manifoldFromFaces start: name=${this.name}`);
|
|
63
|
+
this.updateWorldMatrix(true, true);
|
|
64
|
+
|
|
65
|
+
// Collect meshes recursively under this Solid. Exclude line-based helpers (Line/Line2/etc.)
|
|
66
|
+
const meshes = [];
|
|
67
|
+
this.traverse(obj => {
|
|
68
|
+
if (!obj || !obj.isMesh || !obj.geometry) return;
|
|
69
|
+
// Skip any kind of line visuals (Line, Line2, LineSegments, LineLoop)
|
|
70
|
+
if (obj.isLine || obj.isLine2 || obj.isLineSegments || obj.isLineLoop) return;
|
|
71
|
+
meshes.push(obj);
|
|
72
|
+
});
|
|
73
|
+
if (DEBUG) console.log(`[FacesSolid] found ${meshes.length} mesh children:`, meshes.map(m => m.name));
|
|
74
|
+
if (meshes.length === 0) {
|
|
75
|
+
throw new Error('FacesSolid.manifoldFromFaces: no meshes found under this group');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Determine totals
|
|
79
|
+
let totalVerts = 0;
|
|
80
|
+
let totalTriIndices = 0;
|
|
81
|
+
let totalTris = 0;
|
|
82
|
+
const entries = [];
|
|
83
|
+
for (const mesh of meshes) {
|
|
84
|
+
const geom = mesh.geometry;
|
|
85
|
+
const posAttr = geom.getAttribute('position');
|
|
86
|
+
if (!posAttr) continue;
|
|
87
|
+
const vCount = posAttr.count >>> 0;
|
|
88
|
+
const indexAttr = geom.getIndex();
|
|
89
|
+
let triCount;
|
|
90
|
+
if (indexAttr) triCount = (indexAttr.count / 3) >>> 0;
|
|
91
|
+
else triCount = (vCount / 3) >>> 0;
|
|
92
|
+
if (vCount === 0 || triCount === 0) continue;
|
|
93
|
+
entries.push({ mesh, vCount, triCount, indexed: !!indexAttr });
|
|
94
|
+
totalVerts += vCount;
|
|
95
|
+
totalTris += triCount;
|
|
96
|
+
totalTriIndices += triCount * 3;
|
|
97
|
+
}
|
|
98
|
+
if (entries.length === 0) {
|
|
99
|
+
throw new Error('FacesSolid.manifoldFromFaces: no valid triangle meshes found');
|
|
100
|
+
}
|
|
101
|
+
if (DEBUG) console.log(`[FacesSolid] totals before weld: verts=${totalVerts}, tris=${totalTris}`);
|
|
102
|
+
|
|
103
|
+
// Weld vertices across meshes by exact-coordinate keys (no tolerance snapping).
|
|
104
|
+
// Accumulate canonical vertices and remap triangle indices accordingly.
|
|
105
|
+
const numProp = 3;
|
|
106
|
+
const faceInfo = {};
|
|
107
|
+
// No tolerance: use exact float string keys for positions
|
|
108
|
+
const keyOf = (x, y, z) => `${x},${y},${z}`;
|
|
109
|
+
const key2canon = new Map();
|
|
110
|
+
const canonPos = [];
|
|
111
|
+
let canonCount = 0;
|
|
112
|
+
const triVertsDyn = [];
|
|
113
|
+
const triLabelsDyn = [];
|
|
114
|
+
let nextLabel = 1;
|
|
115
|
+
const v = new THREE.Vector3();
|
|
116
|
+
|
|
117
|
+
for (const { mesh, vCount, triCount, indexed } of entries) {
|
|
118
|
+
const geom = mesh.geometry;
|
|
119
|
+
const posAttr = geom.getAttribute('position');
|
|
120
|
+
const indexAttr = geom.getIndex();
|
|
121
|
+
const label = nextLabel++;
|
|
122
|
+
const meshName = mesh.name || `Face_${label}`;
|
|
123
|
+
faceInfo[label] = { name: meshName };
|
|
124
|
+
|
|
125
|
+
// Build local map: original vertex index -> canonical index
|
|
126
|
+
const local2canon = new Uint32Array(vCount);
|
|
127
|
+
for (let i = 0; i < vCount; i++) {
|
|
128
|
+
v.fromBufferAttribute(posAttr, i).applyMatrix4(mesh.matrixWorld);
|
|
129
|
+
const key = keyOf(v.x, v.y, v.z);
|
|
130
|
+
let ci = key2canon.get(key);
|
|
131
|
+
if (ci == null) {
|
|
132
|
+
ci = canonCount++;
|
|
133
|
+
key2canon.set(key, ci);
|
|
134
|
+
canonPos.push(v.x, v.y, v.z);
|
|
135
|
+
}
|
|
136
|
+
local2canon[i] = ci;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (indexed && indexAttr) {
|
|
140
|
+
for (let k = 0; k < triCount; k++) {
|
|
141
|
+
const a = local2canon[indexAttr.getX(3 * k + 0) >>> 0];
|
|
142
|
+
const b = local2canon[indexAttr.getX(3 * k + 1) >>> 0];
|
|
143
|
+
const c = local2canon[indexAttr.getX(3 * k + 2) >>> 0];
|
|
144
|
+
if (a === b || b === c || c === a) continue; // drop degenerate
|
|
145
|
+
triVertsDyn.push(a, b, c);
|
|
146
|
+
triLabelsDyn.push(label);
|
|
147
|
+
}
|
|
148
|
+
} else {
|
|
149
|
+
for (let k = 0; k < triCount; k++) {
|
|
150
|
+
const a = local2canon[3 * k + 0];
|
|
151
|
+
const b = local2canon[3 * k + 1];
|
|
152
|
+
const c = local2canon[3 * k + 2];
|
|
153
|
+
if (a === b || b === c || c === a) continue;
|
|
154
|
+
triVertsDyn.push(a, b, c);
|
|
155
|
+
triLabelsDyn.push(label);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const vertProperties = new Float32Array(canonPos);
|
|
161
|
+
const triVerts = new Uint32Array(triVertsDyn);
|
|
162
|
+
const triLabels = new Uint32Array(triLabelsDyn);
|
|
163
|
+
// Extra sanity log: max index
|
|
164
|
+
let maxIndex = 0;
|
|
165
|
+
for (let i = 0; i < triVerts.length; i++) if (triVerts[i] > maxIndex) maxIndex = triVerts[i];
|
|
166
|
+
if (maxIndex >= (vertProperties.length / numProp)) {
|
|
167
|
+
console.error('[FacesSolid] index OOB before setArrays', { maxIndex, vCount: vertProperties.length / numProp });
|
|
168
|
+
}
|
|
169
|
+
const dropped = totalTris - triLabels.length;
|
|
170
|
+
if (DEBUG) console.log(`[FacesSolid] after weld: verts=${vertProperties.length / numProp}, tris=${triVerts.length / 3}, droppedDegenerate=${dropped}`);
|
|
171
|
+
|
|
172
|
+
// Install arrays onto this Solid; Manifold will be built on demand
|
|
173
|
+
this.setArrays({ numProp, vertProperties, triVerts, triLabels, faceInfo });
|
|
174
|
+
if (DEBUG) console.log('[FacesSolid] setArrays done:', { numProp, vCount: vertProperties.length / numProp, triCount: triVerts.length / 3 });
|
|
175
|
+
|
|
176
|
+
// Seed faceNames for provenance-aligned display
|
|
177
|
+
const inner = new Map();
|
|
178
|
+
for (const [labelStr, info] of Object.entries(faceInfo)) {
|
|
179
|
+
inner.set(Number(labelStr), info?.name ?? `Face_${labelStr}`);
|
|
180
|
+
}
|
|
181
|
+
const faceNames = new Map();
|
|
182
|
+
faceNames.set(this._originalID, inner);
|
|
183
|
+
this.faceNames = faceNames;
|
|
184
|
+
if (DEBUG) console.log('[FacesSolid] faceNames seeded for originalID', this._originalID, 'labels:', Array.from(inner.entries()));
|
|
185
|
+
|
|
186
|
+
return this;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Sweep: extrude a single Face by a vector (from a path or distance).
|
|
192
|
+
* - Caps use the input face triangles directly; start cap is reversed.
|
|
193
|
+
* - Side faces are generated per face edge (one face per input edge)
|
|
194
|
+
* and named `${edgeName}_SW`.
|
|
195
|
+
*/
|
|
196
|
+
export class Sweep extends FacesSolid {
|
|
197
|
+
/**
|
|
198
|
+
* @param {object} [opts]
|
|
199
|
+
* @param {import('./Face.js').Face} opts.face Base face/profile to sweep
|
|
200
|
+
* @param {any[]} [opts.sweepPathEdges=[]] Edges defining the sweep path
|
|
201
|
+
* @param {number} [opts.distance=1] Forward sweep distance
|
|
202
|
+
* @param {number} [opts.distanceBack=0] Backward sweep distance
|
|
203
|
+
* @param {'translate'|'rotate'|string} [opts.mode='translate'] Sweep mode
|
|
204
|
+
* @param {string} [opts.name='Sweep'] Name of the resulting solid
|
|
205
|
+
* @param {boolean} [opts.omitBaseCap=false] Whether to skip the base cap
|
|
206
|
+
* @param {number} [opts.twistAngle=0] Twist angle in degrees distributed along the path (pathAlign mode)
|
|
207
|
+
*/
|
|
208
|
+
constructor({ face, sweepPathEdges = [], distance = 1, distanceBack = 0, mode = 'translate', name = 'Sweep', omitBaseCap = false, twistAngle = 0 } = {}) {
|
|
209
|
+
super({ name });
|
|
210
|
+
this.params = { face, distance, distanceBack, sweepPathEdges, mode, name, omitBaseCap, twistAngle };
|
|
211
|
+
this.generate();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
generate() {
|
|
215
|
+
const { face, distance, distanceBack, sweepPathEdges, mode, omitBaseCap, twistAngle } = this.params;
|
|
216
|
+
if (!face || !face.geometry) return;
|
|
217
|
+
|
|
218
|
+
// Clear any existing children (visualization) and reset authoring arrays
|
|
219
|
+
for (let i = this.children.length - 1; i >= 0; --i) this.remove(this.children[i]);
|
|
220
|
+
// Reset Solid authoring state to rebuild fresh
|
|
221
|
+
this._numProp = 3;
|
|
222
|
+
this._vertProperties = [];
|
|
223
|
+
this._triVerts = [];
|
|
224
|
+
this._triIDs = [];
|
|
225
|
+
this._vertKeyToIndex = new Map();
|
|
226
|
+
this._faceNameToID = new Map();
|
|
227
|
+
this._idToFaceName = new Map();
|
|
228
|
+
this._dirty = true;
|
|
229
|
+
this._manifold = null;
|
|
230
|
+
this._faceIndex = null;
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
// Helper: robustly split a quad into two triangles choosing the better diagonal.
|
|
234
|
+
// Keeps outward orientation for non-holes and reverses for holes.
|
|
235
|
+
const addQuad = (faceName, A0, B0, B1, A1, isHole) => {
|
|
236
|
+
const v = (p, q) => new THREE.Vector3(q[0] - p[0], q[1] - p[1], q[2] - p[2]);
|
|
237
|
+
const areaTri = (a, b, c) => v(a, b).cross(v(a, c)).length();
|
|
238
|
+
// Two possible diagonals: d1 = A0-B1, d2 = A0-B0
|
|
239
|
+
const areaD1 = areaTri(A0, B0, B1) + areaTri(A0, B1, A1);
|
|
240
|
+
const areaD2 = areaTri(A0, B0, A1) + areaTri(B0, B1, A1);
|
|
241
|
+
const epsA = 1e-18;
|
|
242
|
+
if (!(areaD1 > epsA || areaD2 > epsA)) return; // fully degenerate
|
|
243
|
+
if (areaD2 > areaD1) {
|
|
244
|
+
if (isHole) {
|
|
245
|
+
this.addTriangle(faceName, A0, A1, B0);
|
|
246
|
+
this.addTriangle(faceName, B0, A1, B1);
|
|
247
|
+
} else {
|
|
248
|
+
this.addTriangle(faceName, A0, B0, A1);
|
|
249
|
+
this.addTriangle(faceName, B0, B1, A1);
|
|
250
|
+
}
|
|
251
|
+
} else {
|
|
252
|
+
if (isHole) {
|
|
253
|
+
this.addTriangle(faceName, A0, B1, B0);
|
|
254
|
+
this.addTriangle(faceName, A0, A1, B1);
|
|
255
|
+
} else {
|
|
256
|
+
this.addTriangle(faceName, A0, B0, B1);
|
|
257
|
+
this.addTriangle(faceName, A0, B1, A1);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
// Build a single combined path from multiple selected edges by chaining
|
|
263
|
+
// Matches both start and end points with tolerance and orders edges into
|
|
264
|
+
// a continuous polyline (prefers endpoints with degree 1 when available).
|
|
265
|
+
const combinePathPolylines = (edges, tol = 1e-5) => {
|
|
266
|
+
if (!Array.isArray(edges) || edges.length === 0) return [];
|
|
267
|
+
const polys = [];
|
|
268
|
+
for (const e of edges) {
|
|
269
|
+
const p = getEdgePolylineWorld(e);
|
|
270
|
+
if (p.length >= 2) polys.push(p);
|
|
271
|
+
}
|
|
272
|
+
if (polys.length === 0) return [];
|
|
273
|
+
|
|
274
|
+
// Derive an adaptive tolerance based on scale if caller used default
|
|
275
|
+
if (tol === 1e-5) {
|
|
276
|
+
let minX = Infinity, minY = Infinity, minZ = Infinity;
|
|
277
|
+
let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
|
|
278
|
+
const segLens = [];
|
|
279
|
+
for (const p of polys) {
|
|
280
|
+
for (let i = 0; i < p.length; i++) {
|
|
281
|
+
const v = p[i];
|
|
282
|
+
if (v[0] < minX) minX = v[0]; if (v[0] > maxX) maxX = v[0];
|
|
283
|
+
if (v[1] < minY) minY = v[1]; if (v[1] > maxY) maxY = v[1];
|
|
284
|
+
if (v[2] < minZ) minZ = v[2]; if (v[2] > maxZ) maxZ = v[2];
|
|
285
|
+
if (i > 0) {
|
|
286
|
+
const a = p[i - 1]; const b = v;
|
|
287
|
+
const dx = a[0] - b[0], dy = a[1] - b[1], dz = a[2] - b[2];
|
|
288
|
+
segLens.push(Math.hypot(dx, dy, dz));
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
const dx = maxX - minX, dy = maxY - minY, dz = maxZ - minZ;
|
|
293
|
+
const diag = Math.hypot(dx, dy, dz) || 1;
|
|
294
|
+
segLens.sort((a, b) => a - b);
|
|
295
|
+
const med = segLens.length ? segLens[(segLens.length >> 1)] : diag;
|
|
296
|
+
// Allow up to 0.1% of diag, capped to 10% of median segment length
|
|
297
|
+
const adaptive = Math.min(Math.max(1e-5, diag * 1e-3), med * 0.1);
|
|
298
|
+
tol = adaptive;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const tol2 = tol * tol;
|
|
302
|
+
const d2 = (a, b) => {
|
|
303
|
+
const dx = a[0] - b[0], dy = a[1] - b[1], dz = a[2] - b[2];
|
|
304
|
+
return dx * dx + dy * dy + dz * dz;
|
|
305
|
+
};
|
|
306
|
+
const q = (v) => [
|
|
307
|
+
Math.round(v[0] / tol) * tol,
|
|
308
|
+
Math.round(v[1] / tol) * tol,
|
|
309
|
+
Math.round(v[2] / tol) * tol,
|
|
310
|
+
];
|
|
311
|
+
const k = (v) => `${v[0]},${v[1]},${v[2]}`;
|
|
312
|
+
|
|
313
|
+
// Build endpoint graph: node key -> { p:[x,y,z], edges: Set(index) }
|
|
314
|
+
const nodes = new Map();
|
|
315
|
+
const endpoints = []; // [{sKey,eKey} per poly]
|
|
316
|
+
const addNode = (pt) => {
|
|
317
|
+
const qp = q(pt);
|
|
318
|
+
const key = k(qp);
|
|
319
|
+
if (!nodes.has(key)) nodes.set(key, { p: qp, edges: new Set() });
|
|
320
|
+
return key;
|
|
321
|
+
};
|
|
322
|
+
for (let i = 0; i < polys.length; i++) {
|
|
323
|
+
const p = polys[i];
|
|
324
|
+
const sKey = addNode(p[0]);
|
|
325
|
+
const eKey = addNode(p[p.length - 1]);
|
|
326
|
+
nodes.get(sKey).edges.add(i);
|
|
327
|
+
nodes.get(eKey).edges.add(i);
|
|
328
|
+
endpoints.push({ sKey, eKey });
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Pick a start: prefer a node with odd degree (open chain); else any
|
|
332
|
+
let startNodeKey = null;
|
|
333
|
+
for (const [key, val] of nodes.entries()) {
|
|
334
|
+
if ((val.edges.size % 2) === 1) { startNodeKey = key; break; }
|
|
335
|
+
}
|
|
336
|
+
if (!startNodeKey) startNodeKey = nodes.keys().next().value;
|
|
337
|
+
|
|
338
|
+
const used = new Array(polys.length).fill(false);
|
|
339
|
+
const chain = [];
|
|
340
|
+
|
|
341
|
+
// Helper to append a polyline ensuring joints aren’t duplicated
|
|
342
|
+
const appendPoly = (poly, reverse = false) => {
|
|
343
|
+
const pts = reverse ? poly.slice().reverse() : poly;
|
|
344
|
+
if (chain.length === 0) { chain.push(...pts); return; }
|
|
345
|
+
// remove duplicated joint
|
|
346
|
+
const last = chain[chain.length - 1];
|
|
347
|
+
const first = pts[0];
|
|
348
|
+
if (d2(last, first) <= tol2) chain.push(...pts.slice(1));
|
|
349
|
+
else chain.push(...pts);
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
// Grow forward from chosen start
|
|
353
|
+
let cursorKey = startNodeKey;
|
|
354
|
+
// If multiple edges at the start node, just pick one arbitrarily and then greedily continue
|
|
355
|
+
const tryConsumeFromNode = (nodeKey) => {
|
|
356
|
+
const node = nodes.get(nodeKey);
|
|
357
|
+
if (!node) return false;
|
|
358
|
+
for (const ei of Array.from(node.edges)) {
|
|
359
|
+
if (used[ei]) continue;
|
|
360
|
+
const { sKey, eKey } = endpoints[ei];
|
|
361
|
+
const forward = (sKey === nodeKey);
|
|
362
|
+
used[ei] = true;
|
|
363
|
+
// Remove this edge index from both endpoint sets for cleanliness
|
|
364
|
+
nodes.get(sKey)?.edges.delete(ei);
|
|
365
|
+
nodes.get(eKey)?.edges.delete(ei);
|
|
366
|
+
appendPoly(polys[ei], !forward); // if we enter at end, reverse to keep continuity
|
|
367
|
+
cursorKey = forward ? eKey : sKey;
|
|
368
|
+
return true;
|
|
369
|
+
}
|
|
370
|
+
return false;
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
// Seed chain: if start node has no edges (deg 0), bail
|
|
374
|
+
if (!tryConsumeFromNode(cursorKey)) {
|
|
375
|
+
// Fall back to simple greedy merge of all polylines
|
|
376
|
+
const simple = polys[0].slice();
|
|
377
|
+
const used2 = new Array(polys.length).fill(false); used2[0] = true;
|
|
378
|
+
let extended = true;
|
|
379
|
+
while (extended) {
|
|
380
|
+
extended = false;
|
|
381
|
+
for (let i = 1; i < polys.length; i++) {
|
|
382
|
+
if (used2[i]) continue;
|
|
383
|
+
const curStart = simple[0];
|
|
384
|
+
const curEnd = simple[simple.length - 1];
|
|
385
|
+
const p = polys[i];
|
|
386
|
+
const pStart = p[0];
|
|
387
|
+
const pEnd = p[p.length - 1];
|
|
388
|
+
if (d2(curEnd, pStart) <= tol2) { simple.push(...p.slice(1)); used2[i] = true; extended = true; continue; }
|
|
389
|
+
if (d2(curEnd, pEnd) <= tol2) { const rev = p.slice().reverse(); simple.push(...rev.slice(1)); used2[i] = true; extended = true; continue; }
|
|
390
|
+
if (d2(curStart, pEnd) <= tol2) { simple.unshift(...p.slice(0, p.length - 1)); used2[i] = true; extended = true; continue; }
|
|
391
|
+
if (d2(curStart, pStart) <= tol2) { const rev = p.slice().reverse(); simple.unshift(...rev.slice(0, rev.length - 1)); used2[i] = true; extended = true; continue; }
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
// de-dupe consecutive
|
|
395
|
+
for (let i = simple.length - 2; i >= 0; i--) { const a = simple[i], b = simple[i + 1]; if (a[0] === b[0] && a[1] === b[1] && a[2] === b[2]) simple.splice(i + 1, 1); }
|
|
396
|
+
return simple;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Continue consuming until stuck
|
|
400
|
+
while (tryConsumeFromNode(cursorKey)) { }
|
|
401
|
+
|
|
402
|
+
// If some edges remain unused (disconnected components), return the longest chain across components
|
|
403
|
+
let best = chain.slice();
|
|
404
|
+
for (let s = 0; s < polys.length; s++) {
|
|
405
|
+
if (used[s]) continue;
|
|
406
|
+
// Build a local chain from this unused edge
|
|
407
|
+
const localUsed = new Array(polys.length).fill(false);
|
|
408
|
+
const localChain = [];
|
|
409
|
+
const startForward = true; // arbitrary orientation
|
|
410
|
+
localUsed[s] = true;
|
|
411
|
+
const append = (poly, reverse = false) => {
|
|
412
|
+
const pts = reverse ? poly.slice().reverse() : poly;
|
|
413
|
+
if (localChain.length === 0) { localChain.push(...pts); return; }
|
|
414
|
+
const last = localChain[localChain.length - 1];
|
|
415
|
+
const first = pts[0];
|
|
416
|
+
if (d2(last, first) <= tol2) localChain.push(...pts.slice(1)); else localChain.push(...pts);
|
|
417
|
+
};
|
|
418
|
+
append(polys[s], !startForward);
|
|
419
|
+
let head = k(q(localChain[0]));
|
|
420
|
+
let tail = k(q(localChain[localChain.length - 1]));
|
|
421
|
+
let grew = true;
|
|
422
|
+
while (grew) {
|
|
423
|
+
grew = false;
|
|
424
|
+
for (let i = 0; i < polys.length; i++) {
|
|
425
|
+
if (localUsed[i]) continue;
|
|
426
|
+
const { sKey, eKey } = endpoints[i];
|
|
427
|
+
if (sKey === tail) { append(polys[i], false); tail = eKey; localUsed[i] = true; grew = true; continue; }
|
|
428
|
+
if (eKey === tail) { append(polys[i], true); tail = sKey; localUsed[i] = true; grew = true; continue; }
|
|
429
|
+
if (eKey === head) { const pts = polys[i].slice(); localChain.unshift(...pts.slice(0, pts.length - 1)); head = sKey; localUsed[i] = true; grew = true; continue; }
|
|
430
|
+
if (sKey === head) { const pts = polys[i].slice().reverse(); localChain.unshift(...pts.slice(0, pts.length - 1)); head = eKey; localUsed[i] = true; grew = true; continue; }
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
if (localChain.length > best.length) best = localChain;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Remove duplicate consecutive points in final result
|
|
437
|
+
for (let i = best.length - 2; i >= 0; i--) {
|
|
438
|
+
const a = best[i], b = best[i + 1];
|
|
439
|
+
if (a[0] === b[0] && a[1] === b[1] && a[2] === b[2]) best.splice(i + 1, 1);
|
|
440
|
+
}
|
|
441
|
+
return best;
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
// Determine whether to sweep along a path edge
|
|
445
|
+
let pathPts = [];
|
|
446
|
+
if (Array.isArray(sweepPathEdges) && sweepPathEdges.length > 0) {
|
|
447
|
+
const edges = sweepPathEdges.filter(Boolean);
|
|
448
|
+
if (edges.length > 0) pathPts = combinePathPolylines(edges);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Translate mode should only place cross sections at segment joints.
|
|
452
|
+
// For pathAlign we keep user's direction and joints; translate may simplify.
|
|
453
|
+
if (pathPts.length >= 2) {
|
|
454
|
+
if (mode === 'pathAlign') {
|
|
455
|
+
// no automatic reversal or heavy refinement here
|
|
456
|
+
} else {
|
|
457
|
+
// Simplify by removing collinear interior points
|
|
458
|
+
const isCollinear = (a, b, c, eps = 1e-12) => {
|
|
459
|
+
const abx = b[0] - a[0], aby = b[1] - a[1], abz = b[2] - a[2];
|
|
460
|
+
const bcx = c[0] - b[0], bcy = c[1] - b[1], bcz = c[2] - b[2];
|
|
461
|
+
const cx = aby * bcz - abz * bcy;
|
|
462
|
+
const cy = abz * bcx - abx * bcz;
|
|
463
|
+
const cz = abx * bcy - aby * bcx;
|
|
464
|
+
return (cx*cx + cy*cy + cz*cz) <= eps;
|
|
465
|
+
};
|
|
466
|
+
const simplified = [];
|
|
467
|
+
simplified.push(pathPts[0]);
|
|
468
|
+
for (let i = 1; i < pathPts.length - 1; i++) {
|
|
469
|
+
const prev = simplified[simplified.length - 1];
|
|
470
|
+
const cur = pathPts[i];
|
|
471
|
+
const next = pathPts[i + 1];
|
|
472
|
+
// Drop if exactly duplicated or strictly collinear between prev and next
|
|
473
|
+
if ((cur[0] === prev[0] && cur[1] === prev[1] && cur[2] === prev[2]) || isCollinear(prev, cur, next)) continue;
|
|
474
|
+
simplified.push(cur);
|
|
475
|
+
}
|
|
476
|
+
simplified.push(pathPts[pathPts.length - 1]);
|
|
477
|
+
// Remove any remaining consecutive duplicates
|
|
478
|
+
for (let i = simplified.length - 2; i >= 0; i--) {
|
|
479
|
+
const a = simplified[i], b = simplified[i + 1];
|
|
480
|
+
if (a[0] === b[0] && a[1] === b[1] && a[2] === b[2]) simplified.splice(i + 1, 1);
|
|
481
|
+
}
|
|
482
|
+
pathPts = simplified;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// For pathAlign, ensure path direction starts from the end closest to the profile.
|
|
487
|
+
if (pathPts.length >= 2 && mode === 'pathAlign') {
|
|
488
|
+
const profilePts = [];
|
|
489
|
+
const loops = Array.isArray(face?.userData?.boundaryLoopsWorld) ? face.userData.boundaryLoopsWorld : null;
|
|
490
|
+
if (loops && loops.length) {
|
|
491
|
+
const outerLoops = loops.filter(l => !l?.isHole);
|
|
492
|
+
const useLoops = outerLoops.length ? outerLoops : loops;
|
|
493
|
+
for (const loop of useLoops) {
|
|
494
|
+
const arr = Array.isArray(loop?.pts) ? loop.pts : loop;
|
|
495
|
+
if (!Array.isArray(arr)) continue;
|
|
496
|
+
for (const p of arr) {
|
|
497
|
+
if (Array.isArray(p) && p.length >= 3) profilePts.push([p[0], p[1], p[2]]);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
if (!profilePts.length) {
|
|
502
|
+
const posAttr = face?.geometry?.getAttribute?.('position');
|
|
503
|
+
if (posAttr) {
|
|
504
|
+
const v = new THREE.Vector3();
|
|
505
|
+
for (let i = 0; i < posAttr.count; i++) {
|
|
506
|
+
v.set(posAttr.getX(i), posAttr.getY(i), posAttr.getZ(i)).applyMatrix4(face.matrixWorld);
|
|
507
|
+
profilePts.push([v.x, v.y, v.z]);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
if (profilePts.length) {
|
|
512
|
+
const minD2 = (p) => {
|
|
513
|
+
let best = Infinity;
|
|
514
|
+
for (const q of profilePts) {
|
|
515
|
+
const dx = p[0] - q[0], dy = p[1] - q[1], dz = p[2] - q[2];
|
|
516
|
+
const d2 = dx * dx + dy * dy + dz * dz;
|
|
517
|
+
if (d2 < best) best = d2;
|
|
518
|
+
}
|
|
519
|
+
return best;
|
|
520
|
+
};
|
|
521
|
+
const start = pathPts[0];
|
|
522
|
+
const end = pathPts[pathPts.length - 1];
|
|
523
|
+
const startD = minD2(start);
|
|
524
|
+
const endD = minD2(end);
|
|
525
|
+
if (endD < startD) pathPts.reverse();
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Orient path to start near face centroid (translate mode only).
|
|
530
|
+
if (pathPts.length >= 2 && mode !== 'pathAlign') {
|
|
531
|
+
let centroid = null;
|
|
532
|
+
const loops = Array.isArray(face?.userData?.boundaryLoopsWorld) ? face.userData.boundaryLoopsWorld : null;
|
|
533
|
+
if (loops && loops.length) {
|
|
534
|
+
// use first outer loop (isHole !== true)
|
|
535
|
+
const outer = loops.find(l => !l.isHole) || loops[0];
|
|
536
|
+
const pts = Array.isArray(outer?.pts) ? outer.pts : outer;
|
|
537
|
+
if (Array.isArray(pts) && pts.length >= 3) {
|
|
538
|
+
centroid = new THREE.Vector3();
|
|
539
|
+
for (const p of pts) centroid.add(new THREE.Vector3(p[0], p[1], p[2]));
|
|
540
|
+
centroid.multiplyScalar(1 / pts.length);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
if (!centroid) {
|
|
544
|
+
// fallback to face geometry centroid
|
|
545
|
+
const posAttr = face?.geometry?.getAttribute?.('position');
|
|
546
|
+
if (posAttr) {
|
|
547
|
+
centroid = new THREE.Vector3();
|
|
548
|
+
const v = new THREE.Vector3();
|
|
549
|
+
for (let i = 0; i < posAttr.count; i++) {
|
|
550
|
+
v.set(posAttr.getX(i), posAttr.getY(i), posAttr.getZ(i)).applyMatrix4(face.matrixWorld);
|
|
551
|
+
centroid.add(v);
|
|
552
|
+
}
|
|
553
|
+
centroid.multiplyScalar(1 / Math.max(1, posAttr.count));
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
if (centroid) {
|
|
557
|
+
const d2 = (a, b) => { const dx = a[0] - b.x, dy = a[1] - b.y, dz = a[2] - b.z; return dx * dx + dy * dy + dz * dz; };
|
|
558
|
+
const startD = d2(pathPts[0], centroid);
|
|
559
|
+
const endD = d2(pathPts[pathPts.length - 1], centroid);
|
|
560
|
+
if (endD < startD) pathPts.reverse();
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Build offsets along path (relative to first point)
|
|
565
|
+
let offsets = [];
|
|
566
|
+
if (pathPts.length >= 2) {
|
|
567
|
+
const p0 = pathPts[0];
|
|
568
|
+
const rawOffsets = [];
|
|
569
|
+
for (let i = 0; i < pathPts.length; i++) {
|
|
570
|
+
const p = pathPts[i];
|
|
571
|
+
rawOffsets.push(new THREE.Vector3(p[0] - p0[0], p[1] - p0[1], p[2] - p0[2]));
|
|
572
|
+
}
|
|
573
|
+
// Collapse near-duplicate steps to avoid zero-area side faces
|
|
574
|
+
const filteredOffsets = [rawOffsets[0]];
|
|
575
|
+
const filteredPts = [pathPts[0]];
|
|
576
|
+
for (let i = 1; i < rawOffsets.length; i++) {
|
|
577
|
+
const prev = filteredOffsets[filteredOffsets.length - 1];
|
|
578
|
+
const cur = rawOffsets[i];
|
|
579
|
+
const d2 = cur.clone().sub(prev).lengthSq();
|
|
580
|
+
if (d2 > 1e-14) {
|
|
581
|
+
filteredOffsets.push(cur);
|
|
582
|
+
filteredPts.push(pathPts[i]);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
offsets = filteredOffsets;
|
|
586
|
+
pathPts = filteredPts;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Determine sweep vectors for cap translation only (single-shot extrude or end cap of path)
|
|
590
|
+
let dir = null; // forward vector (legacy name)
|
|
591
|
+
let dirF = null; // forward vector
|
|
592
|
+
let dirB = null; // backward vector (for two-sided extrude)
|
|
593
|
+
if (offsets.length >= 2) {
|
|
594
|
+
dir = offsets[offsets.length - 1].clone();
|
|
595
|
+
dirF = dir.clone();
|
|
596
|
+
} else if (distance instanceof THREE.Vector3) {
|
|
597
|
+
dir = distance.clone();
|
|
598
|
+
dirF = dir.clone();
|
|
599
|
+
} else if (typeof distance === 'number') {
|
|
600
|
+
const n = typeof face.getAverageNormal === 'function'
|
|
601
|
+
? face.getAverageNormal().clone()
|
|
602
|
+
: new THREE.Vector3(0, 1, 0);
|
|
603
|
+
dir = n.multiplyScalar(distance);
|
|
604
|
+
dirF = dir.clone();
|
|
605
|
+
} else {
|
|
606
|
+
dir = new THREE.Vector3(0, 1, 0);
|
|
607
|
+
dirF = dir.clone();
|
|
608
|
+
}
|
|
609
|
+
// Two-sided only applies to translate extrude (no path offsets)
|
|
610
|
+
// Two-sided: allow any non-zero signed back distance so start can be
|
|
611
|
+
// offset on either side of the base face.
|
|
612
|
+
const twoSided = (offsets.length < 2) && typeof distanceBack === 'number' && isFinite(distanceBack) && Math.abs(distanceBack) > 1e-12;
|
|
613
|
+
if (twoSided) {
|
|
614
|
+
// If the forward vector is extremely small (e.g. distance ~ 0 with a tiny
|
|
615
|
+
// bias from certain boolean modes), derive the back direction from the
|
|
616
|
+
// face normal instead of the sign of dirF to avoid flipping semantics.
|
|
617
|
+
const EPS_FWD = 1e-8;
|
|
618
|
+
let n = null;
|
|
619
|
+
if (dirF && dirF.length() > EPS_FWD) {
|
|
620
|
+
n = dirF.clone().normalize();
|
|
621
|
+
} else {
|
|
622
|
+
n = (typeof face.getAverageNormal === 'function') ? face.getAverageNormal().clone() : new THREE.Vector3(0, 1, 0);
|
|
623
|
+
if (n.lengthSq() < 1e-20) n.set(0, 1, 0);
|
|
624
|
+
n.normalize();
|
|
625
|
+
}
|
|
626
|
+
// Preserve the sign of distanceBack: positive means offset "behind"
|
|
627
|
+
// the base along -n; negative moves the start cap in the +n direction.
|
|
628
|
+
dirB = n.multiplyScalar(-distanceBack);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const featureTag = (this.params && this.params.name) ? `${this.params.name}:` : '';
|
|
632
|
+
const startName = `${featureTag}${face.name || 'Face'}_START`;
|
|
633
|
+
const endName = `${featureTag}${face.name || 'Face'}_END`;
|
|
634
|
+
|
|
635
|
+
const setFaceType = (name, faceType) => {
|
|
636
|
+
if (!name || !faceType) return;
|
|
637
|
+
try { this.setFaceMetadata(name, { faceType }); } catch { /* best effort */ }
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
setFaceType(startName, 'STARTCAP');
|
|
641
|
+
setFaceType(endName, 'ENDCAP');
|
|
642
|
+
|
|
643
|
+
// PathAlign uses rotation-minimizing frames to align the profile to the path.
|
|
644
|
+
|
|
645
|
+
// Prefer rebuilding caps using 2D profile groups from the sketch to ensure
|
|
646
|
+
// identical boundary vertices with side walls.
|
|
647
|
+
const groups = Array.isArray(face?.userData?.profileGroups) ? face.userData.profileGroups : null;
|
|
648
|
+
if (groups && groups.length) {
|
|
649
|
+
// Start cap: always uses original profile orientation (reverse winding)
|
|
650
|
+
for (const g of groups) {
|
|
651
|
+
const contour2D = g.contour2D || [];
|
|
652
|
+
const holes2D = g.holes2D || [];
|
|
653
|
+
const contourW = g.contourW || [];
|
|
654
|
+
const holesW = g.holesW || [];
|
|
655
|
+
if (contour2D.length < 3 || contourW.length !== contour2D.length) continue;
|
|
656
|
+
// triangulate using 2D; index into world array built as contourW + holesW
|
|
657
|
+
const contourV2 = contour2D.map(p => new THREE.Vector2(p[0], p[1]));
|
|
658
|
+
const holesV2 = holes2D.map(h => h.map(p => new THREE.Vector2(p[0], p[1])));
|
|
659
|
+
const tris = THREE.ShapeUtils.triangulateShape(contourV2, holesV2);
|
|
660
|
+
const allW = contourW.concat(...holesW);
|
|
661
|
+
for (const t of tris) {
|
|
662
|
+
const p0 = allW[t[0]], p1 = allW[t[1]], p2 = allW[t[2]];
|
|
663
|
+
if (mode !== 'pathAlign') {
|
|
664
|
+
if (twoSided && dirB) {
|
|
665
|
+
// Start cap at back offset (reversed orientation)
|
|
666
|
+
const b0 = [p0[0] + dirB.x, p0[1] + dirB.y, p0[2] + dirB.z];
|
|
667
|
+
const b1 = [p1[0] + dirB.x, p1[1] + dirB.y, p1[2] + dirB.z];
|
|
668
|
+
const b2 = [p2[0] + dirB.x, p2[1] + dirB.y, p2[2] + dirB.z];
|
|
669
|
+
// back-offset cap is never the base cap; always keep
|
|
670
|
+
this.addTriangle(startName, b0, b2, b1);
|
|
671
|
+
} else {
|
|
672
|
+
// Legacy: start cap at base
|
|
673
|
+
if (!omitBaseCap) this.addTriangle(startName, p0, p2, p1);
|
|
674
|
+
}
|
|
675
|
+
// End cap at forward offset
|
|
676
|
+
const q0 = [p0[0] + dirF.x, p0[1] + dirF.y, p0[2] + dirF.z];
|
|
677
|
+
const q1 = [p1[0] + dirF.x, p1[1] + dirF.y, p1[2] + dirF.z];
|
|
678
|
+
const q2 = [p2[0] + dirF.x, p2[1] + dirF.y, p2[2] + dirF.z];
|
|
679
|
+
// If forward vector is zero, this cap lies on the base face
|
|
680
|
+
const isEndBase = Math.abs(dirF.x) < 1e-20 && Math.abs(dirF.y) < 1e-20 && Math.abs(dirF.z) < 1e-20;
|
|
681
|
+
if (!(omitBaseCap && isEndBase)) this.addTriangle(endName, q0, q1, q2);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
} else {
|
|
686
|
+
// Fallback: use face geometry
|
|
687
|
+
const baseGeom = face.geometry;
|
|
688
|
+
const posAttr = baseGeom.getAttribute('position');
|
|
689
|
+
if (!posAttr) return;
|
|
690
|
+
const idxAttr = baseGeom.getIndex();
|
|
691
|
+
const hasIndex = !!idxAttr;
|
|
692
|
+
// Build world-space vertex array for the face once
|
|
693
|
+
const faceWorld = new Array(posAttr.count);
|
|
694
|
+
const v = new THREE.Vector3();
|
|
695
|
+
for (let i = 0; i < posAttr.count; i++) {
|
|
696
|
+
v.set(posAttr.getX(i), posAttr.getY(i), posAttr.getZ(i)).applyMatrix4(face.matrixWorld);
|
|
697
|
+
faceWorld[i] = [v.x, v.y, v.z];
|
|
698
|
+
}
|
|
699
|
+
// Translate-only caps; no path/frame alignment needed
|
|
700
|
+
|
|
701
|
+
const addCapTris = (i0, i1, i2) => {
|
|
702
|
+
const p0 = faceWorld[i0], p1 = faceWorld[i1], p2 = faceWorld[i2];
|
|
703
|
+
if (mode !== 'pathAlign') {
|
|
704
|
+
if (twoSided && dirB) {
|
|
705
|
+
const b0 = [p0[0] + dirB.x, p0[1] + dirB.y, p0[2] + dirB.z];
|
|
706
|
+
const b1 = [p1[0] + dirB.x, p1[1] + dirB.y, p1[2] + dirB.z];
|
|
707
|
+
const b2 = [p2[0] + dirB.x, p2[1] + dirB.y, p2[2] + dirB.z];
|
|
708
|
+
// back-offset cap is not at base; always keep
|
|
709
|
+
this.addTriangle(startName, b0, b2, b1);
|
|
710
|
+
} else {
|
|
711
|
+
if (!omitBaseCap) this.addTriangle(startName, p0, p2, p1);
|
|
712
|
+
}
|
|
713
|
+
const q0 = [p0[0] + dirF.x, p0[1] + dirF.y, p0[2] + dirF.z];
|
|
714
|
+
const q1 = [p1[0] + dirF.x, p1[1] + dirF.y, p1[2] + dirF.z];
|
|
715
|
+
const q2 = [p2[0] + dirF.x, p2[1] + dirF.y, p2[2] + dirF.z];
|
|
716
|
+
const isEndBase = Math.abs(dirF.x) < 1e-20 && Math.abs(dirF.y) < 1e-20 && Math.abs(dirF.z) < 1e-20;
|
|
717
|
+
if (!(omitBaseCap && isEndBase)) this.addTriangle(endName, q0, q1, q2);
|
|
718
|
+
}
|
|
719
|
+
};
|
|
720
|
+
if (hasIndex) {
|
|
721
|
+
for (let t = 0; t < idxAttr.count; t += 3) {
|
|
722
|
+
const i0 = idxAttr.getX(t + 0) >>> 0;
|
|
723
|
+
const i1 = idxAttr.getX(t + 1) >>> 0;
|
|
724
|
+
const i2 = idxAttr.getX(t + 2) >>> 0;
|
|
725
|
+
addCapTris(i0, i1, i2);
|
|
726
|
+
}
|
|
727
|
+
} else {
|
|
728
|
+
const triCount = (posAttr.count / 3) >>> 0;
|
|
729
|
+
for (let t = 0; t < triCount; t++) {
|
|
730
|
+
const i0 = 3 * t + 0, i1 = 3 * t + 1, i2 = 3 * t + 2;
|
|
731
|
+
addCapTris(i0, i1, i2);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
const isCylindricalSketchEdge = (edge) => {
|
|
737
|
+
if (!edge || !edge.userData) return false;
|
|
738
|
+
const kind = edge.userData.sketchGeomType;
|
|
739
|
+
if (kind === 'circle' && typeof edge.userData.circleRadius === 'number') return edge.userData.circleRadius > 0;
|
|
740
|
+
if (kind === 'arc' && typeof edge.userData.arcRadius === 'number') return edge.userData.arcRadius > 0;
|
|
741
|
+
return false;
|
|
742
|
+
};
|
|
743
|
+
|
|
744
|
+
const canEmbedCylMetadata = (mode === 'translate') && !(offsets.length >= 2);
|
|
745
|
+
const cylMetadataByName = new Map();
|
|
746
|
+
const edgeSourceByName = new Map();
|
|
747
|
+
const registerEdgeSource = (faceName, edge) => {
|
|
748
|
+
if (!faceName || !edge) return;
|
|
749
|
+
if (!edgeSourceByName.has(faceName)) {
|
|
750
|
+
edgeSourceByName.set(faceName, edge?.name || 'EDGE');
|
|
751
|
+
}
|
|
752
|
+
};
|
|
753
|
+
const registerCylMetadata = (name, meta) => {
|
|
754
|
+
if (!name || !meta) return;
|
|
755
|
+
if (!Number.isFinite(meta.radius) || meta.radius <= 0) return;
|
|
756
|
+
if (!cylMetadataByName.has(name)) {
|
|
757
|
+
cylMetadataByName.set(name, meta);
|
|
758
|
+
try { this.setFaceMetadata(name, meta); } catch { }
|
|
759
|
+
}
|
|
760
|
+
};
|
|
761
|
+
|
|
762
|
+
const computeCylMetadataForEdge = (edge) => {
|
|
763
|
+
if (!canEmbedCylMetadata || !edge) return null;
|
|
764
|
+
const kind = edge.userData?.sketchGeomType;
|
|
765
|
+
let radius = null;
|
|
766
|
+
let centerArr = null;
|
|
767
|
+
if (kind === 'circle') {
|
|
768
|
+
radius = edge.userData?.circleRadius;
|
|
769
|
+
centerArr = edge.userData?.circleCenter;
|
|
770
|
+
} else if (kind === 'arc') {
|
|
771
|
+
radius = edge.userData?.arcRadius;
|
|
772
|
+
centerArr = edge.userData?.arcCenter;
|
|
773
|
+
} else {
|
|
774
|
+
return null;
|
|
775
|
+
}
|
|
776
|
+
if (!Array.isArray(centerArr) || !Number.isFinite(radius) || radius <= 0) return null;
|
|
777
|
+
const center = new THREE.Vector3(centerArr[0], centerArr[1], centerArr[2]);
|
|
778
|
+
if (!edge?.userData?.polylineWorld && edge?.matrixWorld) center.applyMatrix4(edge.matrixWorld);
|
|
779
|
+
const forwardVec = dirF ? dirF.clone() : new THREE.Vector3(0, 0, 0);
|
|
780
|
+
const backwardVec = dirB ? dirB.clone() : new THREE.Vector3(0, 0, 0);
|
|
781
|
+
const startPoint = center.clone().add(backwardVec);
|
|
782
|
+
const endPoint = center.clone().add(forwardVec);
|
|
783
|
+
const axisVec = endPoint.clone().sub(startPoint);
|
|
784
|
+
let height = axisVec.length();
|
|
785
|
+
let axisDir;
|
|
786
|
+
if (height > 1e-9) {
|
|
787
|
+
axisDir = axisVec.clone().normalize();
|
|
788
|
+
} else {
|
|
789
|
+
axisDir = forwardVec.clone();
|
|
790
|
+
if (axisDir.lengthSq() < 1e-12) axisDir = new THREE.Vector3(0, 1, 0);
|
|
791
|
+
axisDir.normalize();
|
|
792
|
+
if (!Number.isFinite(height) || height <= 1e-9) height = forwardVec.length();
|
|
793
|
+
}
|
|
794
|
+
if (!Number.isFinite(height)) height = 0;
|
|
795
|
+
const axisCenter = startPoint.clone().addScaledVector(axisVec, 0.5);
|
|
796
|
+
return {
|
|
797
|
+
type: 'cylindrical',
|
|
798
|
+
radius,
|
|
799
|
+
height,
|
|
800
|
+
axis: [axisDir.x, axisDir.y, axisDir.z],
|
|
801
|
+
center: [axisCenter.x, axisCenter.y, axisCenter.z],
|
|
802
|
+
};
|
|
803
|
+
};
|
|
804
|
+
|
|
805
|
+
if (canEmbedCylMetadata && Array.isArray(face?.edges)) {
|
|
806
|
+
for (const edge of face.edges) {
|
|
807
|
+
if (!isCylindricalSketchEdge(edge)) continue;
|
|
808
|
+
const meta = computeCylMetadataForEdge(edge);
|
|
809
|
+
if (!meta) continue;
|
|
810
|
+
const edgeName = `${featureTag}${edge?.name || 'EDGE'}_SW`;
|
|
811
|
+
registerCylMetadata(edgeName, meta);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
const ensureMetadataForName = (name) => {
|
|
816
|
+
if (!name) return;
|
|
817
|
+
const meta = cylMetadataByName.get(name);
|
|
818
|
+
if (meta) {
|
|
819
|
+
try { this.setFaceMetadata(name, meta); } catch { }
|
|
820
|
+
}
|
|
821
|
+
const sourceEdgeName = edgeSourceByName.get(name);
|
|
822
|
+
if (sourceEdgeName) {
|
|
823
|
+
try { this.setFaceMetadata(name, { sourceEdgeName }); } catch { }
|
|
824
|
+
}
|
|
825
|
+
};
|
|
826
|
+
|
|
827
|
+
// Side faces: Prefer boundary loops to ensure vertex matching with caps.
|
|
828
|
+
// This avoids T-junctions and ensures a watertight manifold. If loops are
|
|
829
|
+
// unavailable (legacy faces), fall back to per-edge polylines.
|
|
830
|
+
// Try boundary loops from sketch metadata; otherwise reconstruct from face triangles
|
|
831
|
+
let boundaryLoops = Array.isArray(face?.userData?.boundaryLoopsWorld) ? face.userData.boundaryLoopsWorld : null;
|
|
832
|
+
const computeBoundaryLoopsFromFace = (faceObj) => {
|
|
833
|
+
const loops = [];
|
|
834
|
+
const geom = faceObj?.geometry; if (!geom) return loops;
|
|
835
|
+
const pos = geom.getAttribute && geom.getAttribute('position'); if (!pos) return loops;
|
|
836
|
+
const idx = geom.getIndex && geom.getIndex();
|
|
837
|
+
// World-space vertices
|
|
838
|
+
const world = new Array(pos.count);
|
|
839
|
+
const v = new THREE.Vector3();
|
|
840
|
+
for (let i = 0; i < pos.count; i++) { v.set(pos.getX(i), pos.getY(i), pos.getZ(i)).applyMatrix4(faceObj.matrixWorld); world[i] = [v.x, v.y, v.z]; }
|
|
841
|
+
// Canonicalize coincident vertices (handles non-indexed geometry):
|
|
842
|
+
// Map unique world positions -> canonical vertex index used for boundary detection.
|
|
843
|
+
const keyOf = (p) => `${p[0].toFixed(7)},${p[1].toFixed(7)},${p[2].toFixed(7)}`;
|
|
844
|
+
const canonMap = new Map(); // key -> canonical index
|
|
845
|
+
const canonPts = []; // canonical index -> world point
|
|
846
|
+
const origToCanon = new Array(world.length);
|
|
847
|
+
for (let i = 0; i < world.length; i++) {
|
|
848
|
+
const k = keyOf(world[i]);
|
|
849
|
+
let ci = canonMap.get(k);
|
|
850
|
+
if (ci === undefined) { ci = canonPts.length; canonMap.set(k, ci); canonPts.push(world[i]); }
|
|
851
|
+
origToCanon[i] = ci;
|
|
852
|
+
}
|
|
853
|
+
// Count undirected triangle edges
|
|
854
|
+
const edgeCount = new Map(); // key min,max -> count
|
|
855
|
+
const triIter = (cb)=>{
|
|
856
|
+
if (idx) { for (let t=0;t<idx.count;t+=3){ cb(idx.getX(t+0)>>>0, idx.getX(t+1)>>>0, idx.getX(t+2)>>>0); } }
|
|
857
|
+
else { const triCount=(pos.count/3)|0; for(let t=0;t<triCount;t++){ cb(3*t+0,3*t+1,3*t+2); } }
|
|
858
|
+
};
|
|
859
|
+
const inc = (a,b)=>{
|
|
860
|
+
// Use canonical indices so shared positions are treated as one vertex
|
|
861
|
+
const A = origToCanon[a] >>> 0; const B = origToCanon[b] >>> 0;
|
|
862
|
+
const i=Math.min(A,B), j=Math.max(A,B); const k=`${i},${j}`;
|
|
863
|
+
edgeCount.set(k, (edgeCount.get(k)||0)+1);
|
|
864
|
+
};
|
|
865
|
+
triIter((i0,i1,i2)=>{ inc(i0,i1); inc(i1,i2); inc(i2,i0); });
|
|
866
|
+
// Keep only boundary edges (count==1) and build adjacency for both directions
|
|
867
|
+
const adj = new Map(); // index -> Set(neighbor indices)
|
|
868
|
+
const addAdj = (a,b)=>{ let s=adj.get(a); if(!s){ s=new Set(); adj.set(a,s);} s.add(b); };
|
|
869
|
+
for (const [k,c] of edgeCount.entries()) {
|
|
870
|
+
if (c === 1) {
|
|
871
|
+
const [iStr, jStr] = k.split(','); const i = Number(iStr), j = Number(jStr);
|
|
872
|
+
addAdj(i,j); addAdj(j,i);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
// Walk loops by following neighbors not equal to previous
|
|
876
|
+
const visited = new Set(); // canonical edge keys "i,j" with i<j
|
|
877
|
+
const edgeKey = (a,b)=>{ const i=Math.min(a,b), j=Math.max(a,b); return `${i},${j}`; };
|
|
878
|
+
for (const [a, neigh] of adj.entries()) {
|
|
879
|
+
for (const b of neigh) {
|
|
880
|
+
const k = edgeKey(a,b); if (visited.has(k)) continue;
|
|
881
|
+
const ring = [a, b];
|
|
882
|
+
visited.add(k);
|
|
883
|
+
let prev = a, cur = b, guard = 0;
|
|
884
|
+
while (guard++ < 100000) {
|
|
885
|
+
const nset = adj.get(cur) || new Set();
|
|
886
|
+
// Choose the next neighbor that's not where we came from
|
|
887
|
+
let next = null; for (const n of nset) { if (n !== prev) { next = n; break; } }
|
|
888
|
+
if (next == null) break;
|
|
889
|
+
const kk = edgeKey(cur, next); if (visited.has(kk)) break;
|
|
890
|
+
visited.add(kk);
|
|
891
|
+
ring.push(next);
|
|
892
|
+
prev = cur; cur = next;
|
|
893
|
+
if (cur === ring[0]) break; // closed
|
|
894
|
+
}
|
|
895
|
+
if (ring.length >= 3) {
|
|
896
|
+
// Dedup consecutive duplicates and convert to points
|
|
897
|
+
const pts = [];
|
|
898
|
+
for (let i = 0; i < ring.length; i++) {
|
|
899
|
+
const p = canonPts[ring[i]];
|
|
900
|
+
if (pts.length) { const q = pts[pts.length - 1]; if (q[0]===p[0] && q[1]===p[1] && q[2]===p[2]) continue; }
|
|
901
|
+
pts.push([p[0], p[1], p[2]]);
|
|
902
|
+
}
|
|
903
|
+
if (pts.length >= 3) loops.push({ pts, isHole: false });
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
// Classify holes by signed area in the face plane
|
|
908
|
+
if (loops.length) {
|
|
909
|
+
const n = (typeof faceObj.getAverageNormal === 'function') ? faceObj.getAverageNormal().clone() : new THREE.Vector3(0,0,1);
|
|
910
|
+
if (n.lengthSq() < 1e-20) n.set(0,0,1); n.normalize();
|
|
911
|
+
let ux = new THREE.Vector3(1,0,0); if (Math.abs(n.dot(ux)) > 0.99) ux.set(0,1,0);
|
|
912
|
+
const U = new THREE.Vector3().crossVectors(n, ux).normalize();
|
|
913
|
+
const V = new THREE.Vector3().crossVectors(n, U).normalize();
|
|
914
|
+
const area2 = (arr)=>{ let a=0; for (let i=0;i<arr.length;i++){ const p=arr[i], q=arr[(i+1)%arr.length]; a += (p.x*q.y - q.x*p.y); } return 0.5*a; };
|
|
915
|
+
const loopAreas = loops.map(loop => {
|
|
916
|
+
const v2 = loop.pts.map(P => new THREE.Vector2(new THREE.Vector3(P[0],P[1],P[2]).sub(new THREE.Vector3()).dot(U), new THREE.Vector3(P[0],P[1],P[2]).dot(V)));
|
|
917
|
+
return area2(v2);
|
|
918
|
+
});
|
|
919
|
+
let outerIdx = 0; let outerAbs = 0; for (let i=0;i<loopAreas.length;i++){ const ab = Math.abs(loopAreas[i]); if (ab>outerAbs){ outerAbs=ab; outerIdx=i; } }
|
|
920
|
+
const outerSign = Math.sign(loopAreas[outerIdx] || 1);
|
|
921
|
+
for (let i=0;i<loops.length;i++){ const sign = Math.sign(loopAreas[i] || 0); loops[i].isHole = (sign !== outerSign); }
|
|
922
|
+
}
|
|
923
|
+
return loops;
|
|
924
|
+
};
|
|
925
|
+
if (!boundaryLoops || !boundaryLoops.length) boundaryLoops = computeBoundaryLoopsFromFace(face);
|
|
926
|
+
const doPathSweep = offsets.length >= 2;
|
|
927
|
+
|
|
928
|
+
const getFaceWorldPoints = () => {
|
|
929
|
+
const posAttr = face?.geometry?.getAttribute?.('position');
|
|
930
|
+
if (!posAttr) return [];
|
|
931
|
+
const v = new THREE.Vector3();
|
|
932
|
+
const pts = new Array(posAttr.count);
|
|
933
|
+
for (let i = 0; i < posAttr.count; i++) {
|
|
934
|
+
v.set(posAttr.getX(i), posAttr.getY(i), posAttr.getZ(i)).applyMatrix4(face.matrixWorld);
|
|
935
|
+
pts[i] = [v.x, v.y, v.z];
|
|
936
|
+
}
|
|
937
|
+
return pts;
|
|
938
|
+
};
|
|
939
|
+
|
|
940
|
+
const computeProfileBasis = (loops, fallbackPts) => {
|
|
941
|
+
let baseZ = (typeof face.getAverageNormal === 'function') ? face.getAverageNormal().clone() : new THREE.Vector3(0, 0, 1);
|
|
942
|
+
if (!baseZ || !isFinite(baseZ.x) || baseZ.lengthSq() < 1e-20) baseZ = new THREE.Vector3(0, 0, 1);
|
|
943
|
+
baseZ.normalize();
|
|
944
|
+
|
|
945
|
+
let outerPts = null;
|
|
946
|
+
if (loops && loops.length) {
|
|
947
|
+
const outerLoop = loops.find(l => !l.isHole) || loops[0];
|
|
948
|
+
const pts = Array.isArray(outerLoop?.pts) ? outerLoop.pts : outerLoop;
|
|
949
|
+
if (Array.isArray(pts) && pts.length) outerPts = pts;
|
|
950
|
+
}
|
|
951
|
+
const candidates = (outerPts && outerPts.length) ? outerPts : (fallbackPts || []);
|
|
952
|
+
const centroidOf = (arr) => {
|
|
953
|
+
const c = new THREE.Vector3();
|
|
954
|
+
for (const p of arr) c.add(new THREE.Vector3(p[0], p[1], p[2]));
|
|
955
|
+
return c.multiplyScalar(1 / arr.length);
|
|
956
|
+
};
|
|
957
|
+
let baseOriginW = null;
|
|
958
|
+
if (outerPts && outerPts.length) baseOriginW = centroidOf(outerPts);
|
|
959
|
+
else if (candidates.length) baseOriginW = centroidOf(candidates);
|
|
960
|
+
else baseOriginW = new THREE.Vector3(0, 0, 0);
|
|
961
|
+
|
|
962
|
+
let anchorWorld = null;
|
|
963
|
+
if (candidates.length) {
|
|
964
|
+
let bestD2 = -1; let best = candidates[0];
|
|
965
|
+
for (const p of candidates) {
|
|
966
|
+
const dx = p[0] - baseOriginW.x, dy = p[1] - baseOriginW.y, dz = p[2] - baseOriginW.z;
|
|
967
|
+
const d2 = dx*dx + dy*dy + dz*dz;
|
|
968
|
+
if (d2 > bestD2) { bestD2 = d2; best = p; }
|
|
969
|
+
}
|
|
970
|
+
anchorWorld = new THREE.Vector3(best[0], best[1], best[2]);
|
|
971
|
+
} else {
|
|
972
|
+
anchorWorld = baseOriginW.clone();
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
let baseX = anchorWorld.clone().sub(baseOriginW);
|
|
976
|
+
baseX.addScaledVector(baseZ, -baseX.dot(baseZ));
|
|
977
|
+
if (baseX.lengthSq() < 1e-12) {
|
|
978
|
+
baseX = new THREE.Vector3(1, 0, 0);
|
|
979
|
+
if (Math.abs(baseX.dot(baseZ)) > 0.9) baseX.set(0, 1, 0);
|
|
980
|
+
baseX.addScaledVector(baseZ, -baseX.dot(baseZ));
|
|
981
|
+
}
|
|
982
|
+
baseX.normalize();
|
|
983
|
+
let baseY = new THREE.Vector3().crossVectors(baseZ, baseX).normalize();
|
|
984
|
+
baseX = new THREE.Vector3().crossVectors(baseY, baseZ).normalize();
|
|
985
|
+
|
|
986
|
+
return { baseOriginW, baseX, baseY, baseZ, anchorWorld, outerPts };
|
|
987
|
+
};
|
|
988
|
+
|
|
989
|
+
const computePathTangents = (P) => {
|
|
990
|
+
const T = new Array(P.length);
|
|
991
|
+
const EPS = 1e-12;
|
|
992
|
+
for (let i = 0; i < P.length; i++) {
|
|
993
|
+
let t = null;
|
|
994
|
+
if (i === 0) {
|
|
995
|
+
t = P[1].clone().sub(P[0]);
|
|
996
|
+
} else if (i === P.length - 1) {
|
|
997
|
+
t = P[i].clone().sub(P[i - 1]);
|
|
998
|
+
} else {
|
|
999
|
+
const tPrev = P[i].clone().sub(P[i - 1]);
|
|
1000
|
+
const tNext = P[i + 1].clone().sub(P[i]);
|
|
1001
|
+
if (tPrev.lengthSq() < EPS) t = tNext;
|
|
1002
|
+
else if (tNext.lengthSq() < EPS) t = tPrev;
|
|
1003
|
+
else t = tPrev.normalize().add(tNext.normalize());
|
|
1004
|
+
}
|
|
1005
|
+
if (!t || t.lengthSq() < EPS) t = new THREE.Vector3(0, 0, 1);
|
|
1006
|
+
else t.normalize();
|
|
1007
|
+
T[i] = t;
|
|
1008
|
+
}
|
|
1009
|
+
return T;
|
|
1010
|
+
};
|
|
1011
|
+
|
|
1012
|
+
const computeRMFFrames = (P, baseX, baseY, baseZ, tangents = null) => {
|
|
1013
|
+
if (!P || P.length < 2) return null;
|
|
1014
|
+
const T = (Array.isArray(tangents) && tangents.length === P.length)
|
|
1015
|
+
? tangents
|
|
1016
|
+
: computePathTangents(P);
|
|
1017
|
+
const frames = new Array(P.length);
|
|
1018
|
+
let X = baseX.clone();
|
|
1019
|
+
let Y = baseY.clone();
|
|
1020
|
+
let Z = baseZ.clone();
|
|
1021
|
+
frames[0] = { origin: P[0].clone(), X: X.clone(), Y: Y.clone(), Z: Z.clone(), tangent: T[0]?.clone?.() };
|
|
1022
|
+
const EPS = 1e-12;
|
|
1023
|
+
for (let i = 1; i < P.length; i++) {
|
|
1024
|
+
const tPrev = T[i - 1];
|
|
1025
|
+
const t = T[i];
|
|
1026
|
+
const axis = new THREE.Vector3().crossVectors(tPrev, t);
|
|
1027
|
+
const sin = axis.length();
|
|
1028
|
+
const cos = Math.max(-1, Math.min(1, tPrev.dot(t)));
|
|
1029
|
+
if (sin < EPS) {
|
|
1030
|
+
if (cos < 0) {
|
|
1031
|
+
let rotAxis = X.clone();
|
|
1032
|
+
rotAxis.addScaledVector(tPrev, -rotAxis.dot(tPrev));
|
|
1033
|
+
if (rotAxis.lengthSq() < EPS) {
|
|
1034
|
+
rotAxis = Y.clone();
|
|
1035
|
+
rotAxis.addScaledVector(tPrev, -rotAxis.dot(tPrev));
|
|
1036
|
+
}
|
|
1037
|
+
if (rotAxis.lengthSq() < EPS) {
|
|
1038
|
+
rotAxis = new THREE.Vector3(1, 0, 0).cross(tPrev);
|
|
1039
|
+
}
|
|
1040
|
+
if (rotAxis.lengthSq() >= EPS) {
|
|
1041
|
+
rotAxis.normalize();
|
|
1042
|
+
const q = new THREE.Quaternion().setFromAxisAngle(rotAxis, Math.PI);
|
|
1043
|
+
X.applyQuaternion(q);
|
|
1044
|
+
Y.applyQuaternion(q);
|
|
1045
|
+
Z.applyQuaternion(q);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
} else {
|
|
1049
|
+
axis.normalize();
|
|
1050
|
+
const angle = Math.atan2(sin, cos);
|
|
1051
|
+
const q = new THREE.Quaternion().setFromAxisAngle(axis, angle);
|
|
1052
|
+
X.applyQuaternion(q);
|
|
1053
|
+
Y.applyQuaternion(q);
|
|
1054
|
+
Z.applyQuaternion(q);
|
|
1055
|
+
}
|
|
1056
|
+
frames[i] = { origin: P[i].clone(), X: X.clone(), Y: Y.clone(), Z: Z.clone(), tangent: T[i]?.clone?.() };
|
|
1057
|
+
}
|
|
1058
|
+
return frames;
|
|
1059
|
+
};
|
|
1060
|
+
|
|
1061
|
+
const buildPathAlignContext = () => {
|
|
1062
|
+
const P = pathPts.map(p => new THREE.Vector3(p[0], p[1], p[2]));
|
|
1063
|
+
if (P.length < 2) return null;
|
|
1064
|
+
const basis = computeProfileBasis(boundaryLoops, getFaceWorldPoints());
|
|
1065
|
+
if (!basis) return null;
|
|
1066
|
+
let { baseOriginW, baseX, baseY, baseZ, anchorWorld, outerPts } = basis;
|
|
1067
|
+
const tangents = computePathTangents(P);
|
|
1068
|
+
const T0 = tangents[0];
|
|
1069
|
+
if (T0 && baseZ && baseZ.dot(T0) < 0) {
|
|
1070
|
+
baseZ = baseZ.clone().multiplyScalar(-1);
|
|
1071
|
+
baseY = baseY.clone().multiplyScalar(-1);
|
|
1072
|
+
}
|
|
1073
|
+
const frames = computeRMFFrames(P, baseX, baseY, baseZ, tangents);
|
|
1074
|
+
if (!frames || frames.length < 2) return null;
|
|
1075
|
+
|
|
1076
|
+
const P0 = P[0].clone();
|
|
1077
|
+
for (let i = 0; i < frames.length; i++) {
|
|
1078
|
+
const off = P[i].clone().sub(P0);
|
|
1079
|
+
frames[i].origin = baseOriginW.clone().add(off);
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
const uvCache = new Map();
|
|
1083
|
+
const uvOf = (pArr) => {
|
|
1084
|
+
const k = `${pArr[0].toFixed(6)},${pArr[1].toFixed(6)},${pArr[2].toFixed(6)}`;
|
|
1085
|
+
const cached = uvCache.get(k);
|
|
1086
|
+
if (cached) return cached;
|
|
1087
|
+
const v = new THREE.Vector3(pArr[0] - baseOriginW.x, pArr[1] - baseOriginW.y, pArr[2] - baseOriginW.z);
|
|
1088
|
+
const u = v.dot(baseX);
|
|
1089
|
+
const w = v.dot(baseY);
|
|
1090
|
+
const uv = [u, w];
|
|
1091
|
+
uvCache.set(k, uv);
|
|
1092
|
+
return uv;
|
|
1093
|
+
};
|
|
1094
|
+
|
|
1095
|
+
let lockU = 0, lockV = 0;
|
|
1096
|
+
const lockCandidates = (Array.isArray(outerPts) && outerPts.length) ? outerPts : getFaceWorldPoints();
|
|
1097
|
+
if (Array.isArray(lockCandidates) && lockCandidates.length) {
|
|
1098
|
+
let farD = -1; let far = lockCandidates[0];
|
|
1099
|
+
for (const p of lockCandidates) {
|
|
1100
|
+
const uv = uvOf(p);
|
|
1101
|
+
const d = uv[0] * uv[0] + uv[1] * uv[1];
|
|
1102
|
+
if (d > farD) { farD = d; far = p; }
|
|
1103
|
+
}
|
|
1104
|
+
const uvF = uvOf(far);
|
|
1105
|
+
lockU = uvF[0];
|
|
1106
|
+
lockV = uvF[1];
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
if ((lockU*lockU + lockV*lockV) > 1e-20) {
|
|
1110
|
+
for (let i = 1; i < frames.length; i++) {
|
|
1111
|
+
const prevVec = new THREE.Vector3().addScaledVector(frames[i - 1].X, lockU).addScaledVector(frames[i - 1].Y, lockV);
|
|
1112
|
+
const currVec = new THREE.Vector3().addScaledVector(frames[i].X, lockU).addScaledVector(frames[i].Y, lockV);
|
|
1113
|
+
if (prevVec.lengthSq() > 1e-24 && currVec.lengthSq() > 1e-24) {
|
|
1114
|
+
if (currVec.normalize().dot(prevVec.normalize()) < 0) {
|
|
1115
|
+
frames[i].X.multiplyScalar(-1);
|
|
1116
|
+
frames[i].Y.multiplyScalar(-1);
|
|
1117
|
+
frames[i].Z.multiplyScalar(-1);
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
// Apply an optional user twist distributed by arc length so it is uniform
|
|
1124
|
+
// along the path regardless of segment lengths.
|
|
1125
|
+
const twistDeg = Number(twistAngle);
|
|
1126
|
+
const twistRad = Number.isFinite(twistDeg) ? THREE.MathUtils.degToRad(twistDeg) : 0;
|
|
1127
|
+
if (Math.abs(twistRad) > 1e-12 && frames.length >= 2) {
|
|
1128
|
+
const cumulative = new Array(P.length);
|
|
1129
|
+
cumulative[0] = 0;
|
|
1130
|
+
let totalLen = 0;
|
|
1131
|
+
for (let i = 1; i < P.length; i++) {
|
|
1132
|
+
totalLen += P[i].distanceTo(P[i - 1]);
|
|
1133
|
+
cumulative[i] = totalLen;
|
|
1134
|
+
}
|
|
1135
|
+
const invTotal = totalLen > 1e-12 ? (1 / totalLen) : 0;
|
|
1136
|
+
const denom = Math.max(1, frames.length - 1);
|
|
1137
|
+
for (let i = 0; i < frames.length; i++) {
|
|
1138
|
+
const frac = invTotal > 0 ? (cumulative[i] * invTotal) : (i / denom);
|
|
1139
|
+
const angle = twistRad * frac;
|
|
1140
|
+
if (Math.abs(angle) <= 1e-12) continue;
|
|
1141
|
+
const axis = (frames[i].tangent || tangents[i] || new THREE.Vector3(0, 0, 1)).clone();
|
|
1142
|
+
if (axis.lengthSq() <= 1e-20) continue;
|
|
1143
|
+
axis.normalize();
|
|
1144
|
+
const q = new THREE.Quaternion().setFromAxisAngle(axis, angle);
|
|
1145
|
+
frames[i].X.applyQuaternion(q);
|
|
1146
|
+
frames[i].Y.applyQuaternion(q);
|
|
1147
|
+
frames[i].Z.applyQuaternion(q);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
const placeAt = (pArr, segIndex) => {
|
|
1152
|
+
const uv = uvOf(pArr);
|
|
1153
|
+
const idx = Math.max(0, Math.min(frames.length - 1, segIndex | 0));
|
|
1154
|
+
const f = frames[idx];
|
|
1155
|
+
const du = uv[0];
|
|
1156
|
+
const dv = uv[1];
|
|
1157
|
+
return [
|
|
1158
|
+
f.origin.x + f.X.x * du + f.Y.x * dv,
|
|
1159
|
+
f.origin.y + f.X.y * du + f.Y.y * dv,
|
|
1160
|
+
f.origin.z + f.X.z * du + f.Y.z * dv,
|
|
1161
|
+
];
|
|
1162
|
+
};
|
|
1163
|
+
|
|
1164
|
+
if (sweepDebugEnabled()) {
|
|
1165
|
+
const pathDbg = frames.map((f, i) => ({
|
|
1166
|
+
i,
|
|
1167
|
+
p: [ +f.origin.x.toFixed(4), +f.origin.y.toFixed(4), +f.origin.z.toFixed(4) ],
|
|
1168
|
+
X: [ +f.X.x.toFixed(4), +f.X.y.toFixed(4), +f.X.z.toFixed(4) ],
|
|
1169
|
+
Y: [ +f.Y.x.toFixed(4), +f.Y.y.toFixed(4), +f.Y.z.toFixed(4) ],
|
|
1170
|
+
Z: [ +f.Z.x.toFixed(4), +f.Z.y.toFixed(4), +f.Z.z.toFixed(4) ],
|
|
1171
|
+
}));
|
|
1172
|
+
const framesMeta = { baseOriginW: _v3(baseOriginW), baseX: _v3(baseX), baseY: _v3(baseY), anchorWorld: _v3(anchorWorld) };
|
|
1173
|
+
dlog('Frames', 'RMF frames', framesMeta);
|
|
1174
|
+
console.table(pathDbg);
|
|
1175
|
+
djson('Frames', { meta: framesMeta, rows: pathDbg });
|
|
1176
|
+
dlog('Anchor', 'uv and start frame', { lockU, lockV, frame0: frames[0] });
|
|
1177
|
+
djson('Anchor', { lockU, lockV, frame0: { origin: _v3(frames[0].origin), X: _v3(frames[0].X), Y: _v3(frames[0].Y), Z: _v3(frames[0].Z) } });
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
return { frames, placeAt, uvOf, lockU, lockV, baseOriginW, baseX, baseY, baseZ };
|
|
1181
|
+
};
|
|
1182
|
+
|
|
1183
|
+
const pathAlignCtx = (doPathSweep && mode === 'pathAlign' && pathPts.length >= 2)
|
|
1184
|
+
? buildPathAlignContext()
|
|
1185
|
+
: null;
|
|
1186
|
+
|
|
1187
|
+
// Prefer boundary-loop based sidewalls whenever loops are available so
|
|
1188
|
+
// caps and walls share identical vertices and produce a watertight mesh.
|
|
1189
|
+
// This avoids non‑manifold vertical edges when input edges are split into
|
|
1190
|
+
// multiple segments (e.g., PNG trace linear regions). Falls back to
|
|
1191
|
+
// per-edge ribbons only when loops are unavailable.
|
|
1192
|
+
if (boundaryLoops && boundaryLoops.length) {
|
|
1193
|
+
const _inputDbg = { mode, pathCount: pathPts.length, loops: boundaryLoops.length, face: face?.name };
|
|
1194
|
+
dlog('Input', 'pathAlign params', _inputDbg);
|
|
1195
|
+
djson('Input', _inputDbg);
|
|
1196
|
+
// Build a quick lookup from boundary points to their originating sketch edge(s)
|
|
1197
|
+
// so we can label side walls per curve while still using cap-matching vertices.
|
|
1198
|
+
const key = (p) => `${p[0].toFixed(6)},${p[1].toFixed(6)},${p[2].toFixed(6)}`;
|
|
1199
|
+
// Use only non-closed edges for per-segment naming so vertical boundaries
|
|
1200
|
+
// between side panels remain distinct. Closed loop edges (from PNG trace)
|
|
1201
|
+
// cover the whole ring and would otherwise collapse all walls under one name.
|
|
1202
|
+
const edgesAll = Array.isArray(face?.edges) ? face.edges : [];
|
|
1203
|
+
const edges = edgesAll.filter(e => !e.closedLoop || isCylindricalSketchEdge(e));
|
|
1204
|
+
const pointToEdgeNames = new Map(); // key -> Set(edgeName)
|
|
1205
|
+
for (const e of edges) {
|
|
1206
|
+
const name = `${featureTag}${e?.name || 'EDGE'}_SW`;
|
|
1207
|
+
registerEdgeSource(name, e);
|
|
1208
|
+
const poly = e?.userData?.polylineLocal;
|
|
1209
|
+
const isWorld = !!(e?.userData?.polylineWorld);
|
|
1210
|
+
if (Array.isArray(poly) && poly.length >= 2) {
|
|
1211
|
+
for (const p of poly) {
|
|
1212
|
+
const w = isWorld ? p : new THREE.Vector3(p[0], p[1], p[2]).applyMatrix4(e.matrixWorld),
|
|
1213
|
+
arr = Array.isArray(w) ? w : [w.x, w.y, w.z];
|
|
1214
|
+
const k = key(arr);
|
|
1215
|
+
let set = pointToEdgeNames.get(k);
|
|
1216
|
+
if (!set) { set = new Set(); pointToEdgeNames.set(k, set); }
|
|
1217
|
+
set.add(name);
|
|
1218
|
+
}
|
|
1219
|
+
} else {
|
|
1220
|
+
// Fallback: positions attribute if present
|
|
1221
|
+
const pos = e?.geometry?.getAttribute?.('position');
|
|
1222
|
+
if (pos && pos.itemSize === 3) {
|
|
1223
|
+
const v = new THREE.Vector3();
|
|
1224
|
+
for (let i = 0; i < pos.count; i++) {
|
|
1225
|
+
v.set(pos.getX(i), pos.getY(i), pos.getZ(i)).applyMatrix4(e.matrixWorld);
|
|
1226
|
+
const k = key([v.x, v.y, v.z]);
|
|
1227
|
+
let set = pointToEdgeNames.get(k);
|
|
1228
|
+
if (!set) { set = new Set(); pointToEdgeNames.set(k, set); }
|
|
1229
|
+
set.add(name);
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
const frames = pathAlignCtx ? pathAlignCtx.frames : null;
|
|
1236
|
+
const placeAt = pathAlignCtx ? pathAlignCtx.placeAt : null;
|
|
1237
|
+
|
|
1238
|
+
// Deduplicate per-boundary segments so each undirected edge [A,B]
|
|
1239
|
+
// emits exactly one side-wall ribbon. This avoids duplicate walls when
|
|
1240
|
+
// loop reconstruction yields overlapping segments or when edge-name
|
|
1241
|
+
// mapping falls back to the generic face name on the same [A,B].
|
|
1242
|
+
const keyPt = (p) => `${Number(p[0]).toFixed(7)},${Number(p[1]).toFixed(7)},${Number(p[2]).toFixed(7)}`;
|
|
1243
|
+
const segKey = (A,B) => {
|
|
1244
|
+
const a = keyPt(A), b = keyPt(B);
|
|
1245
|
+
return (a < b) ? `${a}|${b}` : `${b}|${a}`;
|
|
1246
|
+
};
|
|
1247
|
+
const seenSegments = new Set();
|
|
1248
|
+
|
|
1249
|
+
for (const loop of boundaryLoops) {
|
|
1250
|
+
const pts = Array.isArray(loop?.pts) ? loop.pts : loop;
|
|
1251
|
+
const isHole = !!(loop && loop.isHole);
|
|
1252
|
+
const base = pts.slice();
|
|
1253
|
+
// ensure closed
|
|
1254
|
+
if (base.length >= 2) {
|
|
1255
|
+
const first = base[0];
|
|
1256
|
+
const last = base[base.length - 1];
|
|
1257
|
+
if (!(first[0] === last[0] && first[1] === last[1] && first[2] === last[2])) base.push([first[0], first[1], first[2]]);
|
|
1258
|
+
}
|
|
1259
|
+
// remove consecutive duplicates if any
|
|
1260
|
+
for (let i = base.length - 2; i >= 0; i--) {
|
|
1261
|
+
const a = base[i], b = base[i + 1];
|
|
1262
|
+
if (a[0] === b[0] && a[1] === b[1] && a[2] === b[2]) base.splice(i + 1, 1);
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
if (!doPathSweep) {
|
|
1266
|
+
// translate-only
|
|
1267
|
+
if (twoSided && dirB) {
|
|
1268
|
+
for (let i = 0; i < base.length - 1; i++) {
|
|
1269
|
+
const a = base[i];
|
|
1270
|
+
const b = base[i + 1];
|
|
1271
|
+
if (a[0] === b[0] && a[1] === b[1] && a[2] === b[2]) continue;
|
|
1272
|
+
const sk = segKey(a,b); if (seenSegments.has(sk)) continue; seenSegments.add(sk);
|
|
1273
|
+
const A0 = [a[0] + dirB.x, a[1] + dirB.y, a[2] + dirB.z];
|
|
1274
|
+
const B0 = [b[0] + dirB.x, b[1] + dirB.y, b[2] + dirB.z];
|
|
1275
|
+
const A1 = [a[0] + dirF.x, a[1] + dirF.y, a[2] + dirF.z];
|
|
1276
|
+
const B1 = [b[0] + dirF.x, b[1] + dirF.y, b[2] + dirF.z];
|
|
1277
|
+
const setA = pointToEdgeNames.get(key(a));
|
|
1278
|
+
const setB = pointToEdgeNames.get(key(b));
|
|
1279
|
+
let name = `${featureTag}${face.name || 'FACE'}_SW`;
|
|
1280
|
+
if (setA && setB) { for (const n of setA) { if (setB.has(n)) { name = n; break; } } }
|
|
1281
|
+
ensureMetadataForName(name);
|
|
1282
|
+
setFaceType(name, 'SIDEWALL');
|
|
1283
|
+
addQuad(name, A0, B0, B1, A1, isHole);
|
|
1284
|
+
}
|
|
1285
|
+
} else {
|
|
1286
|
+
// single-vector extrude (original behavior)
|
|
1287
|
+
for (let i = 0; i < base.length - 1; i++) {
|
|
1288
|
+
const a = base[i];
|
|
1289
|
+
const b = base[i + 1];
|
|
1290
|
+
if (a[0] === b[0] && a[1] === b[1] && a[2] === b[2]) continue;
|
|
1291
|
+
const sk = segKey(a,b); if (seenSegments.has(sk)) continue; seenSegments.add(sk);
|
|
1292
|
+
const a2 = [a[0] + dirF.x, a[1] + dirF.y, a[2] + dirF.z];
|
|
1293
|
+
const b2 = [b[0] + dirF.x, b[1] + dirF.y, b[2] + dirF.z];
|
|
1294
|
+
const setA = pointToEdgeNames.get(key(a));
|
|
1295
|
+
const setB = pointToEdgeNames.get(key(b));
|
|
1296
|
+
let name = `${featureTag}${face.name || 'FACE'}_SW`;
|
|
1297
|
+
if (setA && setB) { for (const n of setA) { if (setB.has(n)) { name = n; break; } } }
|
|
1298
|
+
ensureMetadataForName(name);
|
|
1299
|
+
setFaceType(name, 'SIDEWALL');
|
|
1300
|
+
if (isHole) {
|
|
1301
|
+
this.addTriangle(name, a, b2, b);
|
|
1302
|
+
this.addTriangle(name, a, a2, b2);
|
|
1303
|
+
} else {
|
|
1304
|
+
this.addTriangle(name, a, b, b2);
|
|
1305
|
+
this.addTriangle(name, a, b2, a2);
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
} else {
|
|
1310
|
+
// Path sweep
|
|
1311
|
+
if (mode === 'pathAlign' && frames && placeAt) {
|
|
1312
|
+
for (let seg = 0; seg < offsets.length - 1; seg++) {
|
|
1313
|
+
for (let i = 0; i < base.length - 1; i++) {
|
|
1314
|
+
const a = base[i];
|
|
1315
|
+
const b = base[i + 1];
|
|
1316
|
+
if (a[0] === b[0] && a[1] === b[1] && a[2] === b[2]) continue;
|
|
1317
|
+
const A0 = placeAt(a, seg);
|
|
1318
|
+
const B0 = placeAt(b, seg);
|
|
1319
|
+
const A1 = placeAt(a, seg + 1);
|
|
1320
|
+
const B1 = placeAt(b, seg + 1);
|
|
1321
|
+
const setA = pointToEdgeNames.get(key(a));
|
|
1322
|
+
const setB = pointToEdgeNames.get(key(b));
|
|
1323
|
+
let name = `${featureTag}${face.name || 'FACE'}_SW`;
|
|
1324
|
+
if (setA && setB) { for (const n of setA) { if (setB.has(n)) { name = n; break; } } }
|
|
1325
|
+
ensureMetadataForName(name);
|
|
1326
|
+
setFaceType(name, 'SIDEWALL');
|
|
1327
|
+
addQuad(name, A0, B0, B1, A1, isHole);
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
} else {
|
|
1331
|
+
// Translate-only between successive offsets
|
|
1332
|
+
for (let seg = 0; seg < offsets.length - 1; seg++) {
|
|
1333
|
+
const off0 = offsets[seg], off1 = offsets[seg + 1];
|
|
1334
|
+
if (off1.x === off0.x && off1.y === off0.y && off1.z === off0.z) continue;
|
|
1335
|
+
for (let i = 0; i < base.length - 1; i++) {
|
|
1336
|
+
const a = base[i];
|
|
1337
|
+
const b = base[i + 1];
|
|
1338
|
+
if (a[0] === b[0] && a[1] === b[1] && a[2] === b[2]) continue;
|
|
1339
|
+
const A0 = [a[0] + off0.x, a[1] + off0.y, a[2] + off0.z];
|
|
1340
|
+
const B0 = [b[0] + off0.x, b[1] + off0.y, b[2] + off0.z];
|
|
1341
|
+
const A1 = [a[0] + off1.x, a[1] + off1.y, a[2] + off1.z];
|
|
1342
|
+
const B1 = [b[0] + off1.x, b[1] + off1.y, b[2] + off1.z];
|
|
1343
|
+
const setA = pointToEdgeNames.get(key(a));
|
|
1344
|
+
const setB = pointToEdgeNames.get(key(b));
|
|
1345
|
+
let name = `${featureTag}${face.name || 'FACE'}_SW`;
|
|
1346
|
+
if (setA && setB) { for (const n of setA) { if (setB.has(n)) { name = n; break; } } }
|
|
1347
|
+
ensureMetadataForName(name);
|
|
1348
|
+
setFaceType(name, 'SIDEWALL');
|
|
1349
|
+
// Use robust splitting to avoid skinny/inside-crossing diagonals
|
|
1350
|
+
addQuad(name, A0, B0, B1, A1, isHole);
|
|
1351
|
+
if (sweepDebugEnabled() && seg===0 && i===0) {
|
|
1352
|
+
const walls0 = { A0, B0, B1, A1 };
|
|
1353
|
+
dlog('Walls','first quad', walls0);
|
|
1354
|
+
djson('WallsFirstQuad', walls0);
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
// Build start/end caps for pathAlign using initial and final frames
|
|
1362
|
+
if (doPathSweep && mode === 'pathAlign' && frames && placeAt) {
|
|
1363
|
+
const buildCap = (frameIndex, capName) => {
|
|
1364
|
+
const frame = frames[frameIndex];
|
|
1365
|
+
// Map loops using the same placeAt used for walls so vertices match exactly
|
|
1366
|
+
const mapped = boundaryLoops.map(loop => {
|
|
1367
|
+
const pts = Array.isArray(loop?.pts) ? loop.pts : loop;
|
|
1368
|
+
// Build open ring without duplicate last point
|
|
1369
|
+
const arr = pts.map(p => placeAt(p, frameIndex));
|
|
1370
|
+
// Drop closing duplicate if present (keep interior points as-is)
|
|
1371
|
+
if (arr.length >= 2) {
|
|
1372
|
+
const f = arr[0], l = arr[arr.length - 1];
|
|
1373
|
+
if (f[0] === l[0] && f[1] === l[1] && f[2] === l[2]) arr.pop();
|
|
1374
|
+
}
|
|
1375
|
+
return { pts: arr, isHole: !!(loop && loop.isHole) };
|
|
1376
|
+
});
|
|
1377
|
+
const toXY = (P) => new THREE.Vector2((P[0] - frame.origin.x) * frame.X.x + (P[1] - frame.origin.y) * frame.X.y + (P[2] - frame.origin.z) * frame.X.z,
|
|
1378
|
+
(P[0] - frame.origin.x) * frame.Y.x + (P[1] - frame.origin.y) * frame.Y.y + (P[2] - frame.origin.z) * frame.Y.z);
|
|
1379
|
+
const area2 = (arr) => {
|
|
1380
|
+
let a = 0;
|
|
1381
|
+
for (let i = 0; i < arr.length; i++) { const p = arr[i], q = arr[(i + 1) % arr.length]; a += (p.x * q.y - q.x * p.y); }
|
|
1382
|
+
return 0.5 * a;
|
|
1383
|
+
};
|
|
1384
|
+
const outer = mapped.find(l => !l.isHole) || mapped[0];
|
|
1385
|
+
if (!outer || outer.pts.length < 3) return;
|
|
1386
|
+
const holes = mapped.filter(l => l !== outer && l.isHole).map(l => {
|
|
1387
|
+
const a = l.pts.slice();
|
|
1388
|
+
if (a.length >= 2) {
|
|
1389
|
+
const f = a[0], t = a[a.length - 1];
|
|
1390
|
+
if (f[0] === t[0] && f[1] === t[1] && f[2] === t[2]) a.pop();
|
|
1391
|
+
}
|
|
1392
|
+
return a;
|
|
1393
|
+
});
|
|
1394
|
+
let contourV2 = outer.pts.map(p => toXY(p));
|
|
1395
|
+
let holesV2 = holes.map(h => h.map(p => toXY(p)));
|
|
1396
|
+
if (area2(contourV2) > 0) contourV2 = contourV2.reverse();
|
|
1397
|
+
holesV2 = holesV2.map(h => (area2(h) < 0 ? h.reverse() : h));
|
|
1398
|
+
let tris = THREE.ShapeUtils.triangulateShape(contourV2, holesV2);
|
|
1399
|
+
// Fallback triangulation if library returns too few triangles (rare numeric degeneracy)
|
|
1400
|
+
const need = Math.max(2, (contourV2.length - 2));
|
|
1401
|
+
if (!Array.isArray(tris) || tris.length < need) {
|
|
1402
|
+
const manual = [];
|
|
1403
|
+
// Simple fan triangulation around vertex 0 (no holes); orientation already enforced above
|
|
1404
|
+
for (let i = 1; i < contourV2.length - 1; i++) manual.push([0, i, i + 1]);
|
|
1405
|
+
tris = manual;
|
|
1406
|
+
dlog('Cap', 'fallback triangulation used', { capName, fanCount: manual.length });
|
|
1407
|
+
djson('CapFallback', { capName, contour: contourV2.map(v=>[_round(v.x),_round(v.y)]), holes: holesV2.map(h=> h.map(v=>[_round(v.x),_round(v.y)])) });
|
|
1408
|
+
}
|
|
1409
|
+
const all = outer.pts.concat(...holes);
|
|
1410
|
+
for (const t of tris) {
|
|
1411
|
+
const q0 = all[t[0]], q1 = all[t[1]], q2 = all[t[2]];
|
|
1412
|
+
if (capName.endsWith('_START')) this.addTriangle(capName, q0, q2, q1);
|
|
1413
|
+
else this.addTriangle(capName, q0, q1, q2);
|
|
1414
|
+
}
|
|
1415
|
+
const capInfo = { capName, frameIndex, triCount: tris?.length||0, outerLen: outer?.pts?.length||0, holes: holes?.length||0 };
|
|
1416
|
+
dlog('Cap', `built ${capName}`, capInfo);
|
|
1417
|
+
djson('Cap', capInfo);
|
|
1418
|
+
};
|
|
1419
|
+
buildCap(0, startName);
|
|
1420
|
+
buildCap(frames.length - 1, endName);
|
|
1421
|
+
}
|
|
1422
|
+
} else {
|
|
1423
|
+
// Fallback: build from per-edge polylines (may not match cap vertices exactly)
|
|
1424
|
+
const edges = Array.isArray(face.edges) ? face.edges : [];
|
|
1425
|
+
if (edges.length) {
|
|
1426
|
+
// Per-edge fallback; support translate and pathAlign
|
|
1427
|
+
for (const edge of edges) {
|
|
1428
|
+
const name = `${featureTag}${edge.name || 'EDGE'}_SW`;
|
|
1429
|
+
registerEdgeSource(name, edge);
|
|
1430
|
+
ensureMetadataForName(name);
|
|
1431
|
+
setFaceType(name, 'SIDEWALL');
|
|
1432
|
+
|
|
1433
|
+
// Robustly extract world-space polyline points
|
|
1434
|
+
const pA = [];
|
|
1435
|
+
const wv = new THREE.Vector3();
|
|
1436
|
+
const cached = edge?.userData?.polylineLocal;
|
|
1437
|
+
const isWorld = !!(edge?.userData?.polylineWorld);
|
|
1438
|
+
if (Array.isArray(cached) && cached.length >= 2) {
|
|
1439
|
+
if (isWorld) {
|
|
1440
|
+
for (let i = 0; i < cached.length; i++) { const p = cached[i]; pA.push([p[0], p[1], p[2]]); }
|
|
1441
|
+
} else {
|
|
1442
|
+
for (let i = 0; i < cached.length; i++) { const p = cached[i]; wv.set(p[0], p[1], p[2]).applyMatrix4(edge.matrixWorld); pA.push([wv.x, wv.y, wv.z]); }
|
|
1443
|
+
}
|
|
1444
|
+
} else {
|
|
1445
|
+
const posAttr = edge?.geometry?.getAttribute?.('position');
|
|
1446
|
+
if (posAttr && posAttr.itemSize === 3 && posAttr.count >= 2) {
|
|
1447
|
+
for (let i = 0; i < posAttr.count; i++) { wv.set(posAttr.getX(i), posAttr.getY(i), posAttr.getZ(i)).applyMatrix4(edge.matrixWorld); pA.push([wv.x, wv.y, wv.z]); }
|
|
1448
|
+
} else {
|
|
1449
|
+
const aStart = edge?.geometry?.attributes?.instanceStart;
|
|
1450
|
+
const aEnd = edge?.geometry?.attributes?.instanceEnd;
|
|
1451
|
+
if (aStart && aEnd && aStart.itemSize === 3 && aEnd.itemSize === 3 && aStart.count === aEnd.count && aStart.count >= 1) {
|
|
1452
|
+
wv.set(aStart.getX(0), aStart.getY(0), aStart.getZ(0)).applyMatrix4(edge.matrixWorld); pA.push([wv.x, wv.y, wv.z]);
|
|
1453
|
+
for (let i = 0; i < aEnd.count; i++) { wv.set(aEnd.getX(i), aEnd.getY(i), aEnd.getZ(i)).applyMatrix4(edge.matrixWorld); pA.push([wv.x, wv.y, wv.z]); }
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
// Remove exact duplicate consecutive points to avoid degenerate quads
|
|
1459
|
+
for (let i = pA.length - 2; i >= 0; i--) {
|
|
1460
|
+
const a = pA[i], b = pA[i + 1];
|
|
1461
|
+
if (a[0] === b[0] && a[1] === b[1] && a[2] === b[2]) pA.splice(i + 1, 1);
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
const n = pA.length;
|
|
1465
|
+
if (n < 2) continue;
|
|
1466
|
+
const isHole = !!(edge && edge.userData && edge.userData.isHole);
|
|
1467
|
+
|
|
1468
|
+
if (!doPathSweep) {
|
|
1469
|
+
if (twoSided && dirB) {
|
|
1470
|
+
for (let i = 0; i < n - 1; i++) {
|
|
1471
|
+
const a = pA[i];
|
|
1472
|
+
const b = pA[i + 1];
|
|
1473
|
+
if ((a[0] === b[0] && a[1] === b[1] && a[2] === b[2])) continue; // guard
|
|
1474
|
+
const A0 = [a[0] + dirB.x, a[1] + dirB.y, a[2] + dirB.z];
|
|
1475
|
+
const B0 = [b[0] + dirB.x, b[1] + dirB.y, b[2] + dirB.z];
|
|
1476
|
+
const A1 = [a[0] + dirF.x, a[1] + dirF.y, a[2] + dirF.z];
|
|
1477
|
+
const B1 = [b[0] + dirF.x, b[1] + dirF.y, b[2] + dirF.z];
|
|
1478
|
+
if (isHole) { this.addTriangle(name, A0, B1, B0); this.addTriangle(name, A0, A1, B1); }
|
|
1479
|
+
else { this.addTriangle(name, A0, B0, B1); this.addTriangle(name, A0, B1, A1); }
|
|
1480
|
+
}
|
|
1481
|
+
} else {
|
|
1482
|
+
// Single-vector extrude
|
|
1483
|
+
for (let i = 0; i < n - 1; i++) {
|
|
1484
|
+
const a = pA[i];
|
|
1485
|
+
const b = pA[i + 1];
|
|
1486
|
+
if ((a[0] === b[0] && a[1] === b[1] && a[2] === b[2])) continue; // guard
|
|
1487
|
+
const a2 = [a[0] + dirF.x, a[1] + dirF.y, a[2] + dirF.z];
|
|
1488
|
+
const b2 = [b[0] + dirF.x, b[1] + dirF.y, b[2] + dirF.z];
|
|
1489
|
+
if (isHole) { this.addTriangle(name, a, b2, b); this.addTriangle(name, a, a2, b2); }
|
|
1490
|
+
else { this.addTriangle(name, a, b, b2); this.addTriangle(name, a, b2, a2); }
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
} else {
|
|
1494
|
+
// Path-based
|
|
1495
|
+
if (mode === 'pathAlign' && doPathSweep && pathAlignCtx && pathAlignCtx.placeAt) {
|
|
1496
|
+
const placeAtEdge = pathAlignCtx.placeAt;
|
|
1497
|
+
for (let seg = 0; seg < offsets.length - 1; seg++) {
|
|
1498
|
+
for (let i = 0; i < n - 1; i++) {
|
|
1499
|
+
const A0 = placeAtEdge(pA[i], seg);
|
|
1500
|
+
const B0 = placeAtEdge(pA[i + 1], seg);
|
|
1501
|
+
const A1 = placeAtEdge(pA[i], seg + 1);
|
|
1502
|
+
const B1 = placeAtEdge(pA[i + 1], seg + 1);
|
|
1503
|
+
addQuad(name, A0, B0, B1, A1, isHole);
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
} else {
|
|
1507
|
+
for (let seg = 0; seg < offsets.length - 1; seg++) {
|
|
1508
|
+
const off0 = offsets[seg], off1 = offsets[seg + 1];
|
|
1509
|
+
// Skip degenerate steps
|
|
1510
|
+
if (off1.x === off0.x && off1.y === off0.y && off1.z === off0.z) continue;
|
|
1511
|
+
for (let i = 0; i < n - 1; i++) {
|
|
1512
|
+
const a = pA[i];
|
|
1513
|
+
const b = pA[i + 1];
|
|
1514
|
+
if ((a[0] === b[0] && a[1] === b[1] && a[2] === b[2])) continue;
|
|
1515
|
+
const A0 = [a[0] + off0.x, a[1] + off0.y, a[2] + off0.z];
|
|
1516
|
+
const B0 = [b[0] + off0.x, b[1] + off0.y, b[2] + off0.z];
|
|
1517
|
+
const A1 = [a[0] + off1.x, a[1] + off1.y, a[2] + off1.z];
|
|
1518
|
+
const B1 = [b[0] + off1.x, b[1] + off1.y, b[2] + off1.z];
|
|
1519
|
+
addQuad(name, A0, B0, B1, A1, isHole);
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
// If we are in pathAlign mode here, also build start/end caps from face geometry via frames
|
|
1527
|
+
if (doPathSweep && mode === 'pathAlign' && pathAlignCtx && pathAlignCtx.placeAt) {
|
|
1528
|
+
const placeAtCap = pathAlignCtx.placeAt;
|
|
1529
|
+
const fStart = 0;
|
|
1530
|
+
const fEnd = pathAlignCtx.frames.length - 1;
|
|
1531
|
+
const baseGeom = face.geometry;
|
|
1532
|
+
const posAttr = baseGeom && baseGeom.getAttribute && baseGeom.getAttribute('position');
|
|
1533
|
+
if (posAttr) {
|
|
1534
|
+
const idxAttr = baseGeom.getIndex && baseGeom.getIndex();
|
|
1535
|
+
const hasIndex = !!idxAttr;
|
|
1536
|
+
const v = new THREE.Vector3();
|
|
1537
|
+
const faceWorld = new Array(posAttr.count);
|
|
1538
|
+
for (let i = 0; i < posAttr.count; i++) {
|
|
1539
|
+
v.set(posAttr.getX(i), posAttr.getY(i), posAttr.getZ(i)).applyMatrix4(face.matrixWorld);
|
|
1540
|
+
faceWorld[i] = [v.x, v.y, v.z];
|
|
1541
|
+
}
|
|
1542
|
+
const addTriAt = (i0, i1, i2) => {
|
|
1543
|
+
const p0 = faceWorld[i0], p1 = faceWorld[i1], p2 = faceWorld[i2];
|
|
1544
|
+
const s0 = placeAtCap(p0, fStart), s1 = placeAtCap(p1, fStart), s2 = placeAtCap(p2, fStart);
|
|
1545
|
+
this.addTriangle(startName, s0, s2, s1);
|
|
1546
|
+
const e0 = placeAtCap(p0, fEnd), e1 = placeAtCap(p1, fEnd), e2 = placeAtCap(p2, fEnd);
|
|
1547
|
+
this.addTriangle(endName, e0, e1, e2);
|
|
1548
|
+
};
|
|
1549
|
+
if (hasIndex) {
|
|
1550
|
+
for (let t = 0; t < idxAttr.count; t += 3) {
|
|
1551
|
+
addTriAt(idxAttr.getX(t + 0) >>> 0, idxAttr.getX(t + 1) >>> 0, idxAttr.getX(t + 2) >>> 0);
|
|
1552
|
+
}
|
|
1553
|
+
} else {
|
|
1554
|
+
const triCount = (posAttr.count / 3) >>> 0;
|
|
1555
|
+
for (let t = 0; t < triCount; t++) {
|
|
1556
|
+
addTriAt(3 * t + 0, 3 * t + 1, 3 * t + 2);
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
// Weld seams by an adaptive epsilon to ensure caps and sides share
|
|
1563
|
+
// vertices exactly without collapsing geometry at small scales.
|
|
1564
|
+
// Use ~1e-6 of the overall diagonal, clamped to [1e-7, 1e-4].
|
|
1565
|
+
let eps = 1e-6;
|
|
1566
|
+
if (Array.isArray(this._vertProperties) && this._vertProperties.length >= 6) {
|
|
1567
|
+
const bounds = computeBoundsFromVertices(this._vertProperties);
|
|
1568
|
+
const diag = (bounds && bounds.diag) ? bounds.diag : 1;
|
|
1569
|
+
eps = Math.min(1e-4, Math.max(1e-7, diag * 1e-6));
|
|
1570
|
+
}
|
|
1571
|
+
this.setEpsilon(eps);
|
|
1572
|
+
// Prune tiny floating fragments that can appear at sharp corners.
|
|
1573
|
+
// Skip automatic island removal for extrusions based on traced images; tiny
|
|
1574
|
+
// sliver panels near sharp corners can be valid and removing them can open
|
|
1575
|
+
// the shell. Users can run repair tools explicitly if needed.
|
|
1576
|
+
// Build the manifold now so callers get a ready solid. If it fails due
|
|
1577
|
+
// to borderline vertex mismatches, progressively increase epsilon and
|
|
1578
|
+
// retry a few times.
|
|
1579
|
+
let ok = false; let attempt = 0; let errLast = null;
|
|
1580
|
+
while (!ok && attempt < 3) {
|
|
1581
|
+
try {
|
|
1582
|
+
const __tmpMesh = this.getMesh();
|
|
1583
|
+
try { /* probe only */ } finally { try { if (__tmpMesh && typeof __tmpMesh.delete === 'function') __tmpMesh.delete(); } catch {} }
|
|
1584
|
+
ok = true;
|
|
1585
|
+
} catch (err) {
|
|
1586
|
+
errLast = err;
|
|
1587
|
+
eps *= 2;
|
|
1588
|
+
if (eps > 5e-4) break;
|
|
1589
|
+
try { this.setEpsilon(eps); } catch (_) { }
|
|
1590
|
+
}
|
|
1591
|
+
attempt++;
|
|
1592
|
+
}
|
|
1593
|
+
if (!ok && errLast) { console.warn('[Sweep] Manifold build failed after retries:', errLast.message || errLast); }
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
}
|