brep-io-kernel 1.0.0-ci.10

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 +157 -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,1056 @@
1
+ import { BREP } from "../../BREP/BREP.js";
2
+ import {
3
+ SHEET_METAL_FACE_TYPES,
4
+ setSheetMetalFaceTypeMetadata,
5
+ propagateSheetMetalFaceTypesToEdges,
6
+ } from "./sheetMetalFaceTypes.js";
7
+ import { applySheetMetalMetadata } from "./sheetMetalMetadata.js";
8
+ import { selectionHasSketch } from "../selectionUtils.js";
9
+ import { resolveProfileFace, collectSketchParents } from "./profileUtils.js";
10
+ import { cleanupSheetMetalOppositeEdgeFaces } from "./sheetMetalCleanup.js";
11
+ import { SheetMetalObject } from "./SheetMetalObject.js";
12
+ import { cloneSheetMetalTree, createSheetMetalCutoutNode } from "./sheetMetalTree.js";
13
+ import { cloneProfileGroups, collectProfileEdges, buildFaceFromProfileGroups } from "./sheetMetalProfileUtils.js";
14
+
15
+ const inputParamsSchema = {
16
+ id: {
17
+ type: "string",
18
+ default_value: null,
19
+ hint: "Unique identifier for the sheet metal cutout",
20
+ },
21
+ sheet: {
22
+ type: "reference_selection",
23
+ selectionFilter: ["SOLID"],
24
+ multiple: false,
25
+ default_value: null,
26
+ hint: "Target sheet metal solid to cut",
27
+ },
28
+ profile: {
29
+ type: "reference_selection",
30
+ selectionFilter: ["SOLID", "FACE", "SKETCH",],
31
+ multiple: false,
32
+ default_value: null,
33
+ hint: "Solid tool or sketch/face to extrude as a cutting tool.",
34
+ },
35
+ consumeProfileSketch: {
36
+ type: "boolean",
37
+ default_value: true,
38
+ hint: "Remove the referenced sketch after creating the cutout. Turn off to keep it in the scene.",
39
+ },
40
+ forwardDistance: {
41
+ type: "number",
42
+ default_value: 1,
43
+ min: 0,
44
+ hint: "Extrude distance forward from the profile (sketch/face only).",
45
+ },
46
+ backDistance: {
47
+ type: "number",
48
+ default_value: 0,
49
+ min: 0,
50
+ hint: "Extrude distance backward from the profile (sketch/face only).",
51
+ },
52
+ keepTool: {
53
+ type: "boolean",
54
+ default_value: false,
55
+ hint: "Keep the generated cutting tool in the scene (for debugging).",
56
+ },
57
+ debugCutter: {
58
+ type: "boolean",
59
+ default_value: false,
60
+ hint: "Keep the internal cleanup cutter used for the final subtract.",
61
+ },
62
+ };
63
+
64
+ export class SheetMetalCutoutFeature {
65
+ static shortName = "SM.CUTOUT";
66
+ static longName = "Sheet Metal Cutout";
67
+ static inputParamsSchema = inputParamsSchema;
68
+
69
+ constructor() {
70
+ this.inputParams = {};
71
+ this.persistentData = {};
72
+ this.debugTool = null;
73
+ }
74
+
75
+ uiFieldsTest(context) {
76
+ const params = this.inputParams || context?.params || {};
77
+ const partHistory = context?.history || null;
78
+ const profileRef = firstSelection(params?.profile);
79
+ const resolvedProfile = resolveProfileFace(profileRef, partHistory) || profileRef;
80
+ const profileType = resolvedProfile?.type;
81
+ const allowDistances = (profileType === "FACE" || profileType === "SKETCH");
82
+ const hide = allowDistances ? [] : ["forwardDistance", "backDistance"];
83
+ if (!selectionHasSketch(params?.profile, partHistory)) hide.push("consumeProfileSketch");
84
+ return hide;
85
+ }
86
+
87
+ async run(partHistory) {
88
+ const scene = partHistory?.scene;
89
+ const metadataManager = partHistory?.metadataManager;
90
+ this.debugTool = null;
91
+ const sheetRef = firstSelection(this.inputParams?.sheet);
92
+ let sheetSolid = resolveSolidRef(sheetRef, scene);
93
+
94
+ if (!sheetSolid) {
95
+ sheetSolid = findFirstSheetMetalSolid(scene, metadataManager);
96
+ }
97
+
98
+ if (!sheetSolid) throw new Error("Sheet Metal Cutout requires a valid sheet metal solid selection.");
99
+
100
+ const tree = sheetSolid?.userData?.sheetMetalTree
101
+ || sheetSolid?.userData?.sheetMetal?.tree
102
+ || null;
103
+
104
+ if (!tree) {
105
+ const result = await buildSheetMetalCutoutSolids({
106
+ params: this.inputParams,
107
+ partHistory,
108
+ sheetSolid,
109
+ applyMetadata: true,
110
+ });
111
+ try {
112
+ for (const obj of result.removed || []) {
113
+ if (obj) obj.__removeFlag = true;
114
+ }
115
+ } catch { /* ignore */ }
116
+ this.debugTool = result?.debugTool || null;
117
+ this.persistentData = result?.persistentData || {};
118
+ return { added: result.added || [], removed: result.removed || [] };
119
+ }
120
+
121
+ const profileSelection = firstSelection(this.inputParams?.profile);
122
+ const profileFace = resolveProfileFaceFromSolid(profileSelection, partHistory, sheetSolid);
123
+ const sketchParentsToRemove = profileFace ? collectSketchParents(profileFace) : [];
124
+ const profileGroups = cloneProfileGroups(profileFace);
125
+ const profileEdges = collectProfileEdges(profileFace);
126
+ const profileFaceName = profileFace?.name || null;
127
+
128
+ const baseMeta = sheetSolid?.userData?.sheetMetal || {};
129
+ const sheetMetal = new SheetMetalObject({
130
+ tree,
131
+ kFactor: baseMeta.neutralFactor ?? sheetSolid?.userData?.sheetMetalNeutralFactor ?? null,
132
+ thickness: baseMeta.thickness ?? sheetSolid?.userData?.sheetThickness ?? null,
133
+ bendRadius: baseMeta.bendRadius ?? sheetSolid?.userData?.sheetBendRadius ?? null,
134
+ });
135
+ const cutoutNode = createSheetMetalCutoutNode({
136
+ featureID: this.inputParams?.featureID || null,
137
+ sheetRef: this.inputParams?.sheet ?? null,
138
+ profileRef: this.inputParams?.profile ?? null,
139
+ profileFaceName,
140
+ profileGroups,
141
+ profileEdges,
142
+ consumeProfileSketch: this.inputParams?.consumeProfileSketch !== false,
143
+ forwardDistance: this.inputParams?.forwardDistance,
144
+ backDistance: this.inputParams?.backDistance,
145
+ keepTool: this.inputParams?.keepTool,
146
+ debugCutter: this.inputParams?.debugCutter,
147
+ });
148
+ sheetMetal.appendNode(cutoutNode);
149
+ await sheetMetal.generate({
150
+ partHistory,
151
+ metadataManager: partHistory?.metadataManager,
152
+ mode: "solid",
153
+ });
154
+ try { sheetMetal.name = sheetSolid?.name || sheetMetal.name; } catch { /* ignore */ }
155
+
156
+ const added = [sheetMetal];
157
+ const consumeSketch = this.inputParams?.consumeProfileSketch !== false;
158
+ let removed = [sheetSolid];
159
+ if (consumeSketch && sketchParentsToRemove.length) {
160
+ removed = removed.concat(sketchParentsToRemove);
161
+ }
162
+
163
+ try {
164
+ for (const obj of removed) {
165
+ if (obj) obj.__removeFlag = true;
166
+ }
167
+ } catch { /* ignore */ }
168
+
169
+ cleanupSheetMetalOppositeEdgeFaces(added);
170
+ propagateSheetMetalFaceTypesToEdges(added);
171
+ const sheetThickness = resolveSheetThickness(sheetSolid, metadataManager);
172
+ applySheetMetalMetadata(added, partHistory?.metadataManager, {
173
+ featureID: this.inputParams?.featureID || null,
174
+ thickness: sheetThickness,
175
+ baseType: sheetSolid?.userData?.sheetMetal?.baseType || null,
176
+ bendRadius: sheetSolid?.userData?.sheetMetal?.bendRadius ?? null,
177
+ extra: { sourceFeature: "CUTOUT", consumeProfileSketch: consumeSketch },
178
+ forceBaseOverwrite: false,
179
+ });
180
+
181
+ for (const solid of added) {
182
+ if (!solid) continue;
183
+ solid.userData = solid.userData || {};
184
+ solid.userData.sheetMetalTree = cloneSheetMetalTree(sheetMetal.tree);
185
+ solid.userData.sheetMetalKFactor = sheetMetal.kFactor ?? null;
186
+ }
187
+
188
+ this.persistentData = {
189
+ sheetName: sheetSolid?.name || null,
190
+ toolCount: 1,
191
+ sheetThickness,
192
+ footprintFaceTypes: null,
193
+ tree: sheetMetal.tree,
194
+ };
195
+
196
+ return { added, removed };
197
+ }
198
+ }
199
+
200
+ export async function buildSheetMetalCutoutSolids({
201
+ params,
202
+ partHistory,
203
+ sheetSolid = null,
204
+ applyMetadata = true,
205
+ } = {}) {
206
+ const scene = partHistory?.scene;
207
+ const metadataManager = partHistory?.metadataManager;
208
+ const sheetRef = firstSelection(params?.sheet);
209
+ let targetSheet = sheetSolid || resolveSolidRef(sheetRef, scene);
210
+
211
+ const tools = [];
212
+ const sketchParentsToRemove = [];
213
+ let debugTool = null;
214
+
215
+ const profileSelection = firstSelection(params?.profile);
216
+ const profileSolid = resolveProfileSolid(profileSelection, partHistory);
217
+ if (profileSolid && profileSolid.type === "SOLID") {
218
+ tools.push(profileSolid);
219
+ } else {
220
+ let profileFace = resolveProfileFaceFromSolid(profileSelection, partHistory, targetSheet);
221
+ if (!profileFace && params?.profileGroups) {
222
+ profileFace = buildFaceFromProfileGroups(
223
+ params.profileGroups,
224
+ params.profileName || params.profileRef || params.profile,
225
+ params.profileEdges,
226
+ );
227
+ }
228
+ if (profileFace) sketchParentsToRemove.push(...collectSketchParents(profileFace));
229
+ if (profileFace && profileFace.type === "FACE") {
230
+ const toolName = params?.featureID || "SM_CUTOUT_PROFILE";
231
+ const profileTool = buildToolFromProfile(profileFace, params, toolName);
232
+ if (profileTool) {
233
+ tools.push(profileTool);
234
+ debugTool = profileTool;
235
+ }
236
+ }
237
+ }
238
+
239
+ if (!targetSheet) {
240
+ targetSheet = findFirstSheetMetalSolid(scene, metadataManager);
241
+ }
242
+
243
+ if (!targetSheet) throw new Error("Sheet Metal Cutout requires a valid sheet metal solid selection.");
244
+ if (!tools.length) {
245
+ throw new Error("Sheet Metal Cutout needs a Profile selection (solid or face) to build the cutting tool.");
246
+ }
247
+
248
+ const toolUnion = unionSolids(tools);
249
+ if (!toolUnion) throw new Error("Failed to combine cutting tools for Sheet Metal Cutout.");
250
+
251
+ const sheetThickness = resolveSheetThickness(targetSheet, partHistory?.metadataManager);
252
+ if (!(sheetThickness > 0)) throw new Error("Sheet Metal Cutout could not resolve sheet metal thickness.");
253
+
254
+ let added = [];
255
+ let removed = [];
256
+ const keepCutter = params?.debugCutter === true;
257
+ let working = targetSheet;
258
+
259
+ if (toolUnion) {
260
+ try {
261
+ working = targetSheet.subtract(toolUnion);
262
+ working.visualize?.();
263
+ removed.push(targetSheet, toolUnion);
264
+ } catch (toolErr) {
265
+ console.warn("[SheetMetalCutout] Subtracting cutting tool failed, falling back to boolean", toolErr);
266
+ const effects = await BREP.applyBooleanOperation(
267
+ partHistory || {},
268
+ toolUnion,
269
+ { operation: "SUBTRACT", targets: [targetSheet] },
270
+ params?.featureID,
271
+ );
272
+ const candidate = Array.isArray(effects?.added) ? effects.added[0] : null;
273
+ if (!candidate) throw new Error("Sheet Metal Cutout could not subtract the cutting tool.");
274
+ candidate.visualize?.();
275
+ working = candidate;
276
+ removed.push(...(effects?.removed || []), targetSheet, toolUnion);
277
+ }
278
+ }
279
+
280
+ const sheetFaces = collectSheetFaces(working);
281
+ if (!sheetFaces.A.length && !sheetFaces.B.length) {
282
+ throw new Error("Sheet Metal Cutout could not find sheet metal A/B faces on the target. Ensure the target solid has sheet-metal metadata.");
283
+ }
284
+
285
+ const faceInfoMap = buildSheetFaceInfoMap(sheetFaces);
286
+ const faceTypeMap = buildFaceTypeMap(working);
287
+ let prisms = buildPrismsFromBoundaryLoops(working, faceInfoMap, faceTypeMap, sheetThickness, params?.featureID);
288
+
289
+ if (!prisms.length) {
290
+ const intersection = safeIntersect(targetSheet, toolUnion)
291
+ || await safeIntersectFallbackBoolean(partHistory, targetSheet, toolUnion, params?.featureID);
292
+ if (intersection) {
293
+ prisms = buildPrismsFromIntersection(intersection, targetSheet, sheetThickness, params?.featureID);
294
+ }
295
+ }
296
+
297
+ let cutPrism = null;
298
+ if (!prisms.length) {
299
+ console.warn("[SheetMetalCutout] Cleanup footprint not found; keeping direct subtract result.");
300
+ if (working) {
301
+ try { working.name = params?.featureID || working.name || targetSheet.name; } catch { /* ignore */ }
302
+ added = [working];
303
+ }
304
+ removed.push(...tools);
305
+ } else {
306
+ cutPrism = prisms[0];
307
+ for (let i = 1; i < prisms.length; i++) {
308
+ try { cutPrism = cutPrism.union(prisms[i]); } catch { cutPrism = prisms[i]; }
309
+ }
310
+ if (params?.featureID && cutPrism) {
311
+ try { cutPrism.name = `${params.featureID}_CUTOUT_CUTTER`; } catch { /* best effort */ }
312
+ }
313
+
314
+ try {
315
+ const finalCut = targetSheet.subtract(cutPrism);
316
+ finalCut.visualize?.();
317
+ try { finalCut.name = targetSheet?.name || finalCut.name; } catch { /* ignore */ }
318
+ added = [finalCut];
319
+ removed.push(targetSheet, working, cutPrism, ...tools);
320
+ } catch (directErr) {
321
+ console.warn("[SheetMetalCutout] Subtracting cleanup prisms failed, falling back to boolean", directErr);
322
+ const effects = await BREP.applyBooleanOperation(
323
+ partHistory || {},
324
+ cutPrism,
325
+ { operation: "SUBTRACT", targets: [targetSheet] },
326
+ params?.featureID,
327
+ );
328
+ const candidate = Array.isArray(effects?.added) ? effects.added[0] : null;
329
+ if (!candidate) throw new Error("Sheet Metal Cutout could not complete the cleanup cut.");
330
+ try { candidate.name = targetSheet?.name || candidate.name; } catch { /* ignore */ }
331
+ removed.push(...(effects?.removed || []), targetSheet, working, cutPrism, ...tools);
332
+ added = effects?.added || [];
333
+ }
334
+ }
335
+
336
+ if (keepCutter && cutPrism) {
337
+ added.push(cutPrism);
338
+ removed = removed.filter((o) => o !== cutPrism);
339
+ try { cutPrism.visualize?.(); } catch { /* ignore */ }
340
+ }
341
+
342
+ const consumeSketch = params?.consumeProfileSketch !== false;
343
+ if (consumeSketch && sketchParentsToRemove.length) {
344
+ removed.push(...sketchParentsToRemove);
345
+ }
346
+
347
+ const keepTool = params?.keepTool === true;
348
+ if (keepTool && tools?.length) {
349
+ removed = removed.filter((o) => !tools.includes(o) && o !== toolUnion);
350
+ }
351
+
352
+ cleanupSheetMetalOppositeEdgeFaces(added);
353
+ propagateSheetMetalFaceTypesToEdges(added);
354
+ if (applyMetadata) {
355
+ applySheetMetalMetadata(added, partHistory?.metadataManager, {
356
+ featureID: params?.featureID || null,
357
+ thickness: sheetThickness,
358
+ baseType: targetSheet?.userData?.sheetMetal?.baseType || null,
359
+ bendRadius: targetSheet?.userData?.sheetMetal?.bendRadius ?? null,
360
+ extra: { sourceFeature: "CUTOUT", consumeProfileSketch: consumeSketch },
361
+ forceBaseOverwrite: false,
362
+ });
363
+ }
364
+
365
+ if (params?.keepTool && debugTool) {
366
+ added.push(debugTool);
367
+ try { debugTool.visualize?.(); } catch { /* ignore */ }
368
+ }
369
+
370
+ const persistentData = {
371
+ sheetName: targetSheet?.name || null,
372
+ toolCount: tools.length,
373
+ sheetThickness,
374
+ footprintFaceTypes: {
375
+ A: sheetFaces.A.length,
376
+ B: sheetFaces.B.length,
377
+ },
378
+ };
379
+
380
+ return { added, removed, persistentData, debugTool };
381
+ }
382
+
383
+ function firstSelection(sel) {
384
+ if (Array.isArray(sel)) return sel[0] || null;
385
+ return sel || null;
386
+ }
387
+
388
+ function findFaceInSolidByName(solid, name) {
389
+ if (!solid || !name) return null;
390
+ try {
391
+ if (typeof solid.getObjectByName === "function") {
392
+ const found = solid.getObjectByName(name);
393
+ if (found?.type === "FACE") return found;
394
+ }
395
+ } catch { /* ignore */ }
396
+ const children = Array.isArray(solid.children) ? solid.children : [];
397
+ for (const child of children) {
398
+ if (child?.type === "FACE" && child?.name === name) return child;
399
+ }
400
+ return null;
401
+ }
402
+
403
+ function resolveProfileSolid(selection, partHistory) {
404
+ if (!selection) return null;
405
+ if (selection?.type === "SOLID") return selection;
406
+ if (typeof selection === "string" && partHistory?.getObjectByName) {
407
+ const obj = partHistory.getObjectByName(selection);
408
+ if (obj?.type === "SOLID") return obj;
409
+ }
410
+ return null;
411
+ }
412
+
413
+ function resolveProfileFaceFromSolid(selection, partHistory, sheetSolid) {
414
+ if (!selection) return null;
415
+ if (selection?.type === "FACE") return selection;
416
+ const name = typeof selection === "string" ? selection : selection?.name;
417
+ if (name && sheetSolid) {
418
+ const face = findFaceInSolidByName(sheetSolid, name);
419
+ if (face) return face;
420
+ }
421
+ return resolveProfileFace(selection, partHistory);
422
+ }
423
+
424
+ function resolveSolidRef(ref, scene) {
425
+ const target = firstSelection(ref);
426
+ if (!target) return null;
427
+ if (target.type === "SOLID") return target;
428
+ if (target.type === "FACE") return findAncestorSolid(target);
429
+ if (typeof target === "string" && scene?.getObjectByName) {
430
+ const obj = scene.getObjectByName(target);
431
+ if (obj?.type === "SOLID") return obj;
432
+ if (obj?.type === "FACE") return findAncestorSolid(obj);
433
+ }
434
+ return null;
435
+ }
436
+
437
+ function findAncestorSolid(obj) {
438
+ let current = obj;
439
+ while (current) {
440
+ if (current.type === "SOLID") return current;
441
+ current = current.parent;
442
+ }
443
+ return null;
444
+ }
445
+
446
+ function findFirstSheetMetalSolid(scene, metadataManager) {
447
+ if (!scene || !Array.isArray(scene.children)) return null;
448
+ for (const child of scene.children) {
449
+ if (child && child.type === "SOLID" && resolveSheetThickness(child, metadataManager)) {
450
+ return child;
451
+ }
452
+ }
453
+ return null;
454
+ }
455
+
456
+ function buildToolFromProfile(face, params, name) {
457
+ if (!face) return null;
458
+ const fdRaw = Number(params?.forwardDistance ?? 0);
459
+ const bdRaw = Number(params?.backDistance ?? 0);
460
+ const fd = Number.isFinite(fdRaw) ? Math.max(0, fdRaw) : 0;
461
+ const bd = Number.isFinite(bdRaw) ? Math.max(0, bdRaw) : 0;
462
+ const minTravel = 1e-4;
463
+ const travelF = fd > 0 ? fd : minTravel;
464
+ const travelB = bd > 0 ? bd : 0;
465
+ return new BREP.Sweep({
466
+ face,
467
+ distance: travelF,
468
+ distanceBack: travelB,
469
+ mode: "translate",
470
+ name: name || "SM_CUTOUT_PROFILE",
471
+ omitBaseCap: false,
472
+ });
473
+ }
474
+
475
+
476
+ function unionSolids(solids) {
477
+ if (!Array.isArray(solids) || !solids.length) return null;
478
+ let combined = solids[0];
479
+ for (let i = 1; i < solids.length; i++) {
480
+ try { combined = combined.union(solids[i]); }
481
+ catch { combined = solids[i]; }
482
+ }
483
+ return combined;
484
+ }
485
+
486
+ function safeIntersect(a, b) {
487
+ try {
488
+ const out = a.intersect(b);
489
+ out.visualize?.();
490
+ return out;
491
+ } catch (err) {
492
+ console.warn("[SheetMetalCutout] Intersection failed", err);
493
+ // Fallback: try a light weld/clean if possible
494
+ try {
495
+ const aa = typeof a.clone === "function" ? a.clone() : a;
496
+ const bb = typeof b.clone === "function" ? b.clone() : b;
497
+ const scale = 1;
498
+ const eps = Math.max(1e-9, 1e-6 * scale);
499
+ try { aa.setEpsilon?.(eps); bb.setEpsilon?.(eps); } catch { }
500
+ try { aa.fixTriangleWindingsByAdjacency?.(); bb.fixTriangleWindingsByAdjacency?.(); } catch { }
501
+ const out2 = aa.intersect(bb);
502
+ out2.visualize?.();
503
+ return out2;
504
+ } catch (fallbackErr) {
505
+ console.warn("[SheetMetalCutout] Intersection fallback failed", fallbackErr);
506
+ }
507
+ return null;
508
+ }
509
+ }
510
+
511
+ async function safeIntersectFallbackBoolean(partHistory, sheetSolid, toolUnion, featureID) {
512
+ try {
513
+ const effects = await BREP.applyBooleanOperation(
514
+ partHistory || {},
515
+ sheetSolid,
516
+ { operation: "INTERSECT", targets: [toolUnion] },
517
+ featureID,
518
+ );
519
+ if (effects?.added?.length) {
520
+ const pick = Array.isArray(effects.added) ? effects.added[0] : null;
521
+ pick?.visualize?.();
522
+ return pick;
523
+ }
524
+ } catch (err) {
525
+ console.warn("[SheetMetalCutout] Boolean fallback intersect failed", err);
526
+ }
527
+ return null;
528
+ }
529
+
530
+ function resolveSheetThickness(solid, metadataManager) {
531
+ const candidates = [];
532
+ const push = (v) => { const n = Number(v); if (Number.isFinite(n) && Math.abs(n) > 1e-9) candidates.push(Math.abs(n)); };
533
+ if (solid?.userData?.sheetMetal) {
534
+ const sm = solid.userData.sheetMetal;
535
+ push(sm.thickness); push(sm.baseThickness);
536
+ }
537
+ push(solid?.userData?.sheetThickness);
538
+ if (metadataManager && solid?.name) {
539
+ try {
540
+ const meta = metadataManager.getOwnMetadata(solid.name);
541
+ push(meta?.sheetMetalThickness);
542
+ } catch { /* ignore */ }
543
+ }
544
+ return candidates.find((v) => v > 0) || null;
545
+ }
546
+
547
+ function collectSheetFaces(solid) {
548
+ const faces = { A: [], B: [] };
549
+ if (!solid || typeof solid.getFaceNames !== "function") return faces;
550
+ const THREE = BREP.THREE;
551
+ const matrixWorld = solid.matrixWorld || new THREE.Matrix4();
552
+
553
+ for (const name of solid.getFaceNames()) {
554
+ const meta = solid.getFaceMetadata(name) || {};
555
+ const type = meta.sheetMetalFaceType;
556
+ if (type !== SHEET_METAL_FACE_TYPES.A && type !== SHEET_METAL_FACE_TYPES.B) continue;
557
+ const tris = solid.getFace(name);
558
+ if (!Array.isArray(tris) || !tris.length) continue;
559
+ const { normal, origin } = faceNormalAndOrigin(tris, matrixWorld);
560
+ if (!normal) continue;
561
+ const entry = { faceName: name, triangles: tris, normal, origin };
562
+ if (type === SHEET_METAL_FACE_TYPES.A) faces.A.push(entry);
563
+ else faces.B.push(entry);
564
+ }
565
+ faces.A.sort((a, b) => b.triangles.length - a.triangles.length);
566
+ faces.B.sort((a, b) => b.triangles.length - a.triangles.length);
567
+ return faces;
568
+ }
569
+
570
+ function buildSheetFaceInfoMap(sheetFaces) {
571
+ const map = new Map();
572
+ for (const entry of sheetFaces.A) {
573
+ map.set(entry.faceName, { ...entry, targetType: SHEET_METAL_FACE_TYPES.A });
574
+ }
575
+ for (const entry of sheetFaces.B) {
576
+ map.set(entry.faceName, { ...entry, targetType: SHEET_METAL_FACE_TYPES.B });
577
+ }
578
+ return map;
579
+ }
580
+
581
+ function buildFaceTypeMap(solid) {
582
+ const map = new Map();
583
+ if (!solid || typeof solid.getFaceNames !== "function") return map;
584
+ for (const name of solid.getFaceNames()) {
585
+ const meta = solid.getFaceMetadata(name) || {};
586
+ map.set(name, meta.sheetMetalFaceType || null);
587
+ }
588
+ return map;
589
+ }
590
+
591
+ function renameExtrudeCapFaces(solid, baseName) {
592
+ if (!solid || typeof solid.getFaceNames !== "function" || typeof solid.renameFace !== "function") return;
593
+ const safeBase = baseName || "SM_CUTOUT_PRISM";
594
+ const capName = `${safeBase}_ENDCAP`;
595
+ for (const name of solid.getFaceNames()) {
596
+ if (typeof name !== "string") continue;
597
+ if (name.endsWith("_START")) {
598
+ solid.renameFace(name, capName);
599
+ } else if (name.endsWith("_END")) {
600
+ solid.renameFace(name, capName);
601
+ }
602
+ }
603
+ }
604
+
605
+ function buildPrismsFromBoundaryLoops(solid, faceInfoMap, faceTypeMap, sheetThickness, featureID) {
606
+ const loopsByFace = collectCutoutLoopsFromBoundary(solid, faceTypeMap);
607
+ const prisms = [];
608
+ const THREE = BREP.THREE;
609
+ for (const [faceName, loops] of loopsByFace.entries()) {
610
+ const faceInfo = faceInfoMap.get(faceName);
611
+ if (!faceInfo || !Array.isArray(loops) || !loops.length) continue;
612
+ const loopPoints = loops.map((loop) => loop.points.map((p) => new THREE.Vector3(p[0], p[1], p[2])));
613
+ const loopEdgeGroups = loops.map((loop) => loop.edgeGroups);
614
+ const profiles = buildFaceProfiles(loopPoints, faceInfo.normal, faceInfo.origin, featureID, loopEdgeGroups);
615
+ for (const profile of profiles) {
616
+ const dir = faceInfo.normal.clone().normalize().multiplyScalar(
617
+ faceInfo.targetType === SHEET_METAL_FACE_TYPES.B ? sheetThickness : -sheetThickness
618
+ );
619
+ const travel = sheetThickness;
620
+ const prism = new BREP.ExtrudeSolid({
621
+ face: profile,
622
+ distance: travel * 0.1,
623
+ distanceBack: travel * 1.1,
624
+ name: featureID,
625
+ });
626
+ renameExtrudeCapFaces(prism, featureID || "SM_CUTOUT_PRISM");
627
+ tagThicknessFaces(prism);
628
+ prisms.push(prism);
629
+ }
630
+ }
631
+ return prisms;
632
+ }
633
+
634
+ function buildPrismsFromIntersection(intersection, sheetSolid, sheetThickness, featureID) {
635
+ if (!intersection) return [];
636
+ const footprintFaces = collectSheetFaces(intersection);
637
+ const hasA = footprintFaces.A.length > 0;
638
+ const hasB = footprintFaces.B.length > 0;
639
+ if (!hasA && !hasB) return [];
640
+ const basisFaces = [
641
+ ...footprintFaces.A.map((f) => ({ ...f, targetType: SHEET_METAL_FACE_TYPES.A })),
642
+ ...footprintFaces.B.map((f) => ({ ...f, targetType: SHEET_METAL_FACE_TYPES.B })),
643
+ ];
644
+ const prisms = [];
645
+ for (const faceInfo of basisFaces) {
646
+ const loops = buildBoundaryLoops(faceInfo.triangles, intersection?.matrixWorld || sheetSolid.matrixWorld);
647
+ if (!loops.length) continue;
648
+ const profiles = buildFaceProfiles(loops, faceInfo.normal, faceInfo.origin, featureID);
649
+ for (const profile of profiles) {
650
+ const dir = faceInfo.normal.clone().normalize().multiplyScalar(
651
+ faceInfo.targetType === SHEET_METAL_FACE_TYPES.B ? sheetThickness : -sheetThickness
652
+ );
653
+ const travel = Math.max(sheetThickness, 1e-6);
654
+ const prism = new BREP.ExtrudeSolid({
655
+ face: profile,
656
+ dir,
657
+ distance: travel,
658
+ distanceBack: travel,
659
+ name: featureID || "SM_CUTOUT_PRISM",
660
+ });
661
+ renameExtrudeCapFaces(prism, featureID || "SM_CUTOUT_PRISM");
662
+ tagThicknessFaces(prism);
663
+ prisms.push(prism);
664
+ }
665
+ }
666
+ return prisms;
667
+ }
668
+
669
+ function collectCutoutLoopsFromBoundary(solid, faceTypeMap) {
670
+ const THREE = BREP.THREE;
671
+ const loopsByFace = new Map();
672
+ if (!solid || typeof solid.getBoundaryEdgePolylines !== "function") return loopsByFace;
673
+ const polylines = solid.getBoundaryEdgePolylines() || [];
674
+ const mat = solid.matrixWorld || new THREE.Matrix4();
675
+
676
+ for (const poly of polylines) {
677
+ const typeA = faceTypeMap.get(poly.faceA) || null;
678
+ const typeB = faceTypeMap.get(poly.faceB) || null;
679
+ const isSheetA = typeA === SHEET_METAL_FACE_TYPES.A || typeA === SHEET_METAL_FACE_TYPES.B;
680
+ const isSheetB = typeB === SHEET_METAL_FACE_TYPES.A || typeB === SHEET_METAL_FACE_TYPES.B;
681
+ if (isSheetA === isSheetB) continue;
682
+ const otherType = isSheetA ? typeB : typeA;
683
+ if (otherType === SHEET_METAL_FACE_TYPES.THICKNESS) continue;
684
+ const sheetFace = isSheetA ? poly.faceA : poly.faceB;
685
+ const toolFace = isSheetA ? poly.faceB : poly.faceA;
686
+ const pts = [];
687
+ const raw = Array.isArray(poly.positions) ? poly.positions : [];
688
+ for (const p of raw) {
689
+ const v = new THREE.Vector3(p[0], p[1], p[2]).applyMatrix4(mat);
690
+ pts.push([v.x, v.y, v.z]);
691
+ }
692
+ const clean = dedupPolylinePoints(pts);
693
+ if (clean.length < 2) continue;
694
+ const entry = loopsByFace.get(sheetFace) || [];
695
+ entry.push({ name: toolFace, pts: clean });
696
+ loopsByFace.set(sheetFace, entry);
697
+ }
698
+
699
+ const out = new Map();
700
+ for (const [faceName, segments] of loopsByFace.entries()) {
701
+ const loops = stitchCutoutLoops(segments);
702
+ if (loops.length) out.set(faceName, loops);
703
+ }
704
+ return out;
705
+ }
706
+
707
+ function stitchCutoutLoops(segments) {
708
+ const loops = [];
709
+ const used = new Set();
710
+ const clonePts = (pts) => pts.map((p) => [p[0], p[1], p[2]]);
711
+
712
+ const appendGroup = (groups, name, pts) => {
713
+ if (!pts.length) return;
714
+ if (groups.length && groups[groups.length - 1].name === name) {
715
+ groups[groups.length - 1].pts.push(...pts.slice(1));
716
+ } else {
717
+ groups.push({ name, pts: pts.slice() });
718
+ }
719
+ };
720
+
721
+ const prependGroup = (groups, name, pts) => {
722
+ if (!pts.length) return;
723
+ if (groups.length && groups[0].name === name) {
724
+ groups[0].pts = pts.slice(0, -1).concat(groups[0].pts);
725
+ } else {
726
+ groups.unshift({ name, pts: pts.slice() });
727
+ }
728
+ };
729
+
730
+ for (let i = 0; i < segments.length; i++) {
731
+ if (used.has(i)) continue;
732
+ const seg = segments[i];
733
+ if (!seg || !Array.isArray(seg.pts) || seg.pts.length < 2) continue;
734
+ let loopPts = clonePts(seg.pts);
735
+ let groups = [{ name: seg.name, pts: clonePts(seg.pts) }];
736
+ used.add(i);
737
+
738
+ let advanced = true;
739
+ while (advanced) {
740
+ advanced = false;
741
+ const start = loopPts[0];
742
+ const end = loopPts[loopPts.length - 1];
743
+ if (pointsEqual(start, end)) break;
744
+
745
+ for (let j = 0; j < segments.length; j++) {
746
+ if (used.has(j)) continue;
747
+ const s = segments[j];
748
+ if (!s || !Array.isArray(s.pts) || s.pts.length < 2) continue;
749
+ const sPts = clonePts(s.pts);
750
+ const sStart = sPts[0];
751
+ const sEnd = sPts[sPts.length - 1];
752
+
753
+ if (pointsEqual(end, sStart) || pointsEqual(end, sEnd)) {
754
+ const forward = pointsEqual(end, sStart);
755
+ const segPts = forward ? sPts : sPts.slice().reverse();
756
+ loopPts = loopPts.concat(segPts.slice(1));
757
+ appendGroup(groups, s.name, segPts);
758
+ used.add(j);
759
+ advanced = true;
760
+ break;
761
+ }
762
+
763
+ if (pointsEqual(start, sEnd) || pointsEqual(start, sStart)) {
764
+ const forward = pointsEqual(start, sEnd);
765
+ const segPts = forward ? sPts : sPts.slice().reverse();
766
+ loopPts = segPts.slice(0, -1).concat(loopPts);
767
+ prependGroup(groups, s.name, segPts);
768
+ used.add(j);
769
+ advanced = true;
770
+ break;
771
+ }
772
+ }
773
+ }
774
+
775
+ const closed = loopPts.length >= 3 && pointsEqual(loopPts[0], loopPts[loopPts.length - 1]);
776
+ if (!closed) continue;
777
+ loopPts = loopPts.slice(0, -1);
778
+
779
+ if (loopPts.length >= 3) {
780
+ loops.push({
781
+ points: loopPts,
782
+ edgeGroups: groups.map((g) => ({ name: g.name, pts: dedupPolylinePoints(g.pts) })),
783
+ });
784
+ }
785
+ }
786
+ return loops;
787
+ }
788
+
789
+ function dedupPolylinePoints(pts, eps = 1e-5) {
790
+ const out = [];
791
+ for (const p of pts || []) {
792
+ if (!out.length || !pointsEqual(out[out.length - 1], p, eps)) {
793
+ out.push([p[0], p[1], p[2]]);
794
+ }
795
+ }
796
+ return out;
797
+ }
798
+
799
+ function pointsEqual(a, b, eps = 1e-5) {
800
+ if (!a || !b) return false;
801
+ return Math.abs(a[0] - b[0]) <= eps
802
+ && Math.abs(a[1] - b[1]) <= eps
803
+ && Math.abs(a[2] - b[2]) <= eps;
804
+ }
805
+
806
+ function faceNormalAndOrigin(triangles, matrixWorld) {
807
+ const THREE = BREP.THREE;
808
+ const n = new THREE.Vector3();
809
+ const accum = new THREE.Vector3();
810
+ const a = new THREE.Vector3();
811
+ const b = new THREE.Vector3();
812
+ const c = new THREE.Vector3();
813
+ let count = 0;
814
+ for (const tri of triangles) {
815
+ a.fromArray(tri.p1).applyMatrix4(matrixWorld);
816
+ b.fromArray(tri.p2).applyMatrix4(matrixWorld);
817
+ c.fromArray(tri.p3).applyMatrix4(matrixWorld);
818
+ const ab = b.clone().sub(a);
819
+ const ac = c.clone().sub(a);
820
+ const cross = ac.cross(ab);
821
+ n.add(cross);
822
+ accum.add(a).add(b).add(c);
823
+ count += 3;
824
+ }
825
+ if (n.lengthSq() < 1e-14) return { normal: null, origin: null };
826
+ n.normalize();
827
+ const origin = count ? accum.multiplyScalar(1 / count) : new THREE.Vector3();
828
+ return { normal: n, origin };
829
+ }
830
+
831
+ function buildBoundaryLoops(triangles, matrixWorld) {
832
+ const THREE = BREP.THREE;
833
+ const mat = (matrixWorld && matrixWorld.isMatrix4) ? matrixWorld : new THREE.Matrix4();
834
+ const verts = [];
835
+ const keyToIndex = new Map();
836
+ const indices = [];
837
+ const keyFor = (p) => `${p.x.toFixed(6)},${p.y.toFixed(6)},${p.z.toFixed(6)}`;
838
+
839
+ const tmp = new THREE.Vector3();
840
+ for (const tri of triangles) {
841
+ const pts = [tri.p1, tri.p2, tri.p3].map((p) => tmp.fromArray(p).applyMatrix4(mat).clone());
842
+ const idx = pts.map((p) => {
843
+ const k = keyFor(p);
844
+ if (keyToIndex.has(k)) return keyToIndex.get(k);
845
+ const i = verts.length;
846
+ verts.push(p.clone());
847
+ keyToIndex.set(k, i);
848
+ return i;
849
+ });
850
+ indices.push(...idx);
851
+ }
852
+
853
+ const edgeCount = new Map(); // "a:b" -> count
854
+ const edgeKey = (a, b) => (a < b ? `${a}:${b}` : `${b}:${a}`);
855
+ for (let i = 0; i < indices.length; i += 3) {
856
+ const a = indices[i], b = indices[i + 1], c = indices[i + 2];
857
+ const edges = [[a, b], [b, c], [c, a]];
858
+ for (const [u, v] of edges) {
859
+ const k = edgeKey(u, v);
860
+ edgeCount.set(k, (edgeCount.get(k) || 0) + 1);
861
+ }
862
+ }
863
+
864
+ const adjacency = new Map(); // v -> Set(neighbors)
865
+ for (const [k, count] of edgeCount.entries()) {
866
+ if (count !== 1) continue;
867
+ const [a, b] = k.split(":").map((s) => parseInt(s, 10));
868
+ if (!adjacency.has(a)) adjacency.set(a, new Set());
869
+ if (!adjacency.has(b)) adjacency.set(b, new Set());
870
+ adjacency.get(a).add(b);
871
+ adjacency.get(b).add(a);
872
+ }
873
+
874
+ const visited = new Set();
875
+ const loopList = [];
876
+ const visitEdge = (a, b) => visited.add(edgeKey(a, b));
877
+ const seenEdge = (a, b) => visited.has(edgeKey(a, b));
878
+
879
+ for (const [start, nbrs] of adjacency.entries()) {
880
+ for (const nbor of nbrs) {
881
+ if (seenEdge(start, nbor)) continue;
882
+ const loop = [];
883
+ let prev = start;
884
+ let curr = nbor;
885
+ visitEdge(prev, curr);
886
+ loop.push(prev, curr);
887
+ while (true) {
888
+ const neighbors = adjacency.get(curr) || new Set();
889
+ let next = null;
890
+ for (const cand of neighbors) {
891
+ if (cand === prev) continue;
892
+ if (seenEdge(curr, cand)) continue;
893
+ next = cand; break;
894
+ }
895
+ if (next === null || next === undefined) break;
896
+ prev = curr;
897
+ curr = next;
898
+ visitEdge(prev, curr);
899
+ if (curr === loop[0]) break;
900
+ loop.push(curr);
901
+ }
902
+ if (loop.length >= 3) loopList.push(loop.map((idx) => verts[idx].clone()));
903
+ }
904
+ }
905
+ return loopList;
906
+ }
907
+
908
+ function buildFaceProfiles(loopPoints, normal, originHint, featureID, loopEdgeGroups = null) {
909
+ const THREE = BREP.THREE;
910
+ const origin = originHint ? originHint.clone() : loopPoints[0]?.[0]?.clone() || new THREE.Vector3();
911
+ const { u, v: basisV } = buildBasis(normal);
912
+
913
+ const loops2D = loopPoints.map((pts) => pts.map((p) => {
914
+ const rel = p.clone().sub(origin);
915
+ return new THREE.Vector2(rel.dot(u), rel.dot(basisV));
916
+ }));
917
+
918
+ const meta = loops2D.map((loop, idx) => {
919
+ const area = area2D(loop);
920
+ return {
921
+ idx,
922
+ loop,
923
+ area,
924
+ absArea: Math.abs(area),
925
+ };
926
+ });
927
+
928
+ // Assign each loop to the smallest containing parent (if any) to preserve disjoint cutouts.
929
+ for (const entry of meta) {
930
+ const sample = entry.loop[0];
931
+ let parent = null;
932
+ let parentArea = Infinity;
933
+ for (const candidate of meta) {
934
+ if (candidate.idx === entry.idx) continue;
935
+ if (candidate.absArea <= entry.absArea) continue;
936
+ if (!pointInPoly(sample, candidate.loop)) continue;
937
+ if (candidate.absArea < parentArea) {
938
+ parentArea = candidate.absArea;
939
+ parent = candidate.idx;
940
+ }
941
+ }
942
+ entry.parent = parent;
943
+ }
944
+
945
+ const faces = [];
946
+ const outers = meta.filter((m) => m.parent == null);
947
+ for (const outerMeta of outers) {
948
+ const shape = new THREE.Shape();
949
+ const outerLoop = ensureOrientation(outerMeta.loop, true);
950
+ moveToPath(shape, outerLoop);
951
+
952
+ const holes = meta.filter((m) => m.parent === outerMeta.idx);
953
+ for (const hole of holes) {
954
+ const path = new THREE.Path();
955
+ const oriented = ensureOrientation(hole.loop, false);
956
+ moveToPath(path, oriented);
957
+ shape.holes.push(path);
958
+ }
959
+
960
+ const geom = new THREE.ShapeGeometry(shape);
961
+ const pos = geom.getAttribute("position");
962
+ const tmpV = new THREE.Vector3();
963
+ for (let i = 0; i < pos.count; i++) {
964
+ tmpV.set(pos.getX(i), pos.getY(i), pos.getZ(i));
965
+ const world = origin.clone().addScaledVector(u, tmpV.x).addScaledVector(basisV, tmpV.y);
966
+ pos.setXYZ(i, world.x, world.y, world.z);
967
+ }
968
+ geom.computeVertexNormals();
969
+
970
+ const face = new BREP.Face(geom);
971
+ face.name = featureID ? `${featureID}_CUTOUT_FACE` : "CUTOUT_FACE";
972
+ face.userData = face.userData || {};
973
+ const loopsForFace = [outerMeta.idx, ...holes.map((h) => h.idx)];
974
+ face.userData.boundaryLoopsWorld = loopsForFace.map((idx) => ({
975
+ pts: loopPoints[idx].map((p) => [p.x, p.y, p.z]),
976
+ isHole: idx !== outerMeta.idx,
977
+ }));
978
+ if (Array.isArray(loopEdgeGroups)) {
979
+ const edgeGroups = [];
980
+ for (const idx of loopsForFace) {
981
+ const groups = loopEdgeGroups[idx];
982
+ if (!Array.isArray(groups)) continue;
983
+ for (const g of groups) {
984
+ if (!g || !Array.isArray(g.pts) || g.pts.length < 2) continue;
985
+ edgeGroups.push({
986
+ name: g.name,
987
+ pts: g.pts.map((p) => (Array.isArray(p) ? [p[0], p[1], p[2]] : [p.x, p.y, p.z])),
988
+ isHole: idx !== outerMeta.idx,
989
+ });
990
+ }
991
+ }
992
+ if (edgeGroups.length) {
993
+ face.userData.boundaryEdgeGroups = edgeGroups;
994
+ }
995
+ }
996
+ face.updateMatrixWorld?.(true);
997
+ faces.push(face);
998
+ }
999
+
1000
+ return faces;
1001
+ }
1002
+
1003
+ function buildBasis(normal) {
1004
+ const THREE = BREP.THREE;
1005
+ const n = normal.clone().normalize();
1006
+ const ref = Math.abs(n.z) < 0.9 ? new THREE.Vector3(0, 0, 1) : new THREE.Vector3(0, 1, 0);
1007
+ const u = new THREE.Vector3().crossVectors(ref, n);
1008
+ if (u.lengthSq() < 1e-10) u.set(1, 0, 0);
1009
+ u.normalize();
1010
+ const v = new THREE.Vector3().crossVectors(n, u).normalize();
1011
+ return { u, v, n };
1012
+ }
1013
+
1014
+ function area2D(loop) {
1015
+ let a = 0;
1016
+ for (let i = 0; i < loop.length; i++) {
1017
+ const p = loop[i];
1018
+ const q = loop[(i + 1) % loop.length];
1019
+ a += p.x * q.y - q.x * p.y;
1020
+ }
1021
+ return 0.5 * a;
1022
+ }
1023
+
1024
+ function ensureOrientation(loop, wantCCW) {
1025
+ const a = area2D(loop);
1026
+ const isCCW = a > 0;
1027
+ if ((wantCCW && isCCW) || (!wantCCW && !isCCW)) return loop.slice();
1028
+ return loop.slice().reverse();
1029
+ }
1030
+
1031
+ function pointInPoly(pt, poly) {
1032
+ let inside = false;
1033
+ for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
1034
+ const xi = poly[i].x, yi = poly[i].y;
1035
+ const xj = poly[j].x, yj = poly[j].y;
1036
+ const intersect = ((yi > pt.y) !== (yj > pt.y))
1037
+ && (pt.x < (xj - xi) * (pt.y - yi) / (yj - yi + 1e-16) + xi);
1038
+ if (intersect) inside = !inside;
1039
+ }
1040
+ return inside;
1041
+ }
1042
+
1043
+ function moveToPath(path, loop) {
1044
+ if (!loop.length) return;
1045
+ path.moveTo(loop[0].x, loop[0].y);
1046
+ for (let i = 1; i < loop.length; i++) {
1047
+ path.lineTo(loop[i].x, loop[i].y);
1048
+ }
1049
+ path.lineTo(loop[0].x, loop[0].y);
1050
+ }
1051
+
1052
+ function tagThicknessFaces(solid) {
1053
+ if (!solid || typeof solid.getFaceNames !== "function") return;
1054
+ const sideFaces = solid.getFaceNames().filter((n) => n && n.endsWith("_SW"));
1055
+ setSheetMetalFaceTypeMetadata(solid, sideFaces, SHEET_METAL_FACE_TYPES.THICKNESS);
1056
+ }