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.
Files changed (403) hide show
  1. package/LICENSE.md +32 -0
  2. package/README.md +144 -0
  3. package/dist-kernel/brep-kernel.js +74699 -0
  4. package/dist-kernel/help/CONTRIBUTING.html +248 -0
  5. package/dist-kernel/help/LICENSE.html +248 -0
  6. package/dist-kernel/help/MODELING.png +0 -0
  7. package/dist-kernel/help/PMI.png +0 -0
  8. package/dist-kernel/help/SKETCH.png +0 -0
  9. package/dist-kernel/help/assembly-constraints__Coincident_Constraint_dialog.png +0 -0
  10. package/dist-kernel/help/assembly-constraints___Angle_Constraint_dialog.png +0 -0
  11. package/dist-kernel/help/assembly-constraints___Distance_Constraint_dialog.png +0 -0
  12. package/dist-kernel/help/assembly-constraints___Fixed_Constraint_dialog.png +0 -0
  13. package/dist-kernel/help/assembly-constraints___Parallel_Constraint_dialog.png +0 -0
  14. package/dist-kernel/help/assembly-constraints___Touch_Align_Constraint_dialog.png +0 -0
  15. package/dist-kernel/help/assembly-constraints__angle-constraint.html +248 -0
  16. package/dist-kernel/help/assembly-constraints__coincident-constraint.html +248 -0
  17. package/dist-kernel/help/assembly-constraints__distance-constraint.html +248 -0
  18. package/dist-kernel/help/assembly-constraints__fixed-constraint.html +248 -0
  19. package/dist-kernel/help/assembly-constraints__parallel-constraint.html +248 -0
  20. package/dist-kernel/help/assembly-constraints__solver.html +248 -0
  21. package/dist-kernel/help/assembly-constraints__touch-align-constraint.html +248 -0
  22. package/dist-kernel/help/brep-api.html +263 -0
  23. package/dist-kernel/help/brep-kernel.html +258 -0
  24. package/dist-kernel/help/brep-model.html +248 -0
  25. package/dist-kernel/help/cylindrical-face-radius-embedding.html +290 -0
  26. package/dist-kernel/help/dialog-screenshots.html +248 -0
  27. package/dist-kernel/help/extruded-sketch-radius-embedding.html +336 -0
  28. package/dist-kernel/help/features__Assembly_Component_dialog.png +0 -0
  29. package/dist-kernel/help/features__Boolean_dialog.png +0 -0
  30. package/dist-kernel/help/features__Chamfer_dialog.png +0 -0
  31. package/dist-kernel/help/features__Datium_dialog.png +0 -0
  32. package/dist-kernel/help/features__Extrude_dialog.png +0 -0
  33. package/dist-kernel/help/features__Fillet_dialog.png +0 -0
  34. package/dist-kernel/help/features__Helix_dialog.png +0 -0
  35. package/dist-kernel/help/features__Hole_dialog.png +0 -0
  36. package/dist-kernel/help/features__Image_Heightmap_Solid_dialog.png +0 -0
  37. package/dist-kernel/help/features__Image_to_Face_dialog.png +0 -0
  38. package/dist-kernel/help/features__Import_3D_Model_dialog.png +0 -0
  39. package/dist-kernel/help/features__Loft_dialog.png +0 -0
  40. package/dist-kernel/help/features__Mirror_dialog.png +0 -0
  41. package/dist-kernel/help/features__Offset_Shell_dialog.png +0 -0
  42. package/dist-kernel/help/features__Overlap_Cleanup_dialog.png +0 -0
  43. package/dist-kernel/help/features__Pattern_Linear_dialog.png +0 -0
  44. package/dist-kernel/help/features__Pattern_Radial_dialog.png +0 -0
  45. package/dist-kernel/help/features__Pattern_dialog.png +0 -0
  46. package/dist-kernel/help/features__Plane_dialog.png +0 -0
  47. package/dist-kernel/help/features__Primitive_Cone_dialog.png +0 -0
  48. package/dist-kernel/help/features__Primitive_Cube_dialog.png +0 -0
  49. package/dist-kernel/help/features__Primitive_Cylinder_dialog.png +0 -0
  50. package/dist-kernel/help/features__Primitive_Pyramid_dialog.png +0 -0
  51. package/dist-kernel/help/features__Primitive_Sphere_dialog.png +0 -0
  52. package/dist-kernel/help/features__Primitive_Torus_dialog.png +0 -0
  53. package/dist-kernel/help/features__Remesh_dialog.png +0 -0
  54. package/dist-kernel/help/features__Revolve_dialog.png +0 -0
  55. package/dist-kernel/help/features__Sheet_Metal_Contour_Flange_dialog.png +0 -0
  56. package/dist-kernel/help/features__Sheet_Metal_Cutout_dialog.png +0 -0
  57. package/dist-kernel/help/features__Sheet_Metal_Flange_dialog.png +0 -0
  58. package/dist-kernel/help/features__Sheet_Metal_Tab_dialog.png +0 -0
  59. package/dist-kernel/help/features__Sketch_dialog.png +0 -0
  60. package/dist-kernel/help/features__Spline_dialog.png +0 -0
  61. package/dist-kernel/help/features__Sweep_dialog.png +0 -0
  62. package/dist-kernel/help/features__Transform_dialog.png +0 -0
  63. package/dist-kernel/help/features__Tube_dialog.png +0 -0
  64. package/dist-kernel/help/features__assembly-component.html +248 -0
  65. package/dist-kernel/help/features__boolean.html +248 -0
  66. package/dist-kernel/help/features__chamfer.html +248 -0
  67. package/dist-kernel/help/features__datium.html +248 -0
  68. package/dist-kernel/help/features__datum.html +248 -0
  69. package/dist-kernel/help/features__extrude.html +248 -0
  70. package/dist-kernel/help/features__fillet.html +248 -0
  71. package/dist-kernel/help/features__helix.html +248 -0
  72. package/dist-kernel/help/features__hole.html +248 -0
  73. package/dist-kernel/help/features__image-heightmap-solid.html +248 -0
  74. package/dist-kernel/help/features__image-to-face-2D_dialog.png +0 -0
  75. package/dist-kernel/help/features__image-to-face-3D_dialog.png +0 -0
  76. package/dist-kernel/help/features__image-to-face.html +248 -0
  77. package/dist-kernel/help/features__import-3d-model.html +248 -0
  78. package/dist-kernel/help/features__index.html +248 -0
  79. package/dist-kernel/help/features__loft.html +248 -0
  80. package/dist-kernel/help/features__mirror.html +248 -0
  81. package/dist-kernel/help/features__offset-shell.html +248 -0
  82. package/dist-kernel/help/features__pattern-linear.html +248 -0
  83. package/dist-kernel/help/features__pattern-radial.html +248 -0
  84. package/dist-kernel/help/features__pattern.html +248 -0
  85. package/dist-kernel/help/features__plane.html +248 -0
  86. package/dist-kernel/help/features__primitive-cone.html +248 -0
  87. package/dist-kernel/help/features__primitive-cube.html +248 -0
  88. package/dist-kernel/help/features__primitive-cylinder.html +248 -0
  89. package/dist-kernel/help/features__primitive-pyramid.html +248 -0
  90. package/dist-kernel/help/features__primitive-sphere.html +248 -0
  91. package/dist-kernel/help/features__primitive-torus.html +248 -0
  92. package/dist-kernel/help/features__remesh.html +248 -0
  93. package/dist-kernel/help/features__revolve.html +248 -0
  94. package/dist-kernel/help/features__sheet-metal-contour-flange.html +248 -0
  95. package/dist-kernel/help/features__sheet-metal-flange.html +248 -0
  96. package/dist-kernel/help/features__sheet-metal-tab.html +248 -0
  97. package/dist-kernel/help/features__sketch.html +248 -0
  98. package/dist-kernel/help/features__spline.html +248 -0
  99. package/dist-kernel/help/features__sweep.html +248 -0
  100. package/dist-kernel/help/features__transform.html +248 -0
  101. package/dist-kernel/help/features__tube.html +248 -0
  102. package/dist-kernel/help/file-formats.html +248 -0
  103. package/dist-kernel/help/getting-started.html +248 -0
  104. package/dist-kernel/help/highlights.html +248 -0
  105. package/dist-kernel/help/history-systems.html +248 -0
  106. package/dist-kernel/help/how-it-works.html +248 -0
  107. package/dist-kernel/help/index.html +862 -0
  108. package/dist-kernel/help/input-params-schema.html +363 -0
  109. package/dist-kernel/help/inspector-improvements.html +248 -0
  110. package/dist-kernel/help/inspector.html +248 -0
  111. package/dist-kernel/help/modes__modeling.html +248 -0
  112. package/dist-kernel/help/modes__pmi.html +248 -0
  113. package/dist-kernel/help/modes__sketch.html +248 -0
  114. package/dist-kernel/help/plugins.html +248 -0
  115. package/dist-kernel/help/pmi-annotations__Angle_Dimension_dialog.png +0 -0
  116. package/dist-kernel/help/pmi-annotations__Explode_Body_dialog.png +0 -0
  117. package/dist-kernel/help/pmi-annotations__Hole_Callout_dialog.png +0 -0
  118. package/dist-kernel/help/pmi-annotations__Leader_dialog.png +0 -0
  119. package/dist-kernel/help/pmi-annotations__Linear_Dimension_dialog.png +0 -0
  120. package/dist-kernel/help/pmi-annotations__Note_dialog.png +0 -0
  121. package/dist-kernel/help/pmi-annotations__Radial_Dimension_dialog.png +0 -0
  122. package/dist-kernel/help/pmi-annotations__angle-dimension.html +248 -0
  123. package/dist-kernel/help/pmi-annotations__explode-body.html +248 -0
  124. package/dist-kernel/help/pmi-annotations__hole-callout.html +248 -0
  125. package/dist-kernel/help/pmi-annotations__index.html +248 -0
  126. package/dist-kernel/help/pmi-annotations__leader.html +248 -0
  127. package/dist-kernel/help/pmi-annotations__linear-dimension.html +248 -0
  128. package/dist-kernel/help/pmi-annotations__note.html +248 -0
  129. package/dist-kernel/help/pmi-annotations__radial-dimension.html +248 -0
  130. package/dist-kernel/help/search-index.json +464 -0
  131. package/dist-kernel/help/simplified-radial-dimensions.html +298 -0
  132. package/dist-kernel/help/solid-methods.html +359 -0
  133. package/dist-kernel/help/table-of-contents.html +330 -0
  134. package/dist-kernel/help/ui-overview.html +248 -0
  135. package/dist-kernel/help/whats-new.html +248 -0
  136. package/package.json +54 -0
  137. package/src/BREP/AssemblyComponent.js +42 -0
  138. package/src/BREP/BREP.js +43 -0
  139. package/src/BREP/BetterSolid.js +805 -0
  140. package/src/BREP/Edge.js +103 -0
  141. package/src/BREP/Extrude.js +403 -0
  142. package/src/BREP/Face.js +187 -0
  143. package/src/BREP/MeshRepairer.js +634 -0
  144. package/src/BREP/OffsetShellSolid.js +614 -0
  145. package/src/BREP/PointCloudWrap.js +302 -0
  146. package/src/BREP/Revolve.js +345 -0
  147. package/src/BREP/SolidMethods/authoring.js +112 -0
  148. package/src/BREP/SolidMethods/booleanOps.js +230 -0
  149. package/src/BREP/SolidMethods/chamfer.js +122 -0
  150. package/src/BREP/SolidMethods/edgeResolution.js +25 -0
  151. package/src/BREP/SolidMethods/fillet.js +792 -0
  152. package/src/BREP/SolidMethods/index.js +72 -0
  153. package/src/BREP/SolidMethods/io.js +105 -0
  154. package/src/BREP/SolidMethods/lifecycle.js +103 -0
  155. package/src/BREP/SolidMethods/manifoldOps.js +375 -0
  156. package/src/BREP/SolidMethods/meshCleanup.js +2512 -0
  157. package/src/BREP/SolidMethods/meshQueries.js +264 -0
  158. package/src/BREP/SolidMethods/metadata.js +106 -0
  159. package/src/BREP/SolidMethods/metrics.js +51 -0
  160. package/src/BREP/SolidMethods/transforms.js +361 -0
  161. package/src/BREP/SolidMethods/visualize.js +508 -0
  162. package/src/BREP/SolidShared.js +26 -0
  163. package/src/BREP/Sweep.js +1596 -0
  164. package/src/BREP/Tube.js +857 -0
  165. package/src/BREP/Vertex.js +43 -0
  166. package/src/BREP/applyBooleanOperation.js +704 -0
  167. package/src/BREP/boundsUtils.js +48 -0
  168. package/src/BREP/chamfer.js +551 -0
  169. package/src/BREP/edgePolylineUtils.js +85 -0
  170. package/src/BREP/fillets/common.js +388 -0
  171. package/src/BREP/fillets/fillet.js +1422 -0
  172. package/src/BREP/fillets/filletGeometry.js +15 -0
  173. package/src/BREP/fillets/inset.js +389 -0
  174. package/src/BREP/fillets/offsetHelper.js +143 -0
  175. package/src/BREP/fillets/outset.js +88 -0
  176. package/src/BREP/helix.js +193 -0
  177. package/src/BREP/meshToBrep.js +234 -0
  178. package/src/BREP/primitives.js +279 -0
  179. package/src/BREP/setupManifold.js +71 -0
  180. package/src/BREP/threadGeometry.js +1120 -0
  181. package/src/BREP/triangleUtils.js +8 -0
  182. package/src/BREP/triangulate.js +608 -0
  183. package/src/FeatureRegistry.js +183 -0
  184. package/src/PartHistory.js +1132 -0
  185. package/src/UI/AccordionWidget.js +292 -0
  186. package/src/UI/CADmaterials.js +850 -0
  187. package/src/UI/EnvMonacoEditor.js +522 -0
  188. package/src/UI/FloatingWindow.js +396 -0
  189. package/src/UI/HistoryWidget.js +457 -0
  190. package/src/UI/MainToolbar.js +131 -0
  191. package/src/UI/ModelLibraryView.js +194 -0
  192. package/src/UI/OrthoCameraIdle.js +206 -0
  193. package/src/UI/PluginsWidget.js +280 -0
  194. package/src/UI/SceneListing.js +606 -0
  195. package/src/UI/SelectionFilter.js +629 -0
  196. package/src/UI/ViewCube.js +389 -0
  197. package/src/UI/assembly/AssemblyConstraintCollectionWidget.js +329 -0
  198. package/src/UI/assembly/AssemblyConstraintControlsWidget.js +282 -0
  199. package/src/UI/assembly/AssemblyConstraintsWidget.css +292 -0
  200. package/src/UI/assembly/AssemblyConstraintsWidget.js +1373 -0
  201. package/src/UI/assembly/constraintFaceUtils.js +115 -0
  202. package/src/UI/assembly/constraintHighlightUtils.js +70 -0
  203. package/src/UI/assembly/constraintLabelUtils.js +31 -0
  204. package/src/UI/assembly/constraintPointUtils.js +64 -0
  205. package/src/UI/assembly/constraintSelectionUtils.js +185 -0
  206. package/src/UI/assembly/constraintStatusUtils.js +142 -0
  207. package/src/UI/componentSelectorModal.js +240 -0
  208. package/src/UI/controls/CombinedTransformControls.js +386 -0
  209. package/src/UI/dialogs.js +351 -0
  210. package/src/UI/expressionsManager.js +100 -0
  211. package/src/UI/featureDialogWidgets/booleanField.js +25 -0
  212. package/src/UI/featureDialogWidgets/booleanOperationField.js +97 -0
  213. package/src/UI/featureDialogWidgets/buttonField.js +45 -0
  214. package/src/UI/featureDialogWidgets/componentSelectorField.js +102 -0
  215. package/src/UI/featureDialogWidgets/defaultField.js +23 -0
  216. package/src/UI/featureDialogWidgets/fileField.js +66 -0
  217. package/src/UI/featureDialogWidgets/index.js +34 -0
  218. package/src/UI/featureDialogWidgets/numberField.js +165 -0
  219. package/src/UI/featureDialogWidgets/optionsField.js +33 -0
  220. package/src/UI/featureDialogWidgets/referenceSelectionField.js +208 -0
  221. package/src/UI/featureDialogWidgets/stringField.js +24 -0
  222. package/src/UI/featureDialogWidgets/textareaField.js +28 -0
  223. package/src/UI/featureDialogWidgets/threadDesignationField.js +160 -0
  224. package/src/UI/featureDialogWidgets/transformField.js +252 -0
  225. package/src/UI/featureDialogWidgets/utils.js +43 -0
  226. package/src/UI/featureDialogWidgets/vec3Field.js +133 -0
  227. package/src/UI/featureDialogs.js +1414 -0
  228. package/src/UI/fileManagerWidget.js +615 -0
  229. package/src/UI/history/HistoryCollectionWidget.js +1294 -0
  230. package/src/UI/history/historyCollectionWidget.css.js +257 -0
  231. package/src/UI/history/historyDisplayInfo.js +133 -0
  232. package/src/UI/mobile.js +28 -0
  233. package/src/UI/objectDump.js +442 -0
  234. package/src/UI/pmi/AnnotationCollectionWidget.js +120 -0
  235. package/src/UI/pmi/AnnotationHistory.js +353 -0
  236. package/src/UI/pmi/AnnotationRegistry.js +90 -0
  237. package/src/UI/pmi/BaseAnnotation.js +269 -0
  238. package/src/UI/pmi/LabelOverlay.css +102 -0
  239. package/src/UI/pmi/LabelOverlay.js +191 -0
  240. package/src/UI/pmi/PMIMode.js +1550 -0
  241. package/src/UI/pmi/PMIViewsWidget.js +1098 -0
  242. package/src/UI/pmi/annUtils.js +729 -0
  243. package/src/UI/pmi/dimensions/AngleDimensionAnnotation.js +647 -0
  244. package/src/UI/pmi/dimensions/ExplodeBodyAnnotation.js +507 -0
  245. package/src/UI/pmi/dimensions/HoleCalloutAnnotation.js +462 -0
  246. package/src/UI/pmi/dimensions/LeaderAnnotation.js +403 -0
  247. package/src/UI/pmi/dimensions/LinearDimensionAnnotation.js +532 -0
  248. package/src/UI/pmi/dimensions/NoteAnnotation.js +110 -0
  249. package/src/UI/pmi/dimensions/RadialDimensionAnnotation.js +659 -0
  250. package/src/UI/pmi/pmiStyle.js +44 -0
  251. package/src/UI/sketcher/SketchMode3D.js +4095 -0
  252. package/src/UI/sketcher/dimensions.js +674 -0
  253. package/src/UI/sketcher/glyphs.js +236 -0
  254. package/src/UI/sketcher/highlights.js +60 -0
  255. package/src/UI/toolbarButtons/aboutButton.js +5 -0
  256. package/src/UI/toolbarButtons/exportButton.js +609 -0
  257. package/src/UI/toolbarButtons/flatPatternButton.js +307 -0
  258. package/src/UI/toolbarButtons/importButton.js +160 -0
  259. package/src/UI/toolbarButtons/inspectorToggleButton.js +12 -0
  260. package/src/UI/toolbarButtons/metadataButton.js +1063 -0
  261. package/src/UI/toolbarButtons/orientToFaceButton.js +114 -0
  262. package/src/UI/toolbarButtons/registerDefaultButtons.js +46 -0
  263. package/src/UI/toolbarButtons/saveButton.js +99 -0
  264. package/src/UI/toolbarButtons/scriptRunnerButton.js +302 -0
  265. package/src/UI/toolbarButtons/testsButton.js +26 -0
  266. package/src/UI/toolbarButtons/undoRedoButtons.js +25 -0
  267. package/src/UI/toolbarButtons/wireframeToggleButton.js +5 -0
  268. package/src/UI/toolbarButtons/zoomToFitButton.js +5 -0
  269. package/src/UI/triangleDebuggerWindow.js +945 -0
  270. package/src/UI/viewer.js +4228 -0
  271. package/src/assemblyConstraints/AssemblyConstraintHistory.js +1576 -0
  272. package/src/assemblyConstraints/AssemblyConstraintRegistry.js +120 -0
  273. package/src/assemblyConstraints/BaseAssemblyConstraint.js +66 -0
  274. package/src/assemblyConstraints/constraintExpressionUtils.js +35 -0
  275. package/src/assemblyConstraints/constraintUtils/parallelAlignment.js +676 -0
  276. package/src/assemblyConstraints/constraints/AngleConstraint.js +485 -0
  277. package/src/assemblyConstraints/constraints/CoincidentConstraint.js +194 -0
  278. package/src/assemblyConstraints/constraints/DistanceConstraint.js +616 -0
  279. package/src/assemblyConstraints/constraints/FixedConstraint.js +78 -0
  280. package/src/assemblyConstraints/constraints/ParallelConstraint.js +252 -0
  281. package/src/assemblyConstraints/constraints/TouchAlignConstraint.js +961 -0
  282. package/src/core/entities/HistoryCollectionBase.js +72 -0
  283. package/src/core/entities/ListEntityBase.js +109 -0
  284. package/src/core/entities/schemaProcesser.js +121 -0
  285. package/src/exporters/sheetMetalFlatPattern.js +659 -0
  286. package/src/exporters/sheetMetalUnfold.js +862 -0
  287. package/src/exporters/step.js +1135 -0
  288. package/src/exporters/threeMF.js +575 -0
  289. package/src/features/assemblyComponent/AssemblyComponentFeature.js +780 -0
  290. package/src/features/boolean/BooleanFeature.js +94 -0
  291. package/src/features/chamfer/ChamferFeature.js +116 -0
  292. package/src/features/datium/DatiumFeature.js +80 -0
  293. package/src/features/edgeFeatureUtils.js +41 -0
  294. package/src/features/extrude/ExtrudeFeature.js +143 -0
  295. package/src/features/fillet/FilletFeature.js +197 -0
  296. package/src/features/helix/HelixFeature.js +405 -0
  297. package/src/features/hole/HoleFeature.js +1050 -0
  298. package/src/features/hole/screwClearance.js +86 -0
  299. package/src/features/hole/threadDesignationCatalog.js +149 -0
  300. package/src/features/imageHeightSolid/ImageHeightmapSolidFeature.js +463 -0
  301. package/src/features/imageToFace/ImageToFaceFeature.js +727 -0
  302. package/src/features/imageToFace/imageEditor.js +1270 -0
  303. package/src/features/imageToFace/traceUtils.js +971 -0
  304. package/src/features/import3dModel/Import3dModelFeature.js +151 -0
  305. package/src/features/loft/LoftFeature.js +605 -0
  306. package/src/features/mirror/MirrorFeature.js +151 -0
  307. package/src/features/offsetFace/OffsetFaceFeature.js +370 -0
  308. package/src/features/offsetShell/OffsetShellFeature.js +89 -0
  309. package/src/features/overlapCleanup/OverlapCleanupFeature.js +85 -0
  310. package/src/features/pattern/PatternFeature.js +275 -0
  311. package/src/features/patternLinear/PatternLinearFeature.js +120 -0
  312. package/src/features/patternRadial/PatternRadialFeature.js +186 -0
  313. package/src/features/plane/PlaneFeature.js +154 -0
  314. package/src/features/primitiveCone/primitiveConeFeature.js +99 -0
  315. package/src/features/primitiveCube/primitiveCubeFeature.js +70 -0
  316. package/src/features/primitiveCylinder/primitiveCylinderFeature.js +91 -0
  317. package/src/features/primitivePyramid/primitivePyramidFeature.js +72 -0
  318. package/src/features/primitiveSphere/primitiveSphereFeature.js +62 -0
  319. package/src/features/primitiveTorus/primitiveTorusFeature.js +109 -0
  320. package/src/features/remesh/RemeshFeature.js +97 -0
  321. package/src/features/revolve/RevolveFeature.js +111 -0
  322. package/src/features/selectionUtils.js +118 -0
  323. package/src/features/sheetMetal/SheetMetalContourFlangeFeature.js +1656 -0
  324. package/src/features/sheetMetal/SheetMetalCutoutFeature.js +1056 -0
  325. package/src/features/sheetMetal/SheetMetalFlangeFeature.js +1568 -0
  326. package/src/features/sheetMetal/SheetMetalHemFeature.js +43 -0
  327. package/src/features/sheetMetal/SheetMetalObject.js +141 -0
  328. package/src/features/sheetMetal/SheetMetalTabFeature.js +176 -0
  329. package/src/features/sheetMetal/UNFOLD_NEUTRAL_REQUIREMENTS.md +153 -0
  330. package/src/features/sheetMetal/contour-flange-rebuild-spec.md +261 -0
  331. package/src/features/sheetMetal/profileUtils.js +25 -0
  332. package/src/features/sheetMetal/sheetMetalCleanup.js +9 -0
  333. package/src/features/sheetMetal/sheetMetalFaceTypes.js +146 -0
  334. package/src/features/sheetMetal/sheetMetalMetadata.js +165 -0
  335. package/src/features/sheetMetal/sheetMetalPipeline.js +169 -0
  336. package/src/features/sheetMetal/sheetMetalProfileUtils.js +216 -0
  337. package/src/features/sheetMetal/sheetMetalTabUtils.js +29 -0
  338. package/src/features/sheetMetal/sheetMetalTree.js +210 -0
  339. package/src/features/sketch/SketchFeature.js +955 -0
  340. package/src/features/sketch/sketchSolver2D/ConstraintEngine.js +800 -0
  341. package/src/features/sketch/sketchSolver2D/constraintDefinitions.js +704 -0
  342. package/src/features/sketch/sketchSolver2D/mathHelpersMod.js +307 -0
  343. package/src/features/spline/SplineEditorSession.js +988 -0
  344. package/src/features/spline/SplineFeature.js +1388 -0
  345. package/src/features/spline/splineUtils.js +218 -0
  346. package/src/features/sweep/SweepFeature.js +110 -0
  347. package/src/features/transform/TransformFeature.js +152 -0
  348. package/src/features/tube/TubeFeature.js +635 -0
  349. package/src/fs.proxy.js +625 -0
  350. package/src/idbStorage.js +254 -0
  351. package/src/index.js +12 -0
  352. package/src/main.js +15 -0
  353. package/src/metadataManager.js +64 -0
  354. package/src/path.proxy.js +277 -0
  355. package/src/plugins/ghLoader.worker.js +151 -0
  356. package/src/plugins/pluginManager.js +286 -0
  357. package/src/pmi/PMIViewsManager.js +134 -0
  358. package/src/services/componentLibrary.js +198 -0
  359. package/src/tests/ConsoleCapture.js +189 -0
  360. package/src/tests/S7-diagnostics-2025-12-23T18-37-23-570Z.json +630 -0
  361. package/src/tests/browserTests.js +597 -0
  362. package/src/tests/debugBoolean.js +225 -0
  363. package/src/tests/partFiles/badBoolean.json +957 -0
  364. package/src/tests/partFiles/extrudeTest.json +88 -0
  365. package/src/tests/partFiles/filletFail.json +58 -0
  366. package/src/tests/partFiles/import_TEst.part.part.json +646 -0
  367. package/src/tests/partFiles/sheetMetalHem.BREP.json +734 -0
  368. package/src/tests/test_boolean_subtract.js +27 -0
  369. package/src/tests/test_chamfer.js +17 -0
  370. package/src/tests/test_extrudeFace.js +24 -0
  371. package/src/tests/test_fillet.js +17 -0
  372. package/src/tests/test_fillet_nonClosed.js +45 -0
  373. package/src/tests/test_filletsMoreDifficult.js +46 -0
  374. package/src/tests/test_history_features_basic.js +149 -0
  375. package/src/tests/test_hole.js +282 -0
  376. package/src/tests/test_mirror.js +16 -0
  377. package/src/tests/test_offsetShellGrouping.js +85 -0
  378. package/src/tests/test_plane.js +4 -0
  379. package/src/tests/test_primitiveCone.js +11 -0
  380. package/src/tests/test_primitiveCube.js +7 -0
  381. package/src/tests/test_primitiveCylinder.js +8 -0
  382. package/src/tests/test_primitivePyramid.js +9 -0
  383. package/src/tests/test_primitiveSphere.js +17 -0
  384. package/src/tests/test_primitiveTorus.js +21 -0
  385. package/src/tests/test_pushFace.js +126 -0
  386. package/src/tests/test_sheetMetalContourFlange.js +125 -0
  387. package/src/tests/test_sheetMetal_features.js +80 -0
  388. package/src/tests/test_sketch_openLoop.js +45 -0
  389. package/src/tests/test_solidMetrics.js +58 -0
  390. package/src/tests/test_stlLoader.js +1889 -0
  391. package/src/tests/test_sweepFace.js +55 -0
  392. package/src/tests/test_tube.js +45 -0
  393. package/src/tests/test_tube_closedLoop.js +67 -0
  394. package/src/tests/tests.js +493 -0
  395. package/src/tools/assemblyConstraintDialogCapturePage.js +56 -0
  396. package/src/tools/dialogCapturePageFactory.js +227 -0
  397. package/src/tools/featureDialogCapturePage.js +47 -0
  398. package/src/tools/pmiAnnotationDialogCapturePage.js +60 -0
  399. package/src/utils/axisHelpers.js +99 -0
  400. package/src/utils/deepClone.js +69 -0
  401. package/src/utils/geometryTolerance.js +37 -0
  402. package/src/utils/normalizeTypeString.js +8 -0
  403. package/src/utils/xformMath.js +51 -0
