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,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
+ }