brep-io-kernel 1.0.0-ci.9

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 (271) hide show
  1. package/LICENSE.md +32 -0
  2. package/README.md +154 -0
  3. package/dist-kernel/brep-kernel.js +74699 -0
  4. package/package.json +58 -0
  5. package/src/BREP/AssemblyComponent.js +42 -0
  6. package/src/BREP/BREP.js +43 -0
  7. package/src/BREP/BetterSolid.js +805 -0
  8. package/src/BREP/Edge.js +103 -0
  9. package/src/BREP/Extrude.js +403 -0
  10. package/src/BREP/Face.js +187 -0
  11. package/src/BREP/MeshRepairer.js +634 -0
  12. package/src/BREP/OffsetShellSolid.js +614 -0
  13. package/src/BREP/PointCloudWrap.js +302 -0
  14. package/src/BREP/Revolve.js +345 -0
  15. package/src/BREP/SolidMethods/authoring.js +112 -0
  16. package/src/BREP/SolidMethods/booleanOps.js +230 -0
  17. package/src/BREP/SolidMethods/chamfer.js +122 -0
  18. package/src/BREP/SolidMethods/edgeResolution.js +25 -0
  19. package/src/BREP/SolidMethods/fillet.js +792 -0
  20. package/src/BREP/SolidMethods/index.js +72 -0
  21. package/src/BREP/SolidMethods/io.js +105 -0
  22. package/src/BREP/SolidMethods/lifecycle.js +103 -0
  23. package/src/BREP/SolidMethods/manifoldOps.js +375 -0
  24. package/src/BREP/SolidMethods/meshCleanup.js +2512 -0
  25. package/src/BREP/SolidMethods/meshQueries.js +264 -0
  26. package/src/BREP/SolidMethods/metadata.js +106 -0
  27. package/src/BREP/SolidMethods/metrics.js +51 -0
  28. package/src/BREP/SolidMethods/transforms.js +361 -0
  29. package/src/BREP/SolidMethods/visualize.js +508 -0
  30. package/src/BREP/SolidShared.js +26 -0
  31. package/src/BREP/Sweep.js +1596 -0
  32. package/src/BREP/Tube.js +857 -0
  33. package/src/BREP/Vertex.js +43 -0
  34. package/src/BREP/applyBooleanOperation.js +704 -0
  35. package/src/BREP/boundsUtils.js +48 -0
  36. package/src/BREP/chamfer.js +551 -0
  37. package/src/BREP/edgePolylineUtils.js +85 -0
  38. package/src/BREP/fillets/common.js +388 -0
  39. package/src/BREP/fillets/fillet.js +1422 -0
  40. package/src/BREP/fillets/filletGeometry.js +15 -0
  41. package/src/BREP/fillets/inset.js +389 -0
  42. package/src/BREP/fillets/offsetHelper.js +143 -0
  43. package/src/BREP/fillets/outset.js +88 -0
  44. package/src/BREP/helix.js +193 -0
  45. package/src/BREP/meshToBrep.js +234 -0
  46. package/src/BREP/primitives.js +279 -0
  47. package/src/BREP/setupManifold.js +71 -0
  48. package/src/BREP/threadGeometry.js +1120 -0
  49. package/src/BREP/triangleUtils.js +8 -0
  50. package/src/BREP/triangulate.js +608 -0
  51. package/src/FeatureRegistry.js +183 -0
  52. package/src/PartHistory.js +1132 -0
  53. package/src/UI/AccordionWidget.js +292 -0
  54. package/src/UI/CADmaterials.js +850 -0
  55. package/src/UI/EnvMonacoEditor.js +522 -0
  56. package/src/UI/FloatingWindow.js +396 -0
  57. package/src/UI/HistoryWidget.js +457 -0
  58. package/src/UI/MainToolbar.js +131 -0
  59. package/src/UI/ModelLibraryView.js +194 -0
  60. package/src/UI/OrthoCameraIdle.js +206 -0
  61. package/src/UI/PluginsWidget.js +280 -0
  62. package/src/UI/SceneListing.js +606 -0
  63. package/src/UI/SelectionFilter.js +629 -0
  64. package/src/UI/ViewCube.js +389 -0
  65. package/src/UI/assembly/AssemblyConstraintCollectionWidget.js +329 -0
  66. package/src/UI/assembly/AssemblyConstraintControlsWidget.js +282 -0
  67. package/src/UI/assembly/AssemblyConstraintsWidget.css +292 -0
  68. package/src/UI/assembly/AssemblyConstraintsWidget.js +1373 -0
  69. package/src/UI/assembly/constraintFaceUtils.js +115 -0
  70. package/src/UI/assembly/constraintHighlightUtils.js +70 -0
  71. package/src/UI/assembly/constraintLabelUtils.js +31 -0
  72. package/src/UI/assembly/constraintPointUtils.js +64 -0
  73. package/src/UI/assembly/constraintSelectionUtils.js +185 -0
  74. package/src/UI/assembly/constraintStatusUtils.js +142 -0
  75. package/src/UI/componentSelectorModal.js +240 -0
  76. package/src/UI/controls/CombinedTransformControls.js +386 -0
  77. package/src/UI/dialogs.js +351 -0
  78. package/src/UI/expressionsManager.js +100 -0
  79. package/src/UI/featureDialogWidgets/booleanField.js +25 -0
  80. package/src/UI/featureDialogWidgets/booleanOperationField.js +97 -0
  81. package/src/UI/featureDialogWidgets/buttonField.js +45 -0
  82. package/src/UI/featureDialogWidgets/componentSelectorField.js +102 -0
  83. package/src/UI/featureDialogWidgets/defaultField.js +23 -0
  84. package/src/UI/featureDialogWidgets/fileField.js +66 -0
  85. package/src/UI/featureDialogWidgets/index.js +34 -0
  86. package/src/UI/featureDialogWidgets/numberField.js +165 -0
  87. package/src/UI/featureDialogWidgets/optionsField.js +33 -0
  88. package/src/UI/featureDialogWidgets/referenceSelectionField.js +208 -0
  89. package/src/UI/featureDialogWidgets/stringField.js +24 -0
  90. package/src/UI/featureDialogWidgets/textareaField.js +28 -0
  91. package/src/UI/featureDialogWidgets/threadDesignationField.js +160 -0
  92. package/src/UI/featureDialogWidgets/transformField.js +252 -0
  93. package/src/UI/featureDialogWidgets/utils.js +43 -0
  94. package/src/UI/featureDialogWidgets/vec3Field.js +133 -0
  95. package/src/UI/featureDialogs.js +1414 -0
  96. package/src/UI/fileManagerWidget.js +615 -0
  97. package/src/UI/history/HistoryCollectionWidget.js +1294 -0
  98. package/src/UI/history/historyCollectionWidget.css.js +257 -0
  99. package/src/UI/history/historyDisplayInfo.js +133 -0
  100. package/src/UI/mobile.js +28 -0
  101. package/src/UI/objectDump.js +442 -0
  102. package/src/UI/pmi/AnnotationCollectionWidget.js +120 -0
  103. package/src/UI/pmi/AnnotationHistory.js +353 -0
  104. package/src/UI/pmi/AnnotationRegistry.js +90 -0
  105. package/src/UI/pmi/BaseAnnotation.js +269 -0
  106. package/src/UI/pmi/LabelOverlay.css +102 -0
  107. package/src/UI/pmi/LabelOverlay.js +191 -0
  108. package/src/UI/pmi/PMIMode.js +1550 -0
  109. package/src/UI/pmi/PMIViewsWidget.js +1098 -0
  110. package/src/UI/pmi/annUtils.js +729 -0
  111. package/src/UI/pmi/dimensions/AngleDimensionAnnotation.js +647 -0
  112. package/src/UI/pmi/dimensions/ExplodeBodyAnnotation.js +507 -0
  113. package/src/UI/pmi/dimensions/HoleCalloutAnnotation.js +462 -0
  114. package/src/UI/pmi/dimensions/LeaderAnnotation.js +403 -0
  115. package/src/UI/pmi/dimensions/LinearDimensionAnnotation.js +532 -0
  116. package/src/UI/pmi/dimensions/NoteAnnotation.js +110 -0
  117. package/src/UI/pmi/dimensions/RadialDimensionAnnotation.js +659 -0
  118. package/src/UI/pmi/pmiStyle.js +44 -0
  119. package/src/UI/sketcher/SketchMode3D.js +4095 -0
  120. package/src/UI/sketcher/dimensions.js +674 -0
  121. package/src/UI/sketcher/glyphs.js +236 -0
  122. package/src/UI/sketcher/highlights.js +60 -0
  123. package/src/UI/toolbarButtons/aboutButton.js +5 -0
  124. package/src/UI/toolbarButtons/exportButton.js +609 -0
  125. package/src/UI/toolbarButtons/flatPatternButton.js +307 -0
  126. package/src/UI/toolbarButtons/importButton.js +160 -0
  127. package/src/UI/toolbarButtons/inspectorToggleButton.js +12 -0
  128. package/src/UI/toolbarButtons/metadataButton.js +1063 -0
  129. package/src/UI/toolbarButtons/orientToFaceButton.js +114 -0
  130. package/src/UI/toolbarButtons/registerDefaultButtons.js +46 -0
  131. package/src/UI/toolbarButtons/saveButton.js +99 -0
  132. package/src/UI/toolbarButtons/scriptRunnerButton.js +302 -0
  133. package/src/UI/toolbarButtons/testsButton.js +26 -0
  134. package/src/UI/toolbarButtons/undoRedoButtons.js +25 -0
  135. package/src/UI/toolbarButtons/wireframeToggleButton.js +5 -0
  136. package/src/UI/toolbarButtons/zoomToFitButton.js +5 -0
  137. package/src/UI/triangleDebuggerWindow.js +945 -0
  138. package/src/UI/viewer.js +4228 -0
  139. package/src/assemblyConstraints/AssemblyConstraintHistory.js +1576 -0
  140. package/src/assemblyConstraints/AssemblyConstraintRegistry.js +120 -0
  141. package/src/assemblyConstraints/BaseAssemblyConstraint.js +66 -0
  142. package/src/assemblyConstraints/constraintExpressionUtils.js +35 -0
  143. package/src/assemblyConstraints/constraintUtils/parallelAlignment.js +676 -0
  144. package/src/assemblyConstraints/constraints/AngleConstraint.js +485 -0
  145. package/src/assemblyConstraints/constraints/CoincidentConstraint.js +194 -0
  146. package/src/assemblyConstraints/constraints/DistanceConstraint.js +616 -0
  147. package/src/assemblyConstraints/constraints/FixedConstraint.js +78 -0
  148. package/src/assemblyConstraints/constraints/ParallelConstraint.js +252 -0
  149. package/src/assemblyConstraints/constraints/TouchAlignConstraint.js +961 -0
  150. package/src/core/entities/HistoryCollectionBase.js +72 -0
  151. package/src/core/entities/ListEntityBase.js +109 -0
  152. package/src/core/entities/schemaProcesser.js +121 -0
  153. package/src/exporters/sheetMetalFlatPattern.js +659 -0
  154. package/src/exporters/sheetMetalUnfold.js +862 -0
  155. package/src/exporters/step.js +1135 -0
  156. package/src/exporters/threeMF.js +575 -0
  157. package/src/features/assemblyComponent/AssemblyComponentFeature.js +780 -0
  158. package/src/features/boolean/BooleanFeature.js +94 -0
  159. package/src/features/chamfer/ChamferFeature.js +116 -0
  160. package/src/features/datium/DatiumFeature.js +80 -0
  161. package/src/features/edgeFeatureUtils.js +41 -0
  162. package/src/features/extrude/ExtrudeFeature.js +143 -0
  163. package/src/features/fillet/FilletFeature.js +197 -0
  164. package/src/features/helix/HelixFeature.js +405 -0
  165. package/src/features/hole/HoleFeature.js +1050 -0
  166. package/src/features/hole/screwClearance.js +86 -0
  167. package/src/features/hole/threadDesignationCatalog.js +149 -0
  168. package/src/features/imageHeightSolid/ImageHeightmapSolidFeature.js +463 -0
  169. package/src/features/imageToFace/ImageToFaceFeature.js +727 -0
  170. package/src/features/imageToFace/imageEditor.js +1270 -0
  171. package/src/features/imageToFace/traceUtils.js +971 -0
  172. package/src/features/import3dModel/Import3dModelFeature.js +151 -0
  173. package/src/features/loft/LoftFeature.js +605 -0
  174. package/src/features/mirror/MirrorFeature.js +151 -0
  175. package/src/features/offsetFace/OffsetFaceFeature.js +370 -0
  176. package/src/features/offsetShell/OffsetShellFeature.js +89 -0
  177. package/src/features/overlapCleanup/OverlapCleanupFeature.js +85 -0
  178. package/src/features/pattern/PatternFeature.js +275 -0
  179. package/src/features/patternLinear/PatternLinearFeature.js +120 -0
  180. package/src/features/patternRadial/PatternRadialFeature.js +186 -0
  181. package/src/features/plane/PlaneFeature.js +154 -0
  182. package/src/features/primitiveCone/primitiveConeFeature.js +99 -0
  183. package/src/features/primitiveCube/primitiveCubeFeature.js +70 -0
  184. package/src/features/primitiveCylinder/primitiveCylinderFeature.js +91 -0
  185. package/src/features/primitivePyramid/primitivePyramidFeature.js +72 -0
  186. package/src/features/primitiveSphere/primitiveSphereFeature.js +62 -0
  187. package/src/features/primitiveTorus/primitiveTorusFeature.js +109 -0
  188. package/src/features/remesh/RemeshFeature.js +97 -0
  189. package/src/features/revolve/RevolveFeature.js +111 -0
  190. package/src/features/selectionUtils.js +118 -0
  191. package/src/features/sheetMetal/SheetMetalContourFlangeFeature.js +1656 -0
  192. package/src/features/sheetMetal/SheetMetalCutoutFeature.js +1056 -0
  193. package/src/features/sheetMetal/SheetMetalFlangeFeature.js +1568 -0
  194. package/src/features/sheetMetal/SheetMetalHemFeature.js +43 -0
  195. package/src/features/sheetMetal/SheetMetalObject.js +141 -0
  196. package/src/features/sheetMetal/SheetMetalTabFeature.js +176 -0
  197. package/src/features/sheetMetal/UNFOLD_NEUTRAL_REQUIREMENTS.md +153 -0
  198. package/src/features/sheetMetal/contour-flange-rebuild-spec.md +261 -0
  199. package/src/features/sheetMetal/profileUtils.js +25 -0
  200. package/src/features/sheetMetal/sheetMetalCleanup.js +9 -0
  201. package/src/features/sheetMetal/sheetMetalFaceTypes.js +146 -0
  202. package/src/features/sheetMetal/sheetMetalMetadata.js +165 -0
  203. package/src/features/sheetMetal/sheetMetalPipeline.js +169 -0
  204. package/src/features/sheetMetal/sheetMetalProfileUtils.js +216 -0
  205. package/src/features/sheetMetal/sheetMetalTabUtils.js +29 -0
  206. package/src/features/sheetMetal/sheetMetalTree.js +210 -0
  207. package/src/features/sketch/SketchFeature.js +955 -0
  208. package/src/features/sketch/sketchSolver2D/ConstraintEngine.js +800 -0
  209. package/src/features/sketch/sketchSolver2D/constraintDefinitions.js +704 -0
  210. package/src/features/sketch/sketchSolver2D/mathHelpersMod.js +307 -0
  211. package/src/features/spline/SplineEditorSession.js +988 -0
  212. package/src/features/spline/SplineFeature.js +1388 -0
  213. package/src/features/spline/splineUtils.js +218 -0
  214. package/src/features/sweep/SweepFeature.js +110 -0
  215. package/src/features/transform/TransformFeature.js +152 -0
  216. package/src/features/tube/TubeFeature.js +635 -0
  217. package/src/fs.proxy.js +625 -0
  218. package/src/idbStorage.js +254 -0
  219. package/src/index.js +12 -0
  220. package/src/main.js +15 -0
  221. package/src/metadataManager.js +64 -0
  222. package/src/path.proxy.js +277 -0
  223. package/src/plugins/ghLoader.worker.js +151 -0
  224. package/src/plugins/pluginManager.js +286 -0
  225. package/src/pmi/PMIViewsManager.js +134 -0
  226. package/src/services/componentLibrary.js +198 -0
  227. package/src/tests/ConsoleCapture.js +189 -0
  228. package/src/tests/S7-diagnostics-2025-12-23T18-37-23-570Z.json +630 -0
  229. package/src/tests/browserTests.js +597 -0
  230. package/src/tests/debugBoolean.js +225 -0
  231. package/src/tests/partFiles/badBoolean.json +957 -0
  232. package/src/tests/partFiles/extrudeTest.json +88 -0
  233. package/src/tests/partFiles/filletFail.json +58 -0
  234. package/src/tests/partFiles/import_TEst.part.part.json +646 -0
  235. package/src/tests/partFiles/sheetMetalHem.BREP.json +734 -0
  236. package/src/tests/test_boolean_subtract.js +27 -0
  237. package/src/tests/test_chamfer.js +17 -0
  238. package/src/tests/test_extrudeFace.js +24 -0
  239. package/src/tests/test_fillet.js +17 -0
  240. package/src/tests/test_fillet_nonClosed.js +45 -0
  241. package/src/tests/test_filletsMoreDifficult.js +46 -0
  242. package/src/tests/test_history_features_basic.js +149 -0
  243. package/src/tests/test_hole.js +282 -0
  244. package/src/tests/test_mirror.js +16 -0
  245. package/src/tests/test_offsetShellGrouping.js +85 -0
  246. package/src/tests/test_plane.js +4 -0
  247. package/src/tests/test_primitiveCone.js +11 -0
  248. package/src/tests/test_primitiveCube.js +7 -0
  249. package/src/tests/test_primitiveCylinder.js +8 -0
  250. package/src/tests/test_primitivePyramid.js +9 -0
  251. package/src/tests/test_primitiveSphere.js +17 -0
  252. package/src/tests/test_primitiveTorus.js +21 -0
  253. package/src/tests/test_pushFace.js +126 -0
  254. package/src/tests/test_sheetMetalContourFlange.js +125 -0
  255. package/src/tests/test_sheetMetal_features.js +80 -0
  256. package/src/tests/test_sketch_openLoop.js +45 -0
  257. package/src/tests/test_solidMetrics.js +58 -0
  258. package/src/tests/test_stlLoader.js +1889 -0
  259. package/src/tests/test_sweepFace.js +55 -0
  260. package/src/tests/test_tube.js +45 -0
  261. package/src/tests/test_tube_closedLoop.js +67 -0
  262. package/src/tests/tests.js +493 -0
  263. package/src/tools/assemblyConstraintDialogCapturePage.js +56 -0
  264. package/src/tools/dialogCapturePageFactory.js +227 -0
  265. package/src/tools/featureDialogCapturePage.js +47 -0
  266. package/src/tools/pmiAnnotationDialogCapturePage.js +60 -0
  267. package/src/utils/axisHelpers.js +99 -0
  268. package/src/utils/deepClone.js +69 -0
  269. package/src/utils/geometryTolerance.js +37 -0
  270. package/src/utils/normalizeTypeString.js +8 -0
  271. package/src/utils/xformMath.js +51 -0