@@ -0,0 +1,1422 @@
1
+ import { Solid } from "../BetterSolid.js";
2
+ import * as THREE from 'three';
3
+ import {
4
+ getScaleAdaptiveTolerance,
5
+ getDistanceTolerance,
6
+ getAngleTolerance,
7
+ trimFilletCaches,
8
+ getCachedFaceDataForTris,
9
+ averageFaceNormalObjectSpace,
10
+ localFaceNormalAtPoint,
11
+ projectPointOntoFaceTriangles,
12
+ batchProjectPointsOntoFace,
13
+ clamp,
14
+ isFiniteVec3,
15
+ } from './inset.js';
16
+ import {
17
+ solveCenterFromOffsetPlanesAnchored,
18
+ } from './outset.js';
19
+ import { Tube } from "../Tube.js";
20
+ import { computeFaceAreaFromTriangles } from "./filletGeometry.js";
21
+
22
+ export { clearFilletCaches, trimFilletCaches } from './inset.js';
23
+ export { fixTJunctionsAndPatchHoles } from './outset.js';
24
+
25
+ /**
26
+ * Compute the fillet centerline polyline for an input edge without building the fillet solid.
27
+ *
28
+ * Returns polylines for:
29
+ * - points: locus of arc centers (centerline)
30
+ * - tangentA: tangency curve on face A (cylinder-face A intersection)
31
+ * - tangentB: tangency curve on face B (cylinder-face B intersection)
32
+ * All points are returned as objects {x,y,z} for readability.
33
+ * Downstream consumers that require array triples are still supported
34
+ * via Solid.addAuxEdge, which now accepts both objects and [x,y,z] arrays.
35
+ *
36
+ * @param {any} edgeObj Edge object (expects `.faces[0/1]`, `.userData.polylineLocal`, and `.parent` solid)
37
+ * @param {number} radius Fillet radius (> 0)
38
+ * @param {'INSET'|'OUTSET'} sideMode Preferred side relative to outward normals (default 'INSET')
39
+ * @returns {{ points: {x:number,y:number,z:number}[], tangentA?: {x:number,y:number,z:number}[], tangentB?: {x:number,y:number,z:number}[], edge?: {x:number,y:number,z:number}[], closedLoop: boolean }}
40
+ */
41
+ export function computeFilletCenterline(edgeObj, radius = 1, sideMode = 'INSET') {
42
+ const out = { points: [], tangentA: [], tangentB: [], edge: [], closedLoop: false };
43
+ try {
44
+ if (!edgeObj || !Number.isFinite(radius) || radius <= 0) return out;
45
+ const solid = edgeObj.parentSolid || edgeObj.parent;
46
+ if (!solid) return out;
47
+ const faceA = edgeObj.faces?.[0] || null;
48
+ const faceB = edgeObj.faces?.[1] || null;
49
+ const faceNameA = faceA?.name || edgeObj?.userData?.faceA || null;
50
+ const faceNameB = faceB?.name || edgeObj?.userData?.faceB || null;
51
+ const segmentFacePairs = Array.isArray(edgeObj?.userData?.segmentFacePairs) ? edgeObj.userData.segmentFacePairs : null;
52
+ const useSegmentPairs = Array.isArray(segmentFacePairs) && segmentFacePairs.length > 0;
53
+ if (!useSegmentPairs && (!faceNameA || !faceNameB)) return out;
54
+
55
+ const polyLocal = edgeObj.userData?.polylineLocal;
56
+ if (!Array.isArray(polyLocal) || polyLocal.length < 2) return out;
57
+
58
+ // Tolerances (scale-adaptive to radius)
59
+ const eps = getScaleAdaptiveTolerance(radius, 1e-12);
60
+ const distTol = getDistanceTolerance(radius);
61
+ const angleTol = getAngleTolerance();
62
+ const vecLengthTol = getScaleAdaptiveTolerance(radius, 1e-14);
63
+
64
+ // Average outward normals per face (object space)
65
+ let nAavg = null;
66
+ let nBavg = null;
67
+ let trisA = null;
68
+ let trisB = null;
69
+ let faceKeyA = null;
70
+ let faceKeyB = null;
71
+ let faceDataA = null;
72
+ let faceDataB = null;
73
+
74
+ // Create unique cache keys that include solid identity and geometry hash to prevent cross-contamination
75
+ const solidId = solid.uuid || solid.name || solid.constructor.name;
76
+ if (!useSegmentPairs) {
77
+ nAavg = averageFaceNormalObjectSpace(solid, faceNameA);
78
+ nBavg = averageFaceNormalObjectSpace(solid, faceNameB);
79
+ if (!isFiniteVec3(nAavg) || !isFiniteVec3(nBavg)) return out;
80
+
81
+ // Fetch triangles and cached data for both faces once
82
+ trisA = solid.getFace(faceNameA);
83
+ trisB = solid.getFace(faceNameB);
84
+ if (!Array.isArray(trisA) || !trisA.length || !Array.isArray(trisB) || !trisB.length) return out;
85
+
86
+ const geometryHashA = trisA.length > 0 ? `${trisA.length}_${trisA[0].p1?.[0]?.toFixed(3) || 0}` : '0';
87
+ const geometryHashB = trisB.length > 0 ? `${trisB.length}_${trisB[0].p1?.[0]?.toFixed(3) || 0}` : '0';
88
+ faceKeyA = `${solidId}:${faceNameA}:${geometryHashA}`;
89
+ faceKeyB = `${solidId}:${faceNameB}:${geometryHashB}`;
90
+ faceDataA = getCachedFaceDataForTris(trisA, faceKeyA);
91
+ faceDataB = getCachedFaceDataForTris(trisB, faceKeyB);
92
+ }
93
+
94
+ // Robust closed-loop detection (prefer flags, else compare endpoints)
95
+ let isClosed = !!(edgeObj.closedLoop || edgeObj.userData?.closedLoop);
96
+ if (!isClosed && polyLocal.length > 2) {
97
+ const a = polyLocal[0];
98
+ const b = polyLocal[polyLocal.length - 1];
99
+ if (a && b) {
100
+ const dx = a[0] - b[0], dy = a[1] - b[1], dz = a[2] - b[2];
101
+ const d2 = dx * dx + dy * dy + dz * dz;
102
+ const eps2 = distTol * distTol;
103
+ if (d2 <= eps2) isClosed = true;
104
+ }
105
+ }
106
+ out.closedLoop = isClosed;
107
+
108
+ // Build sampling points: original vertices + midpoints (wrap for closed)
109
+ let samples;
110
+ let sampleSegmentIdx = null;
111
+ {
112
+ const src = polyLocal.slice();
113
+ if (isClosed && src.length > 2) {
114
+ const a = src[0], b = src[src.length - 1];
115
+ if (a && b && a[0] === b[0] && a[1] === b[1] && a[2] === b[2]) src.pop();
116
+ }
117
+
118
+ const outPts = [];
119
+ const segIdxs = [];
120
+ const segCount = useSegmentPairs
121
+ ? Math.max(1, segmentFacePairs.length)
122
+ : Math.max(1, (isClosed ? src.length : (src.length - 1)));
123
+ for (let i = 0; i < src.length; i++) {
124
+ const a = src[i];
125
+ const segIdxVertex = isClosed
126
+ ? ((i - 1 + segCount) % segCount)
127
+ : Math.max(0, Math.min(i - 1, segCount - 1));
128
+ const segIdxMid = isClosed ? (i % segCount) : Math.min(i, segCount - 1);
129
+ outPts.push(new THREE.Vector3(a[0], a[1], a[2]));
130
+ segIdxs.push(segIdxVertex);
131
+ const j = i + 1;
132
+ if (isClosed) {
133
+ const b = src[(i + 1) % src.length];
134
+ outPts.push(new THREE.Vector3(0.5 * (a[0] + b[0]), 0.5 * (a[1] + b[1]), 0.5 * (a[2] + b[2])));
135
+ segIdxs.push(segIdxMid);
136
+ } else if (j < src.length) {
137
+ const b = src[j];
138
+ outPts.push(new THREE.Vector3(0.5 * (a[0] + b[0]), 0.5 * (a[1] + b[1]), 0.5 * (a[2] + b[2])));
139
+ segIdxs.push(segIdxMid);
140
+ }
141
+ }
142
+ samples = outPts;
143
+ if (useSegmentPairs) sampleSegmentIdx = segIdxs;
144
+ }
145
+
146
+ // Project samples to both faces and compute local normals
147
+ const sampleCount = samples.length;
148
+ let qAList = null;
149
+ let qBList = null;
150
+ let normalsA = null;
151
+ let normalsB = null;
152
+ let getFaceEntry = null;
153
+ if (!useSegmentPairs) {
154
+ qAList = batchProjectPointsOntoFace(trisA, samples, faceDataA, faceKeyA);
155
+ qBList = batchProjectPointsOntoFace(trisB, samples, faceDataB, faceKeyB);
156
+ normalsA = new Array(sampleCount);
157
+ normalsB = new Array(sampleCount);
158
+ for (let i = 0; i < sampleCount; i++) {
159
+ normalsA[i] = localFaceNormalAtPoint(solid, faceNameA, qAList[i], faceDataA, faceKeyA) || nAavg;
160
+ normalsB[i] = localFaceNormalAtPoint(solid, faceNameB, qBList[i], faceDataB, faceKeyB) || nBavg;
161
+ }
162
+ } else {
163
+ const faceCache = new Map();
164
+ getFaceEntry = (faceName) => {
165
+ if (!faceName) return null;
166
+ if (faceCache.has(faceName)) return faceCache.get(faceName);
167
+ const tris = solid.getFace(faceName);
168
+ if (!Array.isArray(tris) || !tris.length) return null;
169
+ const geometryHash = tris.length > 0 ? `${tris.length}_${tris[0].p1?.[0]?.toFixed(3) || 0}` : '0';
170
+ const faceKey = `${solidId}:${faceName}:${geometryHash}`;
171
+ const data = getCachedFaceDataForTris(tris, faceKey);
172
+ const avg = averageFaceNormalObjectSpace(solid, faceName);
173
+ if (!isFiniteVec3(avg)) return null;
174
+ const entry = { tris, data, key: faceKey, avg };
175
+ faceCache.set(faceName, entry);
176
+ return entry;
177
+ };
178
+ }
179
+
180
+ // Scratch vectors
181
+ const tangent = new THREE.Vector3();
182
+ const tempU = new THREE.Vector3();
183
+ const tempV = new THREE.Vector3();
184
+ const fallbackDir = new THREE.Vector3();
185
+ const bisector3 = new THREE.Vector3();
186
+ const avgNormalScratch = new THREE.Vector3();
187
+
188
+ const rEff = Math.max(eps, radius);
189
+ let centers = [];
190
+ let tanA = [];
191
+ let tanB = [];
192
+ let edgePts = [];
193
+ for (let i = 0; i < sampleCount; i++) {
194
+ const p = samples[i];
195
+ const pPrev = isClosed ? samples[(i - 1 + sampleCount) % sampleCount] : samples[Math.max(0, i - 1)];
196
+ const pNext = isClosed ? samples[(i + 1) % sampleCount] : samples[Math.min(sampleCount - 1, i + 1)];
197
+
198
+ tangent.copy(pNext).sub(pPrev);
199
+
200
+ if (tangent.lengthSq() < vecLengthTol) continue;
201
+ tangent.normalize();
202
+
203
+ let qA = null;
204
+ let qB = null;
205
+ let nA = null;
206
+ let nB = null;
207
+ let faceNameAUse = faceNameA;
208
+ let faceNameBUse = faceNameB;
209
+ let faceDataAUse = faceDataA;
210
+ let faceDataBUse = faceDataB;
211
+ let trisAUse = trisA;
212
+ let trisBUse = trisB;
213
+ let faceKeyAUse = faceKeyA;
214
+ let faceKeyBUse = faceKeyB;
215
+ let nAavgUse = nAavg;
216
+ let nBavgUse = nBavg;
217
+ let allowRefine = true;
218
+ if (useSegmentPairs && typeof getFaceEntry === 'function') {
219
+ const segIdx = Array.isArray(sampleSegmentIdx) ? sampleSegmentIdx[i] : 0;
220
+ const pair = segmentFacePairs[segIdx] || segmentFacePairs[segmentFacePairs.length - 1];
221
+ if (pair && typeof pair === 'object' && !Array.isArray(pair) && pair.base && pair.sideA && pair.sideB) {
222
+ const baseName = pair.base;
223
+ const sideAName = pair.sideA;
224
+ const sideBName = pair.sideB;
225
+ const tBlend = Number.isFinite(pair.t) ? Math.max(0, Math.min(1, Number(pair.t))) : 0.5;
226
+ const entryBase = getFaceEntry(baseName);
227
+ const entrySideA = getFaceEntry(sideAName);
228
+ const entrySideB = getFaceEntry(sideBName);
229
+ if (!entryBase || !entrySideA || !entrySideB) continue;
230
+ faceNameAUse = baseName;
231
+ faceDataAUse = entryBase.data;
232
+ trisAUse = entryBase.tris;
233
+ faceKeyAUse = entryBase.key;
234
+ nAavgUse = entryBase.avg;
235
+
236
+ const qBase = projectPointOntoFaceTriangles(trisAUse, p, faceDataAUse, faceKeyAUse);
237
+ nA = localFaceNormalAtPoint(solid, baseName, qBase, faceDataAUse, faceKeyAUse) || nAavgUse;
238
+ qA = qBase;
239
+
240
+ const qSideA = projectPointOntoFaceTriangles(entrySideA.tris, p, entrySideA.data, entrySideA.key);
241
+ const qSideB = projectPointOntoFaceTriangles(entrySideB.tris, p, entrySideB.data, entrySideB.key);
242
+ const nSideA = localFaceNormalAtPoint(solid, sideAName, qSideA, entrySideA.data, entrySideA.key) || entrySideA.avg;
243
+ const nSideB = localFaceNormalAtPoint(solid, sideBName, qSideB, entrySideB.data, entrySideB.key) || entrySideB.avg;
244
+ const blend = nSideA.clone().multiplyScalar(1 - tBlend).addScaledVector(nSideB, tBlend);
245
+ nB = (blend.lengthSq() > 0) ? blend.normalize() : nSideA.clone();
246
+ qB = qSideA.clone().lerp(qSideB, tBlend);
247
+ faceNameBUse = sideAName;
248
+ faceDataBUse = entrySideA.data;
249
+ trisBUse = entrySideA.tris;
250
+ faceKeyBUse = entrySideA.key;
251
+ nBavgUse = entrySideA.avg;
252
+ allowRefine = false;
253
+ } else {
254
+ const segA = Array.isArray(pair) ? pair[0] : (pair?.faceA || pair?.a || null);
255
+ const segB = Array.isArray(pair) ? pair[1] : (pair?.faceB || pair?.b || null);
256
+ if (!segA || !segB) continue;
257
+ faceNameAUse = segA;
258
+ faceNameBUse = segB;
259
+ const entryA = getFaceEntry(faceNameAUse);
260
+ const entryB = getFaceEntry(faceNameBUse);
261
+ if (!entryA || !entryB) continue;
262
+ faceDataAUse = entryA.data;
263
+ faceDataBUse = entryB.data;
264
+ trisAUse = entryA.tris;
265
+ trisBUse = entryB.tris;
266
+ faceKeyAUse = entryA.key;
267
+ faceKeyBUse = entryB.key;
268
+ nAavgUse = entryA.avg;
269
+ nBavgUse = entryB.avg;
270
+ qA = projectPointOntoFaceTriangles(trisAUse, p, faceDataAUse, faceKeyAUse);
271
+ qB = projectPointOntoFaceTriangles(trisBUse, p, faceDataBUse, faceKeyBUse);
272
+ nA = localFaceNormalAtPoint(solid, faceNameAUse, qA, faceDataAUse, faceKeyAUse) || nAavgUse;
273
+ nB = localFaceNormalAtPoint(solid, faceNameBUse, qB, faceDataBUse, faceKeyBUse) || nBavgUse;
274
+ }
275
+ } else {
276
+ qA = qAList[i];
277
+ qB = qBList[i];
278
+ nA = normalsA[i] || nAavgUse;
279
+ nB = normalsB[i] || nBavgUse;
280
+ }
281
+
282
+ const vA3 = tempU.copy(nA).cross(tangent);
283
+ const vB3 = tempV.copy(nB).cross(tangent);
284
+ if (vA3.lengthSq() < eps || vB3.lengthSq() < eps) continue;
285
+ vA3.normalize(); vB3.normalize();
286
+
287
+ const u = vA3.clone();
288
+ const v = new THREE.Vector3().crossVectors(tangent, u).normalize();
289
+ const d0_2 = new THREE.Vector2(1, 0);
290
+ const d1_2 = new THREE.Vector2(vB3.dot(u), vB3.dot(v));
291
+ d1_2.normalize();
292
+ const dot2 = clamp(d0_2.x * d1_2.x + d0_2.y * d1_2.y, -1, 1);
293
+ const angAbs = Math.acos(dot2);
294
+ const sinHalf = Math.sin(0.5 * angAbs);
295
+ if (Math.abs(sinHalf) < angleTol) continue;
296
+ const expectDist = rEff / Math.abs(sinHalf);
297
+
298
+ // 2D inward normals in section plane for fallback
299
+ const inA3 = tangent.clone().cross(vA3).negate();
300
+ const inB3 = tangent.clone().cross(vB3).negate();
301
+ const n0_2 = new THREE.Vector2(inA3.dot(u), inA3.dot(v)).normalize();
302
+ const n1_2 = new THREE.Vector2(inB3.dot(u), inB3.dot(v)).normalize();
303
+ let bis2 = new THREE.Vector2(n0_2.x + n1_2.x, n0_2.y + n1_2.y);
304
+ const lenBis2 = bis2.length();
305
+ if (lenBis2 > 1e-9) bis2.multiplyScalar(1 / lenBis2); else bis2.set(0, 0);
306
+
307
+ // Solve with anchored offset planes in 3D
308
+ const C_in = solveCenterFromOffsetPlanesAnchored(p, tangent, nA, qA, -1, nB, qB, -1, rEff);
309
+ const C_out = solveCenterFromOffsetPlanesAnchored(p, tangent, nA, qA, +1, nB, qB, +1, rEff);
310
+ let pick = (String(sideMode).toUpperCase() === 'OUTSET') ? 'out' : 'in';
311
+ let center = (pick === 'in') ? (C_in || C_out) : (C_out || C_in);
312
+
313
+
314
+
315
+ // Initial tangency points from center (used to refine/fallback)
316
+ const sA = (pick === 'in') ? -1 : +1;
317
+ const sB = sA;
318
+ let tA = center ? center.clone().addScaledVector(nA, -sA * rEff) : p.clone();
319
+ let tB = center ? center.clone().addScaledVector(nB, -sB * rEff) : p.clone();
320
+
321
+ // Fallback if intersection failed
322
+ if (!center) {
323
+ if (bis2.lengthSq() > eps) {
324
+ const dir3 = fallbackDir.set(0, 0, 0).addScaledVector(u, bis2.x).addScaledVector(v, bis2.y);
325
+ if (pick === 'out') dir3.negate();
326
+ dir3.normalize();
327
+ center = p.clone().addScaledVector(dir3, expectDist);
328
+ } else {
329
+ const avgN = avgNormalScratch.copy(nA).add(nB);
330
+ if (avgN.lengthSq() > eps) {
331
+ avgN.normalize();
332
+ const sign = (pick === 'in') ? -1 : 1;
333
+ center = p.clone().addScaledVector(avgN, sign * expectDist);
334
+ } else {
335
+ // give up on this sample
336
+ continue;
337
+ }
338
+ }
339
+ }
340
+
341
+ // Optional refinement: if initial p->center distance far from expected, recompute
342
+ const initialDist = center.distanceTo(p);
343
+ const needsRefinement = Math.abs(initialDist - expectDist) > 0.1 * rEff;
344
+ if (needsRefinement && allowRefine) {
345
+ try {
346
+ const qA1 = projectPointOntoFaceTriangles(trisAUse, tA, faceDataAUse);
347
+ const qB1 = projectPointOntoFaceTriangles(trisBUse, tB, faceDataBUse);
348
+ const nA1 = localFaceNormalAtPoint(solid, faceNameAUse, qA1, faceDataAUse, faceKeyAUse) || nAavgUse;
349
+ const nB1 = localFaceNormalAtPoint(solid, faceNameBUse, qB1, faceDataBUse, faceKeyBUse) || nBavgUse;
350
+ const C_ref = solveCenterFromOffsetPlanesAnchored(p, tangent, nA1, qA1, sA, nB1, qB1, sB, rEff);
351
+ if (C_ref) {
352
+ center = C_ref;
353
+ // Update normals used at tangency too
354
+ nA = nA1;
355
+ nB = nB1;
356
+ tA = center.clone().addScaledVector(nA, -sA * rEff);
357
+ tB = center.clone().addScaledVector(nB, -sB * rEff);
358
+ }
359
+ } catch { /* ignore */ }
360
+ }
361
+
362
+ // Safety cap: if center is unreasonably far, snap to 2D bisector expectation
363
+ {
364
+ const pToC = center.distanceTo(p);
365
+ const hardCap = 6 * rEff;
366
+ const factor = 3.0;
367
+ if (!Number.isFinite(pToC) || pToC > hardCap || pToC > factor * expectDist) {
368
+ let dir2 = new THREE.Vector2(bis2.x, bis2.y);
369
+ if (String(sideMode).toUpperCase() === 'OUTSET') dir2.multiplyScalar(-1);
370
+ if (dir2.lengthSq() > 1e-16) {
371
+ dir2.normalize();
372
+ const dir3 = bisector3.set(0, 0, 0).addScaledVector(u, dir2.x).addScaledVector(v, dir2.y).normalize();
373
+ // Clamp the bisector distance so acute/near-parallel face
374
+ // configurations do not explode the centerline far from the edge.
375
+ const safeDist = Math.min(expectDist, hardCap);
376
+ center = p.clone().addScaledVector(dir3, safeDist);
377
+ // Recompute tangency points using latest normals
378
+ tA = center.clone().addScaledVector(nA, -sA * rEff);
379
+ tB = center.clone().addScaledVector(nB, -sB * rEff);
380
+ }
381
+ }
382
+ }
383
+
384
+ centers.push({ x: center.x, y: center.y, z: center.z });
385
+ tanA.push({ x: tA.x, y: tA.y, z: tA.z });
386
+ tanB.push({ x: tB.x, y: tB.y, z: tB.z });
387
+ edgePts.push({ x: p.x, y: p.y, z: p.z });
388
+ }
389
+
390
+ // For closed loops, explicitly duplicate the start point at the end
391
+ // so the centerline is a closed polyline (last point equals first point).
392
+ if (isClosed && centers.length >= 2) {
393
+ const firstCenter = centers[0];
394
+ const lastCenter = centers[centers.length - 1];
395
+
396
+ const exactlyEqual = (a, b) => a.x === b.x && a.y === b.y && a.z === b.z;
397
+
398
+ if (!exactlyEqual(firstCenter, lastCenter)) {
399
+ // Always append an explicit duplicate of the first point
400
+ centers.push({ x: firstCenter.x, y: firstCenter.y, z: firstCenter.z });
401
+
402
+ // Mirror closure on tangency curves and sampled edge points to keep arrays aligned
403
+ if (tanA.length > 0) {
404
+ const a0 = tanA[0];
405
+ tanA.push({ x: a0.x, y: a0.y, z: a0.z });
406
+ }
407
+ if (tanB.length > 0) {
408
+ const b0 = tanB[0];
409
+ tanB.push({ x: b0.x, y: b0.y, z: b0.z });
410
+ }
411
+ if (edgePts.length > 0) {
412
+ const e0 = edgePts[0];
413
+ edgePts.push({ x: e0.x, y: e0.y, z: e0.z });
414
+ }
415
+ }
416
+ }
417
+ out.points = centers;
418
+ out.tangentA = tanA;
419
+ out.tangentB = tanB;
420
+ out.edge = edgePts;
421
+ fixPolylineWinding(centers, tanA, tanB);
422
+ return out;
423
+ } catch (e) {
424
+ console.warn('[computeFilletCenterline] failed:', e?.message || e);
425
+ return out;
426
+ }
427
+ }
428
+
429
+ /**
430
+ * Fix polyline winding order to ensure consistent triangle orientation.
431
+ * Checks all three polylines (centerline, tangentA, tangentB) for consistent winding.
432
+ *
433
+ * @param {Array} centerline - Array of center points {x, y, z}
434
+ * @param {Array} tangentA - Array of tangent A points {x, y, z}
435
+ * @param {Array} tangentB - Array of tangent B points {x, y, z}
436
+ * @returns {Object} - {centerlineReversed: boolean, tangentAReversed: boolean, tangentBReversed: boolean}
437
+ */
438
+ // Decide which polylines to reverse so that point i across
439
+ // centerline/tangentA/tangentB correspond to a consistent cross‑section.
440
+ // Uses an objective based on how close the tangent points are to the fillet
441
+ // radius from the centerline at sampled indices (quarter/half/three‑quarter).
442
+ // Falls back to direction/cross heuristics when radius is unavailable.
443
+ function fixPolylineWinding(centerline, tangentA, tangentB, expectedRadius = null) {
444
+ try {
445
+ // Fast-path: if any array is too small or lengths differ, do nothing
446
+ if (!Array.isArray(centerline) || !Array.isArray(tangentA) || !Array.isArray(tangentB)) {
447
+ return { centerlineReversed: false, tangentAReversed: false, tangentBReversed: false };
448
+ }
449
+ const isValidPoint = (p) => !!p && isFinite(p.x) && isFinite(p.y) && isFinite(p.z);
450
+ const n = Math.min(centerline.length, tangentA.length, tangentB.length);
451
+ if (n < 3) {
452
+ return { centerlineReversed: false, tangentAReversed: false, tangentBReversed: false };
453
+ }
454
+
455
+ // If we have a target radius, use it to search over combinations of
456
+ // {reverse centerline, reverse A, reverse B} that best satisfy
457
+ // dist(center[i], tangentX[i]) ≈ radius at a few sample locations.
458
+ if (Number.isFinite(expectedRadius) && expectedRadius > 0) {
459
+ const dist = (p, q) => {
460
+ const dx = (q.x - p.x), dy = (q.y - p.y), dz = (q.z - p.z);
461
+ return Math.hypot(dx, dy, dz);
462
+ };
463
+
464
+ // Choose robust sample indices near 1/4, 1/2, 3/4 along the polyline
465
+ const idxs = [];
466
+ const idxFromT = (t) => Math.max(0, Math.min(n - 1, Math.round(t * (n - 1))));
467
+ const pushUnique = (i) => { if (!idxs.includes(i)) idxs.push(i); };
468
+ pushUnique(idxFromT(0.25));
469
+ pushUnique(idxFromT(0.5));
470
+ pushUnique(idxFromT(0.75));
471
+
472
+ const combos = [
473
+ [false, false, false],
474
+ [false, true, false],
475
+ [false, false, true],
476
+ [true, false, false],
477
+ [true, true, false],
478
+ [true, false, true],
479
+ [false, true, true],
480
+ [true, true, true]
481
+ ];
482
+
483
+ let best = { cost: Infinity, rc: false, ra: false, rb: false };
484
+ for (const [rc, ra, rb] of combos) {
485
+ let cost = 0;
486
+ for (const i of idxs) {
487
+ const ci = centerline[rc ? (n - 1 - i) : i];
488
+ const ai = tangentA[ra ? (n - 1 - i) : i];
489
+ const bi = tangentB[rb ? (n - 1 - i) : i];
490
+ const dA = dist(ci, ai);
491
+ const dB = dist(ci, bi);
492
+ // Sum absolute deviations from expected radius
493
+ cost += Math.abs(dA - expectedRadius) + Math.abs(dB - expectedRadius);
494
+ }
495
+ if (cost < best.cost) best = { cost, rc, ra, rb };
496
+ }
497
+
498
+ if (best.cost < Infinity) {
499
+ return {
500
+ centerlineReversed: best.rc,
501
+ tangentAReversed: best.ra,
502
+ tangentBReversed: best.rb
503
+ };
504
+ }
505
+ }
506
+
507
+ // Primary heuristic: align the progression direction of tangents to the centerline.
508
+ // We compare average segment directions (normalized sum) and flip if the dot is negative.
509
+ const avgDir = (pts) => {
510
+ let sx = 0, sy = 0, sz = 0;
511
+ for (let i = 0; i < n - 1; i++) {
512
+ const a = pts[i], b = pts[i + 1];
513
+ sx += (b.x - a.x); sy += (b.y - a.y); sz += (b.z - a.z);
514
+ }
515
+ const len = Math.hypot(sx, sy, sz) || 1;
516
+ return { x: sx / len, y: sy / len, z: sz / len };
517
+ };
518
+ const cDir = avgDir(centerline);
519
+ const aDir = avgDir(tangentA);
520
+ const bDir = avgDir(tangentB);
521
+
522
+ const dot = (u, v) => (u.x * v.x + u.y * v.y + u.z * v.z);
523
+ let centerlineReversed = false;
524
+ let tangentAReversed = false;
525
+ let tangentBReversed = false;
526
+
527
+ // If a tangent flows opposite the centerline, flip it.
528
+ if (dot(cDir, aDir) < 0) tangentAReversed = true;
529
+ if (dot(cDir, bDir) < 0) tangentBReversed = true;
530
+
531
+ // If both tangents are flipped by the above, it may be easier to flip the centerline
532
+ // instead to keep A/B in their original indexing. Choose the minimal total reversals.
533
+ if (tangentAReversed && tangentBReversed) {
534
+ centerlineReversed = true;
535
+ tangentAReversed = false;
536
+ tangentBReversed = false;
537
+ }
538
+
539
+ // Secondary heuristic (legacy): examine relative cross-product signs to detect
540
+ // inconsistent relationships. This complements the direction-alignment above
541
+ // and only proposes additional flips if still inconsistent.
542
+ // Sample several points along the polylines to determine consistent orientation
543
+ const sampleCount = Math.min(8, Math.floor(centerline.length / 3));
544
+ const sampleIndices = [];
545
+ for (let i = 1; i < sampleCount - 1; i++) {
546
+ const idx = Math.floor(i * (centerline.length - 2) / (sampleCount - 1));
547
+ if (idx + 1 < centerline.length) {
548
+ sampleIndices.push(idx);
549
+ }
550
+ }
551
+
552
+ let centerlineToTangentA_CrossProducts = [];
553
+ let centerlineToTangentB_CrossProducts = [];
554
+ let tangentAToTangentB_CrossProducts = [];
555
+
556
+ // Analyze the relationship between each pair of polylines
557
+ for (const idx of sampleIndices) {
558
+ if (idx + 1 >= centerline.length) continue;
559
+
560
+ const c1 = centerline[idx];
561
+ const c2 = centerline[idx + 1];
562
+ const tA1 = tangentA[idx];
563
+ const tA2 = tangentA[idx + 1];
564
+ const tB1 = tangentB[idx];
565
+ const tB2 = tangentB[idx + 1];
566
+
567
+ // Validate all points are finite
568
+ if (!isValidPoint(c1) || !isValidPoint(c2) ||
569
+ !isValidPoint(tA1) || !isValidPoint(tA2) ||
570
+ !isValidPoint(tB1) || !isValidPoint(tB2)) {
571
+ continue; // Skip this sample if any point is invalid
572
+ }
573
+
574
+ // Vector along centerline
575
+ const centerVec = { x: c2.x - c1.x, y: c2.y - c1.y, z: c2.z - c1.z };
576
+
577
+ // Vector along tangent A
578
+ const tangentAVec = { x: tA2.x - tA1.x, y: tA2.y - tA1.y, z: tA2.z - tA1.z };
579
+
580
+ // Vector from centerline to tangent A
581
+ const centerToTangentA = { x: tA1.x - c1.x, y: tA1.y - c1.y, z: tA1.z - c1.z };
582
+
583
+ // Vector from centerline to tangent B
584
+ const centerToTangentB = { x: tB1.x - c1.x, y: tB1.y - c1.y, z: tB1.z - c1.z };
585
+
586
+ // Vector from tangent A to tangent B
587
+ const tangentAToTangentB = { x: tB1.x - tA1.x, y: tB1.y - tA1.y, z: tB1.z - tA1.z };
588
+
589
+ // Calculate cross products to determine relative orientations
590
+ // We'll use the dot product of cross products with a consistent reference vector
591
+
592
+ // Cross product: centerline direction × (center to tangentA)
593
+ const cross1 = {
594
+ x: centerVec.y * centerToTangentA.z - centerVec.z * centerToTangentA.y,
595
+ y: centerVec.z * centerToTangentA.x - centerVec.x * centerToTangentA.z,
596
+ z: centerVec.x * centerToTangentA.y - centerVec.y * centerToTangentA.x
597
+ };
598
+
599
+ // Cross product: centerline direction × (center to tangentB)
600
+ const cross2 = {
601
+ x: centerVec.y * centerToTangentB.z - centerVec.z * centerToTangentB.y,
602
+ y: centerVec.z * centerToTangentB.x - centerVec.x * centerToTangentB.z,
603
+ z: centerVec.x * centerToTangentB.y - centerVec.y * centerToTangentB.x
604
+ };
605
+
606
+ // Cross product: tangentA direction × (tangentA to tangentB)
607
+ const cross3 = {
608
+ x: tangentAVec.y * tangentAToTangentB.z - tangentAVec.z * tangentAToTangentB.y,
609
+ y: tangentAVec.z * tangentAToTangentB.x - tangentAVec.x * tangentAToTangentB.z,
610
+ z: tangentAVec.x * tangentAToTangentB.y - tangentAVec.y * tangentAToTangentB.x
611
+ };
612
+
613
+ // Use the magnitude of the Z component as a simple 2D projection heuristic
614
+ centerlineToTangentA_CrossProducts.push(cross1.z);
615
+ centerlineToTangentB_CrossProducts.push(cross2.z);
616
+ tangentAToTangentB_CrossProducts.push(cross3.z);
617
+ }
618
+
619
+ // Analyze the consistency of cross products
620
+ const validCenterToA = centerlineToTangentA_CrossProducts.filter(x => isFinite(x));
621
+ const validCenterToB = centerlineToTangentB_CrossProducts.filter(x => isFinite(x));
622
+ const validAToB = tangentAToTangentB_CrossProducts.filter(x => isFinite(x));
623
+
624
+ const avgCenterToA = validCenterToA.length > 0 ?
625
+ validCenterToA.reduce((a, b) => a + Math.sign(b), 0) / validCenterToA.length : 0;
626
+ const avgCenterToB = validCenterToB.length > 0 ?
627
+ validCenterToB.reduce((a, b) => a + Math.sign(b), 0) / validCenterToB.length : 0;
628
+ const avgAToB = validAToB.length > 0 ?
629
+ validAToB.reduce((a, b) => a + Math.sign(b), 0) / validAToB.length : 0;
630
+
631
+ // For a proper fillet, we expect:
632
+ // 1. Centerline and tangents should have consistent progression direction
633
+ // 2. Tangent A and B should generally go in opposite directions relative to each other
634
+ // 3. All three should form a consistent right-handed coordinate system
635
+
636
+ const centerRelationshipInconsistent = (avgCenterToA > 0) !== (avgCenterToB > 0);
637
+ const tangentsGoSameDirection = avgAToB > 0.5; // Strong positive correlation means same direction
638
+ if (centerRelationshipInconsistent && !(centerlineReversed || tangentAReversed || tangentBReversed)) {
639
+ // If centerline relationships are inconsistent AND tangents go in same direction,
640
+ // this suggests the centerline itself might need reversal
641
+ if (tangentsGoSameDirection) {
642
+ centerlineReversed = true;
643
+ } else {
644
+ // Heuristic: reverse the tangent with stronger inconsistency
645
+ if (Math.abs(avgCenterToB) > Math.abs(avgCenterToA)) {
646
+ tangentBReversed = true;
647
+ } else {
648
+ tangentAReversed = true;
649
+ }
650
+ }
651
+ } else if (tangentsGoSameDirection && !(centerlineReversed || tangentAReversed || tangentBReversed)) {
652
+ // Even if center relationships are consistent, if tangents go in same direction,
653
+ // we likely need to reverse one tangent
654
+ tangentBReversed = true;
655
+ }
656
+
657
+ return {
658
+ centerlineReversed,
659
+ tangentAReversed,
660
+ tangentBReversed
661
+ };
662
+ } catch (error) {
663
+ console.warn('Winding order analysis failed:', error?.message || error);
664
+ return { centerlineReversed: false, tangentAReversed: false, tangentBReversed: false };
665
+ }
666
+ }
667
+
668
+ /**
669
+ * Convenience: compute and attach the fillet centerline as an auxiliary edge on a Solid.
670
+ *
671
+ * @param {any} solid Target solid to receive the aux edge (overlay)
672
+ * @param {any} edgeObj Edge to analyze (must belong to `solid`)
673
+ * @param {number} radius Fillet radius (>0)
674
+ * @param {'INSET'|'OUTSET'} sideMode Side preference
675
+ * @param {string} name Edge name (default 'FILLET_CENTERLINE')
676
+ * @param {object} [options] Additional aux edge options
677
+ * @param {boolean} [options.closedLoop=false] Render as closed loop when visualized
678
+ * @param {boolean} [options.polylineWorld=false] Whether points are already in world space
679
+ * @param {'OVERLAY'|'BASE'|string} [options.materialKey='OVERLAY'] Visualization material tag
680
+ * @returns {{ points: {x:number,y:number,z:number}[], closedLoop: boolean } | null}
681
+ */
682
+ export function attachFilletCenterlineAuxEdge(solid, edgeObj, radius = 1, sideMode = 'INSET', name = 'FILLET_CENTERLINE', options = {}) {
683
+ try {
684
+ if (!solid || !edgeObj) return null;
685
+ const res = computeFilletCenterline(edgeObj, radius, sideMode);
686
+ if (res && Array.isArray(res.points) && res.points.length >= 2) {
687
+ const opts = { materialKey: 'OVERLAY', closedLoop: !!res.closedLoop, ...(options || {}) };
688
+ solid.addAuxEdge(name, res.points, opts);
689
+ return res;
690
+ }
691
+ return null;
692
+ } catch (e) {
693
+ console.warn('[attachFilletCenterlineAuxEdge] failed:', e?.message || e);
694
+ return null;
695
+ }
696
+ }
697
+
698
+
699
+ // Functional API: builds fillet tube and wedge and returns them.
700
+ export function filletSolid({ edgeToFillet, radius = 1, sideMode = 'INSET', debug = false, name = 'fillet', inflate = 0.1, resolution = 32, showTangentOverlays = false } = {}) {
701
+ try {
702
+ // Validate inputs
703
+ if (!edgeToFillet) {
704
+ throw new Error('filletSolid: edgeToFillet is required');
705
+ }
706
+ if (!Number.isFinite(radius) || radius <= 0) {
707
+ throw new Error(`filletSolid: radius must be a positive number, got ${radius}`);
708
+ }
709
+
710
+ const side = String(sideMode).toUpperCase();
711
+ const tubeResolution = (Number.isFinite(Number(resolution)) && Number(resolution) > 0)
712
+ ? Math.max(8, Math.floor(Number(resolution)))
713
+ : 32;
714
+ const logDebug = (...args) => { if (debug) console.log(...args); };
715
+ logDebug(`🔧 Starting fillet operation: edge=${edgeToFillet?.name || 'unnamed'}, radius=${radius}, side=${side}`);
716
+
717
+ const res = computeFilletCenterline(edgeToFillet, radius, side);
718
+ logDebug('The fillet centerline result is:', res);
719
+
720
+ if (!res) {
721
+ throw new Error('computeFilletCenterline returned null/undefined');
722
+ }
723
+
724
+ const centerline = Array.isArray(res?.points) ? res.points : [];
725
+ let tangentA = Array.isArray(res?.tangentA) ? res.tangentA : [];
726
+ let tangentB = Array.isArray(res?.tangentB) ? res.tangentB : [];
727
+ let edgePts = Array.isArray(res?.edge) ? res.edge : [];
728
+ const closedLoop = !!res?.closedLoop;
729
+
730
+ if (debug) {
731
+ try { logDebug('filletSolid: centerline/tangent edges computed'); } catch { }
732
+ }
733
+
734
+ // Clone into plain objects
735
+ const centerlineCopy = centerline.map(pt => ({ x: pt.x, y: pt.y, z: pt.z }));
736
+ let tangentACopy = tangentA.map(pt => ({ x: pt.x, y: pt.y, z: pt.z }));
737
+ let tangentBCopy = tangentB.map(pt => ({ x: pt.x, y: pt.y, z: pt.z }));
738
+ const tangentASnap = tangentACopy.map(pt => ({ x: pt.x, y: pt.y, z: pt.z }));
739
+ const tangentBSnap = tangentBCopy.map(pt => ({ x: pt.x, y: pt.y, z: pt.z }));
740
+ let edgeCopy = edgePts.map(pt => ({ x: pt.x, y: pt.y, z: pt.z }));
741
+ // Working copy of the original edge points used for wedge construction.
742
+ // Kept separate from `edgeCopy` so we can apply small insets/offsets without
743
+ // disturbing other consumers that rely on the original edge sampling.
744
+ let edgeWedgeCopy = edgeCopy.map(pt => ({ x: pt.x, y: pt.y, z: pt.z }));
745
+
746
+ // Visualize original centerline in yellow before any manipulation
747
+ if (debug && centerlineCopy.length >= 2) {
748
+ console.log('🟡 ORIGINAL CENTERLINE (Yellow):');
749
+ const originalVisualization = new Solid();
750
+ originalVisualization.name = `${name}_ORIGINAL_CENTERLINE`;
751
+
752
+ // Add centerline as line segments
753
+ for (let i = 0; i < centerlineCopy.length - 1; i++) {
754
+ const p1 = centerlineCopy[i];
755
+ const p2 = centerlineCopy[i + 1];
756
+ console.log(` Segment ${i}: (${p1.x.toFixed(3)}, ${p1.y.toFixed(3)}, ${p1.z.toFixed(3)}) → (${p2.x.toFixed(3)}, ${p2.y.toFixed(3)}, ${p2.z.toFixed(3)})`);
757
+ }
758
+
759
+ // Convert to array format for addAuxEdge
760
+ const originalCenterlineArray = centerlineCopy.map(pt => [pt.x, pt.y, pt.z]);
761
+ originalVisualization.addAuxEdge('ORIGINAL_CENTERLINE', originalCenterlineArray, {
762
+ materialKey: 'YELLOW',
763
+ closedLoop: closedLoop,
764
+ lineWidth: 3.0
765
+ });
766
+
767
+ try {
768
+ originalVisualization.visualize();
769
+ console.log('🟡 Original centerline visualization created (Yellow)');
770
+ } catch (vizError) {
771
+ console.warn('Failed to visualize original centerline:', vizError?.message || vizError);
772
+ }
773
+ }
774
+
775
+ logDebug('Checking all polyline winding orders...');
776
+ if (centerlineCopy.length >= 2) {
777
+ const c1 = centerlineCopy[0];
778
+ const c2 = centerlineCopy[1];
779
+ const cLast = centerlineCopy[centerlineCopy.length - 1];
780
+ logDebug(`Centerline: start=(${c1.x.toFixed(3)}, ${c1.y.toFixed(3)}, ${c1.z.toFixed(3)}) → (${c2.x.toFixed(3)}, ${c2.y.toFixed(3)}, ${c2.z.toFixed(3)}) ... end=(${cLast.x.toFixed(3)}, ${cLast.y.toFixed(3)}, ${cLast.z.toFixed(3)})`);
781
+ }
782
+
783
+ // Apply a small offset to the tangent curves relative to the centerline.
784
+ // Keep OUTSET behavior unchanged: move tangents slightly toward the centerline;
785
+ // INSET moves them outward. Closed loops skip inflation to avoid self‑intersection.
786
+ {
787
+ // Respect the sign of `inflate` so callers can shrink the tool for
788
+ // OUTSET (negative) while expanding for INSET (positive).
789
+ const offsetDistance = Number.isFinite(inflate) ? Number(inflate) : 0;
790
+ const n = Math.min(centerlineCopy.length, tangentACopy.length, tangentBCopy.length);
791
+ for (let i = 0; i < n; i++) {
792
+ const c = centerlineCopy[i];
793
+ const ta = tangentACopy[i];
794
+ const tb = tangentBCopy[i];
795
+ if (c && ta) {
796
+ const dax = ta.x - c.x, day = ta.y - c.y, daz = ta.z - c.z;
797
+ const daL = Math.hypot(dax, day, daz);
798
+ if (daL > 1e-12) {
799
+ ta.x += (dax / daL) * offsetDistance;
800
+ ta.y += (day / daL) * offsetDistance;
801
+ ta.z += (daz / daL) * offsetDistance;
802
+ }
803
+ }
804
+ if (c && tb) {
805
+ const dbx = tb.x - c.x, dby = tb.y - c.y, dbz = tb.z - c.z;
806
+ const dbL = Math.hypot(dbx, dby, dbz);
807
+ if (dbL > 1e-12) {
808
+ tb.x += (dbx / dbL) * offsetDistance;
809
+ tb.y += (dby / dbL) * offsetDistance;
810
+ tb.z += (dbz / dbL) * offsetDistance;
811
+ }
812
+ }
813
+ }
814
+ try { if (offsetDistance) logDebug(`Applied tangent offsetDistance=${offsetDistance} to ${n} samples`); } catch { }
815
+ }
816
+
817
+ // Push wedge edge points slightly relative to the centerline to ensure
818
+ // the wedge doesn't extend beyond the original geometry. For OUTSET this
819
+ // nudge is inward (toward the centerline). For INSET it must be the
820
+ // opposite direction (away from the centerline) to build the correct wedge.
821
+ // Slightly offset edge points to guarantee robust boolean overlap.
822
+ // Use a small radius-scaled inward nudge for OUTSET, capped to avoid
823
+ // large displacements on big models.
824
+ const outsetInsetMagnitude = Math.max(1e-4, Math.min(0.05, Math.abs(radius) * 0.05));
825
+ const wedgeInsetMagnitude = closedLoop ? 0 : ((side === 'INSET') ? Math.abs(inflate) : outsetInsetMagnitude);
826
+ for (let i = 0; i < edgeWedgeCopy.length; i++) {
827
+ const edgeWedgePt = edgeWedgeCopy[i];
828
+ const centerPt = centerlineCopy[i] || centerlineCopy[centerlineCopy.length - 1]; // Fallback to last point
829
+
830
+ if (edgeWedgePt && centerPt) {
831
+ try {
832
+ const origWedgeEdge = { ...edgeWedgePt };
833
+
834
+ // Calculate direction from edge point toward the centerline (inward direction)
835
+ const inwardDir = {
836
+ x: centerPt.x - edgeWedgePt.x,
837
+ y: centerPt.y - edgeWedgePt.y,
838
+ z: centerPt.z - edgeWedgePt.z
839
+ };
840
+ const inwardLength = Math.sqrt(inwardDir.x * inwardDir.x + inwardDir.y * inwardDir.y + inwardDir.z * inwardDir.z);
841
+
842
+ if (inwardLength > 1e-12) {
843
+ // Normalize and apply inset
844
+ const normalizedInward = {
845
+ x: inwardDir.x / inwardLength,
846
+ y: inwardDir.y / inwardLength,
847
+ z: inwardDir.z / inwardLength
848
+ };
849
+ // Determine direction: OUTSET -> inward, INSET -> outward (opposite)
850
+ const dirSign = (side === 'INSET') ? -1 : 1;
851
+ const step = dirSign * wedgeInsetMagnitude;
852
+ // Apply
853
+ edgeWedgePt.x += normalizedInward.x * step;
854
+ edgeWedgePt.y += normalizedInward.y * step;
855
+ edgeWedgePt.z += normalizedInward.z * step;
856
+
857
+ // Validate the result
858
+ if (!isFiniteVec3(edgeWedgePt)) {
859
+ console.warn(`Invalid wedge edge point after inset at index ${i}, reverting to original`);
860
+ Object.assign(edgeWedgePt, origWedgeEdge);
861
+ }
862
+ } else {
863
+ console.warn(`Edge point ${i} is too close to centerline, skipping wedge inset`);
864
+ }
865
+ } catch (insetError) {
866
+ console.warn(`Wedge edge inset failed at index ${i}: ${insetError?.message || insetError}`);
867
+ }
868
+ }
869
+ }
870
+
871
+ if (wedgeInsetMagnitude) logDebug(`Applied wedge inset of ${wedgeInsetMagnitude} units (${side === 'INSET' ? 'outward' : 'inward'}) to ${edgeWedgeCopy.length} edge points`);
872
+
873
+
874
+ // Do not reorder edge points. Centerline/tangent/edge points are produced in
875
+ // lockstep elsewhere; reindexing the edge points breaks correspondence and
876
+ // can create long crossing triangles. If orientation issues arise, reverse
877
+ // the entire polylines together rather than reordering indices.
878
+
879
+ // Visualize manipulated centerline after all processing
880
+ if (debug && centerlineCopy.length >= 2) {
881
+ console.log('🔵 MANIPULATED CENTERLINE (Blue):');
882
+ const manipulatedVisualization = new Solid();
883
+ manipulatedVisualization.name = `${name}_MANIPULATED_CENTERLINE`;
884
+
885
+ // Add manipulated centerline as line segments
886
+ for (let i = 0; i < centerlineCopy.length - 1; i++) {
887
+ const p1 = centerlineCopy[i];
888
+ const p2 = centerlineCopy[i + 1];
889
+ console.log(` Segment ${i}: (${p1.x.toFixed(3)}, ${p1.y.toFixed(3)}, ${p1.z.toFixed(3)}) → (${p2.x.toFixed(3)}, ${p2.y.toFixed(3)}, ${p2.z.toFixed(3)})`);
890
+ }
891
+
892
+ // Convert to array format for addAuxEdge
893
+ const manipulatedCenterlineArray = centerlineCopy.map(pt => [pt.x, pt.y, pt.z]);
894
+ manipulatedVisualization.addAuxEdge('MANIPULATED_CENTERLINE', manipulatedCenterlineArray, {
895
+ materialKey: 'BLUE',
896
+ closedLoop: closedLoop,
897
+ lineWidth: 3.0
898
+ });
899
+
900
+ try {
901
+ manipulatedVisualization.visualize();
902
+ console.log('🔵 Manipulated centerline visualization created (Blue)');
903
+ } catch (vizError) {
904
+ console.warn('Failed to visualize manipulated centerline:', vizError?.message || vizError);
905
+ }
906
+ }
907
+
908
+ logDebug('centerlines all generated fine');
909
+
910
+ // Validate spacing/variation for the path we will actually use for the tube
911
+ const tubePathOriginal = Array.isArray(centerline) ? centerline : [];
912
+ if (tubePathOriginal.length < 2) {
913
+ console.error('Insufficient centerline points for tube generation');
914
+ // Return debug information even on centerline failure
915
+ return {
916
+ tube: null,
917
+ wedge: null,
918
+ finalSolid: null,
919
+ centerline: centerlineCopy || [],
920
+ tangentA: tangentACopy || [],
921
+ tangentB: tangentBCopy || [],
922
+ tangentASeam: tangentASnap || [],
923
+ tangentBSeam: tangentBSnap || [],
924
+ error: 'Insufficient centerline points for tube generation'
925
+ };
926
+ }
927
+ {
928
+ const firstPt = tubePathOriginal[0];
929
+ const hasVariation = tubePathOriginal.some(pt =>
930
+ Math.abs(pt.x - firstPt.x) > 1e-6 ||
931
+ Math.abs(pt.y - firstPt.y) > 1e-6 ||
932
+ Math.abs(pt.z - firstPt.z) > 1e-6
933
+ );
934
+ if (!hasVariation) {
935
+ console.error('Degenerate centerline: all points are identical');
936
+ // Return debug information even on centerline failure
937
+ return {
938
+ tube: null,
939
+ wedge: null,
940
+ finalSolid: null,
941
+ centerline: centerlineCopy || [],
942
+ tangentA: tangentACopy || [],
943
+ tangentB: tangentBCopy || [],
944
+ tangentASeam: tangentASnap || [],
945
+ tangentBSeam: tangentBSnap || [],
946
+ error: 'Degenerate centerline: all points are identical'
947
+ };
948
+ }
949
+ const minSpacing = radius * 0.01;
950
+ for (let i = 1; i < tubePathOriginal.length; i++) {
951
+ const curr = tubePathOriginal[i];
952
+ const prev = tubePathOriginal[i - 1];
953
+ const distance = Math.hypot(curr.x - prev.x, curr.y - prev.y, curr.z - prev.z);
954
+ if (distance < minSpacing) {
955
+ console.warn(`Centerline points ${i - 1} and ${i} are too close (distance: ${distance}), this may cause tube generation issues`);
956
+ }
957
+ }
958
+ }
959
+
960
+ // Build tube from the ORIGINAL centerline (not the modified copy)
961
+ let filletTube = null;
962
+ try {
963
+ // Tube expects [x,y,z] arrays; convert original {x,y,z} objects
964
+ let tubePoints = tubePathOriginal.map(p => [p.x, p.y, p.z]);
965
+
966
+ if (closedLoop) {
967
+ logDebug('Closed loop detected: preparing tube centerline...');
968
+ // For closed loops: ensure the tube polyline has the same point at start and end
969
+ if (tubePoints.length >= 2) {
970
+ const firstPt = tubePoints[0];
971
+ const lastPt = tubePoints[tubePoints.length - 1];
972
+
973
+ // Check if first and last points are different
974
+ const dx = firstPt[0] - lastPt[0];
975
+ const dy = firstPt[1] - lastPt[1];
976
+ const dz = firstPt[2] - lastPt[2];
977
+ const distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
978
+
979
+ // Add the first point at the end to close the loop
980
+ tubePoints.push([firstPt[0], firstPt[1], firstPt[2]]);
981
+ logDebug('Closed loop: Added first point at end for tube generation');
982
+
983
+ }
984
+ } else {
985
+ logDebug('Non-closed loop detected: preparing tube centerline...');
986
+ // For non-closed loops: extend the start and end segments of the centerline polyline for tube only
987
+ if (tubePoints.length >= 2) {
988
+ logDebug('Non-closed loop: Extending tube centerline segments...');
989
+ const extensionDistance = 0.1;
990
+
991
+ // Extend first segment backwards
992
+ const p0 = tubePoints[0];
993
+ const p1 = tubePoints[1];
994
+ const dir0 = [p0[0] - p1[0], p0[1] - p1[1], p0[2] - p1[2]];
995
+ const len0 = Math.sqrt(dir0[0] * dir0[0] + dir0[1] * dir0[1] + dir0[2] * dir0[2]);
996
+
997
+ if (len0 > 1e-12) {
998
+ const norm0 = [dir0[0] / len0, dir0[1] / len0, dir0[2] / len0];
999
+ const extendedStart = [
1000
+ p0[0] + norm0[0] * extensionDistance,
1001
+ p0[1] + norm0[1] * extensionDistance,
1002
+ p0[2] + norm0[2] * extensionDistance
1003
+ ];
1004
+ tubePoints[0] = extendedStart;
1005
+ }
1006
+
1007
+ // Extend last segment forwards
1008
+ const lastIdx = tubePoints.length - 1;
1009
+ const pLast = tubePoints[lastIdx];
1010
+ const pPrev = tubePoints[lastIdx - 1];
1011
+ const dirLast = [pLast[0] - pPrev[0], pLast[1] - pPrev[1], pLast[2] - pPrev[2]];
1012
+ const lenLast = Math.sqrt(dirLast[0] * dirLast[0] + dirLast[1] * dirLast[1] + dirLast[2] * dirLast[2]);
1013
+
1014
+ if (lenLast > 1e-12) {
1015
+ const normLast = [dirLast[0] / lenLast, dirLast[1] / lenLast, dirLast[2] / lenLast];
1016
+ const extendedEnd = [
1017
+ pLast[0] + normLast[0] * extensionDistance,
1018
+ pLast[1] + normLast[1] * extensionDistance,
1019
+ pLast[2] + normLast[2] * extensionDistance
1020
+ ];
1021
+ tubePoints[lastIdx] = extendedEnd;
1022
+ }
1023
+
1024
+ logDebug(`Extended tube centerline by ${extensionDistance} units at both ends`);
1025
+ }
1026
+ }
1027
+
1028
+ const inflatedTubeRadius = radius ;
1029
+ filletTube = new Tube({
1030
+ points: tubePoints,
1031
+ radius: inflatedTubeRadius,
1032
+ innerRadius: 0,
1033
+ resolution: tubeResolution,
1034
+ name: `${name}_TUBE`,
1035
+ });
1036
+
1037
+ // Store PMI metadata on the outer pipe face so downstream annotations
1038
+ // can recover the user radius instead of the inflated geometry value.
1039
+ try {
1040
+ const faceTag = `${name}_TUBE_Outer`;
1041
+ const overrideMeta = {
1042
+ type: 'pipe',
1043
+ source: 'FilletFeature',
1044
+ featureID: name,
1045
+ inflatedRadius: inflatedTubeRadius,
1046
+ pmiRadiusOverride: radius,
1047
+ radiusOverride: radius,
1048
+ };
1049
+ if (edgeToFillet?.name) overrideMeta.edgeReference = edgeToFillet.name;
1050
+ filletTube.setFaceMetadata(faceTag, overrideMeta);
1051
+
1052
+ if (showTangentOverlays) {
1053
+ const auxOpts = { materialKey: 'OVERLAY', closedLoop: !!closedLoop };
1054
+ if (Array.isArray(tangentASnap) && tangentASnap.length >= 2) {
1055
+ filletTube.addAuxEdge(`${name}_TANGENT_A_PATH`, tangentASnap, auxOpts);
1056
+ }
1057
+ if (Array.isArray(tangentBSnap) && tangentBSnap.length >= 2) {
1058
+ filletTube.addAuxEdge(`${name}_TANGENT_B_PATH`, tangentBSnap, auxOpts);
1059
+ }
1060
+ }
1061
+
1062
+ // Capture tube cap area + round face label for post-boolean retagging (non-closed only).
1063
+ if (!closedLoop) {
1064
+ const roundFaceName = faceTag;
1065
+ const markTubeCap = (capName) => {
1066
+ const tris = filletTube.getFace(capName);
1067
+ const area = computeFaceAreaFromTriangles(tris);
1068
+ if (area > 0) {
1069
+ filletTube.setFaceMetadata(capName, {
1070
+ filletSourceArea: area,
1071
+ filletRoundFace: roundFaceName,
1072
+ filletEndCap: true,
1073
+ });
1074
+ }
1075
+ };
1076
+ markTubeCap(`${name}_TUBE_CapStart`);
1077
+ markTubeCap(`${name}_TUBE_CapEnd`);
1078
+ }
1079
+ } catch {
1080
+ // Best-effort – lack of metadata should not abort fillet creation.
1081
+ }
1082
+ } catch (tubeError) {
1083
+ console.error('Tube creation failed:', tubeError?.message || tubeError);
1084
+
1085
+ // Return debug information even on tube failure
1086
+ const debugWedge = new Solid();
1087
+ debugWedge.name = `${name}_FAILED_TUBE_DEBUG`;
1088
+ return {
1089
+ tube: null,
1090
+ wedge: debugWedge,
1091
+ finalSolid: null,
1092
+ centerline: centerlineCopy,
1093
+ tangentA: tangentACopy,
1094
+ tangentB: tangentBCopy,
1095
+ tangentASeam: tangentASnap || [],
1096
+ tangentBSeam: tangentBSnap || [],
1097
+ error: `Tube generation failed: ${tubeError?.message || tubeError}`
1098
+ };
1099
+ }
1100
+
1101
+
1102
+ // Build wedge solid from triangles between centerline and tangency edges
1103
+ logDebug('Creating wedge solid...');
1104
+ const wedgeSolid = new Solid();
1105
+ wedgeSolid.name = `${name}_WEDGE`;
1106
+
1107
+ if (closedLoop) {
1108
+ // CLOSED LOOP PATH - preserve existing logic exactly
1109
+ try {
1110
+ const minTriangleArea = radius * radius * 1e-8;
1111
+ let validTriangles = 0;
1112
+ let skippedTriangles = 0;
1113
+ for (let i = 0; i < centerlineCopy.length - 1; i++) {
1114
+ const c1 = centerlineCopy[i];
1115
+ const c2 = centerlineCopy[i + 1];
1116
+ const tA1 = tangentACopy[i];
1117
+ const tA2 = tangentACopy[i + 1];
1118
+ const tB1 = tangentBCopy[i];
1119
+ const tB2 = tangentBCopy[i + 1];
1120
+
1121
+ const isValidTriangle = (p1, p2, p3) => {
1122
+ const v1 = { x: p2.x - p1.x, y: p2.y - p1.y, z: p2.z - p1.z };
1123
+ const v2 = { x: p3.x - p1.x, y: p3.y - p1.y, z: p3.z - p1.z };
1124
+ const cross = {
1125
+ x: v1.y * v2.z - v1.z * v2.y,
1126
+ y: v1.z * v2.x - v1.x * v2.z,
1127
+ z: v1.x * v2.y - v1.y * v2.x
1128
+ };
1129
+ const area = 0.5 * Math.sqrt(cross.x * cross.x + cross.y * cross.y + cross.z * cross.z);
1130
+ return area > minTriangleArea;
1131
+ };
1132
+ const isValidPoint = (p) => isFinite(p.x) && isFinite(p.y) && isFinite(p.z);
1133
+ const addTriangleWithValidation = (groupName, p1, p2, p3) => {
1134
+ if (!isValidPoint(p1) || !isValidPoint(p2) || !isValidPoint(p3)) {
1135
+ console.warn(`Invalid points detected - p1:(${p1.x},${p1.y},${p1.z}) p2:(${p2.x},${p2.y},${p2.z}) p3:(${p3.x},${p3.y},${p3.z})`);
1136
+ return false;
1137
+ }
1138
+ wedgeSolid.addTriangle(groupName, [p1.x, p1.y, p1.z], [p2.x, p2.y, p2.z], [p3.x, p3.y, p3.z]);
1139
+ return true;
1140
+ };
1141
+
1142
+ // Tangent A side
1143
+ if (isValidTriangle(c1, tA1, c2) && addTriangleWithValidation(`${name}_WEDGE_A`, c1, tA1, c2)) validTriangles++; else skippedTriangles++;
1144
+ if (isValidTriangle(c2, tA1, tA2) && addTriangleWithValidation(`${name}_WEDGE_A`, c2, tA1, tA2)) validTriangles++; else skippedTriangles++;
1145
+ // Tangent B side
1146
+ if (isValidTriangle(c1, c2, tB1) && addTriangleWithValidation(`${name}_WEDGE_B`, c1, c2, tB1)) validTriangles++; else skippedTriangles++;
1147
+ if (isValidTriangle(c2, tB2, tB1) && addTriangleWithValidation(`${name}_WEDGE_B`, c2, tB2, tB1)) validTriangles++; else skippedTriangles++;
1148
+
1149
+ // Side walls on original faces - use inset wedge edge points
1150
+ const e1 = edgeWedgeCopy[i];
1151
+ const e2 = edgeWedgeCopy[i + 1];
1152
+ if (e1 && e2) {
1153
+ if (isValidTriangle(e1, tA1, e2) && addTriangleWithValidation(`${name}_SIDE_A`, e1, tA1, e2)) validTriangles++; else skippedTriangles++;
1154
+ if (isValidTriangle(e2, tA1, tA2) && addTriangleWithValidation(`${name}_SIDE_A`, e2, tA1, tA2)) validTriangles++; else skippedTriangles++;
1155
+ if (isValidTriangle(e1, e2, tB1) && addTriangleWithValidation(`${name}_SIDE_B`, e1, e2, tB1)) validTriangles++; else skippedTriangles++;
1156
+ if (isValidTriangle(e2, tB2, tB1) && addTriangleWithValidation(`${name}_SIDE_B`, e2, tB2, tB1)) validTriangles++; else skippedTriangles++;
1157
+ }
1158
+ }
1159
+ logDebug(`Wedge triangles added successfully (closed loop): ${validTriangles} valid, ${skippedTriangles} skipped`);
1160
+ if (validTriangles === 0) {
1161
+ console.error('No valid triangles could be created for wedge solid - all were degenerate');
1162
+ // Return debug information even on wedge failure
1163
+ return {
1164
+ tube: filletTube,
1165
+ wedge: wedgeSolid,
1166
+ finalSolid: null,
1167
+ centerline: centerlineCopy,
1168
+ tangentA: tangentACopy,
1169
+ tangentB: tangentBCopy,
1170
+ tangentASeam: tangentASnap || [],
1171
+ tangentBSeam: tangentBSnap || [],
1172
+ error: 'No valid triangles could be created for wedge solid - all were degenerate'
1173
+ };
1174
+ }
1175
+ } catch (wedgeError) {
1176
+ console.error('Failed to create wedge triangles (closed loop):', wedgeError?.message || wedgeError);
1177
+ // Return debug information even on wedge error
1178
+ return {
1179
+ tube: filletTube,
1180
+ wedge: wedgeSolid,
1181
+ finalSolid: null,
1182
+ centerline: centerlineCopy,
1183
+ tangentA: tangentACopy,
1184
+ tangentB: tangentBCopy,
1185
+ tangentASeam: tangentASnap || [],
1186
+ tangentBSeam: tangentBSnap || [],
1187
+ error: `Wedge triangle creation failed: ${wedgeError?.message || wedgeError}`
1188
+ };
1189
+ }
1190
+ } else {
1191
+ // NON-CLOSED LOOP PATH - specialized handling for open edges
1192
+ try {
1193
+ logDebug('Creating wedge solid for non-closed loop...');
1194
+ const minTriangleArea = radius * radius * 1e-8;
1195
+ let validTriangles = 0;
1196
+ let skippedTriangles = 0;
1197
+
1198
+ const isValidTriangle = (p1, p2, p3) => {
1199
+ const v1 = { x: p2.x - p1.x, y: p2.y - p1.y, z: p2.z - p1.z };
1200
+ const v2 = { x: p3.x - p1.x, y: p3.y - p1.y, z: p3.z - p1.z };
1201
+ const cross = {
1202
+ x: v1.y * v2.z - v1.z * v2.y,
1203
+ y: v1.z * v2.x - v1.x * v2.z,
1204
+ z: v1.x * v2.y - v1.y * v2.x
1205
+ };
1206
+ const area = 0.5 * Math.sqrt(cross.x * cross.x + cross.y * cross.y + cross.z * cross.z);
1207
+ return area > minTriangleArea;
1208
+ };
1209
+ const isValidPoint = (p) => isFinite(p.x) && isFinite(p.y) && isFinite(p.z);
1210
+ const addTriangleWithValidation = (groupName, p1, p2, p3) => {
1211
+ if (!isValidPoint(p1) || !isValidPoint(p2) || !isValidPoint(p3)) {
1212
+ console.warn(`Invalid points detected - p1:(${p1.x},${p1.y},${p1.z}) p2:(${p2.x},${p2.y},${p2.z}) p3:(${p3.x},${p3.y},${p3.z})`);
1213
+ return false;
1214
+ }
1215
+ wedgeSolid.addTriangle(groupName, [p1.x, p1.y, p1.z], [p2.x, p2.y, p2.z], [p3.x, p3.y, p3.z]);
1216
+ return true;
1217
+ };
1218
+
1219
+ // Create triangular strip along the fillet path
1220
+ // For open edges, we create a proper triangulated surface between centerline and tangent lines
1221
+ for (let i = 0; i < centerlineCopy.length - 1; i++) {
1222
+ const c1 = centerlineCopy[i];
1223
+ const c2 = centerlineCopy[i + 1];
1224
+ const tA1 = tangentACopy[i];
1225
+ const tA2 = tangentACopy[i + 1];
1226
+ const tB1 = tangentBCopy[i];
1227
+ const tB2 = tangentBCopy[i + 1];
1228
+ const e1 = edgeWedgeCopy[i];
1229
+ const e2 = edgeWedgeCopy[i + 1];
1230
+
1231
+ // Create triangulated surfaces between each pair of curves
1232
+ // Surface between centerline and tangent A
1233
+ if (isValidTriangle(c1, c2, tA1) && addTriangleWithValidation(`${name}_SURFACE_CA`, c1, c2, tA1)) validTriangles++; else skippedTriangles++;
1234
+ if (isValidTriangle(c2, tA2, tA1) && addTriangleWithValidation(`${name}_SURFACE_CA`, c2, tA2, tA1)) validTriangles++; else skippedTriangles++;
1235
+
1236
+ // Surface between centerline and tangent B
1237
+ if (isValidTriangle(c1, tB1, c2) && addTriangleWithValidation(`${name}_SURFACE_CB`, c1, tB1, c2)) validTriangles++; else skippedTriangles++;
1238
+ if (isValidTriangle(c2, tB1, tB2) && addTriangleWithValidation(`${name}_SURFACE_CB`, c2, tB1, tB2)) validTriangles++; else skippedTriangles++;
1239
+
1240
+ // Surface between tangent A and edge (original face A)
1241
+ if (e1 && e2) {
1242
+ if (isValidTriangle(tA1, tA2, e1) && addTriangleWithValidation(`${name}_FACE_A`, tA1, tA2, e1)) validTriangles++; else skippedTriangles++;
1243
+ if (isValidTriangle(tA2, e2, e1) && addTriangleWithValidation(`${name}_FACE_A`, tA2, e2, e1)) validTriangles++; else skippedTriangles++;
1244
+
1245
+ // Surface between tangent B and edge (original face B)
1246
+ if (isValidTriangle(tB1, e1, tB2) && addTriangleWithValidation(`${name}_FACE_B`, tB1, e1, tB2)) validTriangles++; else skippedTriangles++;
1247
+ if (isValidTriangle(tB2, e1, e2) && addTriangleWithValidation(`${name}_FACE_B`, tB2, e1, e2)) validTriangles++; else skippedTriangles++;
1248
+ }
1249
+ }
1250
+
1251
+ // Add end caps for open edges to create a closed solid
1252
+ if (centerlineCopy.length >= 2) {
1253
+ logDebug('Adding end caps for non-closed loop...');
1254
+
1255
+ // First end cap
1256
+ const firstC = centerlineCopy[0];
1257
+ const firstTA = tangentACopy[0];
1258
+ const firstTB = tangentBCopy[0];
1259
+ const firstE = edgeWedgeCopy[0];
1260
+
1261
+ if (firstE && isValidPoint(firstC) && isValidPoint(firstTA) && isValidPoint(firstTB) && isValidPoint(firstE)) {
1262
+ let endCapFirstC = firstC;
1263
+ let endCapFirstTA = firstTA;
1264
+ let endCapFirstTB = firstTB;
1265
+ let endCapFirstE = firstE;
1266
+
1267
+ // Create triangular fan from centerline to form end cap
1268
+ if (isValidTriangle(endCapFirstC, endCapFirstTB, endCapFirstTA) && addTriangleWithValidation(`${name}_END_CAP_1`, endCapFirstC, endCapFirstTB, endCapFirstTA)) validTriangles++; else skippedTriangles++;
1269
+ if (isValidTriangle(endCapFirstTA, endCapFirstTB, endCapFirstE) && addTriangleWithValidation(`${name}_END_CAP_1`, endCapFirstTA, endCapFirstTB, endCapFirstE)) validTriangles++; else skippedTriangles++;
1270
+ }
1271
+
1272
+ // Last end cap
1273
+ const lastIndex = centerlineCopy.length - 1;
1274
+ const lastC = centerlineCopy[lastIndex];
1275
+ const lastTA = tangentACopy[lastIndex];
1276
+ const lastTB = tangentBCopy[lastIndex];
1277
+ const lastE = edgeWedgeCopy[lastIndex];
1278
+
1279
+ if (lastE && isValidPoint(lastC) && isValidPoint(lastTA) && isValidPoint(lastTB) && isValidPoint(lastE)) {
1280
+ let endCapLastC = lastC;
1281
+ let endCapLastTA = lastTA;
1282
+ let endCapLastTB = lastTB;
1283
+ let endCapLastE = lastE;
1284
+
1285
+ // Create triangular fan from centerline to form end cap (reversed winding for proper normal)
1286
+ if (isValidTriangle(endCapLastC, endCapLastTA, endCapLastTB) && addTriangleWithValidation(`${name}_END_CAP_2`, endCapLastC, endCapLastTA, endCapLastTB)) validTriangles++; else skippedTriangles++;
1287
+ if (isValidTriangle(endCapLastTA, endCapLastE, endCapLastTB) && addTriangleWithValidation(`${name}_END_CAP_2`, endCapLastTA, endCapLastE, endCapLastTB)) validTriangles++; else skippedTriangles++;
1288
+ }
1289
+ }
1290
+
1291
+ logDebug(`Wedge triangles added successfully (non-closed loop): ${validTriangles} valid, ${skippedTriangles} skipped`);
1292
+ if (validTriangles === 0) {
1293
+ console.error('No valid triangles could be created for non-closed wedge solid - all were degenerate');
1294
+ // Return debug information even on wedge failure
1295
+ return {
1296
+ tube: filletTube,
1297
+ wedge: wedgeSolid,
1298
+ finalSolid: null,
1299
+ centerline: centerlineCopy,
1300
+ tangentA: tangentACopy,
1301
+ tangentB: tangentBCopy,
1302
+ tangentASeam: tangentASnap || [],
1303
+ tangentBSeam: tangentBSnap || [],
1304
+ error: 'No valid triangles could be created for non-closed wedge solid - all were degenerate'
1305
+ };
1306
+ }
1307
+ } catch (wedgeError) {
1308
+ console.error('Failed to create wedge triangles (non-closed loop):', wedgeError?.message || wedgeError);
1309
+ // Return debug information even on wedge error
1310
+ return {
1311
+ tube: filletTube,
1312
+ wedge: wedgeSolid,
1313
+ finalSolid: null,
1314
+ centerline: centerlineCopy,
1315
+ tangentA: tangentACopy,
1316
+ tangentB: tangentBCopy,
1317
+ tangentASeam: tangentASnap || [],
1318
+ tangentBSeam: tangentBSnap || [],
1319
+ error: `Non-closed wedge triangle creation failed: ${wedgeError?.message || wedgeError}`
1320
+ };
1321
+ }
1322
+ }
1323
+
1324
+ // Triangle winding fix for all cases
1325
+ try {
1326
+ wedgeSolid.fixTriangleWindingsByAdjacency();
1327
+ } catch (windingError) {
1328
+ console.warn('Triangle winding fix failed:', windingError?.message || windingError);
1329
+ }
1330
+
1331
+ if (debug) {
1332
+ console.log('Debug mode: wedge solid stored');
1333
+ }
1334
+ logDebug('Wedge solid creation completed');
1335
+ const triangleCount = wedgeSolid._triVerts ? wedgeSolid._triVerts.length / 3 : 0;
1336
+ logDebug('Wedge solid created with', triangleCount, 'triangles (raw count)');
1337
+ try { wedgeSolid.visualize(); } catch { }
1338
+
1339
+ wedgeSolid.pushFace(`${name}_FACE_A`, 0.0001);
1340
+ wedgeSolid.pushFace(`${name}_FACE_B`, 0.0001);
1341
+
1342
+ // Apply end cap offset for INSET fillets using pushFace method
1343
+ if (side === 'INSET' && !closedLoop) {
1344
+ logDebug('Applying end cap offset to INSET fillet using pushFace...');
1345
+ try {
1346
+ // Push both end caps outward by 0.001
1347
+ wedgeSolid.pushFace(`${name}_END_CAP_1`, 0.0001);
1348
+ wedgeSolid.pushFace(`${name}_END_CAP_2`, 0.0001);
1349
+ wedgeSolid.visualize();
1350
+ logDebug('End cap offset applied successfully');
1351
+ } catch (pushError) {
1352
+ console.warn('Failed to apply end cap offset:', pushError?.message || pushError);
1353
+ }
1354
+ }
1355
+
1356
+ // Record areas and target round-face label for post-boolean relabeling.
1357
+ const roundFaceName = `${name}_TUBE_Outer`;
1358
+ const markFace = (faceName, isEndCap = false) => {
1359
+ const tris = wedgeSolid.getFace(faceName);
1360
+ const area = computeFaceAreaFromTriangles(tris);
1361
+ if (area > 0) {
1362
+ wedgeSolid.setFaceMetadata(faceName, {
1363
+ filletSourceArea: area,
1364
+ filletRoundFace: roundFaceName,
1365
+ filletEndCap: !!isEndCap,
1366
+ });
1367
+ }
1368
+ };
1369
+ if (!closedLoop) {
1370
+ markFace(`${name}_END_CAP_1`, true);
1371
+ markFace(`${name}_END_CAP_2`, true);
1372
+ }
1373
+ markFace(`${name}_WEDGE_A`, false);
1374
+ markFace(`${name}_WEDGE_B`, false);
1375
+
1376
+ try {
1377
+ const finalSolid = wedgeSolid.subtract(filletTube);
1378
+ finalSolid.name = `${name}_FINAL_FILLET`;
1379
+ try { finalSolid.visualize(); } catch { }
1380
+ logDebug('Final fillet solid created by subtracting tube from wedge', finalSolid);
1381
+
1382
+ return {
1383
+ tube: filletTube,
1384
+ wedge: wedgeSolid,
1385
+ finalSolid,
1386
+ centerline: centerlineCopy,
1387
+ tangentA: tangentACopy,
1388
+ tangentB: tangentBCopy,
1389
+ tangentASeam: tangentASnap || [],
1390
+ tangentBSeam: tangentBSnap || [],
1391
+ };
1392
+ } catch (booleanError) {
1393
+ console.error('Boolean operation failed:', booleanError?.message || booleanError);
1394
+ // Return debug information even on boolean failure
1395
+ return {
1396
+ tube: filletTube,
1397
+ wedge: wedgeSolid,
1398
+ finalSolid: null,
1399
+ centerline: centerlineCopy,
1400
+ tangentA: tangentACopy,
1401
+ tangentB: tangentBCopy,
1402
+ tangentASeam: tangentASnap || [],
1403
+ tangentBSeam: tangentBSnap || [],
1404
+ error: `Boolean operation failed: ${booleanError?.message || booleanError}`
1405
+ };
1406
+ }
1407
+ } catch (globalError) {
1408
+ console.error('Fillet operation failed completely:', globalError?.message || globalError);
1409
+ // Return minimal debug information even on complete failure
1410
+ return {
1411
+ tube: null,
1412
+ wedge: null,
1413
+ finalSolid: null,
1414
+ centerline: [],
1415
+ tangentA: [],
1416
+ tangentB: [],
1417
+ tangentASeam: [],
1418
+ tangentBSeam: [],
1419
+ error: `Fillet operation failed: ${globalError?.message || globalError}`
1420
+ };
1421
+ }
1422
+ }