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,955 @@
1
+
2
+ import { ConstraintEngine } from './sketchSolver2D/ConstraintEngine.js';
3
+ import { BREP } from "../../BREP/BREP.js";
4
+ const THREE = BREP.THREE;
5
+ import { LineGeometry } from 'three/examples/jsm/Addons.js';
6
+ import { deepClone } from '../../utils/deepClone.js';
7
+
8
+ const inputParamsSchema = {
9
+ id: {
10
+ type: "string",
11
+ default_value: null,
12
+ hint: "unique identifier for the sketch feature",
13
+ },
14
+ sketchPlane: {
15
+ type: "reference_selection",
16
+ selectionFilter: ["PLANE", "FACE"],
17
+ multiple: false,
18
+ default_value: null,
19
+ hint: "Select the plane or face for the sketch",
20
+ },
21
+ editSketch: {
22
+ type: "button",
23
+ label: "Edit Sketch",
24
+ default_value: null,
25
+ hint: "Launch the 2D sketch editor",
26
+ actionFunction: (ctx) => {
27
+ try {
28
+ if (ctx && ctx.viewer && typeof ctx.viewer.startSketchMode === 'function') {
29
+ ctx.viewer.startSketchMode(ctx.featureID);
30
+ } else {
31
+ throw new Error('viewer.startSketchMode unavailable');
32
+ }
33
+ } catch (e) {
34
+ console.warn('[SketchFeature] Failed to start sketch mode:', e?.message || e);
35
+ }
36
+ }
37
+ },
38
+ dumpSketchDiagnostics: {
39
+ type: "button",
40
+ label: "Dump Diagnostics",
41
+ default_value: null,
42
+ hint: "Download the current sketch and triangulation data for debugging",
43
+ actionFunction: (ctx) => {
44
+ try {
45
+ const ph = ctx?.partHistory || null;
46
+ const fid = ctx?.featureID ?? ctx?.feature?.inputParams?.featureID ?? null;
47
+ let featureData = (ctx && typeof ctx === 'object') ? ctx.feature : null;
48
+ if ((!featureData || typeof featureData !== 'object') && ph && fid != null) {
49
+ const arr = Array.isArray(ph?.features) ? ph.features : [];
50
+ featureData = arr.find((f) => f && f.inputParams && String(f.inputParams.featureID) === String(fid)) || featureData;
51
+ }
52
+ if (!featureData || typeof featureData !== 'object') {
53
+ console.warn('[SketchFeature] Unable to locate sketch feature data for diagnostics');
54
+ return;
55
+ }
56
+ const instance = new SketchFeature(ph);
57
+ instance.inputParams = deepClone(featureData.inputParams || {});
58
+ if (fid != null && (instance.inputParams == null || instance.inputParams.featureID == null)) {
59
+ instance.inputParams = instance.inputParams || {};
60
+ instance.inputParams.featureID = fid;
61
+ }
62
+ instance.persistentData = deepClone(featureData.persistentData || {});
63
+ const payload = instance.dumpDiagnostics({ partHistory: ph, download: true });
64
+ if (!payload) {
65
+ console.warn('[SketchFeature] Diagnostics export produced no payload');
66
+ }
67
+ } catch (e) {
68
+ console.error('[SketchFeature] Failed to dump diagnostics:', e);
69
+ }
70
+ }
71
+ },
72
+ curveResolution: {
73
+ type: "number",
74
+ default_value: 32,
75
+ min: 32,
76
+ max: 512,
77
+ hint: "Segments for circles; arcs scale proportionally",
78
+ },
79
+ };
80
+
81
+ export class SketchFeature {
82
+ static shortName = "S";
83
+ static longName = "Sketch";
84
+ static inputParamsSchema = inputParamsSchema;
85
+
86
+ constructor() {
87
+ this.inputParams = {};
88
+
89
+ // Persisted between edits: { basis, sketch }
90
+ this.persistentData = this.persistentData || {};
91
+ this._sketchChanged = null;
92
+ }
93
+
94
+ // Build (and persist) a plane basis from the selected sketchPlane.
95
+ // Always recompute from the current referenced object transform if available,
96
+ // so the sketch follows moves/updates of the face/plane.
97
+ // basis = { origin: [x,y,z], x: [x,y,z], y: [x,y,z], z: [x,y,z], refName?: string }
98
+ _getOrCreateBasis(partHistory) {
99
+ const currentRef = this.inputParams?.sketchPlane || null;
100
+ const pdBasis = this.persistentData?.basis || null;
101
+ const ph = partHistory;
102
+ // Accept object (preferred, from sanitizeInputParams) or fallback to name
103
+ let refObj = null;
104
+ if (Array.isArray(currentRef)) {
105
+ refObj = currentRef[0] || null;
106
+ } else if (currentRef && typeof currentRef === 'object') {
107
+ refObj = currentRef;
108
+ } else if (currentRef) {
109
+ refObj = ph?.scene?.getObjectByName(currentRef);
110
+ }
111
+
112
+ const x = new THREE.Vector3(1,0,0);
113
+ const y = new THREE.Vector3(0,1,0);
114
+ const z = new THREE.Vector3(0,0,1);
115
+ const origin = new THREE.Vector3();
116
+
117
+ if (refObj) {
118
+ refObj.updateWorldMatrix(true, true);
119
+ // Prefer geometric center if available
120
+ try {
121
+ const g = refObj.geometry;
122
+ if (g) {
123
+ const bs = g.boundingSphere || (g.computeBoundingSphere(), g.boundingSphere);
124
+ if (bs) origin.copy(refObj.localToWorld(bs.center.clone()));
125
+ else origin.copy(refObj.getWorldPosition(new THREE.Vector3()));
126
+ } else origin.copy(refObj.getWorldPosition(new THREE.Vector3()));
127
+ } catch { origin.copy(refObj.getWorldPosition(new THREE.Vector3())); }
128
+ // For Face, use its avg normal; otherwise use object orientation
129
+ if (refObj.type === 'FACE' && typeof refObj.getAverageNormal === 'function') {
130
+ const n = refObj.getAverageNormal();
131
+ const worldUp = new THREE.Vector3(0,1,0);
132
+ const tmp = new THREE.Vector3();
133
+ const zx = Math.abs(n.dot(worldUp)) > 0.9 ? new THREE.Vector3(1,0,0) : worldUp;
134
+ x.copy(tmp.crossVectors(zx, n).normalize());
135
+ y.copy(tmp.crossVectors(n, x).normalize());
136
+ z.copy(n.clone().normalize());
137
+ } else {
138
+ const n = new THREE.Vector3(0,0,1).applyQuaternion(refObj.getWorldQuaternion(new THREE.Quaternion())).normalize();
139
+ const worldUp = new THREE.Vector3(0,1,0);
140
+ const tmp = new THREE.Vector3();
141
+ const zx = Math.abs(n.dot(worldUp)) > 0.9 ? new THREE.Vector3(1,0,0) : worldUp;
142
+ x.copy(tmp.crossVectors(zx, n).normalize());
143
+ y.copy(tmp.crossVectors(n, x).normalize());
144
+ z.copy(n);
145
+ }
146
+ }
147
+
148
+ // If the reference object is missing (e.g., deleted), keep prior basis if present
149
+ if (!refObj && pdBasis) return pdBasis;
150
+
151
+ const basis = {
152
+ origin: [origin.x, origin.y, origin.z],
153
+ x: [x.x, x.y, x.z],
154
+ y: [y.x, y.y, y.z],
155
+ z: [z.x, z.y, z.z],
156
+ refName: (refObj?.name) || undefined,
157
+ };
158
+ this.persistentData = this.persistentData || {};
159
+ this.persistentData.basis = basis;
160
+ return basis;
161
+ }
162
+
163
+ _sketchSignature(sketch) {
164
+ if (!sketch) return null;
165
+ try {
166
+ return JSON.stringify(sketch);
167
+ } catch {
168
+ return null;
169
+ }
170
+ }
171
+
172
+ _updateSketchChangeState(sketch) {
173
+ this.persistentData = this.persistentData || {};
174
+ try {
175
+ const currentSignature = this._sketchSignature(sketch);
176
+ const prevSignature = this.persistentData.lastSketchSignature;
177
+ const changed = prevSignature != null ? prevSignature !== currentSignature : false;
178
+ this.persistentData.lastSketchSignature = currentSignature;
179
+ this.persistentData.lastSketchChanged = changed;
180
+ this._sketchChanged = changed;
181
+ } catch {
182
+ this._sketchChanged = false;
183
+ }
184
+ }
185
+
186
+ hasSketchChanged() {
187
+ if (typeof this._sketchChanged === 'boolean') {
188
+ return this._sketchChanged;
189
+ }
190
+ const persisted = this.persistentData?.lastSketchChanged;
191
+ return Boolean(persisted);
192
+ }
193
+
194
+ _cloneForDump(data) {
195
+ if (data == null) return null;
196
+ try {
197
+ return JSON.parse(JSON.stringify(data));
198
+ } catch {
199
+ return data;
200
+ }
201
+ }
202
+
203
+ _basisToWorldFn(basis) {
204
+ if (!basis || typeof basis !== 'object') return null;
205
+ const origin = Array.isArray(basis.origin) ? basis.origin : [0, 0, 0];
206
+ const bx = Array.isArray(basis.x) ? basis.x : [1, 0, 0];
207
+ const by = Array.isArray(basis.y) ? basis.y : [0, 1, 0];
208
+ let bz = Array.isArray(basis.z) ? basis.z : null;
209
+ if (!bz) {
210
+ const [bx0, bx1, bx2] = bx;
211
+ const [by0, by1, by2] = by;
212
+ const cx = bx1 * by2 - bx2 * by1;
213
+ const cy = bx2 * by0 - bx0 * by2;
214
+ const cz = bx0 * by1 - bx1 * by0;
215
+ const len = Math.hypot(cx, cy, cz) || 1;
216
+ bz = [cx / len, cy / len, cz / len];
217
+ }
218
+ return (u, v, w = 0) => ([
219
+ origin[0] + u * bx[0] + v * by[0] + w * bz[0],
220
+ origin[1] + u * bx[1] + v * by[1] + w * bz[1],
221
+ origin[2] + u * bx[2] + v * by[2] + w * bz[2],
222
+ ]);
223
+ }
224
+
225
+ _buildDiagnosticsFilename(featureID) {
226
+ const safeId = featureID != null && featureID !== ''
227
+ ? String(featureID).replace(/[^a-z0-9_-]/gi, '_')
228
+ : 'sketch';
229
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
230
+ return `${safeId}-diagnostics-${stamp}.json`;
231
+ }
232
+
233
+ _downloadDiagnosticsFile(fileName, payload) {
234
+ if (typeof document === 'undefined' || typeof URL === 'undefined' || typeof Blob === 'undefined') {
235
+ console.warn('[SketchFeature] Browser file APIs unavailable; cannot download diagnostics');
236
+ return;
237
+ }
238
+ try {
239
+ const json = JSON.stringify(payload, null, 2);
240
+ const blob = new Blob([json], { type: 'application/json' });
241
+ const url = URL.createObjectURL(blob);
242
+ try {
243
+ const link = document.createElement('a');
244
+ link.href = url;
245
+ link.download = fileName;
246
+ link.rel = 'noopener';
247
+ link.style.display = 'none';
248
+ document.body.appendChild(link);
249
+ link.click();
250
+ document.body.removeChild(link);
251
+ } finally {
252
+ setTimeout(() => { try { URL.revokeObjectURL(url); } catch { /* noop */ } }, 0);
253
+ }
254
+ } catch (err) {
255
+ console.error('[SketchFeature] Failed to prepare diagnostics download:', err);
256
+ }
257
+ }
258
+
259
+ dumpDiagnostics({ partHistory, download = false, fileName } = {}) {
260
+ try {
261
+ const featureID = this.inputParams?.featureID ?? null;
262
+ const basis = this.persistentData?.basis || (partHistory ? this._getOrCreateBasis(partHistory) : null);
263
+ const sketch = this._cloneForDump(this.persistentData?.sketch);
264
+ const profile = this._cloneForDump(this.persistentData?.lastProfileDiagnostics);
265
+ const payload = {
266
+ featureID,
267
+ timestamp: new Date().toISOString(),
268
+ sketchSignature: this._sketchSignature(this.persistentData?.sketch || null),
269
+ sketch,
270
+ profile,
271
+ basis: this._cloneForDump(basis),
272
+ };
273
+ if (payload.profile && payload.profile.triangles2D && !payload.profile.trianglesWorld && basis) {
274
+ try {
275
+ const toWorld = this._basisToWorldFn(basis);
276
+ if (typeof toWorld === 'function') {
277
+ payload.profile.trianglesWorld = payload.profile.triangles2D.map((tri) => tri.map((pt) => {
278
+ if (!Array.isArray(pt)) return pt;
279
+ const u = Number(pt[0]) || 0;
280
+ const v = Number(pt[1]) || 0;
281
+ const w = Number(pt[2]) || 0;
282
+ return toWorld(u, v, w);
283
+ }));
284
+ }
285
+ } catch (e) {
286
+ payload.profile.trianglesWorldError = e?.message || String(e);
287
+ }
288
+ }
289
+ const label = featureID ? `[SketchFeature] Diagnostics (${featureID})` : '[SketchFeature] Diagnostics';
290
+ try {
291
+ console.groupCollapsed(label);
292
+ } catch {
293
+ console.log(label);
294
+ }
295
+ console.log(payload);
296
+ try { console.groupEnd(); } catch {}
297
+ if (download) {
298
+ const name = fileName || this._buildDiagnosticsFilename(featureID);
299
+ this._downloadDiagnosticsFile(name, payload);
300
+ try {
301
+ console.info(`[SketchFeature] Diagnostics saved as ${name}`);
302
+ } catch { /* noop */ }
303
+ }
304
+ return payload;
305
+ } catch (e) {
306
+ console.error('[SketchFeature] Diagnostic dump failed:', e);
307
+ return null;
308
+ }
309
+ }
310
+
311
+ // Visualize sketch curves and points as a Group for selection (type='SKETCH').
312
+ // Returns [group]
313
+ async run(partHistory) {
314
+ const sceneGroup = new THREE.Group();
315
+ sceneGroup.name = this.inputParams.featureID || 'Sketch';
316
+ const featureId = (typeof sceneGroup.name === 'string' && sceneGroup.name.length)
317
+ ? sceneGroup.name
318
+ : (this.inputParams?.featureID ? String(this.inputParams.featureID) : 'Sketch');
319
+ const edgeNamePrefix = featureId ? `${featureId}:` : '';
320
+ sceneGroup.type = 'SKETCH';
321
+ // Provide a harmless onClick so Scene Manager rows don't error
322
+ sceneGroup.onClick = () => {};
323
+
324
+ const basis = this._getOrCreateBasis(partHistory);
325
+ sceneGroup.userData = sceneGroup.userData || {};
326
+ // Expose the sketch basis so downstream features (e.g., holes) can use the plane normal.
327
+ sceneGroup.userData.sketchBasis = {
328
+ origin: Array.isArray(basis.origin) ? basis.origin.slice() : [0, 0, 0],
329
+ x: Array.isArray(basis.x) ? basis.x.slice() : [1, 0, 0],
330
+ y: Array.isArray(basis.y) ? basis.y.slice() : [0, 1, 0],
331
+ z: Array.isArray(basis.z) ? basis.z.slice() : null,
332
+ };
333
+ const bO = new THREE.Vector3().fromArray(basis.origin);
334
+ const bX = new THREE.Vector3().fromArray(basis.x);
335
+ const bY = new THREE.Vector3().fromArray(basis.y);
336
+
337
+ // Start from persisted sketch
338
+ let sketch = this.persistentData?.sketch || { points: [{ id:0, x:0, y:0, fixed:true }], geometries: [], constraints: [{ id:0, type:"⏚", points:[0]}] };
339
+ this.persistentData = this.persistentData || {};
340
+ this.persistentData.lastProfileDiagnostics = null;
341
+
342
+ // Evaluate any expression-backed values on points/constraints using global expressions
343
+ try {
344
+ const exprSrc = partHistory?.expressions || '';
345
+ const runExpr = (expressions, equation) => {
346
+ try {
347
+ const fn = `${expressions}; return ${equation} ;`;
348
+ let result = Function(fn)();
349
+ if (typeof result === 'string') {
350
+ const num = Number(result);
351
+ if (!Number.isNaN(num)) return num;
352
+ }
353
+ return result;
354
+ } catch { return null; }
355
+ };
356
+ if (Array.isArray(sketch?.points)) {
357
+ for (const p of sketch.points) {
358
+ if (typeof p.x === 'string') {
359
+ const n = runExpr(exprSrc, p.x);
360
+ if (n != null && Number.isFinite(n)) p.x = Number(n);
361
+ }
362
+ if (typeof p.y === 'string') {
363
+ const n = runExpr(exprSrc, p.y);
364
+ if (n != null && Number.isFinite(n)) p.y = Number(n);
365
+ }
366
+ }
367
+ }
368
+ if (Array.isArray(sketch?.constraints)) {
369
+ for (const c of sketch.constraints) {
370
+ if (typeof c?.valueExpr === 'string') {
371
+ const n = runExpr(exprSrc, c.valueExpr);
372
+ if (n != null && Number.isFinite(n)) c.value = Number(n);
373
+ } else if (typeof c?.value === 'string') {
374
+ const n = runExpr(exprSrc, c.value);
375
+ if (n != null && Number.isFinite(n)) c.value = Number(n);
376
+ }
377
+ }
378
+ }
379
+ // Re-solve sketch with evaluated values to reflect latest expressions
380
+ try {
381
+ const engine = new ConstraintEngine(JSON.stringify(sketch));
382
+ const solved = engine.solve(500);
383
+ sketch = solved;
384
+ this.persistentData.sketch = solved;
385
+ } catch {}
386
+ } catch {}
387
+ // Update external reference points by projecting selected model edge endpoints
388
+ try {
389
+ const scene = partHistory?.scene;
390
+ const refs = Array.isArray(this.persistentData?.externalRefs) ? this.persistentData.externalRefs : [];
391
+ if (scene && refs.length) {
392
+ const toUV = (w)=>{ const d = new THREE.Vector3().copy(w).sub(bO); return { u: d.dot(bX), v: d.dot(bY) }; };
393
+ const edgeEndpoints = (edge)=>{
394
+ if (!edge) return null;
395
+ const a = new THREE.Vector3();
396
+ const b = new THREE.Vector3();
397
+ const toW = (v)=> v.applyMatrix4(edge.matrixWorld);
398
+ const pts = Array.isArray(edge?.userData?.polylineLocal) ? edge.userData.polylineLocal : null;
399
+ if (pts && pts.length >= 2) {
400
+ a.set(pts[0][0], pts[0][1], pts[0][2]);
401
+ b.set(pts[pts.length-1][0], pts[pts.length-1][1], pts[pts.length-1][2]);
402
+ return { a: toW(a), b: toW(b) };
403
+ }
404
+ const pos = edge?.geometry?.getAttribute?.('position');
405
+ if (pos && pos.itemSize === 3 && pos.count >= 2) {
406
+ a.set(pos.getX(0), pos.getY(0), pos.getZ(0));
407
+ b.set(pos.getX(pos.count-1), pos.getY(pos.count-1), pos.getZ(pos.count-1));
408
+ return { a: toW(a), b: toW(b) };
409
+ }
410
+ return null;
411
+ };
412
+ const ptById = new Map(sketch.points.map(p=>[p.id,p]));
413
+ let changed = false;
414
+ for (const r of refs) {
415
+ try {
416
+ let edge = scene.getObjectById(r.edgeId);
417
+ if (!edge || edge.type !== 'EDGE') {
418
+ // Fallback by solidName + edgeName, then global by edgeName
419
+ if (r.solidName) {
420
+ const solid = scene?.getObjectByName(r.solidName);
421
+ if (solid) {
422
+ let found = null;
423
+ solid.traverse((obj) => { if (!found && obj.type === 'EDGE' && obj.name === r.edgeName) found = obj; });
424
+ if (found) edge = found;
425
+ }
426
+ }
427
+ if ((!edge || edge.type !== 'EDGE') && r.edgeName) {
428
+ let found = null;
429
+ scene?.traverse((obj) => { if (!found && obj.type === 'EDGE' && obj.name === r.edgeName) found = obj; });
430
+ if (found) edge = found;
431
+ }
432
+ if (edge && edge.type === 'EDGE') {
433
+ // refresh stored id/name metadata
434
+ r.edgeId = edge.id;
435
+ try { r.edgeName = edge.name || r.edgeName || null; } catch {}
436
+ try { r.solidName = edge.parent?.name || r.solidName || null; } catch {}
437
+ changed = true;
438
+ }
439
+ }
440
+ if (!edge || edge.type !== 'EDGE') continue; // keep existing points if edge vanished
441
+ const ends = edgeEndpoints(edge);
442
+ if (!ends) continue;
443
+ const uvA = toUV(ends.a);
444
+ const uvB = toUV(ends.b);
445
+ const p0 = ptById.get(r.p0);
446
+ const p1 = ptById.get(r.p1);
447
+ if (p0 && (p0.x !== uvA.u || p0.y !== uvA.v)) { p0.x = uvA.u; p0.y = uvA.v; changed = true; }
448
+ if (p1 && (p1.x !== uvB.u || p1.y !== uvB.v)) { p1.x = uvB.u; p1.y = uvB.v; changed = true; }
449
+ if (p0) p0.fixed = true; if (p1) p1.fixed = true;
450
+ // Ensure ground constraints exist for these points so solver treats them fixed
451
+ const ensureGround = (pid)=>{
452
+ if (!sketch.constraints.some(c=>c.type==='⏚' && Array.isArray(c.points) && c.points[0]===pid)){
453
+ const cid = Math.max(0, ...sketch.constraints.map(c=> +c.id || 0)) + 1;
454
+ sketch.constraints.push({ id: cid, type: '⏚', points:[pid] });
455
+ changed = true;
456
+ }
457
+ };
458
+ if (p0) ensureGround(p0.id);
459
+ if (p1) ensureGround(p1.id);
460
+ } catch {}
461
+ }
462
+ if (changed) {
463
+ try {
464
+ const engine = new ConstraintEngine(JSON.stringify(sketch));
465
+ const solved = engine.solve(500);
466
+ sketch = solved;
467
+ this.persistentData.sketch = solved;
468
+ } catch {}
469
+ }
470
+ }
471
+ } catch {}
472
+ const curveRes = Math.max(8, Math.floor(Number(this.inputParams?.curveResolution) || 64));
473
+
474
+ // Helper: 2D → 3D
475
+ const to3D = (u, v) => new THREE.Vector3().copy(bO).addScaledVector(bX, u).addScaledVector(bY, v);
476
+
477
+ // Add vertex visuals in 3D for every sketch point (including isolated points)
478
+ try {
479
+ if (Array.isArray(sketch?.points)) {
480
+ let autoId = 0;
481
+ for (const p of sketch.points) {
482
+ if (p == null) continue;
483
+ const u = Number(p.x); const v = Number(p.y);
484
+ if (!Number.isFinite(u) || !Number.isFinite(v)) continue;
485
+ const w = to3D(u, v);
486
+ const hasExplicitId = p.id !== undefined && p.id !== null && `${p.id}` !== '';
487
+ const pointLabel = hasExplicitId ? p.id : autoId++;
488
+ const vertexName = featureId ? `${featureId}:P${pointLabel}` : `P${pointLabel}`;
489
+ try {
490
+ const vertex = new BREP.Vertex([w.x, w.y, w.z], { name: vertexName });
491
+ vertex.userData = vertex.userData || {};
492
+ vertex.userData.sketchPointId = hasExplicitId ? p.id : pointLabel;
493
+ vertex.userData.sketchFeatureId = featureId;
494
+ sceneGroup.add(vertex);
495
+ } catch {}
496
+ }
497
+ }
498
+ } catch {}
499
+
500
+ // Do not add curve preview lines in scene; editor handles those.
501
+
502
+ // ---- Build PROFILE face from sketch with loop detection + holes ----
503
+ const pointById = new Map(sketch.points.map(p => [p.id, { x: p.x, y: p.y }]));
504
+ const segs = [];
505
+ const edges = [];
506
+ const openChains = [];
507
+ const toWorld = (u,v)=> to3D(u,v);
508
+
509
+ const edgeBySegId = new Map();
510
+ for (const g of (sketch.geometries||[])) {
511
+ // Skip construction geometry: used only for constraints, not model edges
512
+ if (g && g.construction) continue;
513
+ if (g.type==='line' && g.points?.length===2) {
514
+ const a = pointById.get(g.points[0]); const b = pointById.get(g.points[1]); if(!a||!b) continue;
515
+ segs.push({ id:g.id, pts:[[a.x,a.y],[b.x,b.y]] });
516
+ const aw = toWorld(a.x,a.y); const bw = toWorld(b.x,b.y);
517
+ const lg = new LineGeometry();
518
+ lg.setPositions([aw.x, aw.y, aw.z, bw.x, bw.y, bw.z]);
519
+ const edgeName = `${edgeNamePrefix}G${g.id}`;
520
+ const e = new BREP.Edge(lg); e.name = edgeName; e.userData = { polylineLocal:[[aw.x,aw.y,aw.z],[bw.x,bw.y,bw.z]], polylineWorld:true, sketchFeatureId: featureId, sketchGeometryId: g.id }; edges.push(e); edgeBySegId.set(g.id, e);
521
+ } else if (g.type==='arc' && g.points?.length===3) {
522
+ const c = pointById.get(g.points[0]); const sa=pointById.get(g.points[1]); const sb=pointById.get(g.points[2]); if(!c||!sa||!sb) continue;
523
+ const cx=c.x, cy=c.y; const r=Math.hypot(sa.x-cx, sa.y-cy);
524
+ let a0=Math.atan2(sa.y-cy, sa.x-cx), a1=Math.atan2(sb.y-cy, sb.x-cx);
525
+ // CCW sweep in [0, 2π). If start≈end, treat as full circle.
526
+ let d = a1 - a0; d = ((d % (2*Math.PI)) + 2*Math.PI) % (2*Math.PI); if (Math.abs(d) < 1e-6) d = 2*Math.PI;
527
+ const n=Math.max(8, Math.ceil(curveRes*(d)/(2*Math.PI)));
528
+ const pts=[];
529
+ for(let i=0;i<=n;i++){
530
+ const t=a0+d*(i/n);
531
+ pts.push([cx+r*Math.cos(t), cy+r*Math.sin(t)]);
532
+ }
533
+ if (pts.length){
534
+ // Snap endpoints to exact sketch values so shared joints line up after discretization
535
+ pts[0] = [sa.x, sa.y];
536
+ pts[pts.length-1] = [sb.x, sb.y];
537
+ }
538
+ segs.push({ id:g.id, pts });
539
+ const flat=[]; const worldPts=[]; for(const p of pts){ const v=toWorld(p[0],p[1]); flat.push(v.x,v.y,v.z); worldPts.push([v.x,v.y,v.z]); }
540
+ const lg = new LineGeometry(); lg.setPositions(flat);
541
+ const edgeName = `${edgeNamePrefix}G${g.id}`;
542
+ const e = new BREP.Edge(lg); e.name = edgeName;
543
+ const cw = toWorld(cx, cy);
544
+ e.userData = { polylineLocal: worldPts, polylineWorld:true, sketchGeomType:'arc', arcCenter:[cw.x, cw.y, cw.z], arcRadius:r, sketchFeatureId: featureId, sketchGeometryId: g.id };
545
+ edges.push(e); edgeBySegId.set(g.id, e);
546
+ } else if (g.type==='circle' && g.points?.length===2) {
547
+ const c = pointById.get(g.points[0]); const rp=pointById.get(g.points[1]); if(!c||!rp) continue;
548
+ const cx=c.x, cy=c.y; const r=Math.hypot(rp.x-cx, rp.y-cy); const n=Math.max(8, curveRes); const pts=[];
549
+ for(let i=0;i<=n;i++){ const t=(i/n)*Math.PI*2; pts.push([cx+r*Math.cos(t), cy+r*Math.sin(t)]);}
550
+ if (pts.length){
551
+ // Ensure perfect closure so hole loops stay connected
552
+ const first=[cx+r, cy];
553
+ pts[0] = first;
554
+ pts[pts.length-1] = [first[0], first[1]];
555
+ }
556
+ segs.push({ id:g.id, pts });
557
+ const flat=[]; const worldPts=[]; for(const p of pts){ const v=toWorld(p[0],p[1]); flat.push(v.x,v.y,v.z); worldPts.push([v.x,v.y,v.z]); }
558
+ const lg = new LineGeometry(); lg.setPositions(flat);
559
+ const edgeName = `${edgeNamePrefix}G${g.id}`;
560
+ const e = new BREP.Edge(lg); e.name = edgeName;
561
+ const cw = toWorld(cx, cy);
562
+ e.userData = { polylineLocal: worldPts, polylineWorld:true, sketchGeomType:'circle', circleCenter:[cw.x,cw.y,cw.z], circleRadius:r, sketchFeatureId: featureId, sketchGeometryId: g.id };
563
+ edges.push(e); edgeBySegId.set(g.id, e);
564
+ } else if (g.type==='bezier' && g.points?.length>=4) {
565
+ const ids = g.points || [];
566
+ const segCount = Math.floor((ids.length - 1) / 3);
567
+ if (segCount < 1) continue;
568
+ const n = Math.max(8, curveRes);
569
+ const pts = [];
570
+ for (let seg = 0; seg < segCount; seg++) {
571
+ const i0 = seg * 3;
572
+ const p0 = pointById.get(ids[i0]);
573
+ const p1 = pointById.get(ids[i0 + 1]);
574
+ const p2 = pointById.get(ids[i0 + 2]);
575
+ const p3 = pointById.get(ids[i0 + 3]);
576
+ if (!p0 || !p1 || !p2 || !p3) continue;
577
+ for (let i=0;i<=n;i++){
578
+ if (seg > 0 && i === 0) continue;
579
+ const t = i/n; const mt = 1 - t;
580
+ const bx = mt*mt*mt*p0.x + 3*mt*mt*t*p1.x + 3*mt*t*t*p2.x + t*t*t*p3.x;
581
+ const by = mt*mt*mt*p0.y + 3*mt*mt*t*p1.y + 3*mt*t*t*p2.y + t*t*t*p3.y;
582
+ pts.push([bx, by]);
583
+ }
584
+ }
585
+ if (!pts.length) continue;
586
+ const firstAnchor = pointById.get(ids[0]);
587
+ const lastAnchor = pointById.get(ids[segCount * 3]);
588
+ if (firstAnchor) pts[0] = [firstAnchor.x, firstAnchor.y];
589
+ if (lastAnchor) pts[pts.length - 1] = [lastAnchor.x, lastAnchor.y];
590
+ segs.push({ id:g.id, pts });
591
+ const flat=[]; const worldPts=[]; for(const p of pts){ const v=toWorld(p[0],p[1]); flat.push(v.x,v.y,v.z); worldPts.push([v.x,v.y,v.z]); }
592
+ const lg = new LineGeometry(); lg.setPositions(flat);
593
+ const edgeName = `${edgeNamePrefix}G${g.id}`;
594
+ const e = new BREP.Edge(lg); e.name = edgeName; e.userData = { polylineLocal: worldPts, polylineWorld:true, sketchFeatureId: featureId, sketchGeometryId: g.id }; edges.push(e); edgeBySegId.set(g.id, e);
595
+ }
596
+ }
597
+
598
+ // Utility helpers for loops
599
+ const key=(x,y)=> `${x.toFixed(6)},${y.toFixed(6)}`;
600
+ const nearlyEqual=(a,b,eps=1e-6)=> Math.abs(a-b)<=eps;
601
+ const closePt=(p,q)=> nearlyEqual(p[0],q[0]) && nearlyEqual(p[1],q[1]);
602
+ const ensureClosed=(arr)=>{
603
+ if (arr.length<3) return arr;
604
+ const f=arr[0], l=arr[arr.length-1];
605
+ if (!closePt(f,l)) arr.push([f[0],f[1]]);
606
+ return arr;
607
+ };
608
+ const dedupeConsecutive=(arr)=>{
609
+ const out=[]; let prev=null;
610
+ for(const p of arr){ if(!prev || !closePt(prev,p)){ out.push([p[0],p[1]]); prev=p; } }
611
+ return out;
612
+ };
613
+ const removeCollinear=(arr, eps=1e-9)=>{
614
+ if (arr.length <= 3) return arr;
615
+ const ring = arr.slice();
616
+ const n0 = ring.length;
617
+ const out = [];
618
+ for (let i=0;i<n0;i++){
619
+ const a = ring[(i-1+n0)%n0];
620
+ const b = ring[i];
621
+ const c = ring[(i+1)%n0];
622
+ const abx = b[0]-a[0], aby=b[1]-a[1];
623
+ const bcx = c[0]-b[0], bcy=c[1]-b[1];
624
+ const cross = abx*bcy - aby*bcx;
625
+ if (Math.abs(cross) > eps) out.push(b);
626
+ }
627
+ return out.length>=3 ? out : arr;
628
+ };
629
+ const signedArea = (loop)=>{
630
+ let a=0; for(let i=0;i<loop.length-1;i++){ const p=loop[i], q=loop[i+1]; a+= (p[0]*q[1]-q[0]*p[1]); } return 0.5*a;
631
+ };
632
+ const pointInPoly = (pt, poly)=>{
633
+ // Winding number test. Poly may be closed; trim duplicate.
634
+ const n = poly.length; if (n<3) return false;
635
+ const first=poly[0], last=poly[n-1];
636
+ const ring = (nearlyEqual(first[0],last[0])&&nearlyEqual(first[1],last[1]))? poly.slice(0,n-1): poly;
637
+ const x = pt[0], y = pt[1];
638
+ let wn=0;
639
+ const isLeft=(ax,ay,bx,by,cx,cy)=> (bx-ax)*(cy-ay) - (by-ay)*(cx-ax);
640
+ for (let i=0;i<ring.length;i++){
641
+ const a = ring[i];
642
+ const b = ring[(i+1)%ring.length];
643
+ if (a[1] <= y) {
644
+ if (b[1] > y && isLeft(a[0],a[1],b[0],b[1],x,y) > 0) wn++;
645
+ } else {
646
+ if (b[1] <= y && isLeft(a[0],a[1],b[0],b[1],x,y) < 0) wn--;
647
+ }
648
+ }
649
+ return wn !== 0;
650
+ };
651
+
652
+ // Build multiple loops by chaining segments greedily per connected component
653
+ const unused = new Set(segs.map((_,i)=>i));
654
+ const startKey = new Map(); // pointKey -> Set(segIndex as start)
655
+ const endKey = new Map(); // pointKey -> Set(segIndex as end)
656
+ const addTo = (map, k, v)=>{ let s=map.get(k); if(!s){ s=new Set(); map.set(k,s);} s.add(v); };
657
+ segs.forEach((s,i)=>{ const a=s.pts[0], b=s.pts[s.pts.length-1]; addTo(startKey, key(a[0],a[1]), i); addTo(endKey, key(b[0],b[1]), i); });
658
+
659
+ const loopsInfo=[]; // { pts, segIDs }
660
+ while (unused.size){
661
+ // seed with any remaining segment
662
+ const seedIndex = unused.values().next().value;
663
+ unused.delete(seedIndex);
664
+ let chain = segs[seedIndex].pts.slice();
665
+ const usedSegs = [seedIndex];
666
+
667
+ let extended=true;
668
+ while(extended){
669
+ extended=false;
670
+ // try extend forward
671
+ const tail = chain[chain.length-1]; const tk = key(tail[0],tail[1]);
672
+ let nextIdx = null; let reverse=false;
673
+ for (const si of (startKey.get(tk)||[])) { if (unused.has(si)) { nextIdx=si; reverse=false; break; } }
674
+ if (nextIdx===null){ for (const ei of (endKey.get(tk)||[])) { if (unused.has(ei)) { nextIdx=ei; reverse=true; break; } } }
675
+ if (nextIdx!==null){
676
+ const pts = segs[nextIdx].pts;
677
+ const add = reverse ? pts.slice().reverse() : pts.slice();
678
+ // avoid duplicating joint point
679
+ chain.pop();
680
+ chain.push(...add);
681
+ unused.delete(nextIdx);
682
+ usedSegs.push(nextIdx);
683
+ extended=true;
684
+ continue;
685
+ }
686
+ // try extend backward
687
+ const head = chain[0]; const hk = key(head[0],head[1]);
688
+ nextIdx = null; reverse=false;
689
+ for (const ei of (endKey.get(hk)||[])) { if (unused.has(ei)) { nextIdx=ei; reverse=false; break; } }
690
+ if (nextIdx===null){ for (const si of (startKey.get(hk)||[])) { if (unused.has(si)) { nextIdx=si; reverse=true; break; } } }
691
+ if (nextIdx!==null){
692
+ const pts = segs[nextIdx].pts;
693
+ const add = reverse ? pts.slice().reverse() : pts.slice();
694
+ // avoid duplicating joint point
695
+ add.pop();
696
+ chain = add.concat(chain);
697
+ unused.delete(nextIdx);
698
+ usedSegs.push(nextIdx);
699
+ extended=true;
700
+ }
701
+ }
702
+
703
+ chain = dedupeConsecutive(chain);
704
+ if (chain.length < 3 || !closePt(chain[0], chain[chain.length-1])) {
705
+ openChains.push({ pts: chain.slice(), segIDs: usedSegs.slice() });
706
+ continue;
707
+ }
708
+ chain = ensureClosed(chain);
709
+ // Simplify to avoid near-collinear noise
710
+ let simple = chain.slice(0, chain.length-1);
711
+ simple = removeCollinear(simple);
712
+ simple.push(simple[0]);
713
+ if (simple.length>=4){ loopsInfo.push({ pts: simple, segIDs: usedSegs.slice() }); }
714
+ }
715
+
716
+ // Always expose sketch edges in 3D, even if no closed profile can be triangulated
717
+ for (const e of edges) {
718
+ sceneGroup.add(e);
719
+ }
720
+
721
+ // Classify loops (outer/holes) by nesting parity and normalize winding
722
+ const normalizedLoops = loopsInfo.map(obj=>{
723
+ const lp = obj.pts;
724
+ // ensure closed single duplicate at end
725
+ let l = lp.slice();
726
+ if (!closePt(l[0], l[l.length-1])) l.push([l[0][0], l[0][1]]);
727
+ // robust area; skip degenerate
728
+ const a = Math.abs(signedArea(l));
729
+ return a < 1e-12 ? null : l;
730
+ }).filter(Boolean);
731
+ // Keep segID lists aligned; drop those for degenerate loops we filtered
732
+ const loopSegIDs = [];
733
+ for (const info of loopsInfo){
734
+ const lp = info.pts;
735
+ let l = lp.slice();
736
+ if (!closePt(l[0], l[l.length-1])) l.push([l[0][0], l[0][1]]);
737
+ const a = Math.abs(signedArea(l));
738
+ if (a >= 1e-12) loopSegIDs.push(info.segIDs.slice());
739
+ }
740
+
741
+ // Compute depth (number of containers)
742
+ const depth = new Array(normalizedLoops.length).fill(0);
743
+ const repPoint = (loop)=>{
744
+ const n = loop.length; if (n===0) return [0,0];
745
+ const first = loop[0]; const last = loop[n-1];
746
+ const ring = (nearlyEqual(first[0], last[0]) && nearlyEqual(first[1], last[1])) ? loop.slice(0, n-1) : loop;
747
+ return ring[0];
748
+ };
749
+ const reps = normalizedLoops.map(repPoint);
750
+ for (let i=0;i<normalizedLoops.length;i++){
751
+ for (let j=0;j<normalizedLoops.length;j++){
752
+ if (i===j) continue;
753
+ if (pointInPoly(reps[i], normalizedLoops[j])) depth[i]++;
754
+ }
755
+ }
756
+
757
+ // Group into shapes: each even-depth loop is an outer; assign immediate odd-depth children as holes
758
+ const groups=[]; // { outer, holes: [] }
759
+ for (let i=0;i<normalizedLoops.length;i++){
760
+ if ((depth[i] % 2) === 0){
761
+ groups.push({ outer:i, holes:[] });
762
+ }
763
+ }
764
+ // Assign holes to nearest containing outer
765
+ for (let h=0; h<normalizedLoops.length; h++){
766
+ if ((depth[h] % 2) !== 1) continue; // only odd-depth are holes
767
+ // find smallest-depth containing outer
768
+ let bestOuter = -1; let bestOuterDepth = Infinity;
769
+ for (let g=0; g<groups.length; g++){
770
+ const oi = groups[g].outer;
771
+ if (pointInPoly(reps[h], normalizedLoops[oi])){
772
+ if (depth[oi] < bestOuterDepth){ bestOuter = g; bestOuterDepth = depth[oi]; }
773
+ }
774
+ }
775
+ if (bestOuter>=0) groups[bestOuter].holes.push(h);
776
+ }
777
+
778
+ // Triangulate groups using THREE.ShapeUtils.triangulateShape
779
+ let profileFace=null;
780
+ if (groups.length){
781
+ const triPositions = [];
782
+ const boundaryEdges = new Set();
783
+ const boundaryLoopsWorld = [];
784
+ const profileGroups = [];
785
+ const diagTriangles2D = [];
786
+ const diagTrianglesWorld = [];
787
+ for (const grp of groups){
788
+ // Prepare contour and holes (remove duplicate last point for API)
789
+ let contour = normalizedLoops[grp.outer].slice(); contour.pop();
790
+ // Earcut expects outer CW, holes CCW. Enforce CW for outer
791
+ if (signedArea([...contour, contour[0]]) > 0) contour = contour.slice().reverse();
792
+ // Record boundary edges for outer
793
+ for (const sid of (loopSegIDs[grp.outer] || [])) {
794
+ const e = edgeBySegId.get(segs[sid]?.id);
795
+ if (e) {
796
+ try { e.userData = e.userData || {}; e.userData.isHole = false; } catch {}
797
+ boundaryEdges.add(e);
798
+ }
799
+ }
800
+ const holes = grp.holes.map(idx=>{
801
+ let h = normalizedLoops[idx].slice(); h.pop();
802
+ // Ensure CCW for holes (outer is CW per earcut convention)
803
+ if (signedArea([...h, h[0]]) < 0) h = h.slice().reverse();
804
+ for (const sid of (loopSegIDs[idx] || [])) {
805
+ const e = edgeBySegId.get(segs[sid]?.id);
806
+ if (e) {
807
+ try { e.userData = e.userData || {}; e.userData.isHole = true; } catch {}
808
+ boundaryEdges.add(e);
809
+ }
810
+ }
811
+ return h;
812
+ });
813
+
814
+ const contourV2 = contour.map(p=> new THREE.Vector2(p[0], p[1]));
815
+ const holesV2 = holes.map(arr => arr.map(p=> new THREE.Vector2(p[0], p[1])));
816
+
817
+ // Triangulate using ShapeUtils (earcut) directly
818
+ const tris = THREE.ShapeUtils.triangulateShape(contourV2, holesV2);
819
+ const allPts = contour.concat(...holes);
820
+ for (const t of tris){
821
+ const a = allPts[t[0]], b = allPts[t[1]], c = allPts[t[2]];
822
+ triPositions.push(a[0],a[1],0, b[0],b[1],0, c[0],c[1],0);
823
+ diagTriangles2D.push([[a[0], a[1], 0], [b[0], b[1], 0], [c[0], c[1], 0]]);
824
+ try {
825
+ const wa = toWorld(a[0], a[1]);
826
+ const wb = toWorld(b[0], b[1]);
827
+ const wc = toWorld(c[0], c[1]);
828
+ diagTrianglesWorld.push([[wa.x, wa.y, wa.z], [wb.x, wb.y, wb.z], [wc.x, wc.y, wc.z]]);
829
+ } catch {}
830
+ }
831
+
832
+ // Save world-space loops for robust sweep side construction
833
+ const toW = (p)=> toWorld(p[0], p[1]);
834
+ const worldOuter = contour.map(p=>{ const v=toW(p); return [v.x,v.y,v.z]; });
835
+ const worldHoles = holes.map(h=> h.map(p=>{ const v=toW(p); return [v.x,v.y,v.z]; }));
836
+ boundaryLoopsWorld.push({ pts: worldOuter, isHole: false });
837
+ for (const h of worldHoles) boundaryLoopsWorld.push({ pts: h, isHole: true });
838
+ profileGroups.push({ contour2D: contour.slice(), holes2D: holes.map(h=>h.slice()), contourW: worldOuter.slice(), holesW: worldHoles.map(h=>h.slice()) });
839
+ }
840
+
841
+ const diagEdges = edges.map((e) => {
842
+ const ud = e?.userData || {};
843
+ const safePts = Array.isArray(ud.polylineLocal)
844
+ ? ud.polylineLocal.map((pt) => Array.isArray(pt) ? [Number(pt[0]) || 0, Number(pt[1]) || 0, Number(pt[2]) || 0] : pt)
845
+ : null;
846
+ return {
847
+ name: e?.name || null,
848
+ sketchGeometryId: ud.sketchGeometryId ?? null,
849
+ sketchFeatureId: ud.sketchFeatureId ?? null,
850
+ isHole: Boolean(ud.isHole),
851
+ sketchGeomType: ud.sketchGeomType || null,
852
+ arcCenter: Array.isArray(ud.arcCenter) ? ud.arcCenter.slice() : null,
853
+ arcRadius: typeof ud.arcRadius === 'number' ? ud.arcRadius : null,
854
+ circleCenter: Array.isArray(ud.circleCenter) ? ud.circleCenter.slice() : null,
855
+ circleRadius: typeof ud.circleRadius === 'number' ? ud.circleRadius : null,
856
+ polyline: safePts,
857
+ };
858
+ });
859
+ const diagOpenChains = openChains.map((chain) => chain.pts.map((pt) => [Number(pt[0]) || 0, Number(pt[1]) || 0]));
860
+
861
+ if (triPositions.length){
862
+ const geom2D = new THREE.BufferGeometry();
863
+ geom2D.setAttribute('position', new THREE.Float32BufferAttribute(triPositions,3));
864
+ // Map from plane to world
865
+ const m = new THREE.Matrix4();
866
+ const bO2 = new THREE.Vector3().fromArray(basis.origin);
867
+ const bX2 = new THREE.Vector3().fromArray(basis.x);
868
+ const bY2 = new THREE.Vector3().fromArray(basis.y);
869
+ const bZ2 = new THREE.Vector3().crossVectors(bX2,bY2).normalize();
870
+ m.makeBasis(bX2,bY2,bZ2); m.setPosition(bO2);
871
+ geom2D.applyMatrix4(m); geom2D.computeVertexNormals(); geom2D.computeBoundingSphere();
872
+ const face = new BREP.Face(geom2D);
873
+ face.name = `${sceneGroup.name}:PROFILE`;
874
+ face.userData.faceName = face.name;
875
+ face.edges = Array.from(boundaryEdges);
876
+ face.userData.boundaryLoopsWorld = boundaryLoopsWorld;
877
+ face.userData.profileGroups = profileGroups;
878
+ try {
879
+ const baseMat = face.material;
880
+ const sketchMat = (baseMat && typeof baseMat.clone === 'function') ? baseMat.clone() : null;
881
+ if (sketchMat) {
882
+ sketchMat.side = THREE.DoubleSide;
883
+ sketchMat.needsUpdate = true;
884
+ face.material = sketchMat;
885
+ face.userData.__baseMaterial = sketchMat;
886
+ }
887
+ } catch { }
888
+ sceneGroup.add(face);
889
+ profileFace = face;
890
+ this.persistentData.lastProfileDiagnostics = {
891
+ status: 'ok',
892
+ loops2D: normalizedLoops.map((loop) => loop.map((pt) => [Number(pt[0]) || 0, Number(pt[1]) || 0])),
893
+ loopDepth: depth.slice(),
894
+ loopSegmentIds: loopSegIDs.map((ids) => ids.slice()),
895
+ groups: groups.map((g) => ({ outer: g.outer, holes: g.holes.slice() })),
896
+ triangles2D: diagTriangles2D.map((tri) => tri.map((pt) => pt.slice())),
897
+ trianglesWorld: diagTrianglesWorld.map((tri) => tri.map((pt) => pt.slice())),
898
+ boundaryLoopsWorld: boundaryLoopsWorld.map((loop) => ({ isHole: Boolean(loop.isHole), pts: loop.pts.map((pt) => pt.slice()) })),
899
+ profileGroups: profileGroups.map((grp) => ({
900
+ contour2D: grp.contour2D.map((pt) => pt.slice()),
901
+ holes2D: grp.holes2D.map((hole) => hole.map((pt) => pt.slice())),
902
+ contourW: grp.contourW.map((pt) => pt.slice()),
903
+ holesW: grp.holesW.map((hole) => hole.map((pt) => pt.slice())),
904
+ })),
905
+ boundaryEdges: Array.from(boundaryEdges).map((edge) => edge?.name || null),
906
+ edges: diagEdges,
907
+ openChains2D: diagOpenChains,
908
+ triangleCount: diagTriangles2D.length,
909
+ };
910
+ } else {
911
+ this.persistentData.lastProfileDiagnostics = {
912
+ status: 'no-triangulation',
913
+ reason: 'Triangulation did not return any triangles',
914
+ loops2D: normalizedLoops.map((loop) => loop.map((pt) => [Number(pt[0]) || 0, Number(pt[1]) || 0])),
915
+ loopDepth: depth.slice(),
916
+ loopSegmentIds: loopSegIDs.map((ids) => ids.slice()),
917
+ groups: groups.map((g) => ({ outer: g.outer, holes: g.holes.slice() })),
918
+ triangles2D: [],
919
+ trianglesWorld: [],
920
+ boundaryLoopsWorld: boundaryLoopsWorld.map((loop) => ({ isHole: Boolean(loop.isHole), pts: loop.pts.map((pt) => pt.slice()) })),
921
+ profileGroups: profileGroups.map((grp) => ({
922
+ contour2D: grp.contour2D.map((pt) => pt.slice()),
923
+ holes2D: grp.holes2D.map((hole) => hole.map((pt) => pt.slice())),
924
+ contourW: grp.contourW.map((pt) => pt.slice()),
925
+ holesW: grp.holesW.map((hole) => hole.map((pt) => pt.slice())),
926
+ })),
927
+ boundaryEdges: Array.from(boundaryEdges).map((edge) => edge?.name || null),
928
+ edges: diagEdges,
929
+ openChains2D: diagOpenChains,
930
+ triangleCount: 0,
931
+ };
932
+ }
933
+ } else {
934
+ this.persistentData.lastProfileDiagnostics = {
935
+ status: 'no-profile',
936
+ reason: 'No closed sketch loops available for triangulation',
937
+ loops2D: normalizedLoops.map((loop) => loop.map((pt) => [Number(pt[0]) || 0, Number(pt[1]) || 0])),
938
+ loopDepth: [],
939
+ loopSegmentIds: [],
940
+ groups: [],
941
+ triangles2D: [],
942
+ trianglesWorld: [],
943
+ boundaryLoopsWorld: [],
944
+ profileGroups: [],
945
+ boundaryEdges: [],
946
+ edges: [],
947
+ openChains2D: openChains.map((chain) => chain.pts.map((pt) => [Number(pt[0]) || 0, Number(pt[1]) || 0])),
948
+ triangleCount: 0,
949
+ };
950
+ }
951
+
952
+ this._updateSketchChangeState(this.persistentData?.sketch || sketch);
953
+ return { added: [sceneGroup], removed: [] };
954
+ }
955
+ }