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,634 @@
1
+ import * as THREE from 'three';
2
+
3
+ // ---------------------------------------------
4
+ // MeshRepairer: weld / T-junction fix / remove overlaps / hole fill / fix triangle normals
5
+ // ---------------------------------------------
6
+ export class MeshRepairer {
7
+ constructor() { }
8
+
9
+ // ---------- Helpers ----------
10
+ static _ensureIndexed(geom) {
11
+ if (!geom.index) {
12
+ const count = geom.attributes.position.count;
13
+ const idx = new Uint32Array(count);
14
+ for (let i = 0; i < count; i++) idx[i] = i;
15
+ geom.setIndex(new THREE.BufferAttribute(idx, 1));
16
+ }
17
+ return geom;
18
+ }
19
+ static _getArrays(geom) {
20
+ const pos = geom.attributes.position.array;
21
+ const uv = geom.attributes.uv ? geom.attributes.uv.array : null;
22
+ const norm = geom.attributes.normal ? geom.attributes.normal.array : null;
23
+ const idx = geom.index.array;
24
+ return { pos, uv, norm, idx };
25
+ }
26
+ static _vec3Of(arr, i) { const o = 3 * i; return [arr[o], arr[o + 1], arr[o + 2]]; }
27
+ static _sub(a, b) { return [a[0] - b[0], a[1] - b[1], a[2] - b[2]]; }
28
+ static _dot(a, b) { return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; }
29
+ static _len2(a) { return this._dot(a, a); }
30
+ static _cross(a, b) { return [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]; }
31
+ static _norm(a) { const l = Math.sqrt(this._len2(a)) || 1; return [a[0] / l, a[1] / l, a[2] / l]; }
32
+ static _add(a, b) { return [a[0] + b[0], a[1] + b[1], a[2] + b[2]]; }
33
+ static _scale(a, s) { return [a[0] * s, a[1] * s, a[2] * s]; }
34
+ static _triangleArea2(a, b, c) {
35
+ const ab = this._sub(b, a), ac = this._sub(c, a);
36
+ const cr = this._cross(ab, ac);
37
+ return Math.sqrt(this._dot(cr, cr));
38
+ }
39
+ static _edgeKey(i, j) { return i < j ? `${i}/${j}` : `${j}/${i}`; }
40
+
41
+ static _newellNormal(points) {
42
+ let nx = 0, ny = 0, nz = 0;
43
+ for (let i = 0; i < points.length; i++) {
44
+ const a = points[i], b = points[(i + 1) % points.length];
45
+ nx += (a[1] - b[1]) * (a[2] + b[2]);
46
+ ny += (a[2] - b[2]) * (a[0] + b[0]);
47
+ nz += (a[0] - b[0]) * (a[1] + b[1]);
48
+ }
49
+ return this._norm([nx, ny, nz]);
50
+ }
51
+ static _basisFromNormal(n) {
52
+ const ax = Math.abs(n[0]), ay = Math.abs(n[1]), az = Math.abs(n[2]);
53
+ const t = ax < 0.9 ? [1, 0, 0] : (ay < 0.9 ? [0, 1, 0] : [0, 0, 1]);
54
+ const u = this._norm(this._cross(t, n));
55
+ const v = this._norm(this._cross(n, u));
56
+ return { u, v, n };
57
+ }
58
+ static _projectToPlane(points, basis) {
59
+ const { u, v } = basis;
60
+ return points.map(p => [this._dot(p, u), this._dot(p, v)]);
61
+ }
62
+ static _polyArea2D(poly) {
63
+ let a = 0;
64
+ for (let i = 0; i < poly.length; i++) {
65
+ const [x1, y1] = poly[i], [x2, y2] = poly[(i + 1) % poly.length];
66
+ a += x1 * y2 - x2 * y1;
67
+ }
68
+ return a * 0.5;
69
+ }
70
+ static _pointInTri2D(p, a, b, c) {
71
+ const v0 = [c[0] - a[0], c[1] - a[1]];
72
+ const v1 = [b[0] - a[0], b[1] - a[1]];
73
+ const v2 = [p[0] - a[0], p[1] - a[1]];
74
+ const den = v0[0] * v1[1] - v1[0] * v0[1];
75
+ if (Math.abs(den) < 1e-20) return false;
76
+ const u = (v2[0] * v1[1] - v1[0] * v2[1]) / den;
77
+ const v = (v0[0] * v2[1] - v2[0] * v0[1]) / den;
78
+ return u >= -1e-10 && v >= -1e-10 && (u + v) <= 1 + 1e-10;
79
+ }
80
+ static _isConvex2D(prev, curr, next, sign) {
81
+ const ax = curr[0] - prev[0], ay = curr[1] - prev[1];
82
+ const bx = next[0] - curr[0], by = next[1] - curr[1];
83
+ const cross = ax * by - ay * bx;
84
+ return sign > 0 ? cross > 1e-20 : cross < -1e-20;
85
+ }
86
+ static _earClip2D(loop2D) {
87
+ const n = loop2D.length;
88
+ if (n < 3) return [];
89
+ const idx = Array.from({ length: n }, (_, i) => i);
90
+ const area = this._polyArea2D(loop2D);
91
+ const sign = area >= 0 ? 1 : -1;
92
+ if (sign < 0) idx.reverse();
93
+ const tris = [];
94
+ let counter = 0, maxIters = n * n;
95
+ while (idx.length > 3 && counter++ < maxIters) {
96
+ let ear = false;
97
+ for (let i = 0; i < idx.length; i++) {
98
+ const i0 = idx[(i - 1 + idx.length) % idx.length], i1 = idx[i], i2 = idx[(i + 1) % idx.length];
99
+ const a = loop2D[i0], b = loop2D[i1], c = loop2D[i2];
100
+ if (!this._isConvex2D(a, b, c, 1)) continue;
101
+ let inside = false;
102
+ for (let k = 0; k < idx.length; k++) {
103
+ const ik = idx[k];
104
+ if (ik === i0 || ik === i1 || ik === i2) continue;
105
+ if (this._pointInTri2D(loop2D[ik], a, b, c)) { inside = true; break; }
106
+ }
107
+ if (!inside) { tris.push([i0, i1, i2]); idx.splice(i, 1); ear = true; break; }
108
+ }
109
+ if (!ear) break;
110
+ }
111
+ if (idx.length === 3) tris.push([idx[0], idx[1], idx[2]]);
112
+ return tris;
113
+ }
114
+ static _buildEdgeUse(idx) {
115
+ const map = new Map(); // key -> {count, faces:[{tri, i, j, k}]}
116
+ for (let t = 0; t < idx.length; t += 3) {
117
+ const a = idx[t], b = idx[t + 1], c = idx[t + 2];
118
+ const edges = [[a, b, c], [b, c, a], [c, a, b]];
119
+ for (const [i, j, k] of edges) {
120
+ const key = this._edgeKey(i, j);
121
+ let e = map.get(key);
122
+ if (!e) { e = { count: 0, faces: [] }; map.set(key, e); }
123
+ e.count++;
124
+ e.faces.push({ tri: t, i, j, k });
125
+ }
126
+ }
127
+ return map;
128
+ }
129
+ static _boundaryEdges(edgeUse) {
130
+ const boundary = [];
131
+ for (const [key, e] of edgeUse.entries()) {
132
+ if (e.count === 1) {
133
+ const [a, b] = key.split('/').map(s => parseInt(s, 10));
134
+ boundary.push([a, b]);
135
+ }
136
+ }
137
+ return boundary;
138
+ }
139
+ static _buildBoundaryLoops(boundaryEdges) {
140
+ const adj = new Map();
141
+ for (const [a, b] of boundaryEdges) {
142
+ if (!adj.has(a)) adj.set(a, new Set());
143
+ if (!adj.has(b)) adj.set(b, new Set());
144
+ adj.get(a).add(b); adj.get(b).add(a);
145
+ }
146
+ const visited = new Set();
147
+ const loops = [];
148
+ const edgeId = (u, v) => u < v ? `${u}-${v}` : `${v}-${u}`;
149
+ for (const [start, nbrs] of adj.entries()) {
150
+ for (const n of nbrs) {
151
+ const eid = edgeId(start, n);
152
+ if (visited.has(eid)) continue;
153
+ const loop = [start, n];
154
+ visited.add(eid);
155
+ let prev = start, curr = n;
156
+ while (true) {
157
+ const neighbors = Array.from(adj.get(curr) || []);
158
+ const next = neighbors.find(x => x !== prev && !visited.has(edgeId(curr, x)));
159
+ if (next == null) break;
160
+ visited.add(edgeId(curr, next));
161
+ loop.push(next);
162
+ prev = curr; curr = next;
163
+ if (next === start) break;
164
+ }
165
+ if (loop.length >= 3 && loop[0] === loop[loop.length - 1]) {
166
+ loop.pop(); loops.push(loop);
167
+ }
168
+ }
169
+ }
170
+ return loops;
171
+ }
172
+
173
+ // ---------- 1) Weld identical/nearby vertices ----------
174
+ weldVertices(geometry, epsilon = 1e-4) {
175
+ MeshRepairer._ensureIndexed(geometry);
176
+ const { pos, uv, norm, idx } = MeshRepairer._getArrays(geometry);
177
+
178
+ const quant = (v) => [Math.round(v[0] / epsilon), Math.round(v[1] / epsilon), Math.round(v[2] / epsilon)].join(',');
179
+ const map = new Map(); // key -> new index
180
+ const sums = [];
181
+ const remap = new Uint32Array(geometry.attributes.position.count);
182
+
183
+ for (let i = 0; i < remap.length; i++) {
184
+ const p = MeshRepairer._vec3Of(pos, i);
185
+ const key = quant(p);
186
+ let ni = map.get(key);
187
+ if (ni === undefined) {
188
+ ni = sums.length; map.set(key, ni);
189
+ sums.push({
190
+ pos: [p[0], p[1], p[2]],
191
+ uv: uv ? [uv[2 * i], uv[2 * i + 1]] : null,
192
+ norm: norm ? [norm[3 * i], norm[3 * i + 1], norm[3 * i + 2]] : null,
193
+ count: 1
194
+ });
195
+ } else {
196
+ const s = sums[ni];
197
+ s.pos[0] += p[0]; s.pos[1] += p[1]; s.pos[2] += p[2];
198
+ if (s.uv) { s.uv[0] += uv[2 * i]; s.uv[1] += uv[2 * i + 1]; }
199
+ if (s.norm) { s.norm[0] += norm[3 * i]; s.norm[1] += norm[3 * i + 1]; s.norm[2] += norm[3 * i + 2]; }
200
+ s.count++;
201
+ }
202
+ remap[i] = ni;
203
+ }
204
+
205
+ const newPos = new Float32Array(sums.length * 3);
206
+ const newUv = uv ? new Float32Array(sums.length * 2) : null;
207
+ const newNo = norm ? new Float32Array(sums.length * 3) : null;
208
+
209
+ for (let i = 0; i < sums.length; i++) {
210
+ const s = sums[i], inv = 1 / s.count;
211
+ newPos[3 * i] = s.pos[0] * inv; newPos[3 * i + 1] = s.pos[1] * inv; newPos[3 * i + 2] = s.pos[2] * inv;
212
+ if (newUv) { newUv[2 * i] = s.uv[0] * inv; newUv[2 * i + 1] = s.uv[1] * inv; }
213
+ if (newNo) {
214
+ const n = MeshRepairer._norm([s.norm[0], s.norm[1], s.norm[2]]);
215
+ newNo[3 * i] = n[0]; newNo[3 * i + 1] = n[1]; newNo[3 * i + 2] = n[2];
216
+ }
217
+ }
218
+
219
+ const outIdx = [];
220
+ for (let t = 0; t < idx.length; t += 3) {
221
+ const a = remap[idx[t]], b = remap[idx[t + 1]], c = remap[idx[t + 2]];
222
+ if (a === b || b === c || c === a) continue;
223
+ const A = MeshRepairer._vec3Of(newPos, a);
224
+ const B = MeshRepairer._vec3Of(newPos, b);
225
+ const C = MeshRepairer._vec3Of(newPos, c);
226
+ if (MeshRepairer._triangleArea2(A, B, C) < 1e-20) continue;
227
+ outIdx.push(a, b, c);
228
+ }
229
+
230
+ const out = new THREE.BufferGeometry();
231
+ out.setAttribute('position', new THREE.BufferAttribute(newPos, 3));
232
+ if (newUv) out.setAttribute('uv', new THREE.BufferAttribute(newUv, 2));
233
+ if (newNo) out.setAttribute('normal', new THREE.BufferAttribute(newNo, 3));
234
+ out.setIndex(outIdx);
235
+ out.computeVertexNormals();
236
+ return out;
237
+ }
238
+
239
+ // ---------- 2) Fix T-junctions ----------
240
+ fixTJunctions(geometry, lineEps = 5e-4, gridCell = 0.01) {
241
+ MeshRepairer._ensureIndexed(geometry);
242
+ const pos = geometry.attributes.position.array;
243
+ const vertCount = pos.length / 3;
244
+ const idx = Array.from(geometry.index.array);
245
+
246
+ // Precompute triangle planes (unit normal + offset) for coplanarity checks
247
+ const triCount = Math.floor(idx.length / 3);
248
+ const triN = new Float32Array(triCount * 3);
249
+ const triD = new Float32Array(triCount);
250
+ for (let t = 0; t < triCount; t++) {
251
+ const ia = idx[3 * t], ib = idx[3 * t + 1], ic = idx[3 * t + 2];
252
+ const ax = pos[3 * ia], ay = pos[3 * ia + 1], az = pos[3 * ia + 2];
253
+ const bx = pos[3 * ib], by = pos[3 * ib + 1], bz = pos[3 * ib + 2];
254
+ const cx = pos[3 * ic], cy = pos[3 * ic + 1], cz = pos[3 * ic + 2];
255
+ const abx = bx - ax, aby = by - ay, abz = bz - az;
256
+ const acx = cx - ax, acy = cy - ay, acz = cz - az;
257
+ let nx = aby * acz - abz * acy;
258
+ let ny = abz * acx - abx * acz;
259
+ let nz = abx * acy - aby * acx;
260
+ const len = Math.hypot(nx, ny, nz);
261
+ if (len > 1e-20) {
262
+ nx /= len; ny /= len; nz /= len;
263
+ triN[3 * t] = nx; triN[3 * t + 1] = ny; triN[3 * t + 2] = nz;
264
+ triD[t] = -(nx * ax + ny * ay + nz * az);
265
+ } else {
266
+ triN[3 * t] = 0; triN[3 * t + 1] = 0; triN[3 * t + 2] = 0;
267
+ triD[t] = 0;
268
+ }
269
+ }
270
+
271
+ // Build edge usage to know which planes are relevant for an edge
272
+ const edgeUse = MeshRepairer._buildEdgeUse(idx);
273
+
274
+ // Spatial grid of vertices (hash grid)
275
+ const cellKey = (x, y, z) => `${Math.floor(x / gridCell)}|${Math.floor(y / gridCell)}|${Math.floor(z / gridCell)}`;
276
+ const grid = new Map();
277
+ for (let i = 0; i < vertCount; i++) {
278
+ const x = pos[3 * i], y = pos[3 * i + 1], z = pos[3 * i + 2];
279
+ const k = cellKey(x, y, z);
280
+ let arr = grid.get(k); if (!arr) { arr = []; grid.set(k, arr); }
281
+ arr.push(i);
282
+ }
283
+
284
+ // Fast per-edge candidate collector using 3D DDA voxel traversal along the segment
285
+ function candidatesNearEdge(i, j) {
286
+ const ax = pos[3 * i], ay = pos[3 * i + 1], az = pos[3 * i + 2];
287
+ const bx = pos[3 * j], by = pos[3 * j + 1], bz = pos[3 * j + 2];
288
+
289
+ // If the edge is extremely short, just query its containing cell
290
+ const dx = bx - ax, dy = by - ay, dz = bz - az;
291
+ const len2 = dx * dx + dy * dy + dz * dz;
292
+ const set = new Set();
293
+ if (len2 < 1e-24) {
294
+ const key = cellKey(ax, ay, az);
295
+ const arr = grid.get(key);
296
+ if (arr) for (let k = 0; k < arr.length; k++) set.add(arr[k]);
297
+ return set;
298
+ }
299
+
300
+ // Amanatides & Woo 3D DDA for grid traversal
301
+ let ix = Math.floor(ax / gridCell), iy = Math.floor(ay / gridCell), iz = Math.floor(az / gridCell);
302
+ const ix1 = Math.floor(bx / gridCell), iy1 = Math.floor(by / gridCell), iz1 = Math.floor(bz / gridCell);
303
+
304
+ const stepX = dx > 0 ? 1 : (dx < 0 ? -1 : 0);
305
+ const stepY = dy > 0 ? 1 : (dy < 0 ? -1 : 0);
306
+ const stepZ = dz > 0 ? 1 : (dz < 0 ? -1 : 0);
307
+
308
+ const invDx = dx !== 0 ? 1 / Math.abs(dx) : 0;
309
+ const invDy = dy !== 0 ? 1 / Math.abs(dy) : 0;
310
+ const invDz = dz !== 0 ? 1 / Math.abs(dz) : 0;
311
+
312
+ // Compute param t at which we cross first voxel boundary on each axis
313
+ let tMaxX;
314
+ if (stepX > 0) tMaxX = ((ix + 1) * gridCell - ax) * invDx;
315
+ else if (stepX < 0) tMaxX = (ax - ix * gridCell) * invDx;
316
+ else tMaxX = Infinity;
317
+ let tMaxY;
318
+ if (stepY > 0) tMaxY = ((iy + 1) * gridCell - ay) * invDy;
319
+ else if (stepY < 0) tMaxY = (ay - iy * gridCell) * invDy;
320
+ else tMaxY = Infinity;
321
+ let tMaxZ;
322
+ if (stepZ > 0) tMaxZ = ((iz + 1) * gridCell - az) * invDz;
323
+ else if (stepZ < 0) tMaxZ = (az - iz * gridCell) * invDz;
324
+ else tMaxZ = Infinity;
325
+ const tDeltaX = stepX !== 0 ? gridCell * invDx : Infinity;
326
+ const tDeltaY = stepY !== 0 ? gridCell * invDy : Infinity;
327
+ const tDeltaZ = stepZ !== 0 ? gridCell * invDz : Infinity;
328
+
329
+ // Visit cells until reaching the end cell; include both endpoints' cells
330
+ const maxVisits = 1 + Math.abs(ix1 - ix) + Math.abs(iy1 - iy) + Math.abs(iz1 - iz) + 2; // small cushion
331
+ let visits = 0;
332
+ while (true) {
333
+ const key = `${ix}|${iy}|${iz}`;
334
+ const arr = grid.get(key);
335
+ if (arr) { for (let k = 0; k < arr.length; k++) set.add(arr[k]); }
336
+ if (ix === ix1 && iy === iy1 && iz === iz1) break;
337
+ if (++visits > maxVisits) break; // safety
338
+ // Step along the smallest tMax
339
+ if (tMaxX <= tMaxY && tMaxX <= tMaxZ) { ix += stepX; tMaxX += tDeltaX; }
340
+ else if (tMaxY <= tMaxX && tMaxY <= tMaxZ) { iy += stepY; tMaxY += tDeltaY; }
341
+ else { iz += stepZ; tMaxZ += tDeltaZ; }
342
+ }
343
+ return set; // return Set to avoid duplicate work
344
+ }
345
+
346
+ // Return t in (0,1) if p projects inside segment ab and is within lineEps; else null
347
+ function onSegmentParam(i, j, p) {
348
+ const ax = pos[3 * i], ay = pos[3 * i + 1], az = pos[3 * i + 2];
349
+ const bx = pos[3 * j], by = pos[3 * j + 1], bz = pos[3 * j + 2];
350
+ const px = pos[3 * p], py = pos[3 * p + 1], pz = pos[3 * p + 2];
351
+ const abx = bx - ax, aby = by - ay, abz = bz - az;
352
+ const apx = px - ax, apy = py - ay, apz = pz - az;
353
+ const ab2 = abx * abx + aby * aby + abz * abz;
354
+ if (ab2 < 1e-24) return null;
355
+ const t = (apx * abx + apy * aby + apz * abz) / ab2;
356
+ if (t <= 1e-6 || t >= 1 - 1e-6) return null;
357
+ const qx = ax + abx * t, qy = ay + aby * t, qz = az + abz * t;
358
+ const dx = px - qx, dy = py - qy, dz = pz - qz;
359
+ if (dx * dx + dy * dy + dz * dz <= lineEps * lineEps) return t;
360
+ return null;
361
+ }
362
+
363
+ // Plane filter: p must be near at least one incident triangle plane
364
+ const planeEps = Math.max(lineEps * 2, 1e-7);
365
+ function nearIncidentPlane(i, j, p) {
366
+ const key = MeshRepairer._edgeKey(i, j);
367
+ const e = edgeUse.get(key);
368
+ if (!e) return true; // fallback if unexpected
369
+ const px = pos[3 * p], py = pos[3 * p + 1], pz = pos[3 * p + 2];
370
+ for (let f = 0; f < e.faces.length; f++) {
371
+ const triBase = e.faces[f].tri; // base index in idx array
372
+ const t = Math.floor(triBase / 3);
373
+ const nx = triN[3 * t], ny = triN[3 * t + 1], nz = triN[3 * t + 2];
374
+ const d = triD[t];
375
+ const dist = Math.abs(nx * px + ny * py + nz * pz + d);
376
+ if (dist <= planeEps) return true;
377
+ }
378
+ return false;
379
+ }
380
+
381
+ // Collect splits per unique edge with coplanarity filter
382
+ const splits = new Map(); // edge key -> [{p,t}] (t is along min->max orientation)
383
+ function addSplit(i, j, p, t) {
384
+ const key = MeshRepairer._edgeKey(i, j);
385
+ const tt = i < j ? t : 1 - t;
386
+ let arr = splits.get(key); if (!arr) { arr = []; splits.set(key, arr); }
387
+ // avoid duplicates
388
+ for (let k = 0; k < arr.length; k++) { if (arr[k].p === p) return; }
389
+ arr.push({ p, t: tt });
390
+ }
391
+
392
+ // Iterate each unique unordered edge exactly once
393
+ for (const [ekey, e] of edgeUse.entries()) {
394
+ const parts = ekey.split('/');
395
+ const i = parseInt(parts[0], 10);
396
+ const j = parseInt(parts[1], 10);
397
+ const candSet = candidatesNearEdge(i, j);
398
+ for (const p of candSet) {
399
+ if (p === i || p === j) continue;
400
+ if (!nearIncidentPlane(i, j, p)) continue;
401
+ const tp = onSegmentParam(i, j, p);
402
+ if (tp !== null) addSplit(i, j, p, tp);
403
+ }
404
+ }
405
+ for (const arr of splits.values()) arr.sort((a, b) => a.t - b.t);
406
+
407
+ // Helper to get splits in i->j order (return vertex indices only)
408
+ function orientedSplits(i, j) {
409
+ const arr = splits.get(MeshRepairer._edgeKey(i, j)) || [];
410
+ if (!arr.length) return [];
411
+ if (i < j) return arr.map(s => s.p);
412
+ const r = new Array(arr.length);
413
+ for (let k = 0; k < arr.length; k++) r[k] = arr[arr.length - 1 - k].p;
414
+ return r;
415
+ }
416
+
417
+ let totalSplitsApplied = 0;
418
+ const newIdx = [];
419
+ for (let t = 0; t < idx.length; t += 3) {
420
+ const a = idx[t], b = idx[t + 1], c = idx[t + 2];
421
+ const sAB = orientedSplits(a, b);
422
+ const sBC = orientedSplits(b, c);
423
+ const sCA = orientedSplits(c, a);
424
+ const polyLen = 3 + sAB.length + sBC.length + sCA.length;
425
+ if (polyLen === 3) {
426
+ newIdx.push(a, b, c);
427
+ continue;
428
+ }
429
+ // Fan triangulation from the first vertex of the original tri
430
+ const poly = new Array(polyLen);
431
+ let w = 0;
432
+ poly[w++] = a; for (let k = 0; k < sAB.length; k++) poly[w++] = sAB[k];
433
+ poly[w++] = b; for (let k = 0; k < sBC.length; k++) poly[w++] = sBC[k];
434
+ poly[w++] = c; for (let k = 0; k < sCA.length; k++) poly[w++] = sCA[k];
435
+ for (let i = 1; i < poly.length - 1; i++) newIdx.push(poly[0], poly[i], poly[i + 1]);
436
+ totalSplitsApplied += poly.length - 3;
437
+ }
438
+
439
+ const out = new THREE.BufferGeometry();
440
+ out.setAttribute('position', geometry.attributes.position);
441
+ if (geometry.attributes.uv) out.setAttribute('uv', geometry.attributes.uv);
442
+ if (geometry.attributes.normal) out.setAttribute('normal', geometry.attributes.normal);
443
+ out.setIndex(newIdx);
444
+ out.computeVertexNormals();
445
+ out.userData.__tjunctionSplits = totalSplitsApplied;
446
+ return out;
447
+ }
448
+
449
+ // ---------- 3) Remove overlapping triangles ----------
450
+ removeOverlappingTriangles(geometry, posEps = 1e-6) {
451
+ MeshRepairer._ensureIndexed(geometry);
452
+ const pos = geometry.attributes.position.array;
453
+ const idx = Array.from(geometry.index.array);
454
+ const keyOf = (a, b, c) => {
455
+ const pts = [a, b, c].map(i => [pos[3 * i], pos[3 * i + 1], pos[3 * i + 2]]);
456
+ pts.sort((p, q) => p[0] - q[0] || p[1] - q[1] || p[2] - q[2]);
457
+ return pts.map(p => {
458
+ return `${Math.round(p[0] / posEps)}|${Math.round(p[1] / posEps)}|${Math.round(p[2] / posEps)}`;
459
+ }).join("|");
460
+ };
461
+ const seen = new Set();
462
+ const newIdx = [];
463
+ let removed = 0;
464
+ for (let t = 0; t < idx.length; t += 3) {
465
+ const key = keyOf(idx[t], idx[t + 1], idx[t + 2]);
466
+ if (seen.has(key)) { removed++; continue; }
467
+ seen.add(key);
468
+ newIdx.push(idx[t], idx[t + 1], idx[t + 2]);
469
+ }
470
+ const out = new THREE.BufferGeometry();
471
+ out.setAttribute('position', geometry.attributes.position);
472
+ if (geometry.attributes.uv) out.setAttribute('uv', geometry.attributes.uv);
473
+ if (geometry.attributes.normal) out.setAttribute('normal', geometry.attributes.normal);
474
+ out.setIndex(newIdx);
475
+ out.computeVertexNormals();
476
+ out.userData.__overlapsRemoved = removed;
477
+ return out;
478
+ }
479
+
480
+ // ---------- 4) Fill holes ----------
481
+ fillHoles(geometry) {
482
+ MeshRepairer._ensureIndexed(geometry);
483
+ const { pos } = MeshRepairer._getArrays(geometry);
484
+ const idx = Array.from(geometry.index.array);
485
+
486
+ const edgeUse = MeshRepairer._buildEdgeUse(idx);
487
+ const boundary = MeshRepairer._boundaryEdges(edgeUse);
488
+ const loops = MeshRepairer._buildBoundaryLoops(boundary);
489
+
490
+ const addedTris = [];
491
+ for (const loop of loops) {
492
+ if (loop.length < 3) continue;
493
+ const pts3 = loop.map(i => MeshRepairer._vec3Of(pos, i));
494
+ const n = MeshRepairer._newellNormal(pts3);
495
+ const basis = MeshRepairer._basisFromNormal(n);
496
+ const loop2D = MeshRepairer._projectToPlane(pts3, basis);
497
+ const area = MeshRepairer._polyArea2D(loop2D);
498
+ const order = area >= 0 ? loop.slice() : loop.slice().reverse();
499
+ const loop2Dccw = area >= 0 ? loop2D : loop2D.slice().reverse();
500
+ const trisLocal = MeshRepairer._earClip2D(loop2Dccw);
501
+ for (const [i0, i1, i2] of trisLocal) {
502
+ addedTris.push(order[i0], order[i1], order[i2]);
503
+ }
504
+ }
505
+
506
+ const out = new THREE.BufferGeometry();
507
+ out.setAttribute('position', geometry.attributes.position);
508
+ if (geometry.attributes.uv) out.setAttribute('uv', geometry.attributes.uv);
509
+ if (geometry.attributes.normal) out.setAttribute('normal', geometry.attributes.normal);
510
+ out.setIndex(idx.concat(addedTris));
511
+ out.computeVertexNormals();
512
+ out.userData.__boundaryEdges = boundary.length;
513
+ out.userData.__holesFilled = addedTris.length / 3;
514
+ this.fixTriangleNormals(out);
515
+ this.fixTriangleNormals(out);
516
+ this.fixTriangleNormals(out);
517
+ return out;
518
+ }
519
+
520
+ // (removed) 4.5) Split self-intersections and discard interior triangles
521
+
522
+ // ---------- 5) Fix triangle normals (consistent winding + outward orientation) ----------
523
+ fixTriangleNormals(geometry) {
524
+ MeshRepairer._ensureIndexed(geometry);
525
+ const idx = Array.from(geometry.index.array);
526
+ const pos = geometry.attributes.position.array;
527
+
528
+ // For each unordered edge, track the two triangles and their local edge directions
529
+ const triCount = idx.length / 3;
530
+ const triVerts = Array.from({ length: triCount }, (_, t) => [idx[3 * t], idx[3 * t + 1], idx[3 * t + 2]]);
531
+ const edgeToTris = new Map(); // key -> [{t, dir:+1/-1 w.r.t (i->j) in that tri}, ...]
532
+
533
+ function addEdge(t, i, j) {
534
+ const key = MeshRepairer._edgeKey(i, j);
535
+ const dir = (i < j) ? +1 : -1; // store whether tri uses (min->max) or (max->min)
536
+ if (!edgeToTris.has(key)) edgeToTris.set(key, []);
537
+ edgeToTris.get(key).push({ t, dir });
538
+ }
539
+
540
+ for (let t = 0; t < triCount; t++) {
541
+ const [a, b, c] = triVerts[t];
542
+ addEdge(t, a, b);
543
+ addEdge(t, b, c);
544
+ addEdge(t, c, a);
545
+ }
546
+
547
+ // BFS to make windings consistent
548
+ const visited = new Uint8Array(triCount);
549
+ const flip = new Uint8Array(triCount); // 0 keep, 1 flip
550
+
551
+ for (let seed = 0; seed < triCount; seed++) {
552
+ if (visited[seed]) continue;
553
+ visited[seed] = 1;
554
+ flip[seed] = 0;
555
+ const q = [seed];
556
+
557
+ while (q.length) {
558
+ const t = q.shift();
559
+ const [a, b, c] = triVerts[t];
560
+ const edges = [[a, b], [b, c], [c, a]];
561
+
562
+ for (const [i, j] of edges) {
563
+ const key = MeshRepairer._edgeKey(i, j);
564
+ const group = edgeToTris.get(key) || [];
565
+ // Determine our local direction for this unordered edge:
566
+ const ourDir = (i < j) ? +1 : -1;
567
+
568
+ for (const { t: u, dir: neighborDir } of group) {
569
+ if (u === t || visited[u]) continue;
570
+
571
+ // If both triangles reference the shared edge with the SAME direction,
572
+ // neighbor must be flipped relative to us; else keep parity.
573
+ const sameDirection = (neighborDir === ourDir);
574
+ flip[u] = flip[t] ^ (sameDirection ? 1 : 0);
575
+ visited[u] = 1;
576
+ q.push(u);
577
+ }
578
+ }
579
+ }
580
+ }
581
+
582
+ // Apply flips for consistency
583
+ for (let t = 0; t < triCount; t++) {
584
+ if (flip[t]) {
585
+ const base = 3 * t;
586
+ const tmp = idx[base + 1];
587
+ idx[base + 1] = idx[base + 2];
588
+ idx[base + 2] = tmp;
589
+ // Update triVerts for volume calc
590
+ const [a, b, c] = triVerts[t];
591
+ triVerts[t] = [a, c, b];
592
+ }
593
+ }
594
+
595
+ // Outward orientation using signed volume (if roughly closed)
596
+ let vol6 = 0.0;
597
+ for (let t = 0; t < triCount; t++) {
598
+ const [a, b, c] = triVerts[t];
599
+ const A = MeshRepairer._vec3Of(pos, a);
600
+ const B = MeshRepairer._vec3Of(pos, b);
601
+ const C = MeshRepairer._vec3Of(pos, c);
602
+ const AxB = MeshRepairer._cross(A, B);
603
+ vol6 += MeshRepairer._dot(AxB, C);
604
+ }
605
+ if (Math.abs(vol6) > 1e-18 && vol6 < 0) {
606
+ for (let t = 0; t < idx.length; t += 3) {
607
+ const tmp = idx[t + 1];
608
+ idx[t + 1] = idx[t + 2];
609
+ idx[t + 2] = tmp;
610
+ }
611
+ }
612
+
613
+ const out = new THREE.BufferGeometry();
614
+ out.setAttribute('position', geometry.attributes.position);
615
+ if (geometry.attributes.uv) out.setAttribute('uv', geometry.attributes.uv);
616
+ if (geometry.attributes.normal) out.setAttribute('normal', geometry.attributes.normal);
617
+ out.setIndex(idx);
618
+ out.computeVertexNormals();
619
+ return out;
620
+ }
621
+
622
+ // ---------- Convenience pipeline ----------
623
+ repairAll(geometry, { weldEps = 5e-4, lineEps = 5e-4, gridCell = 0.01 } = {}) {
624
+ let g = this.weldVertices(geometry, weldEps);
625
+ g = this.fixTJunctions(g, lineEps, gridCell);
626
+ g = this.removeOverlappingTriangles(g);
627
+ g = this.fillHoles(g);
628
+ g = this.fixTriangleNormals(g);
629
+ g = this.fillHoles(g);
630
+ g = this.fixTriangleNormals(g);
631
+ g.computeVertexNormals();
632
+ return g;
633
+ }
634
+ }