@@ -0,0 +1,1568 @@
1
+ import { BREP } from "../../BREP/BREP.js";
2
+ import {
3
+ SHEET_METAL_FACE_TYPES,
4
+ resolveSheetMetalFaceType as resolveSMFaceType,
5
+ propagateSheetMetalFaceTypesToEdges,
6
+ } from "./sheetMetalFaceTypes.js";
7
+ import { applySheetMetalMetadata } from "./sheetMetalMetadata.js";
8
+ import { normalizeSelectionList } from "../selectionUtils.js";
9
+ import { cleanupSheetMetalOppositeEdgeFaces } from "./sheetMetalCleanup.js";
10
+ import { computeBoundsFromVertices } from "../../BREP/boundsUtils.js";
11
+ import { SheetMetalObject } from "./SheetMetalObject.js";
12
+ import { cloneSheetMetalTree, createSheetMetalFlangeNode } from "./sheetMetalTree.js";
13
+
14
+ const inputParamsSchema = {
15
+ id: {
16
+ type: "string",
17
+ default_value: null,
18
+ hint: "Unique identifier for the flange feature",
19
+ },
20
+ faces: {
21
+ type: "reference_selection",
22
+ selectionFilter: ["FACE"],
23
+ multiple: true,
24
+ default_value: null,
25
+ hint: "Select one or more thin side faces where the flange will be constructed.",
26
+ },
27
+ useOppositeCenterline: {
28
+ label: "Reverse direction",
29
+ type: "boolean",
30
+ default_value: false,
31
+ hint: "Flip to use the opposite edge for the hinge centerline.",
32
+ },
33
+ flangeLength: {
34
+ type: "number",
35
+ default_value: 10,
36
+ min: 0,
37
+ hint: "Placeholder: retained for UI compatibility (currently unused).",
38
+ },
39
+ flangeLengthReference: {
40
+ type: "options",
41
+ options: ["inside", "outside", "web"],
42
+ default_value: "outside",
43
+ hint: "Placeholder: retained for UI compatibility (currently unused).",
44
+ },
45
+ angle: {
46
+ type: "number",
47
+ default_value: 90,
48
+ min: 0,
49
+ max: 180,
50
+ hint: "Flange angle relative to the parent sheet (0° = flat, 90° = perpendicular).",
51
+ },
52
+ inset: {
53
+ type: "options",
54
+ options: ["material_inside", "material_outside", "bend_outside"],
55
+ default_value: "material_inside",
56
+ hint: "Placeholder: retained for UI compatibility (currently unused).",
57
+ },
58
+ reliefWidth: {
59
+ type: "number",
60
+ default_value: 0,
61
+ step: 0.1,
62
+ min: 0,
63
+ hint: "Placeholder reserved for future relief cut options.",
64
+ },
65
+ bendRadius: {
66
+ type: "number",
67
+ default_value: 0,
68
+ min: 0,
69
+ hint: "Placeholder reserved for future bend radius overrides.",
70
+ },
71
+
72
+ offset: {
73
+ type: "number",
74
+ default_value: 0,
75
+ hint: "Placeholder reserved for future offset support.",
76
+ },
77
+ debugSkipUnion: {
78
+ type: "boolean",
79
+ default_value: false,
80
+ hint: "Debug: Skip boolean union with the parent sheet metal.",
81
+ },
82
+ };
83
+
84
+ export class SheetMetalFlangeFeature {
85
+ static shortName = "SM.F";
86
+ static longName = "Sheet Metal Flange";
87
+ static inputParamsSchema = inputParamsSchema;
88
+ static baseType = "FLANGE";
89
+ static logTag = "SheetMetalFlange";
90
+ static defaultAngle = 90;
91
+ static angleOverride = null;
92
+ static defaultBendRadius = null;
93
+
94
+ constructor() {
95
+ this.inputParams = {};
96
+ this.persistentData = {};
97
+ }
98
+
99
+ async run(partHistory) {
100
+ const FeatureClass = this?.constructor || {};
101
+ const featureLabel = FeatureClass.longName || "Sheet Metal Flange";
102
+ const logTag = FeatureClass.logTag || "SheetMetalFlange";
103
+ const baseType = FeatureClass.baseType || "FLANGE";
104
+ const defaultAngle = Number.isFinite(FeatureClass.defaultAngle)
105
+ ? FeatureClass.defaultAngle
106
+ : 90;
107
+ const angleOverride = Number.isFinite(FeatureClass.angleOverride)
108
+ ? FeatureClass.angleOverride
109
+ : null;
110
+ const defaultBendRadius = Number.isFinite(FeatureClass.defaultBendRadius)
111
+ ? FeatureClass.defaultBendRadius
112
+ : null;
113
+
114
+ const faces = resolveSelectedFaces(this.inputParams?.faces, partHistory?.scene);
115
+ if (!faces.length) {
116
+ throw new Error(`${featureLabel} requires selecting at least one FACE.`);
117
+ }
118
+
119
+ const parentSolid = findAncestorSolid(faces[0]);
120
+ const sameParentFaces = faces.filter((face) => findAncestorSolid(face) === parentSolid);
121
+ if (!parentSolid || sameParentFaces.length !== faces.length) {
122
+ throw new Error(`${featureLabel} selections must belong to a single sheet metal solid.`);
123
+ }
124
+
125
+ const tree = parentSolid?.userData?.sheetMetalTree
126
+ || parentSolid?.userData?.sheetMetal?.tree
127
+ || null;
128
+
129
+ if (!tree) {
130
+ const result = await buildSheetMetalFlangeSolids({
131
+ params: this.inputParams,
132
+ partHistory,
133
+ faces,
134
+ featureClass: this?.constructor,
135
+ applyMetadata: true,
136
+ });
137
+ this.persistentData = this.persistentData || {};
138
+ if (result?.persistentData) {
139
+ this.persistentData.sheetMetal = result.persistentData;
140
+ }
141
+ return { added: result.added || [], removed: result.removed || [] };
142
+ }
143
+
144
+ const baseFace = sameParentFaces[0];
145
+ const parentSolidName = parentSolid?.name || null;
146
+ const thicknessInfo = resolveThickness(baseFace, parentSolid, partHistory?.metadataManager);
147
+ const thickness = thicknessInfo?.thickness ?? 1;
148
+ const baseBendRadius = thicknessInfo?.defaultBendRadius ?? thickness;
149
+
150
+ const angleDeg = angleOverride != null ? angleOverride : this.inputParams.angle;
151
+ const angleFallback = Math.max(0, Math.min(180, defaultAngle));
152
+ const appliedAngle = Number.isFinite(angleDeg)
153
+ ? Math.max(0, Math.min(180, angleDeg))
154
+ : angleFallback;
155
+ const bendRadiusFallback = defaultBendRadius != null ? defaultBendRadius : 0;
156
+ const bendRadiusInput = Math.max(0, Number(this.inputParams?.bendRadius ?? bendRadiusFallback));
157
+ const bendRadiusOverride = bendRadiusInput > 0 ? bendRadiusInput : null;
158
+ const bendRadiusUsed = bendRadiusOverride ?? baseBendRadius;
159
+ const useOppositeCenterline = this.inputParams?.useOppositeCenterline === true;
160
+
161
+ try {
162
+ const radiusSource = bendRadiusOverride != null ? "feature_override" : "parent_solid";
163
+ console.log(`[${logTag}] Bend radius resolved`, {
164
+ featureId: this.inputParams?.featureID || this.inputParams?.id || null,
165
+ parentSolid: parentSolidName,
166
+ bendRadiusInput,
167
+ baseBendRadius,
168
+ bendRadiusUsed,
169
+ radiusSource,
170
+ });
171
+ } catch { /* logging best-effort */ }
172
+
173
+ let insetOffsetValue = 0;
174
+ if (this.inputParams?.inset === "material_inside") insetOffsetValue = -bendRadiusUsed - thickness;
175
+ if (this.inputParams?.inset === "material_outside") insetOffsetValue = -bendRadiusUsed;
176
+ if (this.inputParams?.inset === "bend_outside") insetOffsetValue = 0;
177
+ const offsetValue = Number(this.inputParams?.offset ?? 0) + insetOffsetValue;
178
+
179
+ const sheetMetalMetadata = {
180
+ featureID: this.inputParams?.featureID || null,
181
+ thickness,
182
+ bendRadius: baseBendRadius,
183
+ baseType,
184
+ extra: {
185
+ angleDegrees: appliedAngle,
186
+ insetMode: this.inputParams?.inset || null,
187
+ useOppositeCenterline,
188
+ offsetValue,
189
+ bendRadiusOverride,
190
+ bendRadiusUsed,
191
+ baseBendRadius,
192
+ },
193
+ };
194
+ const persistentData = {
195
+ baseType,
196
+ thickness,
197
+ bendRadius: bendRadiusUsed,
198
+ defaultBendRadius: baseBendRadius,
199
+ bendRadiusOverride,
200
+ angleDegrees: appliedAngle,
201
+ insetMode: this.inputParams?.inset || null,
202
+ useOppositeCenterline,
203
+ offsetValue,
204
+ };
205
+
206
+ const baseMeta = parentSolid?.userData?.sheetMetal || {};
207
+ const sheetMetal = new SheetMetalObject({
208
+ tree,
209
+ kFactor: baseMeta.neutralFactor ?? parentSolid?.userData?.sheetMetalNeutralFactor ?? null,
210
+ thickness: baseMeta.thickness ?? parentSolid?.userData?.sheetThickness ?? null,
211
+ bendRadius: baseMeta.bendRadius ?? parentSolid?.userData?.sheetBendRadius ?? null,
212
+ });
213
+ const flangeNode = createSheetMetalFlangeNode({
214
+ featureID: this.inputParams?.featureID || null,
215
+ faceRefs: normalizeSelectionList(this.inputParams?.faces),
216
+ useOppositeCenterline: this.inputParams?.useOppositeCenterline === true,
217
+ flangeLength: this.inputParams?.flangeLength,
218
+ flangeLengthReference: this.inputParams?.flangeLengthReference,
219
+ angle: this.inputParams?.angle,
220
+ inset: this.inputParams?.inset,
221
+ reliefWidth: this.inputParams?.reliefWidth,
222
+ bendRadius: this.inputParams?.bendRadius,
223
+ offset: this.inputParams?.offset,
224
+ debugSkipUnion: this.inputParams?.debugSkipUnion === true,
225
+ baseType,
226
+ defaultAngle,
227
+ angleOverride,
228
+ defaultBendRadius,
229
+ });
230
+ sheetMetal.appendNode(flangeNode);
231
+ await sheetMetal.generate({
232
+ partHistory,
233
+ metadataManager: partHistory?.metadataManager,
234
+ mode: "solid",
235
+ });
236
+ if (parentSolidName) {
237
+ try { sheetMetal.name = parentSolidName; } catch { /* ignore */ }
238
+ }
239
+
240
+ const added = [sheetMetal];
241
+ let removed = [parentSolid];
242
+ if (this.inputParams?.debug) removed = [];
243
+
244
+ cleanupSheetMetalOppositeEdgeFaces(added);
245
+ propagateSheetMetalFaceTypesToEdges(added);
246
+ applySheetMetalMetadata(added, partHistory?.metadataManager, {
247
+ ...sheetMetalMetadata,
248
+ forceBaseOverwrite: false,
249
+ });
250
+
251
+ for (const solid of added) {
252
+ if (!solid) continue;
253
+ solid.userData = solid.userData || {};
254
+ solid.userData.sheetMetalTree = cloneSheetMetalTree(sheetMetal.tree);
255
+ solid.userData.sheetMetalKFactor = sheetMetal.kFactor ?? null;
256
+ }
257
+
258
+ this.persistentData = this.persistentData || {};
259
+ this.persistentData.sheetMetal = {
260
+ ...persistentData,
261
+ tree: sheetMetal.tree,
262
+ };
263
+
264
+ return { added, removed };
265
+ }
266
+ }
267
+
268
+ export async function buildSheetMetalFlangeSolids({
269
+ params,
270
+ partHistory,
271
+ faces = null,
272
+ featureClass = null,
273
+ applyMetadata = true,
274
+ } = {}) {
275
+ const FeatureClass = featureClass || {};
276
+ const featureLabel = FeatureClass.longName || "Sheet Metal Flange";
277
+ const logTag = FeatureClass.logTag || "SheetMetalFlange";
278
+ const baseType = FeatureClass.baseType || "FLANGE";
279
+ const defaultAngle = Number.isFinite(FeatureClass.defaultAngle)
280
+ ? FeatureClass.defaultAngle
281
+ : 90;
282
+ const angleOverride = Number.isFinite(FeatureClass.angleOverride)
283
+ ? FeatureClass.angleOverride
284
+ : null;
285
+ const defaultBendRadius = Number.isFinite(FeatureClass.defaultBendRadius)
286
+ ? FeatureClass.defaultBendRadius
287
+ : null;
288
+
289
+ const resolvedFaces = Array.isArray(faces)
290
+ ? faces.filter(Boolean)
291
+ : resolveSelectedFaces(params?.faces, partHistory?.scene);
292
+ if (!resolvedFaces.length) {
293
+ throw new Error(`${featureLabel} requires selecting at least one FACE.`);
294
+ }
295
+
296
+ const featureID = params?.featureID || params?.id || null;
297
+
298
+ // Assume all selected faces share the same sheet thickness; resolve it once up front.
299
+ const baseFace = resolvedFaces[0];
300
+ const baseParentSolid = findAncestorSolid(baseFace);
301
+ const parentSolidName = baseParentSolid?.name || null;
302
+ const thicknessInfo = resolveThickness(baseFace, baseParentSolid, partHistory?.metadataManager);
303
+ const thickness = thicknessInfo?.thickness ?? 1;
304
+ const baseBendRadius = thicknessInfo?.defaultBendRadius ?? thickness;
305
+
306
+ const angleDeg = angleOverride != null ? angleOverride : params?.angle;
307
+ const angleFallback = Math.max(0, Math.min(180, defaultAngle));
308
+ let angle = Number.isFinite(angleDeg) ? Math.max(0, Math.min(180, angleDeg)) : angleFallback;
309
+ const bendRadiusFallback = defaultBendRadius != null ? defaultBendRadius : 0;
310
+ const bendRadiusInput = Math.max(0, Number(params?.bendRadius ?? bendRadiusFallback));
311
+ const bendRadiusOverride = bendRadiusInput > 0 ? bendRadiusInput : null;
312
+ const bendRadiusUsed = bendRadiusOverride ?? baseBendRadius;
313
+ const useOppositeCenterline = params?.useOppositeCenterline === true;
314
+
315
+ try {
316
+ const radiusSource = bendRadiusOverride != null ? "feature_override" : "parent_solid";
317
+ console.log(`[${logTag}] Bend radius resolved`, {
318
+ featureId: featureID,
319
+ parentSolid: parentSolidName,
320
+ bendRadiusInput,
321
+ baseBendRadius,
322
+ bendRadiusUsed,
323
+ radiusSource,
324
+ });
325
+ } catch { /* logging best-effort */ }
326
+
327
+ const skipUnion = params?.debugSkipUnion === true;
328
+
329
+ let insetOffsetValue = 0;
330
+ if (params?.inset === "material_inside") insetOffsetValue = -bendRadiusUsed - thickness;
331
+ if (params?.inset === "material_outside") insetOffsetValue = -bendRadiusUsed;
332
+ if (params?.inset === "bend_outside") insetOffsetValue = 0;
333
+
334
+ const offsetValue = Number(params?.offset ?? 0) + insetOffsetValue;
335
+ const shouldExtrudeOffset = Number.isFinite(offsetValue) && offsetValue !== 0;
336
+
337
+ const appliedAngle = angle;
338
+ const sheetMetalMetadata = {
339
+ featureID,
340
+ thickness,
341
+ bendRadius: baseBendRadius,
342
+ baseType,
343
+ extra: {
344
+ angleDegrees: appliedAngle,
345
+ insetMode: params?.inset || null,
346
+ useOppositeCenterline,
347
+ offsetValue,
348
+ bendRadiusOverride,
349
+ bendRadiusUsed,
350
+ baseBendRadius,
351
+ },
352
+ };
353
+ const persistentData = {
354
+ baseType,
355
+ thickness,
356
+ bendRadius: bendRadiusUsed,
357
+ defaultBendRadius: baseBendRadius,
358
+ bendRadiusOverride,
359
+ angleDegrees: appliedAngle,
360
+ insetMode: params?.inset || null,
361
+ useOppositeCenterline,
362
+ offsetValue,
363
+ };
364
+
365
+ const generatedSolids = [];
366
+ const parentSolidStates = new Map();
367
+ const orphanSolids = [];
368
+ const solidParentNames = new WeakMap();
369
+ const recordParentName = (solid, parentSolid) => {
370
+ try {
371
+ if (solid && parentSolid?.name) solidParentNames.set(solid, parentSolid.name);
372
+ } catch { /* ignore */ }
373
+ };
374
+ const registerSolid = (solid, parentSolid) => {
375
+ if (!solid) return;
376
+ generatedSolids.push(solid);
377
+ if (parentSolid) {
378
+ const state = getParentState(parentSolidStates, parentSolid);
379
+ if (state) state.solids.push(solid);
380
+ recordParentName(solid, parentSolid);
381
+ } else {
382
+ orphanSolids.push(solid);
383
+ }
384
+ };
385
+ const subtractRemoved = [];
386
+ const debugSubtractionSolids = [];
387
+
388
+ let faceIndex = 0;
389
+ for (const face of resolvedFaces) {
390
+ const context = analyzeFace(face);
391
+ if (!context) continue;
392
+ const orientationInfo = resolveABOrientation(face, context);
393
+ const desiredBendSide = useOppositeCenterline
394
+ ? SHEET_METAL_FACE_TYPES.B
395
+ : SHEET_METAL_FACE_TYPES.A;
396
+
397
+ const offsetNormal = shouldExtrudeOffset
398
+ ? resolveOffsetNormal(context, face)
399
+ : null;
400
+ const offsetVector = shouldExtrudeOffset
401
+ ? buildOffsetTranslationVector(offsetNormal || context.baseNormal, offsetValue)
402
+ : null;
403
+
404
+ const targetRadius = bendRadiusUsed;
405
+ const tolerance = Math.max(1e-4, targetRadius * 0.01);
406
+ const hingeOptions = [];
407
+ const primaryHinge = pickCenterlineEdge(face, context, useOppositeCenterline);
408
+ if (primaryHinge) hingeOptions.push(primaryHinge);
409
+ const altHinge = pickCenterlineEdge(face, context, !useOppositeCenterline);
410
+ if (altHinge) hingeOptions.push(altHinge);
411
+
412
+ let chosen = null;
413
+ for (const hingeEdge of hingeOptions) {
414
+ const defaultOffset = bendRadiusUsed + thickness;
415
+ const primary = evaluateFlangeCandidate({
416
+ raw: buildFlangeRevolve({
417
+ face,
418
+ context,
419
+ hingeEdge,
420
+ appliedAngle,
421
+ bendRadiusUsed,
422
+ thickness,
423
+ offsetVector,
424
+ featureID,
425
+ offsetMagnitudeOverride: defaultOffset,
426
+ }),
427
+ targetRadius,
428
+ desiredBendSide,
429
+ orientationInfo,
430
+ });
431
+ chosen = pickBetterFlangeCandidate(chosen, primary);
432
+
433
+ if ((primary?.radiusErr ?? Infinity) > tolerance) {
434
+ const tighter = evaluateFlangeCandidate({
435
+ raw: buildFlangeRevolve({
436
+ face,
437
+ context,
438
+ hingeEdge,
439
+ appliedAngle,
440
+ bendRadiusUsed,
441
+ thickness,
442
+ offsetVector,
443
+ featureID,
444
+ offsetMagnitudeOverride: bendRadiusUsed,
445
+ }),
446
+ targetRadius,
447
+ desiredBendSide,
448
+ orientationInfo,
449
+ });
450
+ chosen = pickBetterFlangeCandidate(chosen, tighter);
451
+ }
452
+ if (chosen?.revolve
453
+ && (chosen.orientationPenalty ?? 0) === 0
454
+ && (chosen.radiusErr ?? Infinity) <= tolerance) {
455
+ break;
456
+ }
457
+ }
458
+
459
+ if (!chosen?.revolve) continue;
460
+
461
+ const revolve = chosen.revolve;
462
+ const bendEndFace = chosen.bendEndFace;
463
+ registerSolid(revolve, context.parentSolid);
464
+
465
+ const zFightNudge = Math.max(1e-6, Math.min(0.001, thickness * 0.0001));
466
+
467
+ if (offsetVector) {
468
+ const useForSubtraction = offsetValue < 0 && !!context.parentSolid;
469
+ // Avoid inflating the subtraction cutter; it creates visible clearance gaps.
470
+ const reliefPushDistance = useForSubtraction ? 0 : zFightNudge;
471
+ const offsetSolid = createOffsetExtrudeSolid({
472
+ face,
473
+ faceNormal: offsetNormal || context.baseNormal,
474
+ lengthValue: offsetValue,
475
+ featureID,
476
+ faceIndex,
477
+ applyReliefPush: !useForSubtraction,
478
+ reliefPushDistance,
479
+ reliefPushNormal: context.baseNormal,
480
+ });
481
+ if (offsetSolid) {
482
+ let usedForSubtraction = false;
483
+ if (useForSubtraction) {
484
+ const state = getParentState(parentSolidStates, context.parentSolid);
485
+ const subtractionTarget = state?.target || context.parentSolid;
486
+ if (subtractionTarget) {
487
+ try {
488
+ const subtraction = await BREP.applyBooleanOperation(
489
+ partHistory || {},
490
+ offsetSolid,
491
+ { operation: "SUBTRACT", targets: [subtractionTarget] },
492
+ featureID,
493
+ );
494
+ if (Array.isArray(subtraction?.removed)) subtractRemoved.push(...subtraction.removed);
495
+ const replacement = pickReplacementSolid(subtraction?.added);
496
+ if (replacement) {
497
+ state.target = replacement;
498
+ usedForSubtraction = true;
499
+ }
500
+ } catch {
501
+ usedForSubtraction = false;
502
+ }
503
+ }
504
+ }
505
+ if (usedForSubtraction && skipUnion) {
506
+ debugSubtractionSolids.push(offsetSolid);
507
+ }
508
+ if (!usedForSubtraction) {
509
+ registerSolid(offsetSolid, context.parentSolid);
510
+ }
511
+ }
512
+ }
513
+ const flangeRef = String(params?.flangeLengthReference || "web").toLowerCase();
514
+ let flangeLength = Number(params?.flangeLength ?? 0);
515
+ if (!Number.isFinite(flangeLength)) flangeLength = 0;
516
+ if (flangeRef === "inside") flangeLength = flangeLength - bendRadiusUsed;
517
+ if (flangeRef === "outside") flangeLength = flangeLength - bendRadiusUsed - thickness;
518
+ if (flangeRef === "web") flangeLength = flangeLength;
519
+ if (bendEndFace && Number.isFinite(flangeLength) && flangeLength !== 0) {
520
+ const flatSolid = createOffsetExtrudeSolid({
521
+ face: bendEndFace,
522
+ faceNormal: bendEndFace?.getAverageNormal ? bendEndFace.getAverageNormal() : null,
523
+ lengthValue: flangeLength,
524
+ featureID,
525
+ faceIndex,
526
+ reliefPushDistance: zFightNudge,
527
+ });
528
+ if (flatSolid) {
529
+ if (offsetVector) {
530
+ applyTranslationToSolid(flatSolid, offsetVector);
531
+ }
532
+ registerSolid(flatSolid, context.parentSolid);
533
+ }
534
+ }
535
+ faceIndex++;
536
+ }
537
+
538
+ if (!generatedSolids.length) {
539
+ throw new Error(`${featureLabel} failed to generate any geometry for the selected faces.`);
540
+ }
541
+
542
+ if (skipUnion || parentSolidStates.size === 0) {
543
+ const added = skipUnion && debugSubtractionSolids.length
544
+ ? [...generatedSolids, ...debugSubtractionSolids]
545
+ : generatedSolids;
546
+ cleanupSheetMetalOppositeEdgeFaces(added);
547
+ if (applyMetadata) {
548
+ applySheetMetalMetadata(added, partHistory?.metadataManager, sheetMetalMetadata);
549
+ }
550
+ return { added, removed: subtractRemoved, persistentData };
551
+ }
552
+
553
+ const unionResults = [];
554
+ const unionRemoved = [];
555
+ const fallbackSolids = [...orphanSolids];
556
+ let groupIndex = 0;
557
+
558
+ for (const state of parentSolidStates.values()) {
559
+ const parentSolid = state?.target || state?.original;
560
+ const solids = state?.solids || [];
561
+ if (!parentSolid || !Array.isArray(solids) || !solids.length) continue;
562
+ const baseSolid = solids.length === 1
563
+ ? solids[0]
564
+ : combineSolids({
565
+ solids,
566
+ featureID,
567
+ groupIndex: groupIndex++,
568
+ });
569
+ recordParentName(baseSolid, parentSolid);
570
+ if (!baseSolid) {
571
+ fallbackSolids.push(...solids);
572
+ continue;
573
+ }
574
+
575
+ let unionSucceeded = false;
576
+ try {
577
+ const effects = await BREP.applyBooleanOperation(
578
+ partHistory || {},
579
+ baseSolid,
580
+ { operation: "UNION", targets: [parentSolid] },
581
+ featureID,
582
+ );
583
+ if (Array.isArray(effects?.added)) {
584
+ for (const addedSolid of effects.added) {
585
+ if (parentSolid?.name) setSolidNameSafe(addedSolid, parentSolid.name);
586
+ recordParentName(addedSolid, parentSolid);
587
+ }
588
+ unionResults.push(...effects.added);
589
+ }
590
+ if (Array.isArray(effects?.removed)) unionRemoved.push(...effects.removed);
591
+ unionSucceeded = Array.isArray(effects?.removed)
592
+ && effects.removed.some((solid) => solidsMatch(solid, parentSolid));
593
+ } catch {
594
+ unionSucceeded = false;
595
+ }
596
+
597
+ if (!unionSucceeded) {
598
+ fallbackSolids.push(...solids);
599
+ }
600
+ }
601
+
602
+ const finalAdded = [];
603
+ if (unionResults.length) finalAdded.push(...unionResults);
604
+ if (fallbackSolids.length) finalAdded.push(...fallbackSolids);
605
+ if (!finalAdded.length) finalAdded.push(...generatedSolids);
606
+
607
+ // Preserve parent solid names on outputs derived from that parent.
608
+ for (const state of parentSolidStates.values()) {
609
+ const parentName = state?.original?.name;
610
+ if (!parentName || !Array.isArray(state?.solids)) continue;
611
+ const known = new Set(state.solids);
612
+ for (const solid of finalAdded) {
613
+ if (known.has(solid)) setSolidNameSafe(solid, parentName);
614
+ }
615
+ }
616
+ for (const solid of finalAdded) {
617
+ const name = solidParentNames.get(solid);
618
+ if (name) setSolidNameSafe(solid, name);
619
+ }
620
+
621
+ cleanupSheetMetalOppositeEdgeFaces(finalAdded);
622
+ if (applyMetadata) {
623
+ applySheetMetalMetadata(finalAdded, partHistory?.metadataManager, sheetMetalMetadata);
624
+ }
625
+
626
+ // Ensure final solids keep the original parent solid name (never the feature ID).
627
+ for (const solid of finalAdded) {
628
+ if (parentSolidName) setSolidNameSafe(solid, parentSolidName);
629
+ }
630
+
631
+ let removed = [...subtractRemoved, ...unionRemoved];
632
+
633
+ if (params?.debug) removed = [];
634
+
635
+ return { added: finalAdded, removed, persistentData };
636
+ }
637
+
638
+ function resolveSelectedFaces(selectionRefs, scene) {
639
+ const refs = Array.isArray(selectionRefs) ? selectionRefs : (selectionRefs ? [selectionRefs] : []);
640
+ const out = [];
641
+ for (const ref of refs) {
642
+ let face = ref;
643
+ if (typeof face === "string" && scene?.getObjectByName) {
644
+ face = scene.getObjectByName(face);
645
+ }
646
+ if (!face || face.type !== "FACE") continue;
647
+ out.push(face);
648
+ }
649
+ return out;
650
+ }
651
+
652
+ function analyzeFace(face) {
653
+ try {
654
+ if (!face || face.type !== "FACE") return null;
655
+ const THREE = BREP.THREE;
656
+ const loops = Array.isArray(face?.userData?.boundaryLoopsWorld) ? face.userData.boundaryLoopsWorld : null;
657
+ const outer = loops?.find((loop) => !loop?.isHole) || loops?.[0];
658
+ const rawPoints = Array.isArray(outer?.pts) ? outer.pts : null;
659
+ const points = rawPoints && rawPoints.length
660
+ ? rawPoints.map((p) => new THREE.Vector3(p[0], p[1], p[2]))
661
+ : extractFacePointsFromGeometry(face);
662
+ if (!points || points.length < 2) return null;
663
+
664
+ const baseNormal = (typeof face.getAverageNormal === "function")
665
+ ? face.getAverageNormal().clone()
666
+ : new THREE.Vector3(0, 0, 1);
667
+ if (baseNormal.lengthSq() < 1e-10) baseNormal.set(0, 0, 1);
668
+ baseNormal.normalize();
669
+
670
+ const origin = points.reduce((acc, pt) => acc.add(pt), new THREE.Vector3()).multiplyScalar(1 / points.length);
671
+ let axisGuess = points[points.length - 1].clone().sub(points[0]);
672
+ axisGuess.sub(baseNormal.clone().multiplyScalar(axisGuess.dot(baseNormal)));
673
+ if (axisGuess.lengthSq() < 1e-10) {
674
+ axisGuess = new THREE.Vector3().crossVectors(baseNormal, new THREE.Vector3(1, 0, 0));
675
+ }
676
+ if (axisGuess.lengthSq() < 1e-10) {
677
+ axisGuess = new THREE.Vector3().crossVectors(baseNormal, new THREE.Vector3(0, 1, 0));
678
+ }
679
+ axisGuess.normalize();
680
+ let perpGuess = new THREE.Vector3().crossVectors(baseNormal, axisGuess).normalize();
681
+ if (perpGuess.lengthSq() < 1e-10) {
682
+ perpGuess = new THREE.Vector3().crossVectors(baseNormal, new THREE.Vector3(0, 0, 1)).normalize();
683
+ }
684
+
685
+ const projectSpan = (axis) => {
686
+ let min = Infinity;
687
+ let max = -Infinity;
688
+ for (const pt of points) {
689
+ const value = pt.clone().sub(origin).dot(axis);
690
+ if (value < min) min = value;
691
+ if (value > max) max = value;
692
+ }
693
+ return { min, max, span: max - min };
694
+ };
695
+
696
+ let tangent = axisGuess.clone();
697
+ let tangentSpan = projectSpan(tangent);
698
+ let secondaryAxis = perpGuess.clone();
699
+ let secondarySpan = projectSpan(secondaryAxis);
700
+ if (secondarySpan.span > tangentSpan.span) {
701
+ tangent = secondaryAxis.clone();
702
+ tangentSpan = secondarySpan;
703
+ secondaryAxis = axisGuess.clone();
704
+ secondarySpan = projectSpan(secondaryAxis);
705
+ }
706
+ if (tangentSpan.span < 1e-6) return null;
707
+
708
+ tangent.normalize();
709
+ let sheetDir = new THREE.Vector3().crossVectors(baseNormal, tangent).normalize();
710
+ const orientOrigin = origin.clone();
711
+ sheetDir = orientSheetDir(face, sheetDir, orientOrigin);
712
+ const sheetSpan = projectSpan(sheetDir);
713
+
714
+ const hingeStart = origin.clone()
715
+ .add(tangent.clone().multiplyScalar(tangentSpan.min))
716
+ .add(sheetDir.clone().multiplyScalar(sheetSpan.max));
717
+ const hingeEnd = origin.clone()
718
+ .add(tangent.clone().multiplyScalar(tangentSpan.max))
719
+ .add(sheetDir.clone().multiplyScalar(sheetSpan.max));
720
+
721
+ return {
722
+ hingeLine: { start: hingeStart, end: hingeEnd },
723
+ baseNormal,
724
+ sheetDir,
725
+ sheetSpan,
726
+ origin,
727
+ parentSolid: findAncestorSolid(face),
728
+ };
729
+ } catch {
730
+ return null;
731
+ }
732
+ }
733
+
734
+ function resolveOffsetNormal(context, face) {
735
+ const THREE = BREP.THREE;
736
+ const baseNormal = (context?.baseNormal && typeof context.baseNormal.clone === "function")
737
+ ? context.baseNormal.clone()
738
+ : (typeof face?.getAverageNormal === "function"
739
+ ? face.getAverageNormal().clone()
740
+ : new THREE.Vector3(0, 0, 1));
741
+ if (!baseNormal || baseNormal.lengthSq() < 1e-12) return null;
742
+ baseNormal.normalize();
743
+
744
+ const parentSolid = context?.parentSolid || findAncestorSolid(face);
745
+ const faceCenter = context?.origin?.clone?.() || computeFaceCenter(face);
746
+ const solidCenter = computeSolidCenter(parentSolid);
747
+ if (faceCenter && solidCenter) {
748
+ const toCenter = solidCenter.clone().sub(faceCenter);
749
+ if (toCenter.lengthSq() > 1e-12) {
750
+ const dot = baseNormal.dot(toCenter);
751
+ if (Math.abs(dot) > 1e-9 && dot < 0) {
752
+ baseNormal.multiplyScalar(-1);
753
+ }
754
+ }
755
+ }
756
+ return baseNormal;
757
+ }
758
+
759
+ function computeSolidCenter(solid) {
760
+ if (!solid) return null;
761
+ const THREE = BREP.THREE;
762
+ let center = null;
763
+ let fromLocal = false;
764
+ try {
765
+ const verts = solid._vertProperties || null;
766
+ const bounds = computeBoundsFromVertices(verts);
767
+ if (bounds) {
768
+ center = new THREE.Vector3(
769
+ (bounds.min[0] + bounds.max[0]) * 0.5,
770
+ (bounds.min[1] + bounds.max[1]) * 0.5,
771
+ (bounds.min[2] + bounds.max[2]) * 0.5,
772
+ );
773
+ fromLocal = true;
774
+ }
775
+ } catch { /* best effort */ }
776
+
777
+ if (!center) {
778
+ try {
779
+ const box = new THREE.Box3().setFromObject(solid);
780
+ if (box && box.min && box.max) {
781
+ center = new THREE.Vector3(
782
+ (box.min.x + box.max.x) * 0.5,
783
+ (box.min.y + box.max.y) * 0.5,
784
+ (box.min.z + box.max.z) * 0.5,
785
+ );
786
+ }
787
+ } catch { /* ignore */ }
788
+ }
789
+
790
+ if (center && fromLocal) {
791
+ try {
792
+ if (solid.matrixWorld) center.applyMatrix4(solid.matrixWorld);
793
+ } catch { /* ignore */ }
794
+ }
795
+
796
+ return center;
797
+ }
798
+
799
+ function extractFacePointsFromGeometry(face) {
800
+ const pts = [];
801
+ try {
802
+ const pos = face?.geometry?.getAttribute?.("position");
803
+ if (!pos) return pts;
804
+ const THREE = BREP.THREE;
805
+ const v = new THREE.Vector3();
806
+ for (let i = 0; i < pos.count; i++) {
807
+ v.set(pos.getX(i), pos.getY(i), pos.getZ(i)).applyMatrix4(face.matrixWorld);
808
+ pts.push(v.clone());
809
+ }
810
+ } catch { /* best effort */ }
811
+ return pts;
812
+ }
813
+
814
+ function findAncestorSolid(obj) {
815
+ let current = obj;
816
+ while (current) {
817
+ if (current.type === "SOLID") return current;
818
+ current = current.parent;
819
+ }
820
+ return null;
821
+ }
822
+
823
+ function resolveThickness(face, parentSolid, metadataManager) {
824
+ const thicknessCandidates = [];
825
+ const radiusCandidates = [];
826
+ const metaSources = [];
827
+ const metaKeys = new Set();
828
+ const pushMeta = (key) => {
829
+ if (!key || !metadataManager || typeof metadataManager.getOwnMetadata !== "function") return;
830
+ const normalized = String(key).trim();
831
+ if (!normalized || metaKeys.has(normalized)) return;
832
+ metaKeys.add(normalized);
833
+ const entry = metadataManager.getOwnMetadata(normalized);
834
+ if (entry && typeof entry === "object") metaSources.push(entry);
835
+ };
836
+ pushMeta(parentSolid?.name);
837
+ pushMeta(parentSolid?.userData?.sheetMetal?.featureID);
838
+ pushMeta(parentSolid?.owningFeatureID);
839
+ pushMeta(face?.name);
840
+ pushMeta(face?.userData?.sheetMetal?.featureID);
841
+ pushMeta(face?.owningFeatureID);
842
+
843
+ const addIfValid = (arr, value, validator) => {
844
+ const num = Number(value);
845
+ if (validator(num)) arr.push(num);
846
+ };
847
+
848
+ for (const meta of metaSources) {
849
+ addIfValid(thicknessCandidates, meta?.sheetMetalThickness, (v) => Number.isFinite(v) && v > 0);
850
+ }
851
+ addIfValid(thicknessCandidates, face?.userData?.sheetThickness, (v) => Number.isFinite(v) && v > 0);
852
+ addIfValid(thicknessCandidates, parentSolid?.userData?.sheetThickness, (v) => Number.isFinite(v) && v > 0);
853
+ addIfValid(thicknessCandidates, face?.userData?.sheetMetal?.baseThickness, (v) => Number.isFinite(v) && v > 0);
854
+ addIfValid(thicknessCandidates, parentSolid?.userData?.sheetMetal?.baseThickness, (v) => Number.isFinite(v) && v > 0);
855
+ addIfValid(thicknessCandidates, parentSolid?.userData?.sheetMetal?.thickness, (v) => Number.isFinite(v) && v > 0);
856
+ const thicknessVal = thicknessCandidates.find((t) => Number.isFinite(t) && t > 0);
857
+ const thickness = thicknessVal ? Number(thicknessVal) : 1;
858
+
859
+ // Prefer base bend radius first so overrides never change the base attribute chain.
860
+ for (const meta of metaSources) {
861
+ addIfValid(radiusCandidates, meta?.sheetMetalBendRadius, (v) => Number.isFinite(v) && v >= 0);
862
+ }
863
+ addIfValid(radiusCandidates, face?.userData?.sheetMetal?.baseBendRadius, (v) => Number.isFinite(v) && v >= 0);
864
+ addIfValid(radiusCandidates, parentSolid?.userData?.sheetMetal?.baseBendRadius, (v) => Number.isFinite(v) && v >= 0);
865
+ addIfValid(radiusCandidates, parentSolid?.userData?.sheetMetal?.bendRadius, (v) => Number.isFinite(v) && v >= 0);
866
+ addIfValid(radiusCandidates, face?.userData?.sheetMetal?.bendRadius, (v) => Number.isFinite(v) && v >= 0);
867
+ addIfValid(radiusCandidates, parentSolid?.userData?.sheetBendRadius, (v) => Number.isFinite(v) && v >= 0);
868
+ addIfValid(radiusCandidates, face?.userData?.sheetBendRadius, (v) => Number.isFinite(v) && v >= 0);
869
+ const radiusVal = radiusCandidates.find((r) => Number.isFinite(r) && r >= 0);
870
+ const defaultBendRadius = radiusVal != null ? Number(radiusVal) : thickness;
871
+ return { thickness, defaultBendRadius };
872
+ }
873
+
874
+ function pickCenterlineEdge(face, context, useOppositeEdge) {
875
+ const sheetDir = context.sheetDir.clone().normalize();
876
+ const origin = context.origin.clone();
877
+ const sheetSpan = context.sheetSpan || { min: -1, max: 1 };
878
+ const segments = collectFaceEdgeSegments(face);
879
+ const targetFaceType = useOppositeEdge
880
+ ? SHEET_METAL_FACE_TYPES.B
881
+ : SHEET_METAL_FACE_TYPES.A;
882
+ if (!segments.length) {
883
+ const fallback = context.hingeLine;
884
+ return fallback
885
+ ? { start: fallback.start.clone(), end: fallback.end.clone(), target: useOppositeEdge ? "MAX" : "MIN" }
886
+ : null;
887
+ }
888
+
889
+ const alignmentThreshold = 0.5;
890
+ const notThicknessEdges = segments.filter((seg) => {
891
+ const dir = seg.end.clone().sub(seg.start).normalize();
892
+ const alignment = Math.abs(dir.dot(sheetDir));
893
+ return alignment < alignmentThreshold;
894
+ });
895
+ const candidates = notThicknessEdges.length ? notThicknessEdges : segments;
896
+ const sheetTagged = candidates.filter((seg) => seg.sheetFaceType === targetFaceType);
897
+ const adjacentMatches = !sheetTagged.length
898
+ ? candidates.filter((seg) => Array.isArray(seg.adjacentSheetFaceTypes)
899
+ && seg.adjacentSheetFaceTypes.includes(targetFaceType))
900
+ : sheetTagged;
901
+ const anySheetSegments = adjacentMatches.length
902
+ ? adjacentMatches
903
+ : candidates.filter((seg) => !!seg.sheetFaceType);
904
+ const pool = anySheetSegments.length ? anySheetSegments : candidates;
905
+ const targetValue = useOppositeEdge ? sheetSpan.max : sheetSpan.min;
906
+ const midPlane = (sheetSpan.min + sheetSpan.max) * 0.5;
907
+
908
+ let best = null;
909
+ let bestScore = Infinity;
910
+ for (const seg of pool) {
911
+ const mid = seg.start.clone().add(seg.end).multiplyScalar(0.5);
912
+ const value = mid.clone().sub(origin).dot(sheetDir);
913
+ const score = Math.abs(value - targetValue);
914
+ if (score < bestScore) {
915
+ bestScore = score;
916
+ best = {
917
+ start: seg.start.clone(),
918
+ end: seg.end.clone(),
919
+ target: value < midPlane ? "MIN" : "MAX",
920
+ };
921
+ }
922
+ }
923
+ return best;
924
+ }
925
+
926
+ function collectFaceEdgeSegments(face) {
927
+ const result = [];
928
+ const edges = Array.isArray(face?.edges) ? face.edges : [];
929
+ for (const edge of edges) {
930
+ const pts = extractEdgePolyline(edge);
931
+ if (pts.length < 2) continue;
932
+ const start = pts[0];
933
+ const end = pts[pts.length - 1];
934
+ const length = start.distanceTo(end);
935
+ const adjacency = classifyEdgeSheetMetalTypes(edge, face);
936
+ result.push({
937
+ start,
938
+ end,
939
+ length,
940
+ sourceEdge: edge,
941
+ sheetFaceType: adjacency.primaryType,
942
+ adjacentSheetFaceTypes: adjacency.adjacentTypes,
943
+ });
944
+ }
945
+ return result;
946
+ }
947
+
948
+ function extractEdgePolyline(edge) {
949
+ const pts = [];
950
+ if (!edge) return pts;
951
+ const tmp = new BREP.THREE.Vector3();
952
+ const local = Array.isArray(edge?.userData?.polylineLocal) ? edge.userData.polylineLocal : null;
953
+ const isWorld = !!edge?.userData?.polylineWorld;
954
+ if (local && local.length >= 2) {
955
+ for (const pt of local) {
956
+ if (isWorld) {
957
+ pts.push(new BREP.THREE.Vector3(pt[0], pt[1], pt[2]));
958
+ } else {
959
+ tmp.set(pt[0], pt[1], pt[2]).applyMatrix4(edge.matrixWorld);
960
+ pts.push(tmp.clone());
961
+ }
962
+ }
963
+ return pts;
964
+ }
965
+
966
+ const pos = edge?.geometry?.getAttribute?.("position");
967
+ if (pos && pos.itemSize === 3 && pos.count >= 2) {
968
+ for (let i = 0; i < pos.count; i++) {
969
+ tmp.set(pos.getX(i), pos.getY(i), pos.getZ(i)).applyMatrix4(edge.matrixWorld);
970
+ pts.push(tmp.clone());
971
+ }
972
+ }
973
+ return pts;
974
+ }
975
+
976
+ function orientSheetDir(face, sheetDir, originFallback) {
977
+ const origin = originFallback || computeFaceCenter(face) || new BREP.THREE.Vector3();
978
+ const neighbors = new Set();
979
+ for (const edge of face?.edges || []) {
980
+ if (!edge?.faces) continue;
981
+ for (const neighbor of edge.faces) {
982
+ if (neighbor && neighbor !== face) neighbors.add(neighbor);
983
+ }
984
+ }
985
+ for (const neighbor of neighbors) {
986
+ const normal = typeof neighbor.getAverageNormal === "function"
987
+ ? neighbor.getAverageNormal().clone()
988
+ : null;
989
+ if (!normal || normal.lengthSq() < 1e-10) continue;
990
+ normal.normalize();
991
+ const alignment = Math.abs(normal.dot(sheetDir));
992
+ if (alignment > 0.9) {
993
+ const neighborCenter = computeFaceCenter(neighbor);
994
+ if (!neighborCenter) continue;
995
+ const toNeighbor = neighborCenter.clone().sub(origin);
996
+ if (toNeighbor.dot(sheetDir) < 0) {
997
+ sheetDir.multiplyScalar(-1);
998
+ }
999
+ break;
1000
+ }
1001
+ }
1002
+ return sheetDir;
1003
+ }
1004
+
1005
+ function computeFaceCenter(face) {
1006
+ try {
1007
+ const pos = face?.geometry?.getAttribute?.("position");
1008
+ if (pos && pos.count >= 1) {
1009
+ const v = new BREP.THREE.Vector3();
1010
+ const center = new BREP.THREE.Vector3();
1011
+ for (let i = 0; i < pos.count; i++) {
1012
+ v.set(pos.getX(i), pos.getY(i), pos.getZ(i)).applyMatrix4(face.matrixWorld);
1013
+ center.add(v);
1014
+ }
1015
+ return center.multiplyScalar(1 / pos.count);
1016
+ }
1017
+ } catch { /* ignore */ }
1018
+ const loops = Array.isArray(face?.userData?.boundaryLoopsWorld) ? face.userData.boundaryLoopsWorld : null;
1019
+ const loop = loops?.find((l) => Array.isArray(l?.pts) && l.pts.length);
1020
+ if (loop) {
1021
+ const center = new BREP.THREE.Vector3();
1022
+ let count = 0;
1023
+ for (const pt of loop.pts) {
1024
+ center.add(new BREP.THREE.Vector3(pt[0], pt[1], pt[2]));
1025
+ count++;
1026
+ }
1027
+ if (count) {
1028
+ return center.multiplyScalar(1 / count);
1029
+ }
1030
+ }
1031
+ return null;
1032
+ }
1033
+
1034
+ function collectNeighborFaces(face) {
1035
+ const neighbors = new Set();
1036
+ for (const edge of face?.edges || []) {
1037
+ if (!edge?.faces) continue;
1038
+ for (const neighbor of edge.faces) {
1039
+ if (neighbor && neighbor !== face) neighbors.add(neighbor);
1040
+ }
1041
+ }
1042
+ return Array.from(neighbors);
1043
+ }
1044
+
1045
+ function resolveABOrientation(face, context) {
1046
+ try {
1047
+ const THREE = BREP.THREE;
1048
+ const origin = context?.origin?.clone?.() || computeFaceCenter(face) || new THREE.Vector3();
1049
+ const neighbors = collectNeighborFaces(face);
1050
+ let centerA = null;
1051
+ let centerB = null;
1052
+ let countA = 0;
1053
+ let countB = 0;
1054
+ let normalA = null;
1055
+ let normalB = null;
1056
+ const accumulate = (acc, vec) => {
1057
+ if (!vec || typeof vec.clone !== "function") return acc;
1058
+ const clone = vec.clone();
1059
+ if (clone.lengthSq && clone.lengthSq() < 1e-12) return acc;
1060
+ return acc ? acc.add(clone) : clone;
1061
+ };
1062
+ for (const neighbor of neighbors) {
1063
+ const type = resolveSMFaceType(neighbor);
1064
+ if (type !== SHEET_METAL_FACE_TYPES.A && type !== SHEET_METAL_FACE_TYPES.B) continue;
1065
+ const center = computeFaceCenter(neighbor);
1066
+ const normal = typeof neighbor.getAverageNormal === "function"
1067
+ ? neighbor.getAverageNormal().clone()
1068
+ : null;
1069
+ if (type === SHEET_METAL_FACE_TYPES.A) {
1070
+ if (center) { centerA = accumulate(centerA, center); countA++; }
1071
+ if (normal) normalA = accumulate(normalA, normal);
1072
+ } else if (type === SHEET_METAL_FACE_TYPES.B) {
1073
+ if (center) { centerB = accumulate(centerB, center); countB++; }
1074
+ if (normal) normalB = accumulate(normalB, normal);
1075
+ }
1076
+ }
1077
+ const average = (vec, count) => (vec && count ? vec.multiplyScalar(1 / count) : null);
1078
+ centerA = average(centerA, countA);
1079
+ centerB = average(centerB, countB);
1080
+ if (normalA && normalA.lengthSq() > 1e-12) normalA.normalize(); else normalA = null;
1081
+ if (normalB && normalB.lengthSq() > 1e-12) normalB.normalize(); else normalB = null;
1082
+
1083
+ let dir = null;
1084
+ if (centerA && centerB) {
1085
+ dir = centerA.clone().sub(centerB);
1086
+ } else if (centerA && normalA) {
1087
+ dir = normalA.clone();
1088
+ } else if (centerB && normalB) {
1089
+ dir = normalB.clone().multiplyScalar(-1);
1090
+ }
1091
+ if (dir && dir.lengthSq() > 1e-12) {
1092
+ dir.normalize();
1093
+ } else {
1094
+ dir = null;
1095
+ }
1096
+
1097
+ if (!dir && !centerA && !centerB) return null;
1098
+ return {
1099
+ dir,
1100
+ origin,
1101
+ hasA: countA > 0,
1102
+ hasB: countB > 0,
1103
+ };
1104
+ } catch {
1105
+ return null;
1106
+ }
1107
+ }
1108
+
1109
+ function buildAxisEdge(start, end, featureID) {
1110
+ const geom = new BREP.THREE.BufferGeometry();
1111
+ const positions = new Float32Array([
1112
+ start.x, start.y, start.z,
1113
+ end.x, end.y, end.z,
1114
+ ]);
1115
+ geom.setAttribute("position", new BREP.THREE.BufferAttribute(positions, 3));
1116
+ const edge = new BREP.Edge(geom);
1117
+ edge.name = featureID ? `${featureID}:AXIS` : "SM.FLANGE_AXIS";
1118
+ edge.userData = {
1119
+ polylineLocal: [
1120
+ [start.x, start.y, start.z],
1121
+ [end.x, end.y, end.z],
1122
+ ],
1123
+ polylineWorld: true,
1124
+ };
1125
+ edge.matrixWorld = new BREP.THREE.Matrix4();
1126
+ edge.updateWorldMatrix = () => { };
1127
+ return edge;
1128
+ }
1129
+
1130
+ function classifyEdgeSheetMetalTypes(edge, sourceFace) {
1131
+ const adjacentTypes = new Set();
1132
+ const neighbors = Array.isArray(edge?.faces) ? edge.faces : [];
1133
+ for (const neighbor of neighbors) {
1134
+ if (!neighbor || neighbor === sourceFace) continue;
1135
+ const type = resolveSMFaceType(neighbor);
1136
+ if (type) adjacentTypes.add(type);
1137
+ }
1138
+
1139
+ const hasA = adjacentTypes.has(SHEET_METAL_FACE_TYPES.A);
1140
+ const hasB = adjacentTypes.has(SHEET_METAL_FACE_TYPES.B);
1141
+ let primaryType = null;
1142
+ if (hasA && !hasB) {
1143
+ primaryType = SHEET_METAL_FACE_TYPES.A;
1144
+ } else if (hasB && !hasA) {
1145
+ primaryType = SHEET_METAL_FACE_TYPES.B;
1146
+ }
1147
+
1148
+ return {
1149
+ primaryType,
1150
+ adjacentTypes: Array.from(adjacentTypes),
1151
+ };
1152
+ }
1153
+
1154
+ function getParentState(stateMap, parentSolid) {
1155
+ if (!parentSolid || !stateMap) return null;
1156
+ let state = stateMap.get(parentSolid);
1157
+ if (!state) {
1158
+ state = { original: parentSolid, target: parentSolid, solids: [] };
1159
+ stateMap.set(parentSolid, state);
1160
+ }
1161
+ return state;
1162
+ }
1163
+
1164
+ function pickReplacementSolid(addedList) {
1165
+ if (!Array.isArray(addedList) || !addedList.length) return null;
1166
+ for (const solid of addedList) {
1167
+ if (solid) return solid;
1168
+ }
1169
+ return null;
1170
+ }
1171
+
1172
+ function applyTranslationToSolid(solid, vector) {
1173
+ if (!solid || !vector || typeof vector.x !== "number" || typeof vector.y !== "number" || typeof vector.z !== "number") {
1174
+ return;
1175
+ }
1176
+ try {
1177
+ if (typeof solid.bakeTransform === "function") {
1178
+ const translation = new BREP.THREE.Matrix4().makeTranslation(vector.x, vector.y, vector.z);
1179
+ solid.bakeTransform(translation);
1180
+ return;
1181
+ }
1182
+ } catch { /* fallthrough to try Object3D translation */ }
1183
+ try {
1184
+ if (typeof solid.applyMatrix4 === "function") {
1185
+ const translation = new BREP.THREE.Matrix4().makeTranslation(vector.x, vector.y, vector.z);
1186
+ solid.applyMatrix4(translation);
1187
+ return;
1188
+ }
1189
+ } catch { /* ignore */ }
1190
+ try {
1191
+ if (solid.position && typeof solid.position.add === "function") {
1192
+ solid.position.add(vector);
1193
+ }
1194
+ } catch { /* ignore */ }
1195
+ }
1196
+
1197
+ function buildOffsetTranslationVector(baseNormal, offsetValue) {
1198
+ if (!Number.isFinite(offsetValue) || offsetValue === 0) return null;
1199
+ const THREE = BREP.THREE;
1200
+ const normal = (baseNormal && typeof baseNormal.clone === "function" && baseNormal.lengthSq() > 1e-12)
1201
+ ? baseNormal.clone()
1202
+ : new THREE.Vector3(0, 0, 1);
1203
+ if (!normal || normal.lengthSq() < 1e-12) return null;
1204
+ normal.normalize();
1205
+ const vector = normal.multiplyScalar(-offsetValue);
1206
+ if (vector.lengthSq() < 1e-18) return null;
1207
+ return vector;
1208
+ }
1209
+
1210
+ function findRevolveEndFace(revolveSolid) {
1211
+ if (!revolveSolid || !Array.isArray(revolveSolid.faces)) return null;
1212
+ for (const face of revolveSolid.faces) {
1213
+ const meta = typeof face.getMetadata === "function" ? face.getMetadata() : null;
1214
+ if (meta?.faceType === "ENDCAP") return face;
1215
+ }
1216
+ return revolveSolid.faces[revolveSolid.faces.length - 1] || null;
1217
+ }
1218
+
1219
+ function createOffsetExtrudeSolid(params = {}) {
1220
+ const {
1221
+ face,
1222
+ faceNormal,
1223
+ lengthValue,
1224
+ featureID,
1225
+ faceIndex,
1226
+ applyReliefPush = true,
1227
+ reliefPushDistance = null,
1228
+ reliefPushNormal = null,
1229
+ } = params;
1230
+ if (!face || !Number.isFinite(lengthValue) || lengthValue === 0) return null;
1231
+ const THREE = BREP.THREE;
1232
+ const normal = (faceNormal && typeof faceNormal.clone === "function" && faceNormal.lengthSq() > 1e-12)
1233
+ ? faceNormal.clone()
1234
+ : (typeof face?.getAverageNormal === "function"
1235
+ ? face.getAverageNormal().clone()
1236
+ : new THREE.Vector3(0, 0, 1));
1237
+ if (!normal || normal.lengthSq() < 1e-12) return null;
1238
+ normal.normalize();
1239
+
1240
+ // This is working correctly. Don't change how it inverts the lengthValue.
1241
+ const distance = normal.multiplyScalar(-lengthValue);
1242
+ if (distance.lengthSq() < 1e-18) return null;
1243
+ const suffix = Number.isFinite(faceIndex) ? `:${faceIndex}` : "";
1244
+ const sweep = new BREP.Sweep({
1245
+ face,
1246
+ distance,
1247
+ mode: "translate",
1248
+ name: featureID ? `${featureID}:OFFSET${suffix}` : "SM.FLANGE_OFFSET",
1249
+ omitBaseCap: false,
1250
+ });
1251
+ sweep.visualize();
1252
+
1253
+ applyFaceSheetMetalData(face, sweep);
1254
+
1255
+ const tinyPush = Number.isFinite(reliefPushDistance)
1256
+ ? Math.max(0, reliefPushDistance)
1257
+ : 0;
1258
+ let pushNormal = null;
1259
+ if (reliefPushNormal && typeof reliefPushNormal.clone === "function") {
1260
+ pushNormal = reliefPushNormal.clone();
1261
+ } else if (reliefPushNormal && Number.isFinite(reliefPushNormal.x)) {
1262
+ pushNormal = new THREE.Vector3(reliefPushNormal.x, reliefPushNormal.y, reliefPushNormal.z);
1263
+ }
1264
+ if (pushNormal && pushNormal.lengthSq() > 1e-12) pushNormal.normalize();
1265
+ else pushNormal = null;
1266
+
1267
+ // use the solid.pushFace() method to nudge the A/B faces outward by a tiny amount to avoid z-fighting
1268
+ if (applyReliefPush && 0 > lengthValue && tinyPush > 0) {
1269
+ for (const solidFace of sweep.faces) {
1270
+ const faceMetadata = solidFace.getMetadata();
1271
+ if (faceMetadata?.faceType === "STARTCAP" || faceMetadata?.faceType === "ENDCAP") continue;
1272
+ const sheetType = faceMetadata?.sheetMetalFaceType;
1273
+ let shouldPush = sheetType === SHEET_METAL_FACE_TYPES.A || sheetType === SHEET_METAL_FACE_TYPES.B;
1274
+ if (!shouldPush && pushNormal && typeof solidFace.getAverageNormal === "function") {
1275
+ const faceNormal = solidFace.getAverageNormal();
1276
+ if (faceNormal && faceNormal.lengthSq() > 1e-12) {
1277
+ faceNormal.normalize();
1278
+ const align = Math.abs(faceNormal.dot(pushNormal));
1279
+ if (align > 0.95) shouldPush = true;
1280
+ }
1281
+ }
1282
+ if (!shouldPush) continue;
1283
+ sweep.pushFace(solidFace.name, tinyPush);
1284
+ }
1285
+ }
1286
+ return sweep;
1287
+ }
1288
+
1289
+ function applyCylMetadataToRevolve(revolve, axisEdge, radiusValue, baseNormal, offsetVector = null) {
1290
+ if (!revolve || !Array.isArray(revolve.faces) || !axisEdge) return;
1291
+ const THREE = BREP.THREE;
1292
+ try {
1293
+ const posAttr = axisEdge?.geometry?.getAttribute?.("position");
1294
+ const mat = axisEdge.matrixWorld || new THREE.Matrix4();
1295
+ const A = new THREE.Vector3(0, 0, 0);
1296
+ const B = new THREE.Vector3(0, 1, 0);
1297
+ if (posAttr && posAttr.count >= 2) {
1298
+ A.set(posAttr.getX(0), posAttr.getY(0), posAttr.getZ(0)).applyMatrix4(mat);
1299
+ B.set(posAttr.getX(posAttr.count - 1), posAttr.getY(posAttr.count - 1), posAttr.getZ(posAttr.count - 1)).applyMatrix4(mat);
1300
+ }
1301
+ if (offsetVector && offsetVector.x !== undefined) {
1302
+ A.add(offsetVector);
1303
+ B.add(offsetVector);
1304
+ }
1305
+ const axisDir = B.clone().sub(A);
1306
+ const height = axisDir.length();
1307
+ if (height < 1e-9) return;
1308
+ axisDir.normalize();
1309
+ const center = A.clone().addScaledVector(axisDir, height * 0.5);
1310
+
1311
+ // Fit radius/center per side face from geometry to avoid relying on input radius alone.
1312
+ const axisOrigin = A.clone();
1313
+ const tmp = new THREE.Vector3();
1314
+ for (const face of revolve.faces) {
1315
+ const meta = face.getMetadata?.() || {};
1316
+ if (meta.faceType && meta.faceType !== "SIDEWALL") continue;
1317
+ const pos = face.geometry?.getAttribute?.("position");
1318
+ if (!pos || pos.itemSize !== 3 || pos.count < 3) continue;
1319
+ let projMin = Infinity;
1320
+ let projMax = -Infinity;
1321
+ let sumRadius = 0;
1322
+ for (let i = 0; i < pos.count; i++) {
1323
+ tmp.set(pos.getX(i), pos.getY(i), pos.getZ(i)).applyMatrix4(face.matrixWorld);
1324
+ const t = tmp.clone().sub(axisOrigin).dot(axisDir);
1325
+ if (t < projMin) projMin = t;
1326
+ if (t > projMax) projMax = t;
1327
+ const proj = axisOrigin.clone().add(axisDir.clone().multiplyScalar(t));
1328
+ sumRadius += tmp.distanceTo(proj);
1329
+ }
1330
+ const fitRadius = sumRadius / pos.count;
1331
+ const midT = (projMin + projMax) * 0.5;
1332
+ const fitCenter = axisOrigin.clone().add(axisDir.clone().multiplyScalar(midT));
1333
+ // Keep a slight preference for the intended radius but store the fit value so PMI reads geometry.
1334
+ const radius = Number.isFinite(fitRadius) && fitRadius > 1e-6 ? fitRadius : radiusValue;
1335
+ revolve.setFaceMetadata(face.name, {
1336
+ type: "cylindrical",
1337
+ radius,
1338
+ height: projMax - projMin,
1339
+ axis: [axisDir.x, axisDir.y, axisDir.z],
1340
+ center: [fitCenter.x, fitCenter.y, fitCenter.z],
1341
+ pmiRadiusOverride: radius,
1342
+ });
1343
+ }
1344
+ } catch { /* ignore cyl metadata errors */ }
1345
+ }
1346
+
1347
+ function setSolidNameSafe(solid, name) {
1348
+ try {
1349
+ if (solid && name && typeof name === "string" && name.length) {
1350
+ solid.name = name;
1351
+ }
1352
+ } catch { /* ignore naming errors */ }
1353
+ }
1354
+
1355
+ function applyFaceSheetMetalData(inputFace, inputSolid) {
1356
+ //console.log(inputFace, inputFace.getMetadata());
1357
+ const inputFaceMetadata = inputFace.getMetadata();
1358
+ //console.log(inputSolid.visualize());
1359
+ inputSolid.visualize();
1360
+
1361
+
1362
+
1363
+ // extract all the faces of the input solid
1364
+ for (const solidFace of inputSolid.faces) {
1365
+ const faceMetadata = solidFace.getMetadata();
1366
+ //console.log("Comparing Solid Face:", solidFace.name, "with Input Face:", inputFace.name);
1367
+ if (faceMetadata.faceType == "STARTCAP" || faceMetadata.faceType == "ENDCAP") {
1368
+ solidFace.setMetadata(inputFaceMetadata);
1369
+ continue;
1370
+ }
1371
+
1372
+
1373
+ solidFace.setMetadata({ sheetMetalFaceType: "THICKNESS" });
1374
+
1375
+ }
1376
+
1377
+
1378
+
1379
+ // loop over each edge of the input face
1380
+ for (const edge of inputFace.edges) {
1381
+ const edgeMetadata = edge.getMetadata();
1382
+ //console.log("Input Face Edge Metadata:", edge.name, edgeMetadata);
1383
+
1384
+ // copy over the metadata from the input face edge to all edges in the solid that have a name that starts with the input edge name
1385
+ for (const solidFace of inputSolid.faces) {
1386
+ // look at the sourceEdgeName metadata for each face of the solid. Compare the faces to the current edge name
1387
+ if (solidFace.getMetadata()?.sourceEdgeName === edge.name) {
1388
+ //console.log("Matching Solid Face Edge found:", solidFace.name, "for Input Edge:", edge.name);
1389
+
1390
+ if (edgeMetadata?.sheetMetalEdgeType) {
1391
+ solidFace.setMetadata({ sheetMetalFaceType: edgeMetadata?.sheetMetalEdgeType });
1392
+ }
1393
+ }
1394
+ }
1395
+ }
1396
+
1397
+ }
1398
+
1399
+ function buildFlangeRevolve({
1400
+ face,
1401
+ context,
1402
+ hingeEdge,
1403
+ appliedAngle,
1404
+ bendRadiusUsed,
1405
+ thickness,
1406
+ offsetVector,
1407
+ featureID,
1408
+ offsetMagnitudeOverride = null,
1409
+ }) {
1410
+ //console.log(appliedAngle);
1411
+ if (!hingeEdge?.start || !hingeEdge?.end || !context) return null;
1412
+ const hingeDir = hingeEdge.end.clone().sub(hingeEdge.start).normalize();
1413
+ let sheetDir = new BREP.THREE.Vector3().crossVectors(context.baseNormal, hingeDir);
1414
+ if (sheetDir.lengthSq() < 1e-10) {
1415
+ sheetDir = context.sheetDir.clone();
1416
+ }
1417
+ sheetDir.normalize();
1418
+ const offsetSign = hingeEdge.target === "MIN" ? 1 : -1;
1419
+ const offsetMagnitude = Number.isFinite(offsetMagnitudeOverride) && offsetMagnitudeOverride >= 0
1420
+ ? offsetMagnitudeOverride
1421
+ : bendRadiusUsed + thickness;
1422
+ const offsetVec = sheetDir.clone().multiplyScalar(offsetSign * offsetMagnitude);
1423
+ const axisEdge = buildAxisEdge(
1424
+ hingeEdge.start.clone().add(offsetVec),
1425
+ hingeEdge.end.clone().add(offsetVec),
1426
+ featureID,
1427
+ );
1428
+
1429
+ const revolve = new BREP.Revolve({
1430
+ face,
1431
+ axis: axisEdge,
1432
+ angle: appliedAngle,
1433
+ resolution: 128,
1434
+ name: featureID ? `${featureID}:BEND` : "SM.FLANGE_BEND",
1435
+ }).visualize();
1436
+ const bendEndFace = findRevolveEndFace(revolve);
1437
+
1438
+ applyFaceSheetMetalData(face, revolve);
1439
+ if (offsetVector) {
1440
+ applyTranslationToSolid(revolve, offsetVector);
1441
+ }
1442
+ applyCylMetadataToRevolve(
1443
+ revolve,
1444
+ axisEdge,
1445
+ bendRadiusUsed + thickness,
1446
+ context.baseNormal,
1447
+ offsetVector,
1448
+ );
1449
+ revolve.visualize();
1450
+
1451
+ const measuredRadius = measureSmallestCylRadius(revolve);
1452
+
1453
+ return { revolve, bendEndFace, hingeEdge, measuredRadius };
1454
+ }
1455
+
1456
+ function measureSmallestCylRadius(solid) {
1457
+ if (!solid || !Array.isArray(solid.faces)) return null;
1458
+ let minRadius = null;
1459
+ for (const face of solid.faces) {
1460
+ const meta = face.getMetadata?.() || {};
1461
+ const r = Number(meta.radius ?? meta.pmiRadiusOverride ?? meta.pmiRadius ?? meta.sheetMetalRadius);
1462
+ if (Number.isFinite(r) && r > 0) {
1463
+ if (minRadius == null || r < minRadius) minRadius = r;
1464
+ }
1465
+ }
1466
+ return minRadius;
1467
+ }
1468
+
1469
+ function radiusError(measured, target) {
1470
+ if (!Number.isFinite(target)) return Infinity;
1471
+ if (!Number.isFinite(measured)) return Infinity;
1472
+ return Math.abs(measured - target);
1473
+ }
1474
+
1475
+ function evaluateFlangeCandidate({
1476
+ raw,
1477
+ targetRadius,
1478
+ desiredBendSide = null,
1479
+ orientationInfo = null,
1480
+ }) {
1481
+ if (!raw?.revolve) return null;
1482
+ const result = {
1483
+ ...raw,
1484
+ radiusErr: radiusError(raw.measuredRadius, targetRadius),
1485
+ orientationPenalty: 0,
1486
+ bendSide: null,
1487
+ };
1488
+ const bendOrientation = determineBendSide(raw, orientationInfo);
1489
+ if (bendOrientation?.side) {
1490
+ result.bendSide = bendOrientation.side;
1491
+ if (desiredBendSide) {
1492
+ result.orientationPenalty = bendOrientation.side === desiredBendSide ? 0 : 1;
1493
+ }
1494
+ }
1495
+ return result;
1496
+ }
1497
+
1498
+ function determineBendSide(candidate, orientationInfo) {
1499
+ const dir = orientationInfo?.dir;
1500
+ const origin = orientationInfo?.origin;
1501
+ if (!candidate?.bendEndFace || !dir || typeof dir.dot !== "function" || dir.lengthSq() < 1e-12) {
1502
+ return null;
1503
+ }
1504
+ const center = computeFaceCenter(candidate.bendEndFace);
1505
+ if (!center || !origin) return null;
1506
+ const offset = center.clone().sub(origin);
1507
+ const dot = offset.dot(dir);
1508
+ if (!Number.isFinite(dot) || Math.abs(dot) < 1e-9) return null;
1509
+ const side = dot >= 0 ? SHEET_METAL_FACE_TYPES.A : SHEET_METAL_FACE_TYPES.B;
1510
+ return { side, alignment: Math.abs(dot) };
1511
+ }
1512
+
1513
+ function pickBetterFlangeCandidate(current, candidate) {
1514
+ if (!candidate?.revolve) return current;
1515
+ if (!current?.revolve) return candidate;
1516
+ if (candidate.orientationPenalty !== current.orientationPenalty) {
1517
+ return candidate.orientationPenalty < current.orientationPenalty ? candidate : current;
1518
+ }
1519
+ const currErr = current.radiusErr ?? Infinity;
1520
+ const candErr = candidate.radiusErr ?? Infinity;
1521
+ return candErr + 1e-9 < currErr ? candidate : current;
1522
+ }
1523
+
1524
+ function combineSolids(params = {}) {
1525
+ const {
1526
+ solids,
1527
+ featureID,
1528
+ groupIndex = 0,
1529
+ } = params;
1530
+ if (!Array.isArray(solids) || solids.length === 0) return null;
1531
+ let combined = null;
1532
+ for (const solid of solids) {
1533
+ if (!solid) continue;
1534
+ if (!combined) {
1535
+ combined = solid;
1536
+ continue;
1537
+ }
1538
+ let merged = null;
1539
+ try {
1540
+ merged = combined.union(solid);
1541
+ } catch {
1542
+ try {
1543
+ merged = solid.union(combined);
1544
+ } catch {
1545
+ merged = null;
1546
+ }
1547
+ }
1548
+ if (!merged) return null;
1549
+ combined = merged;
1550
+ }
1551
+ if (!combined) return null;
1552
+ try {
1553
+ const suffix = Number.isFinite(groupIndex) ? `_${groupIndex}` : "";
1554
+ combined.name = featureID
1555
+ ? `${featureID}:BENDS${suffix}`
1556
+ : combined.name || `SM.FLANGE_BENDS${suffix}`;
1557
+ } catch { /* optional */ }
1558
+ try { combined.visualize(); } catch { }
1559
+ return combined;
1560
+ }
1561
+
1562
+ function solidsMatch(a, b) {
1563
+ if (!a || !b) return false;
1564
+ if (a === b) return true;
1565
+ if (a.uuid && b.uuid && a.uuid === b.uuid) return true;
1566
+ if (a.name && b.name && a.name === b.name) return true;
1567
+ return false;
1568
+ }