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,2512 @@
1
+ /**
2
+ * Mesh cleanup and refinement utilities.
3
+ */
4
+ /**
5
+ * Remove small disconnected triangle islands relative to the largest shell.
6
+ * @param {object} [options]
7
+ * @param {number} [options.maxTriangles=30] triangle-count threshold for removal
8
+ * @param {boolean} [options.removeInternal=true] drop islands inside the main shell
9
+ * @param {boolean} [options.removeExternal=true] drop islands outside the main shell
10
+ */
11
+ export function removeSmallIslands({ maxTriangles = 30, removeInternal = true, removeExternal = true } = {}) {
12
+ const tv = this._triVerts;
13
+ const vp = this._vertProperties;
14
+ const triCount = (tv.length / 3) | 0;
15
+ if (triCount === 0) return 0;
16
+
17
+ const nv = (vp.length / 3) | 0;
18
+ const NV = BigInt(Math.max(1, nv));
19
+ const eKey = (a, b) => {
20
+ const A = BigInt(a), B = BigInt(b);
21
+ return (A < B) ? (A * NV + B) : (B * NV + A);
22
+ };
23
+
24
+ const edgeToTris = new Map(); // key -> [tri indices]
25
+ for (let t = 0; t < triCount; t++) {
26
+ const b = t * 3;
27
+ const i0 = tv[b + 0] >>> 0;
28
+ const i1 = tv[b + 1] >>> 0;
29
+ const i2 = tv[b + 2] >>> 0;
30
+ const edges = [[i0, i1], [i1, i2], [i2, i0]];
31
+ for (let k = 0; k < 3; k++) {
32
+ const a = edges[k][0], c = edges[k][1];
33
+ const key = eKey(a, c);
34
+ let arr = edgeToTris.get(key);
35
+ if (!arr) { arr = []; edgeToTris.set(key, arr); }
36
+ arr.push(t);
37
+ }
38
+ }
39
+
40
+ const adj = new Array(triCount);
41
+ for (let t = 0; t < triCount; t++) adj[t] = [];
42
+ for (const [, arr] of edgeToTris.entries()) {
43
+ if (arr.length === 2) {
44
+ const a = arr[0], b = arr[1];
45
+ adj[a].push(b);
46
+ adj[b].push(a);
47
+ }
48
+ }
49
+
50
+ const compId = new Int32Array(triCount);
51
+ for (let i = 0; i < triCount; i++) compId[i] = -1;
52
+ const comps = [];
53
+ let compIdx = 0;
54
+ const stack = [];
55
+ for (let seed = 0; seed < triCount; seed++) {
56
+ if (compId[seed] !== -1) continue;
57
+ compId[seed] = compIdx;
58
+ stack.length = 0;
59
+ stack.push(seed);
60
+ const tris = [];
61
+ while (stack.length) {
62
+ const t = stack.pop();
63
+ tris.push(t);
64
+ const nbrs = adj[t];
65
+ for (let j = 0; j < nbrs.length; j++) {
66
+ const u = nbrs[j];
67
+ if (compId[u] !== -1) continue;
68
+ compId[u] = compIdx;
69
+ stack.push(u);
70
+ }
71
+ }
72
+ comps.push(tris);
73
+ compIdx++;
74
+ }
75
+
76
+ if (comps.length <= 1) return 0;
77
+
78
+ let mainIdx = 0;
79
+ for (let i = 1; i < comps.length; i++) {
80
+ if (comps[i].length > comps[mainIdx].length) mainIdx = i;
81
+ }
82
+ const mainTris = comps[mainIdx];
83
+
84
+ const mainFaces = new Array(mainTris.length);
85
+ for (let k = 0; k < mainTris.length; k++) {
86
+ const t = mainTris[k];
87
+ const b = t * 3;
88
+ const i0 = tv[b + 0] * 3, i1 = tv[b + 1] * 3, i2 = tv[b + 2] * 3;
89
+ mainFaces[k] = [
90
+ [vp[i0 + 0], vp[i0 + 1], vp[i0 + 2]],
91
+ [vp[i1 + 0], vp[i1 + 1], vp[i1 + 2]],
92
+ [vp[i2 + 0], vp[i2 + 1], vp[i2 + 2]],
93
+ ];
94
+ }
95
+
96
+ const rayTri = (orig, dir, tri) => {
97
+ const EPS = 1e-12;
98
+ const ax = tri[0][0], ay = tri[0][1], az = tri[0][2];
99
+ const bx = tri[1][0], by = tri[1][1], bz = tri[1][2];
100
+ const cx = tri[2][0], cy = tri[2][1], cz = tri[2][2];
101
+ const e1x = bx - ax, e1y = by - ay, e1z = bz - az;
102
+ const e2x = cx - ax, e2y = cy - ay, e2z = cz - az;
103
+ const px = dir[1] * e2z - dir[2] * e2y;
104
+ const py = dir[2] * e2x - dir[0] * e2z;
105
+ const pz = dir[0] * e2y - dir[1] * e2x;
106
+ const det = e1x * px + e1y * py + e1z * pz;
107
+ if (Math.abs(det) < EPS) return null;
108
+ const invDet = 1.0 / det;
109
+ const tvecx = orig[0] - ax, tvecy = orig[1] - ay, tvecz = orig[2] - az;
110
+ const u = (tvecx * px + tvecy * py + tvecz * pz) * invDet;
111
+ if (u < 0 || u > 1) return null;
112
+ const qx = tvecy * e1z - tvecz * e1y;
113
+ const qy = tvecz * e1x - tvecx * e1z;
114
+ const qz = tvecx * e1y - tvecy * e1x;
115
+ const v = (dir[0] * qx + dir[1] * qy + dir[2] * qz) * invDet;
116
+ if (v < 0 || u + v > 1) return null;
117
+ const tHit = (e2x * qx + e2y * qy + e2z * qz) * invDet;
118
+ return tHit > EPS ? tHit : null;
119
+ };
120
+
121
+ const pointInsideMain = (p) => {
122
+ const dir = [1, 0, 0];
123
+ let hits = 0;
124
+ for (let i = 0; i < mainFaces.length; i++) {
125
+ const th = rayTri(p, dir, mainFaces[i]);
126
+ if (th !== null) hits++;
127
+ }
128
+ return (hits % 2) === 1;
129
+ };
130
+
131
+ const triCentroid = (t) => {
132
+ const b = t * 3;
133
+ const i0 = tv[b + 0] * 3, i1 = tv[b + 1] * 3, i2 = tv[b + 2] * 3;
134
+ const x = (vp[i0 + 0] + vp[i1 + 0] + vp[i2 + 0]) / 3;
135
+ const y = (vp[i0 + 1] + vp[i1 + 1] + vp[i2 + 1]) / 3;
136
+ const z = (vp[i0 + 2] + vp[i1 + 2] + vp[i2 + 2]) / 3;
137
+ return [x + 1e-8, y + 1e-8, z + 1e-8];
138
+ };
139
+
140
+ const removeComp = new Array(comps.length).fill(false);
141
+ for (let i = 0; i < comps.length; i++) {
142
+ if (i === mainIdx) continue;
143
+ const tris = comps[i];
144
+ if (tris.length === 0 || tris.length > maxTriangles) continue;
145
+ const probe = triCentroid(tris[0]);
146
+ const inside = pointInsideMain(probe);
147
+ if ((inside && removeInternal) || (!inside && removeExternal)) {
148
+ removeComp[i] = true;
149
+ }
150
+ }
151
+
152
+ const keepTri = new Uint8Array(triCount);
153
+ for (let t = 0; t < triCount; t++) keepTri[t] = 1;
154
+ let removed = 0;
155
+ for (let i = 0; i < comps.length; i++) {
156
+ if (!removeComp[i]) continue;
157
+ const tris = comps[i];
158
+ for (let k = 0; k < tris.length; k++) {
159
+ const t = tris[k];
160
+ if (keepTri[t]) { keepTri[t] = 0; removed++; }
161
+ }
162
+ }
163
+ if (removed === 0) return 0;
164
+
165
+ const usedVert = new Uint8Array(nv);
166
+ const newTriVerts = [];
167
+ const newTriIDs = [];
168
+ for (let t = 0; t < triCount; t++) {
169
+ if (!keepTri[t]) continue;
170
+ const b = t * 3;
171
+ const a = tv[b + 0] >>> 0;
172
+ const b1 = tv[b + 1] >>> 0;
173
+ const c = tv[b + 2] >>> 0;
174
+ newTriVerts.push(a, b1, c);
175
+ newTriIDs.push(this._triIDs[t]);
176
+ usedVert[a] = 1; usedVert[b1] = 1; usedVert[c] = 1;
177
+ }
178
+
179
+ const oldToNew = new Int32Array(nv);
180
+ for (let i = 0; i < nv; i++) oldToNew[i] = -1;
181
+ const newVP = [];
182
+ let write = 0;
183
+ for (let i = 0; i < nv; i++) {
184
+ if (!usedVert[i]) continue;
185
+ oldToNew[i] = write++;
186
+ newVP.push(vp[i * 3 + 0], vp[i * 3 + 1], vp[i * 3 + 2]);
187
+ }
188
+ for (let i = 0; i < newTriVerts.length; i++) {
189
+ newTriVerts[i] = oldToNew[newTriVerts[i]];
190
+ }
191
+
192
+ this._vertProperties = newVP;
193
+ this._triVerts = newTriVerts;
194
+ this._triIDs = newTriIDs;
195
+ this._vertKeyToIndex = new Map();
196
+ for (let i = 0; i < this._vertProperties.length; i += 3) {
197
+ const x = this._vertProperties[i], y = this._vertProperties[i + 1], z = this._vertProperties[i + 2];
198
+ this._vertKeyToIndex.set(`${x},${y},${z}`, (i / 3) | 0);
199
+ }
200
+ this._dirty = true;
201
+ this._faceIndex = null;
202
+ return removed;
203
+ }
204
+
205
+ /** Backwards-compatible wrapper that removes only internal small islands. */
206
+ export function removeSmallInternalIslands(maxTriangles = 30) {
207
+ return this.removeSmallIslands({ maxTriangles, removeInternal: true, removeExternal: false });
208
+ }
209
+
210
+ /**
211
+ * Remove faces that only connect via a single shared edge chain to an opposite-facing neighbor.
212
+ * @param {object} [options]
213
+ * @param {number} [options.normalDotThreshold=-0.95] dot-product threshold for opposite normals
214
+ * @returns {number} triangles removed
215
+ */
216
+ export function removeOppositeSingleEdgeFaces({ normalDotThreshold = -0.95 } = {}) {
217
+ const tv = this._triVerts;
218
+ const vp = this._vertProperties;
219
+ const ids = this._triIDs;
220
+ if (!tv || !vp || !ids) return 0;
221
+ const triCount = (tv.length / 3) | 0;
222
+ if (triCount === 0 || ids.length !== triCount) return 0;
223
+ const nv = (vp.length / 3) | 0;
224
+ if (nv === 0) return 0;
225
+
226
+ const NV = BigInt(Math.max(1, nv));
227
+ const eKey = (a, b) => {
228
+ const A = BigInt(a), B = BigInt(b);
229
+ return A < B ? (A * NV + B) : (B * NV + A);
230
+ };
231
+
232
+ const faceNormals = new Map(); // id -> [nx, ny, nz]
233
+ const addNormal = (id, nx, ny, nz) => {
234
+ let entry = faceNormals.get(id);
235
+ if (!entry) { entry = [0, 0, 0]; faceNormals.set(id, entry); }
236
+ entry[0] += nx; entry[1] += ny; entry[2] += nz;
237
+ };
238
+
239
+ const edgeMap = new Map(); // key -> {faces:Set, a, b}
240
+
241
+ for (let t = 0; t < triCount; t++) {
242
+ const id = ids[t];
243
+ if (id === undefined || id === null) continue;
244
+ const base = t * 3;
245
+ const i0 = tv[base + 0] >>> 0;
246
+ const i1 = tv[base + 1] >>> 0;
247
+ const i2 = tv[base + 2] >>> 0;
248
+
249
+ const ax = vp[i0 * 3 + 0], ay = vp[i0 * 3 + 1], az = vp[i0 * 3 + 2];
250
+ const bx = vp[i1 * 3 + 0], by = vp[i1 * 3 + 1], bz = vp[i1 * 3 + 2];
251
+ const cx = vp[i2 * 3 + 0], cy = vp[i2 * 3 + 1], cz = vp[i2 * 3 + 2];
252
+ const ux = bx - ax, uy = by - ay, uz = bz - az;
253
+ const vx = cx - ax, vy = cy - ay, vz = cz - az;
254
+ const nx = uy * vz - uz * vy;
255
+ const ny = uz * vx - ux * vz;
256
+ const nz = ux * vy - uy * vx;
257
+ addNormal(id, nx, ny, nz);
258
+
259
+ const edges = [[i0, i1], [i1, i2], [i2, i0]];
260
+ for (let k = 0; k < 3; k++) {
261
+ let a = edges[k][0];
262
+ let b = edges[k][1];
263
+ if (a === b) continue;
264
+ const key = eKey(a, b);
265
+ let entry = edgeMap.get(key);
266
+ if (!entry) {
267
+ if (a > b) { const tmp = a; a = b; b = tmp; }
268
+ entry = { faces: new Set(), a, b };
269
+ edgeMap.set(key, entry);
270
+ }
271
+ entry.faces.add(id);
272
+ }
273
+ }
274
+
275
+ const pairEdges = new Map(); // key -> { ids: [idA, idB], edges: [[u, v], ...] }
276
+ const facePairs = new Map(); // faceId -> Set(pairKey)
277
+ const addPair = (faceId, pairKey) => {
278
+ let set = facePairs.get(faceId);
279
+ if (!set) { set = new Set(); facePairs.set(faceId, set); }
280
+ set.add(pairKey);
281
+ };
282
+
283
+ for (const entry of edgeMap.values()) {
284
+ if (entry.faces.size !== 2) continue;
285
+ const faces = Array.from(entry.faces);
286
+ const idA = faces[0];
287
+ const idB = faces[1];
288
+ if (idA === idB) continue;
289
+ const pairKey = idA < idB ? `${idA}|${idB}` : `${idB}|${idA}`;
290
+ let pair = pairEdges.get(pairKey);
291
+ if (!pair) {
292
+ pair = {
293
+ ids: idA < idB ? [idA, idB] : [idB, idA],
294
+ edges: [],
295
+ };
296
+ pairEdges.set(pairKey, pair);
297
+ }
298
+ pair.edges.push([entry.a, entry.b]);
299
+ addPair(idA, pairKey);
300
+ addPair(idB, pairKey);
301
+ }
302
+
303
+ const isSingleEdgeChain = (edges) => {
304
+ if (!edges || edges.length === 0) return false;
305
+ const adj = new Map();
306
+ const verts = new Set();
307
+ for (const [u, v] of edges) {
308
+ verts.add(u); verts.add(v);
309
+ if (!adj.has(u)) adj.set(u, new Set());
310
+ if (!adj.has(v)) adj.set(v, new Set());
311
+ adj.get(u).add(v);
312
+ adj.get(v).add(u);
313
+ }
314
+ let components = 0;
315
+ const visited = new Set();
316
+ for (const v of verts) {
317
+ if (visited.has(v)) continue;
318
+ components++;
319
+ if (components > 1) return false;
320
+ const stack = [v];
321
+ visited.add(v);
322
+ while (stack.length) {
323
+ const cur = stack.pop();
324
+ const nbrs = adj.get(cur);
325
+ if (!nbrs) continue;
326
+ for (const n of nbrs) {
327
+ if (visited.has(n)) continue;
328
+ visited.add(n);
329
+ stack.push(n);
330
+ }
331
+ }
332
+ }
333
+ return components === 1;
334
+ };
335
+
336
+ const toRemove = new Set();
337
+ for (const [faceId, pairs] of facePairs.entries()) {
338
+ if (pairs.size !== 1) continue;
339
+ const pairKey = pairs.values().next().value;
340
+ const pair = pairEdges.get(pairKey);
341
+ if (!pair || !pair.edges.length) continue;
342
+ if (!isSingleEdgeChain(pair.edges)) continue;
343
+ const otherId = pair.ids[0] === faceId ? pair.ids[1] : pair.ids[0];
344
+ const n0 = faceNormals.get(faceId);
345
+ const n1 = faceNormals.get(otherId);
346
+ if (!n0 || !n1) continue;
347
+ const len0 = Math.hypot(n0[0], n0[1], n0[2]);
348
+ const len1 = Math.hypot(n1[0], n1[1], n1[2]);
349
+ if (!(len0 > 1e-12) || !(len1 > 1e-12)) continue;
350
+ const dot = (n0[0] * n1[0] + n0[1] * n1[1] + n0[2] * n1[2]) / (len0 * len1);
351
+ if (dot <= normalDotThreshold) toRemove.add(faceId);
352
+ }
353
+
354
+ if (!toRemove.size) return 0;
355
+
356
+ const keepTri = new Uint8Array(triCount);
357
+ let removed = 0;
358
+ for (let t = 0; t < triCount; t++) {
359
+ if (toRemove.has(ids[t])) {
360
+ removed++;
361
+ continue;
362
+ }
363
+ keepTri[t] = 1;
364
+ }
365
+ if (removed === 0) return 0;
366
+
367
+ const usedVert = new Uint8Array(nv);
368
+ const newTriVerts = [];
369
+ const newTriIDs = [];
370
+ for (let t = 0; t < triCount; t++) {
371
+ if (!keepTri[t]) continue;
372
+ const b = t * 3;
373
+ const a = tv[b + 0] >>> 0;
374
+ const b1 = tv[b + 1] >>> 0;
375
+ const c = tv[b + 2] >>> 0;
376
+ newTriVerts.push(a, b1, c);
377
+ newTriIDs.push(ids[t]);
378
+ usedVert[a] = 1; usedVert[b1] = 1; usedVert[c] = 1;
379
+ }
380
+
381
+ const oldToNew = new Int32Array(nv);
382
+ for (let i = 0; i < nv; i++) oldToNew[i] = -1;
383
+ const newVP = [];
384
+ let write = 0;
385
+ for (let i = 0; i < nv; i++) {
386
+ if (!usedVert[i]) continue;
387
+ oldToNew[i] = write++;
388
+ newVP.push(vp[i * 3 + 0], vp[i * 3 + 1], vp[i * 3 + 2]);
389
+ }
390
+ for (let i = 0; i < newTriVerts.length; i++) {
391
+ newTriVerts[i] = oldToNew[newTriVerts[i]];
392
+ }
393
+
394
+ this._vertProperties = newVP;
395
+ this._triVerts = newTriVerts;
396
+ this._triIDs = newTriIDs;
397
+ this._vertKeyToIndex = new Map();
398
+ for (let i = 0; i < this._vertProperties.length; i += 3) {
399
+ const x = this._vertProperties[i], y = this._vertProperties[i + 1], z = this._vertProperties[i + 2];
400
+ this._vertKeyToIndex.set(`${x},${y},${z}`, (i / 3) | 0);
401
+ }
402
+ this._dirty = true;
403
+ this._faceIndex = null;
404
+ this._manifold = null;
405
+ return removed;
406
+ }
407
+
408
+ /**
409
+ * Remove tiny triangles that lie along boundaries between faces by performing
410
+ * local 2–2 edge flips across inter-face edges.
411
+ */
412
+ export function removeTinyBoundaryTriangles(areaThreshold, maxIterations = 1) {
413
+ const thr = Number(areaThreshold);
414
+ if (!Number.isFinite(thr) || thr <= 0) return 0;
415
+ const vp = this._vertProperties;
416
+ if (!vp || vp.length < 9 || this._triVerts.length < 3) return 0;
417
+
418
+ const triArea = (i0, i1, i2) => {
419
+ const x0 = vp[i0 * 3 + 0], y0 = vp[i0 * 3 + 1], z0 = vp[i0 * 3 + 2];
420
+ const x1 = vp[i1 * 3 + 0], y1 = vp[i1 * 3 + 1], z1 = vp[i1 * 3 + 2];
421
+ const x2 = vp[i2 * 3 + 0], y2 = vp[i2 * 3 + 1], z2 = vp[i2 * 3 + 2];
422
+ const ux = x1 - x0, uy = y1 - y0, uz = z1 - z0;
423
+ const vx = x2 - x0, vy = y2 - y0, vz = z2 - z0;
424
+ const cx = uy * vz - uz * vy;
425
+ const cy = uz * vx - ux * vz;
426
+ const cz = ux * vy - uy * vx;
427
+ return 0.5 * Math.hypot(cx, cy, cz);
428
+ };
429
+
430
+ let totalFlips = 0;
431
+ const iterMax = Math.max(1, (maxIterations | 0));
432
+
433
+ for (let iter = 0; iter < iterMax; iter++) {
434
+ const tv = this._triVerts;
435
+ const ids = this._triIDs;
436
+ const triCount = (tv.length / 3) | 0;
437
+ if (triCount < 2) break;
438
+
439
+ const tris = new Array(triCount);
440
+ const areas = new Float64Array(triCount);
441
+ for (let t = 0; t < triCount; t++) {
442
+ const b = t * 3;
443
+ const i0 = tv[b + 0] >>> 0;
444
+ const i1 = tv[b + 1] >>> 0;
445
+ const i2 = tv[b + 2] >>> 0;
446
+ tris[t] = [i0, i1, i2];
447
+ areas[t] = triArea(i0, i1, i2);
448
+ }
449
+
450
+ const nv = (vp.length / 3) | 0;
451
+ const NV = BigInt(nv);
452
+ const eKey = (a, b) => {
453
+ const A = BigInt(a), B = BigInt(b);
454
+ return A < B ? A * NV + B : B * NV + A;
455
+ };
456
+ const e2t = new Map(); // key -> [{tri, id, a, b}]
457
+ for (let t = 0; t < triCount; t++) {
458
+ const [i0, i1, i2] = tris[t];
459
+ const face = ids[t];
460
+ const edges = [[i0, i1], [i1, i2], [i2, i0]];
461
+ for (let k = 0; k < 3; k++) {
462
+ const a = edges[k][0], b = edges[k][1];
463
+ const key = eKey(a, b);
464
+ let arr = e2t.get(key);
465
+ if (!arr) { arr = []; e2t.set(key, arr); }
466
+ arr.push({ tri: t, id: face, a, b });
467
+ }
468
+ }
469
+
470
+ const candidates = [];
471
+ for (const [key, arr] of e2t.entries()) {
472
+ if (arr.length !== 2) continue;
473
+ const a = arr[0], b = arr[1];
474
+ if (a.id === b.id) continue;
475
+ const areaA = areas[a.tri];
476
+ const areaB = areas[b.tri];
477
+ const minAB = Math.min(areaA, areaB);
478
+ if (!(minAB < thr)) continue;
479
+ candidates.push({ key, a, b, minAB });
480
+ }
481
+
482
+ candidates.sort((p, q) => p.minAB - q.minAB);
483
+
484
+ const triLocked = new Uint8Array(triCount);
485
+ let flipsThisIter = 0;
486
+
487
+ const removeUse = (aa, bb, triIdx) => {
488
+ const k = eKey(aa, bb);
489
+ const arr = e2t.get(k);
490
+ if (!arr) return;
491
+ for (let i = 0; i < arr.length; i++) {
492
+ const u = arr[i];
493
+ if (u.tri === triIdx && u.a === aa && u.b === bb) { arr.splice(i, 1); break; }
494
+ }
495
+ if (arr.length === 0) e2t.delete(k);
496
+ };
497
+
498
+ const addUse = (aa, bb, triIdx, id) => {
499
+ const k = eKey(aa, bb);
500
+ let arr = e2t.get(k);
501
+ if (!arr) { arr = []; e2t.set(k, arr); }
502
+ arr.push({ tri: triIdx, id, a: aa, b: bb });
503
+ };
504
+
505
+ for (const { a, b } of candidates) {
506
+ const t0 = a.tri, t1 = b.tri;
507
+ if (triLocked[t0] || triLocked[t1]) continue;
508
+
509
+ const u = a.a, v = a.b;
510
+ if (!(b.a === v && b.b === u)) {
511
+ continue;
512
+ }
513
+
514
+ const tri0 = tris[t0];
515
+ const tri1 = tris[t1];
516
+ let c0 = -1, c1 = -1;
517
+ for (let k = 0; k < 3; k++) { const idx = tri0[k]; if (idx !== u && idx !== v) { c0 = idx; break; } }
518
+ for (let k = 0; k < 3; k++) { const idx = tri1[k]; if (idx !== u && idx !== v) { c1 = idx; break; } }
519
+ if (c0 < 0 || c1 < 0 || c0 === c1) continue;
520
+
521
+ const diagKey = eKey(c0, c1);
522
+ const diagUses = e2t.get(diagKey);
523
+ if (diagUses && diagUses.length) continue;
524
+
525
+ const area0 = areas[t0];
526
+ const area1 = areas[t1];
527
+ const minArea = Math.min(area0, area1);
528
+ if (minArea >= thr) continue;
529
+
530
+ const newArea0 = triArea(c0, c1, u);
531
+ const newArea1 = triArea(c1, c0, v);
532
+ if (!(Number.isFinite(newArea0) && Number.isFinite(newArea1))) continue;
533
+ if (newArea0 <= 0 || newArea1 <= 0) continue;
534
+ const newMin = Math.min(newArea0, newArea1);
535
+ if (newMin < minArea) continue;
536
+
537
+ tris[t0] = [c0, c1, u];
538
+ tris[t1] = [c1, c0, v];
539
+ areas[t0] = newArea0;
540
+ areas[t1] = newArea1;
541
+
542
+ removeUse(u, v, t0);
543
+ removeUse(v, u, t1);
544
+ removeUse(v, u, t0);
545
+ removeUse(u, v, t1);
546
+ addUse(c0, c1, t0, ids[t0]);
547
+ addUse(c1, c0, t0, ids[t0]);
548
+ addUse(c1, c0, t1, ids[t1]);
549
+ addUse(c0, c1, t1, ids[t1]);
550
+
551
+ triLocked[t0] = 1;
552
+ triLocked[t1] = 1;
553
+ flipsThisIter++;
554
+ }
555
+
556
+ if (!flipsThisIter) break;
557
+ totalFlips += flipsThisIter;
558
+
559
+ for (let t = 0; t < triCount; t++) {
560
+ const tri = tris[t];
561
+ const base = t * 3;
562
+ tv[base + 0] = tri[0];
563
+ tv[base + 1] = tri[1];
564
+ tv[base + 2] = tri[2];
565
+ }
566
+ this._dirty = true;
567
+ this._faceIndex = null;
568
+ }
569
+
570
+ if (totalFlips > 0) {
571
+ this.fixTriangleWindingsByAdjacency();
572
+ }
573
+ return totalFlips;
574
+ }
575
+
576
+ /**
577
+ * Remesh by splitting long edges to improve triangle regularity while
578
+ * preserving face labels.
579
+ * @param {object} [options]
580
+ * @param {number} options.maxEdgeLength maximum allowed edge length before splitting (required)
581
+ * @param {number} [options.maxIterations=10] number of remesh passes to attempt
582
+ */
583
+ export function remesh({ maxEdgeLength, maxIterations = 10 } = {}) {
584
+ const Lmax = Number(maxEdgeLength);
585
+ if (!Number.isFinite(Lmax) || Lmax <= 0) return this;
586
+ const L2 = Lmax * Lmax;
587
+
588
+ const pass = () => {
589
+ const vp = this._vertProperties;
590
+ const tv = this._triVerts;
591
+ const ids = this._triIDs;
592
+ const triCount = (tv.length / 3) | 0;
593
+ const nv = (vp.length / 3) | 0;
594
+ const NV = BigInt(Math.max(1, nv));
595
+ const ukey = (a, b) => {
596
+ const A = BigInt(a); const B = BigInt(b); return A < B ? A * NV + B : B * NV + A;
597
+ };
598
+ const len2 = (i, j) => {
599
+ const ax = vp[i * 3 + 0], ay = vp[i * 3 + 1], az = vp[i * 3 + 2];
600
+ const bx = vp[j * 3 + 0], by = vp[j * 3 + 1], bz = vp[j * 3 + 2];
601
+ const dx = ax - bx, dy = ay - by, dz = az - bz; return dx * dx + dy * dy + dz * dz;
602
+ };
603
+
604
+ const longEdge = new Set();
605
+ for (let t = 0; t < triCount; t++) {
606
+ const b = t * 3;
607
+ const i0 = tv[b + 0] >>> 0;
608
+ const i1 = tv[b + 1] >>> 0;
609
+ const i2 = tv[b + 2] >>> 0;
610
+ if (len2(i0, i1) > L2) longEdge.add(ukey(i0, i1));
611
+ if (len2(i1, i2) > L2) longEdge.add(ukey(i1, i2));
612
+ if (len2(i2, i0) > L2) longEdge.add(ukey(i2, i0));
613
+ }
614
+
615
+ if (longEdge.size === 0) return false;
616
+
617
+ const newVP = vp.slice();
618
+ const edgeMid = new Map(); // key -> new vert index
619
+ const midpointIndex = (a, b) => {
620
+ const key = ukey(a, b);
621
+ let idx = edgeMid.get(key);
622
+ if (idx !== undefined) return idx;
623
+ const ax = vp[a * 3 + 0], ay = vp[a * 3 + 1], az = vp[a * 3 + 2];
624
+ const bx = vp[b * 3 + 0], by = vp[b * 3 + 1], bz = vp[b * 3 + 2];
625
+ const mx = 0.5 * (ax + bx), my = 0.5 * (ay + by), mz = 0.5 * (az + bz);
626
+ idx = (newVP.length / 3) | 0;
627
+ newVP.push(mx, my, mz);
628
+ edgeMid.set(key, idx);
629
+ return idx;
630
+ };
631
+
632
+ const newTV = [];
633
+ const newIDs = [];
634
+ const emit = (i, j, k, faceId) => { newTV.push(i, j, k); newIDs.push(faceId); };
635
+
636
+ for (let t = 0; t < triCount; t++) {
637
+ const base = t * 3;
638
+ const i0 = tv[base + 0] >>> 0;
639
+ const i1 = tv[base + 1] >>> 0;
640
+ const i2 = tv[base + 2] >>> 0;
641
+ const fid = ids[t];
642
+
643
+ const k01 = ukey(i0, i1), k12 = ukey(i1, i2), k20 = ukey(i2, i0);
644
+ const s01 = longEdge.has(k01);
645
+ const s12 = longEdge.has(k12);
646
+ const s20 = longEdge.has(k20);
647
+
648
+ const count = (s01 ? 1 : 0) + (s12 ? 1 : 0) + (s20 ? 1 : 0);
649
+
650
+ if (count === 0) {
651
+ emit(i0, i1, i2, fid);
652
+ continue;
653
+ }
654
+
655
+ if (count === 1) {
656
+ if (s01) {
657
+ const m01 = midpointIndex(i0, i1);
658
+ emit(i0, m01, i2, fid);
659
+ emit(m01, i1, i2, fid);
660
+ } else if (s12) {
661
+ const m12 = midpointIndex(i1, i2);
662
+ emit(i1, m12, i0, fid);
663
+ emit(m12, i2, i0, fid);
664
+ } else {
665
+ const m20 = midpointIndex(i2, i0);
666
+ emit(i2, m20, i1, fid);
667
+ emit(m20, i0, i1, fid);
668
+ }
669
+ continue;
670
+ }
671
+
672
+ if (count === 2) {
673
+ if (s01 && s12) {
674
+ const m01 = midpointIndex(i0, i1);
675
+ const m12 = midpointIndex(i1, i2);
676
+ emit(i0, m01, i2, fid);
677
+ emit(i1, m12, m01, fid);
678
+ emit(m01, m12, i2, fid);
679
+ } else if (s12 && s20) {
680
+ const m12 = midpointIndex(i1, i2);
681
+ const m20 = midpointIndex(i2, i0);
682
+ emit(i1, m12, i0, fid);
683
+ emit(i2, m20, m12, fid);
684
+ emit(m12, m20, i0, fid);
685
+ } else {
686
+ const m20 = midpointIndex(i2, i0);
687
+ const m01 = midpointIndex(i0, i1);
688
+ emit(i2, m20, i1, fid);
689
+ emit(i0, m01, m20, fid);
690
+ emit(m20, m01, i1, fid);
691
+ }
692
+ continue;
693
+ }
694
+
695
+ const m01 = midpointIndex(i0, i1);
696
+ const m12 = midpointIndex(i1, i2);
697
+ const m20 = midpointIndex(i2, i0);
698
+ emit(i0, m01, m20, fid);
699
+ emit(i1, m12, m01, fid);
700
+ emit(i2, m20, m12, fid);
701
+ emit(m01, m12, m20, fid);
702
+ }
703
+
704
+ this._vertProperties = newVP;
705
+ this._triVerts = newTV;
706
+ this._triIDs = newIDs;
707
+ this._vertKeyToIndex = new Map();
708
+ for (let i = 0; i < this._vertProperties.length; i += 3) {
709
+ const x = this._vertProperties[i], y = this._vertProperties[i + 1], z = this._vertProperties[i + 2];
710
+ this._vertKeyToIndex.set(`${x},${y},${z}`, (i / 3) | 0);
711
+ }
712
+ this._dirty = true;
713
+ this._faceIndex = null;
714
+ return true;
715
+ };
716
+
717
+ let changed = false;
718
+ for (let it = 0; it < maxIterations; it++) {
719
+ const did = pass();
720
+ if (!did) break;
721
+ changed = true;
722
+ }
723
+
724
+ if (changed) {
725
+ this.fixTriangleWindingsByAdjacency();
726
+ }
727
+ return this;
728
+ }
729
+
730
+ /**
731
+ * Collapse tiny triangles by snapping the shortest edge of any triangle
732
+ * below a length threshold. The collapse is implemented by moving one
733
+ * endpoint of the short edge onto the other (preferring the lower index
734
+ * as the representative), which produces degenerate triangles. Those are
735
+ * then cleaned up by intersecting the result with a large bounding box
736
+ * and adopting the manifold surface back into this Solid.
737
+ *
738
+ * Returns the number of edge-collapses (unique unions) applied.
739
+ */
740
+ export function collapseTinyTriangles(lengthThreshold) {
741
+ const thr = Number(lengthThreshold);
742
+ if (!Number.isFinite(thr) || thr <= 0) return 0;
743
+ const vp = this._vertProperties;
744
+ const tv = this._triVerts;
745
+ const triCount = (tv.length / 3) | 0;
746
+ const nv = (vp.length / 3) | 0;
747
+ if (triCount === 0 || nv === 0) return 0;
748
+
749
+ const thr2 = thr * thr;
750
+
751
+ // Disjoint set union (union-find) to map vertices to representatives
752
+ const parent = new Int32Array(nv);
753
+ for (let i = 0; i < nv; i++) parent[i] = i;
754
+ const find = (i) => {
755
+ while (parent[i] !== i) { parent[i] = parent[parent[i]]; i = parent[i]; }
756
+ return i;
757
+ };
758
+ const unite = (a, b) => {
759
+ let ra = find(a), rb = find(b);
760
+ if (ra === rb) return false;
761
+ // Prefer lower index as stable representative
762
+ if (rb < ra) { const tmp = ra; ra = rb; rb = tmp; }
763
+ parent[rb] = ra;
764
+ return true;
765
+ };
766
+ const len2 = (i, j) => {
767
+ const ax = vp[i * 3 + 0], ay = vp[i * 3 + 1], az = vp[i * 3 + 2];
768
+ const bx = vp[j * 3 + 0], by = vp[j * 3 + 1], bz = vp[j * 3 + 2];
769
+ const dx = ax - bx, dy = ay - by, dz = az - bz;
770
+ return dx * dx + dy * dy + dz * dz;
771
+ };
772
+
773
+ // Identify and unify the endpoints of the shortest edge in triangles
774
+ // that fall below the threshold.
775
+ let unions = 0;
776
+ for (let t = 0; t < triCount; t++) {
777
+ const base = t * 3;
778
+ const i0 = tv[base + 0] >>> 0;
779
+ const i1 = tv[base + 1] >>> 0;
780
+ const i2 = tv[base + 2] >>> 0;
781
+ const d01 = len2(i0, i1);
782
+ const d12 = len2(i1, i2);
783
+ const d20 = len2(i2, i0);
784
+ let minD = d01, a = i0, b = i1;
785
+ if (d12 < minD) { minD = d12; a = i1; b = i2; }
786
+ if (d20 < minD) { minD = d20; a = i2; b = i0; }
787
+ if (minD < thr2) {
788
+ if (unite(a, b)) unions++;
789
+ }
790
+ }
791
+
792
+ if (unions === 0) return 0;
793
+
794
+ // Apply the collapse: move non-representative vertices onto their root.
795
+ for (let i = 0; i < nv; i++) {
796
+ const r = find(i);
797
+ if (r !== i) {
798
+ vp[i * 3 + 0] = vp[r * 3 + 0];
799
+ vp[i * 3 + 1] = vp[r * 3 + 1];
800
+ vp[i * 3 + 2] = vp[r * 3 + 2];
801
+ }
802
+ }
803
+
804
+ // Mark dirty and refresh quick vertex index map
805
+ this._vertKeyToIndex = new Map();
806
+ for (let i = 0; i < nv; i++) {
807
+ const x = vp[i * 3 + 0], y = vp[i * 3 + 1], z = vp[i * 3 + 2];
808
+ this._vertKeyToIndex.set(`${x},${y},${z}`, i);
809
+ }
810
+ this._dirty = true;
811
+ this._faceIndex = null;
812
+
813
+ // Cleanup degenerate triangles by intersecting with a large bounding box
814
+ let minX = Infinity, minY = Infinity, minZ = Infinity;
815
+ let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
816
+ for (let i = 0; i < nv; i++) {
817
+ const x = vp[i * 3 + 0], y = vp[i * 3 + 1], z = vp[i * 3 + 2];
818
+ if (x < minX) minX = x; if (x > maxX) maxX = x;
819
+ if (y < minY) minY = y; if (y > maxY) maxY = y;
820
+ if (z < minZ) minZ = z; if (z > maxZ) maxZ = z;
821
+ }
822
+ if (!Number.isFinite(minX) || !Number.isFinite(maxX)) return unions;
823
+ const dx = Math.max(1e-9, maxX - minX);
824
+ const dy = Math.max(1e-9, maxY - minY);
825
+ const dz = Math.max(1e-9, maxZ - minZ);
826
+ const maxDim = Math.max(dx, dy, dz, thr);
827
+ const margin = Math.max(thr * 10, maxDim * 0.1 + 1e-6);
828
+ const width = dx + 2 * margin;
829
+ const height = dy + 2 * margin;
830
+ const depth = dz + 2 * margin;
831
+ const ox = minX - margin, oy = minY - margin, oz = minZ - margin;
832
+
833
+ // Build a box Solid inline (avoid importing primitives to keep dependencies acyclic)
834
+ const SolidCtor = this.constructor;
835
+ const box = new SolidCtor();
836
+ const p000 = [ox, oy, oz];
837
+ const p100 = [ox + width, oy, oz];
838
+ const p010 = [ox, oy + height, oz];
839
+ const p110 = [ox + width, oy + height, oz];
840
+ const p001 = [ox, oy, oz + depth];
841
+ const p101 = [ox + width, oy, oz + depth];
842
+ const p011 = [ox, oy + height, oz + depth];
843
+ const p111 = [ox + width, oy + height, oz + depth];
844
+ box.addTriangle('__BIGBOX_NX', p000, p001, p011);
845
+ box.addTriangle('__BIGBOX_NX', p000, p011, p010);
846
+ box.addTriangle('__BIGBOX_PX', p100, p110, p111);
847
+ box.addTriangle('__BIGBOX_PX', p100, p111, p101);
848
+ box.addTriangle('__BIGBOX_NY', p000, p100, p101);
849
+ box.addTriangle('__BIGBOX_NY', p000, p101, p001);
850
+ box.addTriangle('__BIGBOX_PY', p010, p011, p111);
851
+ box.addTriangle('__BIGBOX_PY', p010, p111, p110);
852
+ box.addTriangle('__BIGBOX_NZ', p000, p010, p110);
853
+ box.addTriangle('__BIGBOX_NZ', p000, p110, p100);
854
+ box.addTriangle('__BIGBOX_PZ', p001, p101, p111);
855
+ box.addTriangle('__BIGBOX_PZ', p001, p111, p011);
856
+
857
+ const result = this.intersect(box);
858
+
859
+ // Adopt the result's manifold surface back into this Solid
860
+ const mesh = result.getMesh();
861
+ try {
862
+ this._numProp = mesh.numProp || 3;
863
+ this._vertProperties = Array.from(mesh.vertProperties || []);
864
+ this._triVerts = Array.from(mesh.triVerts || []);
865
+ const triCountAfter = (this._triVerts.length / 3) | 0;
866
+ if (mesh.faceID && mesh.faceID.length === triCountAfter) {
867
+ this._triIDs = Array.from(mesh.faceID);
868
+ } else {
869
+ const SolidClass = this.constructor;
870
+ this._triIDs = SolidClass._expandTriIDsFromMesh(mesh);
871
+ }
872
+ this._vertKeyToIndex = new Map();
873
+ for (let i = 0; i < this._vertProperties.length; i += 3) {
874
+ const x = this._vertProperties[i], y = this._vertProperties[i + 1], z = this._vertProperties[i + 2];
875
+ this._vertKeyToIndex.set(`${x},${y},${z}`, (i / 3) | 0);
876
+ }
877
+ // Adopt face label mapping from the boolean result to keep IDs consistent
878
+ try { this._idToFaceName = new Map(result._idToFaceName); } catch {
879
+ // throw an error if it fails
880
+ throw new Error("Failed to adopt face label mapping from boolean result");
881
+ }
882
+ try { this._faceNameToID = new Map([...this._idToFaceName.entries()].map(([id, name]) => [name, id])); } catch { }
883
+ this._dirty = false;
884
+ this._faceIndex = null;
885
+ this._manifold = null; // Rebuild lazily on next need
886
+ } finally {
887
+ try { if (mesh && typeof mesh.delete === 'function') mesh.delete(); } catch { }
888
+ }
889
+
890
+ return unions;
891
+ }
892
+
893
+ /**
894
+ * MANIFOLD-SAFE: Detect and split self-intersecting triangle pairs.
895
+ * - Uses conservative intersection detection to maintain manifold properties
896
+ * - Only splits when intersection creates proper interior segments
897
+ * - Ensures all new triangles maintain proper adjacency relationships
898
+ * - Preserves face IDs and avoids creating T-junctions or non-manifold edges
899
+ * - Returns the number of pairwise splits applied.
900
+ */
901
+ export function splitSelfIntersectingTriangles(diagnostics = false) {
902
+ const vp = this._vertProperties;
903
+ const tv = this._triVerts;
904
+ const ids = this._triIDs;
905
+ const triCount0 = (tv.length / 3) | 0;
906
+ if (triCount0 < 2) return 0;
907
+
908
+ if (diagnostics) {
909
+ console.log(`\n=== splitSelfIntersectingTriangles Diagnostics ===`);
910
+ console.log(`Initial triangle count: ${triCount0}`);
911
+ console.log(`Initial vertex count: ${vp.length / 3}`);
912
+ }
913
+
914
+ // Use conservative tolerance to avoid creating near-degenerate geometry
915
+ const EPS = 1e-6;
916
+
917
+ // Basic vector math
918
+ const vec = {
919
+ sub(a, b) { return [a[0] - b[0], a[1] - b[1], a[2] - b[2]]; },
920
+ add(a, b) { return [a[0] + b[0], a[1] + b[1], a[2] + b[2]]; },
921
+ dot(a, b) { return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; },
922
+ 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]]; },
923
+ mul(a, s) { return [a[0] * s, a[1] * s, a[2] * s]; },
924
+ len(a) { return Math.hypot(a[0], a[1], a[2]); },
925
+ norm(a) { const l = Math.hypot(a[0], a[1], a[2]) || 1; return [a[0] / l, a[1] / l, a[2] / l]; },
926
+ };
927
+
928
+ const pointOf = (i) => [vp[i * 3 + 0], vp[i * 3 + 1], vp[i * 3 + 2]];
929
+ const triArea = (ia, ib, ic) => {
930
+ const A = pointOf(ia), B = pointOf(ib), C = pointOf(ic);
931
+ const ab = vec.sub(B, A), ac = vec.sub(C, A);
932
+ const cr = vec.cross(ab, ac);
933
+ return 0.5 * Math.hypot(cr[0], cr[1], cr[2]);
934
+ };
935
+
936
+ // Plane from triangle
937
+ const planeOf = (A, B, C) => {
938
+ const n = vec.cross(vec.sub(B, A), vec.sub(C, A));
939
+ const ln = vec.len(n);
940
+ if (ln < 1e-18) return { n: [0, 0, 0], d: 0 };
941
+ const nn = [n[0] / ln, n[1] / ln, n[2] / ln];
942
+ const d = -vec.dot(nn, A);
943
+ return { n: nn, d };
944
+ };
945
+
946
+ const sd = (pl, P) => vec.dot(pl.n, P) + pl.d;
947
+
948
+ // Clip triangle by plane -> segment endpoints on triangle edges
949
+ const triPlaneClipSegment = (A, B, C, pl) => {
950
+ const sA = sd(pl, A), sB = sd(pl, B), sC = sd(pl, C);
951
+ const pts = [];
952
+ const pushIfUnique = (P) => {
953
+ for (let k = 0; k < pts.length; k++) {
954
+ const Q = pts[k];
955
+ if (Math.hypot(P[0] - Q[0], P[1] - Q[1], P[2] - Q[2]) < 1e-9) return;
956
+ }
957
+ pts.push(P);
958
+ };
959
+ const edgeHit = (P, sP, Q, sQ) => {
960
+ if (sP === 0 && sQ === 0) return; // coplanar edge, skip
961
+ if ((sP > 0 && sQ < 0) || (sP < 0 && sQ > 0)) {
962
+ const t = sP / (sP - sQ);
963
+ const hit = [P[0] + (Q[0] - P[0]) * t, P[1] + (Q[1] - P[1]) * t, P[2] + (Q[2] - P[2]) * t];
964
+ pushIfUnique(hit);
965
+ } else if (Math.abs(sP) < 1e-12) {
966
+ pushIfUnique(P);
967
+ } else if (Math.abs(sQ) < 1e-12) {
968
+ pushIfUnique(Q);
969
+ }
970
+ };
971
+ edgeHit(A, sA, B, sB);
972
+ edgeHit(B, sB, C, sC);
973
+ edgeHit(C, sC, A, sA);
974
+ if (pts.length < 2) return null;
975
+ if (pts.length > 2) {
976
+ // In degenerate near-coplanar cases we may collect 3 points; keep the two farthest
977
+ let bestI = 0, bestJ = 1, bestD = -1;
978
+ for (let i = 0; i < pts.length; i++) for (let j = i + 1; j < pts.length; j++) {
979
+ const dx = pts[i][0] - pts[j][0];
980
+ const dy = pts[i][1] - pts[j][1];
981
+ const dz = pts[i][2] - pts[j][2];
982
+ const d2 = dx * dx + dy * dy + dz * dz;
983
+ if (d2 > bestD) { bestD = d2; bestI = i; bestJ = j; }
984
+ }
985
+ return [pts[bestI], pts[bestJ]];
986
+ }
987
+ return [pts[0], pts[1]];
988
+ };
989
+
990
+ // Enhanced triangle-triangle intersection that handles coplanar overlapping cases
991
+ const triTriIntersectSegment = (A, B, C, D, E, F) => {
992
+ const p1 = planeOf(A, B, C);
993
+ const p2 = planeOf(D, E, F);
994
+ const n1 = p1.n, n2 = p2.n;
995
+ const cr = vec.cross(n1, n2);
996
+ const crLen = vec.len(cr);
997
+
998
+ // Check if planes are nearly parallel (coplanar case)
999
+ if (crLen < 0.1) { // Allow more parallel cases for coplanar detection
1000
+ // For coplanar/nearly coplanar triangles, check for overlap
1001
+ const coplanarResult = handleCoplanarTriangles(A, B, C, D, E, F, p1, p2);
1002
+ if (coplanarResult) return coplanarResult;
1003
+ return null;
1004
+ }
1005
+
1006
+ // Check if triangles are on opposite sides of each other's planes
1007
+ const sD = sd(p1, D), sE = sd(p1, E), sF = sd(p1, F);
1008
+ if ((sD > EPS && sE > EPS && sF > EPS) || (sD < -EPS && sE < -EPS && sF < -EPS)) return null;
1009
+
1010
+ const sA = sd(p2, A), sB = sd(p2, B), sC = sd(p2, C);
1011
+ if ((sA > EPS && sB > EPS && sC > EPS) || (sA < -EPS && sB < -EPS && sC < -EPS)) return null;
1012
+
1013
+ const seg1 = triPlaneClipSegment(A, B, C, p2);
1014
+ const seg2 = triPlaneClipSegment(D, E, F, p1);
1015
+ if (!seg1 || !seg2) return null;
1016
+
1017
+ const [P1, P2] = seg1;
1018
+ const [Q1, Q2] = seg2;
1019
+ const dir = vec.sub(P2, P1);
1020
+ const L = vec.len(dir);
1021
+ if (L < 1e-9) return null; // Reject very short intersection segments
1022
+ const Lhat = vec.mul(dir, 1 / L);
1023
+
1024
+ const tP1 = 0;
1025
+ const tP2 = L;
1026
+ const tQ1 = vec.dot(vec.sub(Q1, P1), Lhat);
1027
+ const tQ2 = vec.dot(vec.sub(Q2, P1), Lhat);
1028
+ const i1 = Math.min(tP1, tP2), i2 = Math.max(tP1, tP2);
1029
+ const j1 = Math.min(tQ1, tQ2), j2 = Math.max(tQ1, tQ2);
1030
+ const a = Math.max(i1, j1), b = Math.min(i2, j2);
1031
+
1032
+ // Require significant overlap to avoid edge cases
1033
+ if (!(b > a + 1e-8)) return null;
1034
+
1035
+ const X = [P1[0] + Lhat[0] * a, P1[1] + Lhat[1] * a, P1[2] + Lhat[2] * a];
1036
+ const Y = [P1[0] + Lhat[0] * b, P1[1] + Lhat[1] * b, P1[2] + Lhat[2] * b];
1037
+
1038
+ return [X, Y];
1039
+ };
1040
+
1041
+ // Handle coplanar or nearly coplanar triangles
1042
+ const handleCoplanarTriangles = (A, B, C, D, E, F, p1, p2) => {
1043
+ // Check if triangles are on roughly the same plane
1044
+ const maxDist1 = Math.max(Math.abs(sd(p1, D)), Math.abs(sd(p1, E)), Math.abs(sd(p1, F)));
1045
+ const maxDist2 = Math.max(Math.abs(sd(p2, A)), Math.abs(sd(p2, B)), Math.abs(sd(p2, C)));
1046
+
1047
+ // Use a more generous threshold for coplanar detection
1048
+ const threshold = Math.max(1e-6, EPS * 100);
1049
+
1050
+ if (maxDist1 > threshold || maxDist2 > threshold) return null;
1051
+
1052
+ // For coplanar overlapping triangles, we need to create valid cutting lines
1053
+ // that allow both triangles to be subdivided properly
1054
+
1055
+ const n1 = vec.cross(vec.sub(B, A), vec.sub(C, A));
1056
+ const n2 = vec.cross(vec.sub(E, D), vec.sub(F, D));
1057
+ const avgN = vec.norm(vec.add(n1, n2));
1058
+
1059
+ // Choose projection axis
1060
+ const absN = [Math.abs(avgN[0]), Math.abs(avgN[1]), Math.abs(avgN[2])];
1061
+ let dropAxis = 0;
1062
+ if (absN[1] > absN[dropAxis]) dropAxis = 1;
1063
+ if (absN[2] > absN[dropAxis]) dropAxis = 2;
1064
+
1065
+ const project = (P) => {
1066
+ if (dropAxis === 0) return [P[1], P[2]];
1067
+ if (dropAxis === 1) return [P[0], P[2]];
1068
+ return [P[0], P[1]];
1069
+ };
1070
+
1071
+ const tri1_2d = [project(A), project(B), project(C)];
1072
+ const tri2_2d = [project(D), project(E), project(F)];
1073
+
1074
+ // Find all intersection points between triangle edges
1075
+ const intersectionPoints = [];
1076
+
1077
+ // Edge-edge intersections
1078
+ const edges1 = [[A, B], [B, C], [C, A]];
1079
+ const edges1_2d = [[tri1_2d[0], tri1_2d[1]], [tri1_2d[1], tri1_2d[2]], [tri1_2d[2], tri1_2d[0]]];
1080
+ const edges2_2d = [[tri2_2d[0], tri2_2d[1]], [tri2_2d[1], tri2_2d[2]], [tri2_2d[2], tri2_2d[0]]];
1081
+
1082
+ for (let i = 0; i < 3; i++) {
1083
+ for (let j = 0; j < 3; j++) {
1084
+ const int2d = lineIntersection2D(edges1_2d[i], edges2_2d[j]);
1085
+ if (int2d) {
1086
+ // Convert back to 3D using parametric interpolation on edge1
1087
+ const t1 = getParameterOnSegment2D(edges1_2d[i], int2d);
1088
+ if (t1 >= 0 && t1 <= 1) {
1089
+ const int3d = [
1090
+ edges1[i][0][0] + t1 * (edges1[i][1][0] - edges1[i][0][0]),
1091
+ edges1[i][0][1] + t1 * (edges1[i][1][1] - edges1[i][0][1]),
1092
+ edges1[i][0][2] + t1 * (edges1[i][1][2] - edges1[i][0][2])
1093
+ ];
1094
+ intersectionPoints.push(int3d);
1095
+ }
1096
+ }
1097
+ }
1098
+ }
1099
+
1100
+ // If we don't have edge intersections, try a different approach for overlapping triangles
1101
+ if (intersectionPoints.length === 0) {
1102
+ // For completely contained triangles or other overlap cases,
1103
+ // create a cutting line across the overlapping region
1104
+
1105
+ // Find the centroid of the overlapping region
1106
+ const allPoints = [A, B, C, D, E, F];
1107
+ const centroid = [
1108
+ allPoints.reduce((sum, p) => sum + p[0], 0) / allPoints.length,
1109
+ allPoints.reduce((sum, p) => sum + p[1], 0) / allPoints.length,
1110
+ allPoints.reduce((sum, p) => sum + p[2], 0) / allPoints.length
1111
+ ];
1112
+
1113
+ // Create a cutting line that passes through the overlap
1114
+ // Use the longest edge of the smaller triangle as the basis
1115
+ const tri1Area = 0.5 * vec.len(vec.cross(vec.sub(B, A), vec.sub(C, A)));
1116
+ const tri2Area = 0.5 * vec.len(vec.cross(vec.sub(E, D), vec.sub(F, D)));
1117
+
1118
+ let cutStart, cutEnd;
1119
+ if (tri1Area > tri2Area) {
1120
+ // Triangle 1 is larger, use triangle 2's longest edge as cut direction
1121
+ const edges2Lens = [
1122
+ vec.len(vec.sub(E, D)),
1123
+ vec.len(vec.sub(F, E)),
1124
+ vec.len(vec.sub(D, F))
1125
+ ];
1126
+ const maxEdgeIdx = edges2Lens.indexOf(Math.max(...edges2Lens));
1127
+ cutStart = [D, E, F][maxEdgeIdx];
1128
+ cutEnd = [D, E, F][(maxEdgeIdx + 1) % 3];
1129
+ } else {
1130
+ // Triangle 2 is larger, use triangle 1's longest edge as cut direction
1131
+ const edges1Lens = [
1132
+ vec.len(vec.sub(B, A)),
1133
+ vec.len(vec.sub(C, B)),
1134
+ vec.len(vec.sub(A, C))
1135
+ ];
1136
+ const maxEdgeIdx = edges1Lens.indexOf(Math.max(...edges1Lens));
1137
+ cutStart = [A, B, C][maxEdgeIdx];
1138
+ cutEnd = [A, B, C][(maxEdgeIdx + 1) % 3];
1139
+ }
1140
+
1141
+ return [cutStart, cutEnd];
1142
+ }
1143
+
1144
+ // Remove duplicate intersection points
1145
+ const uniquePoints = [];
1146
+ for (const pt of intersectionPoints) {
1147
+ let isDuplicate = false;
1148
+ for (const existing of uniquePoints) {
1149
+ if (vec.len(vec.sub(pt, existing)) < 1e-9) {
1150
+ isDuplicate = true;
1151
+ break;
1152
+ }
1153
+ }
1154
+ if (!isDuplicate) uniquePoints.push(pt);
1155
+ }
1156
+
1157
+ if (uniquePoints.length >= 2) {
1158
+ // Return the two most distant points as the cutting line
1159
+ let maxDist = 0;
1160
+ let bestPair = [uniquePoints[0], uniquePoints[1]];
1161
+
1162
+ for (let i = 0; i < uniquePoints.length; i++) {
1163
+ for (let j = i + 1; j < uniquePoints.length; j++) {
1164
+ const dist = vec.len(vec.sub(uniquePoints[i], uniquePoints[j]));
1165
+ if (dist > maxDist) {
1166
+ maxDist = dist;
1167
+ bestPair = [uniquePoints[i], uniquePoints[j]];
1168
+ }
1169
+ }
1170
+ }
1171
+
1172
+ return maxDist > 1e-8 ? bestPair : null;
1173
+ }
1174
+
1175
+ return null;
1176
+ };
1177
+
1178
+ // Helper function to subdivide a triangle around a contained triangle
1179
+ const subdivideContainingTriangle = (containingTri, containedTri) => {
1180
+ // For a triangle A containing triangle B, create triangles that fill A but exclude B
1181
+ // This creates a "frame" around the contained triangle
1182
+
1183
+ const A = [containingTri.A, containingTri.B, containingTri.C];
1184
+ const B = [containedTri.A, containedTri.B, containedTri.C];
1185
+
1186
+ // Create triangles connecting vertices of A to vertices of B
1187
+ const subdivisions = [];
1188
+
1189
+ // Check if triangles are nearly identical (would create degenerate subdivisions)
1190
+ const areTrianglesNearlyIdentical = (tri1, tri2, tolerance = 1e-6) => {
1191
+ for (let i = 0; i < 3; i++) {
1192
+ let minDist = Infinity;
1193
+ for (let j = 0; j < 3; j++) {
1194
+ const dist = vec.len(vec.sub(tri1[i], tri2[j]));
1195
+ minDist = Math.min(minDist, dist);
1196
+ }
1197
+ if (minDist > tolerance) return false;
1198
+ }
1199
+ return true;
1200
+ };
1201
+
1202
+ // Check triangle area to avoid degenerate triangles
1203
+ const triangleArea = (p1, p2, p3) => {
1204
+ const cross = vec.cross(vec.sub(p2, p1), vec.sub(p3, p1));
1205
+ return 0.5 * vec.len(cross);
1206
+ };
1207
+
1208
+ if (areTrianglesNearlyIdentical(A, B, 1e-3)) {
1209
+ // Triangles are too similar, skip subdivision to avoid degeneracies
1210
+ return null;
1211
+ }
1212
+
1213
+ // Strategy: Create triangles by connecting each vertex of A to nearest edge of B
1214
+ // This avoids creating very small or degenerate triangles
1215
+
1216
+ for (let i = 0; i < 3; i++) {
1217
+ const vertexA = A[i];
1218
+
1219
+ // Find the best connection points on triangle B's edges
1220
+ const edgesB = [
1221
+ [B[0], B[1]], [B[1], B[2]], [B[2], B[0]]
1222
+ ];
1223
+
1224
+ let bestEdgeIdx = -1;
1225
+ let bestDist = Infinity;
1226
+
1227
+ // Find the edge of B that's closest to this vertex of A
1228
+ for (let j = 0; j < 3; j++) {
1229
+ const edgeStart = edgesB[j][0];
1230
+ const edgeEnd = edgesB[j][1];
1231
+ const midPoint = vec.add(edgeStart, vec.mul(vec.sub(edgeEnd, edgeStart), 0.5));
1232
+ const dist = vec.len(vec.sub(vertexA, midPoint));
1233
+
1234
+ if (dist < bestDist) {
1235
+ bestDist = dist;
1236
+ bestEdgeIdx = j;
1237
+ }
1238
+ }
1239
+
1240
+ if (bestEdgeIdx >= 0) {
1241
+ const edgeStart = edgesB[bestEdgeIdx][0];
1242
+ const edgeEnd = edgesB[bestEdgeIdx][1];
1243
+
1244
+ // Create triangle from vertex A to the edge of B
1245
+ const area = triangleArea(vertexA, edgeStart, edgeEnd);
1246
+
1247
+ // Only add if triangle has significant area (avoid degenerates)
1248
+ if (area > 1e-8) {
1249
+ const newTri = [
1250
+ this._getPointIndex(vertexA),
1251
+ this._getPointIndex(edgeStart),
1252
+ this._getPointIndex(edgeEnd)
1253
+ ];
1254
+ subdivisions.push(newTri);
1255
+ }
1256
+ }
1257
+ }
1258
+
1259
+ return subdivisions.length > 0 ? subdivisions : null;
1260
+ };
1261
+
1262
+ // 2D line segment intersection
1263
+ const lineIntersection2D = ([p1, p2], [p3, p4]) => {
1264
+ const x1 = p1[0], y1 = p1[1], x2 = p2[0], y2 = p2[1];
1265
+ const x3 = p3[0], y3 = p3[1], x4 = p4[0], y4 = p4[1];
1266
+
1267
+ const denom = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
1268
+ if (Math.abs(denom) < 1e-10) return null; // Parallel lines
1269
+
1270
+ const t = ((x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)) / denom;
1271
+ const u = -((x1 - x2) * (y1 - y3) - (y1 - y2) * (x1 - x3)) / denom;
1272
+
1273
+ if (t >= 0 && t <= 1 && u >= 0 && u <= 1) {
1274
+ return [x1 + t * (x2 - x1), y1 + t * (y2 - y1)];
1275
+ }
1276
+
1277
+ return null;
1278
+ };
1279
+
1280
+ // Get parameter along 2D segment
1281
+ const getParameterOnSegment2D = ([p1, p2], point) => {
1282
+ const dx = p2[0] - p1[0];
1283
+ const dy = p2[1] - p1[1];
1284
+
1285
+ if (Math.abs(dx) > Math.abs(dy)) {
1286
+ return (point[0] - p1[0]) / dx;
1287
+ } else {
1288
+ return (point[1] - p1[1]) / dy;
1289
+ }
1290
+ };
1291
+
1292
+ // Manifold-safe barycentric coordinates
1293
+ const barycentric = (A, B, C, X) => {
1294
+ const v0 = vec.sub(C, A);
1295
+ const v1 = vec.sub(B, A);
1296
+ const v2 = vec.sub(X, A);
1297
+
1298
+ const dot00 = vec.dot(v0, v0);
1299
+ const dot01 = vec.dot(v0, v1);
1300
+ const dot02 = vec.dot(v0, v2);
1301
+ const dot11 = vec.dot(v1, v1);
1302
+ const dot12 = vec.dot(v1, v2);
1303
+
1304
+ const denom = dot00 * dot11 - dot01 * dot01;
1305
+ if (Math.abs(denom) < 1e-14) return null; // Degenerate triangle
1306
+
1307
+ const invDenom = 1.0 / denom;
1308
+ const u = (dot11 * dot02 - dot01 * dot12) * invDenom;
1309
+ const v = (dot00 * dot12 - dot01 * dot02) * invDenom;
1310
+ const w = 1.0 - u - v;
1311
+
1312
+ return [w, v, u]; // [A, B, C] weights
1313
+ };
1314
+
1315
+ // Conservative edge classification that avoids T-junctions
1316
+ const classifyEdge = (w) => {
1317
+ const [wa, wb, wc] = w;
1318
+ const t = 0.05; // Conservative margin: points must be well away from vertices
1319
+
1320
+ // Only classify as on an edge if clearly on that edge and not near vertices
1321
+ if (wc < t && wa > t && wb > t) return 0; // AB edge
1322
+ if (wa < t && wb > t && wc > t) return 1; // BC edge
1323
+ if (wb < t && wa > t && wc > t) return 2; // CA edge
1324
+
1325
+ return -1; // Not clearly on any edge
1326
+ };
1327
+
1328
+ // Enhanced triangle splitting for coplanar overlapping triangles
1329
+ const splitOneTriangle = (ia, ib, ic, P, Q) => {
1330
+ const A = pointOf(ia), B = pointOf(ib), C = pointOf(ic);
1331
+ const wP = barycentric(A, B, C, P);
1332
+ const wQ = barycentric(A, B, C, Q);
1333
+
1334
+ if (!wP || !wQ) {
1335
+ if (diagnostics) console.log(` FAIL: Degenerate barycentric coordinates`);
1336
+ return null; // Degenerate case
1337
+ }
1338
+
1339
+ // For coplanar case, allow points on or near edges - be more permissive
1340
+ const reasonablyInside = (w) => w[0] >= -0.1 && w[1] >= -0.1 && w[2] >= -0.1 &&
1341
+ (w[0] + w[1] + w[2]) >= 0.9 && (w[0] + w[1] + w[2]) <= 1.1;
1342
+ if (!reasonablyInside(wP) || !reasonablyInside(wQ)) {
1343
+ if (diagnostics) {
1344
+ console.log(` FAIL: Points not reasonably inside triangle`);
1345
+ console.log(` P weights: [${wP[0].toFixed(4)}, ${wP[1].toFixed(4)}, ${wP[2].toFixed(4)}] sum=${(wP[0] + wP[1] + wP[2]).toFixed(4)}`);
1346
+ console.log(` Q weights: [${wQ[0].toFixed(4)}, ${wQ[1].toFixed(4)}, ${wQ[2].toFixed(4)}] sum=${(wQ[0] + wQ[1] + wQ[2]).toFixed(4)}`);
1347
+ }
1348
+ return null;
1349
+ }
1350
+
1351
+ // Check if points are too close to existing vertices - be more permissive for coplanar cases
1352
+ const minVertexDist = 1e-6; // Increased from 1e-8 to allow closer points
1353
+ const nearVertex = (pt, vertex) => vec.len(vec.sub(pt, vertex)) < minVertexDist;
1354
+ if (nearVertex(P, A) || nearVertex(P, B) || nearVertex(P, C) ||
1355
+ nearVertex(Q, A) || nearVertex(Q, B) || nearVertex(Q, C)) {
1356
+ if (diagnostics) console.log(` FAIL: Points too close to existing vertices`);
1357
+ return null;
1358
+ }
1359
+
1360
+ const edgeP = classifyEdge(wP);
1361
+ const edgeQ = classifyEdge(wQ);
1362
+
1363
+ const ip = this._getPointIndex(P);
1364
+ const iq = this._getPointIndex(Q);
1365
+
1366
+ // More lenient area check for coplanar cases
1367
+ const emit = (i0, i1, i2, out) => {
1368
+ if (i0 === i1 || i1 === i2 || i2 === i0) return;
1369
+ const area = triArea(i0, i1, i2);
1370
+ if (!(area > 1e-12)) return; // More lenient for coplanar splitting
1371
+ out.push([i0, i1, i2]);
1372
+ };
1373
+
1374
+ const out = [];
1375
+ const iA = ia, iB = ib, iC = ic;
1376
+
1377
+ // Enhanced splitting: handle both interior and edge cases
1378
+ if (edgeP === -1 && edgeQ === -1) {
1379
+ // Both points are interior - create fan triangulation
1380
+ emit(iA, ip, iq, out);
1381
+ emit(iA, iB, ip, out);
1382
+ emit(ip, iB, iq, out);
1383
+ emit(iB, iC, iq, out);
1384
+ emit(iq, iC, iA, out);
1385
+ } else if (edgeP === -1 || edgeQ === -1) {
1386
+ // One interior, one on edge
1387
+ const interior = edgeP === -1 ? ip : iq;
1388
+ const edge = edgeP === -1 ? iq : ip;
1389
+ const edgeId = edgeP === -1 ? edgeQ : edgeP;
1390
+
1391
+ const E_AB = 0, E_BC = 1, E_CA = 2;
1392
+
1393
+ if (edgeId === E_AB) {
1394
+ emit(iA, edge, interior, out);
1395
+ emit(edge, iB, interior, out);
1396
+ emit(iB, iC, interior, out);
1397
+ emit(iC, iA, interior, out);
1398
+ } else if (edgeId === E_BC) {
1399
+ emit(iB, edge, interior, out);
1400
+ emit(edge, iC, interior, out);
1401
+ emit(iC, iA, interior, out);
1402
+ emit(iA, iB, interior, out);
1403
+ } else if (edgeId === E_CA) {
1404
+ emit(iC, edge, interior, out);
1405
+ emit(edge, iA, interior, out);
1406
+ emit(iA, iB, interior, out);
1407
+ emit(iB, iC, interior, out);
1408
+ }
1409
+ } else {
1410
+ // Both on edges - handle specific edge combinations
1411
+ const E_AB = 0, E_BC = 1, E_CA = 2;
1412
+
1413
+ if ((edgeP === E_AB && edgeQ === E_CA) || (edgeQ === E_AB && edgeP === E_CA)) {
1414
+ // Cut near vertex A
1415
+ emit(iA, ip, iq, out);
1416
+ emit(ip, iB, iC, out);
1417
+ emit(ip, iC, iq, out);
1418
+ } else if ((edgeP === E_AB && edgeQ === E_BC) || (edgeQ === E_AB && edgeP === E_BC)) {
1419
+ // Cut near vertex B
1420
+ emit(iB, ip, iq, out);
1421
+ emit(iA, ip, iq, out);
1422
+ emit(iA, iq, iC, out);
1423
+ } else if ((edgeP === E_BC && edgeQ === E_CA) || (edgeQ === E_BC && edgeP === E_CA)) {
1424
+ // Cut near vertex C
1425
+ emit(iC, ip, iq, out);
1426
+ emit(iA, iB, ip, out);
1427
+ emit(iA, ip, iq, out);
1428
+ } else if (edgeP !== edgeQ) {
1429
+ // Different edges - create diagonal split
1430
+ emit(ip, iq, iA, out);
1431
+ emit(ip, iq, iB, out);
1432
+ emit(ip, iq, iC, out);
1433
+ // Add remaining coverage
1434
+ if (edgeP === E_AB && edgeQ === E_BC) {
1435
+ emit(iA, ip, iq, out);
1436
+ emit(iq, iC, iA, out);
1437
+ } // Add other combinations as needed
1438
+ }
1439
+ }
1440
+
1441
+ // Require at least 2 triangles for a valid split
1442
+ return out.length >= 2 ? out : null;
1443
+ };
1444
+
1445
+ // Build an adjacency set of triangle pairs that share an edge
1446
+ const buildAdjacencyPairs = () => {
1447
+ const triCount = (this._triVerts.length / 3) | 0;
1448
+ const nv = (this._vertProperties.length / 3) | 0;
1449
+ const NV = BigInt(Math.max(1, nv));
1450
+ const ukey = (a, b) => {
1451
+ const A = BigInt(a), B = BigInt(b);
1452
+ return (A < B) ? (A * NV + B) : (B * NV + A);
1453
+ };
1454
+ const e2t = new Map();
1455
+ for (let t = 0; t < triCount; t++) {
1456
+ const b = t * 3;
1457
+ const i0 = this._triVerts[b + 0] >>> 0;
1458
+ const i1 = this._triVerts[b + 1] >>> 0;
1459
+ const i2 = this._triVerts[b + 2] >>> 0;
1460
+ const edges = [[i0, i1], [i1, i2], [i2, i0]];
1461
+ for (let k = 0; k < 3; k++) {
1462
+ const a = edges[k][0], c = edges[k][1];
1463
+ const key = ukey(a, c);
1464
+ let arr = e2t.get(key);
1465
+ if (!arr) { arr = []; e2t.set(key, arr); }
1466
+ arr.push(t);
1467
+ }
1468
+ }
1469
+ const adj = new Set();
1470
+ const pkey = (a, b) => a < b ? `${a},${b}` : `${b},${a}`;
1471
+ for (const [, arr] of e2t.entries()) {
1472
+ if (arr.length === 2) {
1473
+ const a = arr[0], b = arr[1];
1474
+ adj.add(pkey(a, b));
1475
+ } else if (arr.length > 2) {
1476
+ // Non-manifold edge: mark all pairs as adjacent so we don't split across it
1477
+ for (let i = 0; i < arr.length; i++) for (let j = i + 1; j < arr.length; j++) adj.add(pkey(arr[i], arr[j]));
1478
+ }
1479
+ }
1480
+ return adj;
1481
+ };
1482
+
1483
+ let totalSplits = 0;
1484
+ const seenSegments = new Set();
1485
+ const Q = 1e-7;
1486
+ const qpt = (P) => `${Math.round(P[0]/Q)},${Math.round(P[1]/Q)},${Math.round(P[2]/Q)}`;
1487
+ const skey = (P, Qp) => {
1488
+ const a = qpt(P), b = qpt(Qp);
1489
+ return a < b ? `${a}__${b}` : `${b}__${a}`;
1490
+ };
1491
+
1492
+ // Conservative iteration limit to prevent infinite loops
1493
+ const maxIterations = Math.min(20, Math.max(3, triCount0));
1494
+
1495
+ iteration: for (let pass = 0; pass < maxIterations; pass++) {
1496
+ const triCount = (this._triVerts.length / 3) | 0;
1497
+ if (triCount < 2) break;
1498
+
1499
+ if (diagnostics) {
1500
+ console.log(`\nPass ${pass + 1}: checking ${triCount} triangles`);
1501
+ }
1502
+
1503
+ const adjPairs = buildAdjacencyPairs();
1504
+
1505
+ if (diagnostics) {
1506
+ console.log(`Adjacent pairs count: ${adjPairs.size}`);
1507
+ }
1508
+
1509
+ // Standard AABB sweep setup
1510
+ const tris = new Array(triCount);
1511
+ for (let t = 0; t < triCount; t++) {
1512
+ const b = t * 3;
1513
+ const i0 = this._triVerts[b + 0] >>> 0;
1514
+ const i1 = this._triVerts[b + 1] >>> 0;
1515
+ const i2 = this._triVerts[b + 2] >>> 0;
1516
+ const A = pointOf(i0), B = pointOf(i1), C = pointOf(i2);
1517
+ const minX = Math.min(A[0], B[0], C[0]);
1518
+ const minY = Math.min(A[1], B[1], C[1]);
1519
+ const minZ = Math.min(A[2], B[2], C[2]);
1520
+ const maxX = Math.max(A[0], B[0], C[0]);
1521
+ const maxY = Math.max(A[1], B[1], C[1]);
1522
+ const maxZ = Math.max(A[2], B[2], C[2]);
1523
+ tris[t] = { t, i0, i1, i2, A, B, C, minX, minY, minZ, maxX, maxY, maxZ };
1524
+ }
1525
+ const order = Array.from({ length: triCount }, (_, i) => i);
1526
+ order.sort((p, q) => tris[p].minX - tris[q].minX);
1527
+
1528
+ const pairKey = (a, b) => a < b ? `${a},${b}` : `${b},${a}`;
1529
+ const tried = new Set();
1530
+ let splitsThisPass = 0;
1531
+
1532
+ let checkedPairs = 0;
1533
+ let adjacentSkips = 0;
1534
+ let intersectionTests = 0;
1535
+ let intersectionHits = 0;
1536
+
1537
+ for (let ii = 0; ii < order.length && splitsThisPass < 5; ii++) {
1538
+ const ai = order[ii];
1539
+ const A = tris[ai];
1540
+
1541
+ for (let jj = ii + 1; jj < order.length; jj++) {
1542
+ const bi = order[jj];
1543
+ const B = tris[bi];
1544
+ if (B.minX > A.maxX + 1e-12) break; // sweep prune by X
1545
+ if (B.maxY < A.minY - 1e-12 || B.minY > A.maxY + 1e-12) continue;
1546
+ if (B.maxZ < A.minZ - 1e-12 || B.minZ > A.maxZ + 1e-12) continue;
1547
+
1548
+ checkedPairs++;
1549
+
1550
+ const pk = pairKey(A.t, B.t);
1551
+ if (adjPairs.has(pk)) {
1552
+ adjacentSkips++;
1553
+ continue; // skip adjacent triangles sharing an edge
1554
+ }
1555
+ if (tried.has(pk)) continue;
1556
+ tried.add(pk);
1557
+
1558
+ intersectionTests++;
1559
+ const seg = triTriIntersectSegment(A.A, A.B, A.C, B.A, B.B, B.C);
1560
+ if (!seg) continue;
1561
+
1562
+ intersectionHits++;
1563
+
1564
+ const [P, Q] = seg;
1565
+ const keySeg = skey(P, Q);
1566
+ if (seenSegments.has(keySeg)) continue;
1567
+
1568
+ const dPQ = Math.hypot(P[0] - Q[0], P[1] - Q[1], P[2] - Q[2]);
1569
+ if (!(dPQ > EPS)) continue;
1570
+
1571
+ // Special handling for overlapping coplanar triangles
1572
+ // Check if this is a coplanar containment case where P and Q are both vertices of one triangle
1573
+ const isCoplanarContainment = (
1574
+ (vec.len(vec.sub(P, A.A)) < 1e-9 || vec.len(vec.sub(P, A.B)) < 1e-9 || vec.len(vec.sub(P, A.C)) < 1e-9) &&
1575
+ (vec.len(vec.sub(Q, A.A)) < 1e-9 || vec.len(vec.sub(Q, A.B)) < 1e-9 || vec.len(vec.sub(Q, A.C)) < 1e-9)
1576
+ ) || (
1577
+ (vec.len(vec.sub(P, B.A)) < 1e-9 || vec.len(vec.sub(P, B.B)) < 1e-9 || vec.len(vec.sub(P, B.C)) < 1e-9) &&
1578
+ (vec.len(vec.sub(Q, B.A)) < 1e-9 || vec.len(vec.sub(Q, B.B)) < 1e-9 || vec.len(vec.sub(Q, B.C)) < 1e-9)
1579
+ );
1580
+
1581
+ if (isCoplanarContainment) {
1582
+ // For coplanar overlapping triangles, we need to handle subdivision differently
1583
+ // Instead of trying to split both triangles with the same line,
1584
+ // we subdivide the containing triangle and keep overlapping triangles
1585
+
1586
+ // Determine which triangle contains the other by checking vertices
1587
+ const pointInTriangle3D = (pt, [t1, t2, t3]) => {
1588
+ // Project to 2D for point-in-triangle test
1589
+ const n = vec.norm(vec.cross(vec.sub(t2, t1), vec.sub(t3, t1)));
1590
+ const absN = [Math.abs(n[0]), Math.abs(n[1]), Math.abs(n[2])];
1591
+ let dropAxis = 0;
1592
+ if (absN[1] > absN[dropAxis]) dropAxis = 1;
1593
+ if (absN[2] > absN[dropAxis]) dropAxis = 2;
1594
+
1595
+ const project = (P) => {
1596
+ if (dropAxis === 0) return [P[1], P[2]];
1597
+ if (dropAxis === 1) return [P[0], P[2]];
1598
+ return [P[0], P[1]];
1599
+ };
1600
+
1601
+ const pt2d = project(pt);
1602
+ const tri2d = [project(t1), project(t2), project(t3)];
1603
+
1604
+ const v0 = [tri2d[2][0] - tri2d[0][0], tri2d[2][1] - tri2d[0][1]];
1605
+ const v1 = [tri2d[1][0] - tri2d[0][0], tri2d[1][1] - tri2d[0][1]];
1606
+ const v2 = [pt2d[0] - tri2d[0][0], pt2d[1] - tri2d[0][1]];
1607
+
1608
+ const dot00 = v0[0] * v0[0] + v0[1] * v0[1];
1609
+ const dot01 = v0[0] * v1[0] + v0[1] * v1[1];
1610
+ const dot02 = v0[0] * v2[0] + v0[1] * v2[1];
1611
+ const dot11 = v1[0] * v1[0] + v1[1] * v1[1];
1612
+ const dot12 = v1[0] * v2[0] + v1[1] * v2[1];
1613
+
1614
+ const denom = (dot00 * dot11 - dot01 * dot01);
1615
+ if (Math.abs(denom) < 1e-12) return false;
1616
+
1617
+ const invDenom = 1 / denom;
1618
+ const u = (dot11 * dot02 - dot01 * dot12) * invDenom;
1619
+ const v = (dot00 * dot12 - dot01 * dot02) * invDenom;
1620
+
1621
+ return (u >= -1e-10) && (v >= -1e-10) && (u + v <= 1 + 1e-10);
1622
+ };
1623
+
1624
+ const bInA = pointInTriangle3D(B.A, [A.A, A.B, A.C]) &&
1625
+ pointInTriangle3D(B.B, [A.A, A.B, A.C]) &&
1626
+ pointInTriangle3D(B.C, [A.A, A.B, A.C]);
1627
+
1628
+ if (bInA) {
1629
+ // Triangle B is contained in Triangle A
1630
+ // We need to actually subdivide triangle A around triangle B
1631
+
1632
+ // For true subdivision, we need to create multiple triangles from A that exclude the B region
1633
+ // This requires complex triangulation - let's create a simpler approach first
1634
+
1635
+ // Create new triangles that subdivide A around B
1636
+ const newTriangles = subdivideContainingTriangle(A, B);
1637
+
1638
+ if (newTriangles && newTriangles.length > 0) {
1639
+ // Replace triangle A with the subdivision
1640
+ // Keep triangle B as-is to create the overlapping effect
1641
+
1642
+ // CORRECTED: Build new arrays properly by copying all triangles except A,
1643
+ // then adding subdivided triangles in place of A
1644
+ const newTV = [];
1645
+ const newIDs = [];
1646
+
1647
+ // Copy all triangles except A
1648
+ for (let t = 0; t < triCount; t++) {
1649
+ if (t === A.t) {
1650
+ // Skip triangle A - we'll replace it with subdivisions
1651
+ continue;
1652
+ }
1653
+ const base = t * 3;
1654
+ newTV.push(this._triVerts[base], this._triVerts[base + 1], this._triVerts[base + 2]);
1655
+ newIDs.push(this._triIDs[t]);
1656
+ }
1657
+
1658
+ // Add subdivided triangles to replace triangle A
1659
+ for (const tri of newTriangles) {
1660
+ newTV.push(tri[0], tri[1], tri[2]);
1661
+ newIDs.push(this._triIDs[A.t]); // Preserve original face ID
1662
+ }
1663
+
1664
+ this._triVerts = newTV;
1665
+ this._triIDs = newIDs;
1666
+ this._dirty = true;
1667
+
1668
+ seenSegments.add(keySeg);
1669
+ splitsThisPass++;
1670
+ totalSplits++;
1671
+ continue iteration; // Restart with new triangle set
1672
+ }
1673
+
1674
+ // If subdivision failed, fall through to normal splitting
1675
+ }
1676
+
1677
+ // For other cases, continue with normal splitting
1678
+ }
1679
+
1680
+ // Attempt to split both triangles
1681
+ const newA = splitOneTriangle(A.i0, A.i1, A.i2, P, Q);
1682
+ const newB = splitOneTriangle(B.i0, B.i1, B.i2, P, Q);
1683
+
1684
+ if (diagnostics) {
1685
+ console.log(`\n=== Triangle Splitting Attempt ===`);
1686
+ console.log(`Triangle A (${A.t}): [${A.i0}, ${A.i1}, ${A.i2}] -> ${newA ? newA.length + ' new triangles' : 'FAILED'}`);
1687
+ if (newA) {
1688
+ newA.forEach((tri, i) => console.log(` A${i}: [${tri[0]}, ${tri[1]}, ${tri[2]}]`));
1689
+ }
1690
+ console.log(`Triangle B (${B.t}): [${B.i0}, ${B.i1}, ${B.i2}] -> ${newB ? newB.length + ' new triangles' : 'FAILED'}`);
1691
+ if (newB) {
1692
+ newB.forEach((tri, i) => console.log(` B${i}: [${tri[0]}, ${tri[1]}, ${tri[2]}]`));
1693
+ }
1694
+ }
1695
+
1696
+ if (!newA || !newB) continue;
1697
+
1698
+ // Manifold safety: ensure both splits are successful before applying
1699
+ // Rebuild authoring arrays: replace triangles A.t and B.t with new splits
1700
+ const newTV = [];
1701
+ const newIDs = [];
1702
+
1703
+ if (diagnostics) {
1704
+ console.log(`\n=== Rebuilding Triangle Arrays ===`);
1705
+ console.log(`Original triangle count: ${triCount}`);
1706
+ console.log(`Replacing triangle ${A.t} with ${newA.length} triangles`);
1707
+ console.log(`Replacing triangle ${B.t} with ${newB.length} triangles`);
1708
+ }
1709
+
1710
+ for (let t = 0; t < triCount; t++) {
1711
+ if (t === A.t) {
1712
+ if (diagnostics) console.log(` Replacing triangle A(${A.t}) with subdivisions`);
1713
+ for (const tri of newA) {
1714
+ newTV.push(tri[0], tri[1], tri[2]);
1715
+ newIDs.push(this._triIDs[A.t]); // Preserve original face ID
1716
+ }
1717
+ continue;
1718
+ }
1719
+ if (t === B.t) {
1720
+ if (diagnostics) console.log(` Replacing triangle B(${B.t}) with subdivisions`);
1721
+ for (const tri of newB) {
1722
+ newTV.push(tri[0], tri[1], tri[2]);
1723
+ newIDs.push(this._triIDs[B.t]); // Preserve original face ID
1724
+ }
1725
+ continue;
1726
+ }
1727
+ const base = t * 3;
1728
+ newTV.push(this._triVerts[base + 0] >>> 0, this._triVerts[base + 1] >>> 0, this._triVerts[base + 2] >>> 0);
1729
+ newIDs.push(this._triIDs[t]);
1730
+ }
1731
+
1732
+ if (diagnostics) {
1733
+ console.log(`New triangle count: ${newTV.length / 3}`);
1734
+ console.log(`Net change: +${(newTV.length / 3) - triCount} triangles`);
1735
+ }
1736
+
1737
+ this._triVerts = newTV;
1738
+ this._triIDs = newIDs;
1739
+ // Update vertex key index
1740
+ this._vertKeyToIndex = new Map();
1741
+ for (let i = 0; i < this._vertProperties.length; i += 3) {
1742
+ const x = this._vertProperties[i], y = this._vertProperties[i + 1], z = this._vertProperties[i + 2];
1743
+ this._vertKeyToIndex.set(`${x},${y},${z}`, (i / 3) | 0);
1744
+ }
1745
+ this._dirty = true;
1746
+ this._faceIndex = null;
1747
+
1748
+ totalSplits++;
1749
+ splitsThisPass++;
1750
+ seenSegments.add(keySeg);
1751
+
1752
+ // Conservative restart: only restart if we found a critical intersection
1753
+ break; // Process one split at a time for safety
1754
+ }
1755
+
1756
+ if (splitsThisPass > 0) {
1757
+ // Restart iteration after any successful split
1758
+ continue iteration;
1759
+ }
1760
+ }
1761
+
1762
+ if (diagnostics) {
1763
+ console.log(` Pass ${pass + 1} results:`);
1764
+ console.log(` Checked pairs: ${checkedPairs}`);
1765
+ console.log(` Adjacent skips: ${adjacentSkips}`);
1766
+ console.log(` Intersection tests: ${intersectionTests}`);
1767
+ console.log(` Intersection hits: ${intersectionHits}`);
1768
+ console.log(` Splits this pass: ${splitsThisPass}`);
1769
+ }
1770
+
1771
+ // If no splits this pass, we're done
1772
+ if (splitsThisPass === 0) break;
1773
+ }
1774
+
1775
+ if (totalSplits > 0) {
1776
+ // CRITICAL: Ensure manifold properties are maintained after splitting
1777
+ // 1. Fix triangle windings to ensure consistent orientation
1778
+ this.fixTriangleWindingsByAdjacency();
1779
+
1780
+ // 2. For overlapping triangle splitting, we intentionally allow non-manifold
1781
+ // intermediate states where overlapping regions have triangles with opposite normals
1782
+ // This is expected and will be resolved by duplicate removal later
1783
+ try {
1784
+ // Test manifold creation without storing the object
1785
+ this._manifoldize();
1786
+ // If we get here, the mesh is still manifold
1787
+ } catch (error) {
1788
+ // For overlapping triangles, we expect non-manifold intermediate states
1789
+ console.log('INFO: Non-manifold geometry detected after triangle splitting (expected for overlaps):', error.message);
1790
+ // Continue execution - this is expected when splitting overlapping triangles
1791
+ }
1792
+ }
1793
+
1794
+ if (diagnostics) {
1795
+ const finalTriCount = (this._triVerts.length / 3) | 0;
1796
+ console.log(`\n=== Final Results ===`);
1797
+ console.log(`Total splits: ${totalSplits}`);
1798
+ console.log(`Initial triangles: ${triCount0}`);
1799
+ console.log(`Final triangles: ${finalTriCount}`);
1800
+ console.log(`Net triangles added: ${finalTriCount - triCount0}`);
1801
+
1802
+ if (totalSplits === 0) {
1803
+ console.log(`\n❌ No triangles were split. Common reasons:`);
1804
+ console.log(` 1. No overlapping coplanar triangles found`);
1805
+ console.log(` 2. All overlapping triangles marked as adjacent (share vertices/edges)`);
1806
+ console.log(` 3. Coplanar threshold too strict for mesh precision`);
1807
+ console.log(` 4. Intersection detection failing for real mesh geometry`);
1808
+ }
1809
+ }
1810
+
1811
+ return totalSplits;
1812
+ }
1813
+
1814
+ /**
1815
+ * Removes triangles with duplicate or collinear vertices (degenerate triangles)
1816
+ * @returns {number} Number of triangles removed
1817
+ */
1818
+ export function removeDegenerateTriangles() {
1819
+ if (!this._triVerts || !this._vertProperties) {
1820
+ return 0;
1821
+ }
1822
+
1823
+ // Vector utilities
1824
+ const vec = {
1825
+ sub: (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]],
1826
+ len: (v) => Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]),
1827
+ cross: (a, b) => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]
1828
+ };
1829
+
1830
+ const originalCount = this._triVerts.length / 3;
1831
+ const newTriVerts = [];
1832
+ const newTriIDs = [];
1833
+ let removedCount = 0;
1834
+
1835
+ // Helper function to check if triangle is degenerate
1836
+ const isDegenerate = (triIndex) => {
1837
+ const i = triIndex * 3;
1838
+ const v1Idx = this._triVerts[i] * 3;
1839
+ const v2Idx = this._triVerts[i + 1] * 3;
1840
+ const v3Idx = this._triVerts[i + 2] * 3;
1841
+
1842
+ // Get vertex positions
1843
+ const v1 = [
1844
+ this._vertProperties[v1Idx],
1845
+ this._vertProperties[v1Idx + 1],
1846
+ this._vertProperties[v1Idx + 2]
1847
+ ];
1848
+ const v2 = [
1849
+ this._vertProperties[v2Idx],
1850
+ this._vertProperties[v2Idx + 1],
1851
+ this._vertProperties[v2Idx + 2]
1852
+ ];
1853
+ const v3 = [
1854
+ this._vertProperties[v3Idx],
1855
+ this._vertProperties[v3Idx + 1],
1856
+ this._vertProperties[v3Idx + 2]
1857
+ ];
1858
+
1859
+ // Check for duplicate vertices (tolerance based)
1860
+ const tolerance = 1e-10;
1861
+ const dist12 = vec.len(vec.sub(v1, v2));
1862
+ const dist23 = vec.len(vec.sub(v2, v3));
1863
+ const dist31 = vec.len(vec.sub(v3, v1));
1864
+
1865
+ if (dist12 < tolerance || dist23 < tolerance || dist31 < tolerance) {
1866
+ return true; // Duplicate vertices
1867
+ }
1868
+
1869
+ // Check for zero area (collinear vertices)
1870
+ const cross = vec.cross(vec.sub(v2, v1), vec.sub(v3, v1));
1871
+ const area = 0.5 * vec.len(cross);
1872
+
1873
+ return area < 1e-12; // Near-zero area
1874
+ };
1875
+
1876
+ // Filter out degenerate triangles
1877
+ for (let i = 0; i < originalCount; i++) {
1878
+ if (!isDegenerate(i)) {
1879
+ // Keep this triangle
1880
+ const triStart = i * 3;
1881
+ newTriVerts.push(this._triVerts[triStart]);
1882
+ newTriVerts.push(this._triVerts[triStart + 1]);
1883
+ newTriVerts.push(this._triVerts[triStart + 2]);
1884
+ newTriIDs.push(this._triIDs[triStart]);
1885
+ newTriIDs.push(this._triIDs[triStart + 1]);
1886
+ newTriIDs.push(this._triIDs[triStart + 2]);
1887
+ } else {
1888
+ removedCount++;
1889
+ }
1890
+ }
1891
+
1892
+ // Update arrays
1893
+ this._triVerts = newTriVerts;
1894
+ this._triIDs = newTriIDs;
1895
+
1896
+ console.log(`[removeDegenerateTriangles] Removed ${removedCount} degenerate triangles (${originalCount} → ${this._triVerts.length / 3})`);
1897
+
1898
+ return removedCount;
1899
+ }
1900
+
1901
+ /**
1902
+ * Remove internal triangles by rebuilding from the Manifold surface.
1903
+ * - Primary path: `_manifoldize().getMesh()` yields only the exterior faces.
1904
+ * - Fallback: if manifoldization fails (e.g., self‑intersections), falls back
1905
+ * to a winding-based classifier (or raycast if requested) to cull interior tris.
1906
+ * - Returns the number of triangles removed.
1907
+ * @param {object|string} [options] optional fallback settings; string -> fallback mode
1908
+ * @param {'winding'|'raycast'|'ray'} [options.fallback='winding'] fallback classifier
1909
+ * @param {object} [options.windingOptions] forwarded to removeInternalTrianglesByWinding
1910
+ */
1911
+ export function removeInternalTriangles(options = {}) {
1912
+ const triCountBefore = (this._triVerts.length / 3) | 0;
1913
+ if (triCountBefore === 0) return 0;
1914
+
1915
+ const opts = (options && typeof options === 'object')
1916
+ ? options
1917
+ : { fallback: options };
1918
+ const fallback = (opts.fallback || 'winding').toString().toLowerCase();
1919
+
1920
+ let mesh = null;
1921
+ try {
1922
+ const manifoldObj = this._manifoldize();
1923
+ mesh = manifoldObj.getMesh();
1924
+ const triVerts = Array.from(mesh.triVerts || []);
1925
+ const vertProps = Array.from(mesh.vertProperties || []);
1926
+ const triCountAfter = (triVerts.length / 3) | 0;
1927
+ const ids = (mesh.faceID && mesh.faceID.length === triCountAfter)
1928
+ ? Array.from(mesh.faceID)
1929
+ : new Array(triCountAfter).fill(0);
1930
+
1931
+ // Overwrite our authoring arrays with the exterior-only mesh
1932
+ this._numProp = mesh.numProp || 3;
1933
+ this._vertProperties = vertProps;
1934
+ this._triVerts = triVerts;
1935
+ this._triIDs = ids;
1936
+
1937
+ // Rebuild quick index map
1938
+ this._vertKeyToIndex = new Map();
1939
+ for (let i = 0; i < this._vertProperties.length; i += 3) {
1940
+ const x = this._vertProperties[i], y = this._vertProperties[i + 1], z = this._vertProperties[i + 2];
1941
+ this._vertKeyToIndex.set(`${x},${y},${z}`, (i / 3) | 0);
1942
+ }
1943
+
1944
+ // These arrays now match the current manifold, so mark clean
1945
+ this._dirty = false;
1946
+ this._faceIndex = null;
1947
+
1948
+ // Keep existing id/name maps; Manifold preserves triangle faceIDs.
1949
+ const removed = triCountBefore - triCountAfter;
1950
+ return removed > 0 ? removed : 0;
1951
+ } catch (err) {
1952
+ const mode = (fallback === 'ray' || fallback === 'raycast') ? 'raycast' : 'winding';
1953
+ try { console.warn(`[removeInternalTriangles] Manifold rebuild failed (${err?.message || err}); falling back to ${mode} classifier.`); } catch { }
1954
+ } finally {
1955
+ try { if (mesh && typeof mesh.delete === 'function') mesh.delete(); } catch { }
1956
+ }
1957
+
1958
+ // Fallback path for non-manifold/self-intersecting meshes
1959
+ if (fallback === 'ray' || fallback === 'raycast') {
1960
+ return this.removeInternalTrianglesByRaycast();
1961
+ }
1962
+ return this.removeInternalTrianglesByWinding(opts.windingOptions || {});
1963
+ }
1964
+
1965
+ /**
1966
+ * Remove internal triangles using a point-in-solid ray test.
1967
+ * Does not require manifold to succeed. For each triangle, cast a ray from its
1968
+ * centroid along +X and count intersections with all triangles. If the count is
1969
+ * odd (inside), the triangle is removed. Returns the number of triangles removed.
1970
+ */
1971
+ export function removeInternalTrianglesByRaycast() {
1972
+ const vp = this._vertProperties;
1973
+ const tv = this._triVerts;
1974
+ const ids = this._triIDs;
1975
+ const triCount = (tv.length / 3) | 0;
1976
+ if (triCount === 0) return 0;
1977
+
1978
+ // Build triangle list in point form for ray tests
1979
+ const faces = new Array(triCount);
1980
+ for (let t = 0; t < triCount; t++) {
1981
+ const b = t * 3;
1982
+ const i0 = tv[b + 0] >>> 0;
1983
+ const i1 = tv[b + 1] >>> 0;
1984
+ const i2 = tv[b + 2] >>> 0;
1985
+ faces[t] = [
1986
+ [vp[i0 * 3 + 0], vp[i0 * 3 + 1], vp[i0 * 3 + 2]],
1987
+ [vp[i1 * 3 + 0], vp[i1 * 3 + 1], vp[i1 * 3 + 2]],
1988
+ [vp[i2 * 3 + 0], vp[i2 * 3 + 1], vp[i2 * 3 + 2]],
1989
+ ];
1990
+ }
1991
+
1992
+ // Bounding box for jitter
1993
+ let minX = Infinity, minY = Infinity, minZ = Infinity;
1994
+ let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
1995
+ for (let i = 0; i < vp.length; i += 3) {
1996
+ const x = vp[i], y = vp[i + 1], z = vp[i + 2];
1997
+ if (x < minX) minX = x; if (x > maxX) maxX = x;
1998
+ if (y < minY) minY = y; if (y > maxY) maxY = y;
1999
+ if (z < minZ) minZ = z; if (z > maxZ) maxZ = z;
2000
+ }
2001
+ const diag = Math.hypot(maxX - minX, maxY - minY, maxZ - minZ) || 1;
2002
+ const jitter = 1e-6 * diag;
2003
+
2004
+ // Robust ray-triangle intersection (Möller–Trumbore), returns t > 0
2005
+ const rayTri = (orig, dir, tri) => {
2006
+ const EPS = 1e-12;
2007
+ const ax = tri[0][0], ay = tri[0][1], az = tri[0][2];
2008
+ const bx = tri[1][0], by = tri[1][1], bz = tri[1][2];
2009
+ const cx = tri[2][0], cy = tri[2][1], cz = tri[2][2];
2010
+ const e1x = bx - ax, e1y = by - ay, e1z = bz - az;
2011
+ const e2x = cx - ax, e2y = cy - ay, e2z = cz - az;
2012
+ const px = dir[1] * e2z - dir[2] * e2y;
2013
+ const py = dir[2] * e2x - dir[0] * e2z;
2014
+ const pz = dir[0] * e2y - dir[1] * e2x;
2015
+ const det = e1x * px + e1y * py + e1z * pz;
2016
+ if (Math.abs(det) < EPS) return null;
2017
+ const invDet = 1.0 / det;
2018
+ const tvecx = orig[0] - ax, tvecy = orig[1] - ay, tvecz = orig[2] - az;
2019
+ const u = (tvecx * px + tvecy * py + tvecz * pz) * invDet;
2020
+ if (u < -1e-12 || u > 1 + 1e-12) return null;
2021
+ const qx = tvecy * e1z - tvecz * e1y;
2022
+ const qy = tvecz * e1x - tvecx * e1z;
2023
+ const qz = tvecx * e1y - tvecy * e1x;
2024
+ const v = (dir[0] * qx + dir[1] * qy + dir[2] * qz) * invDet;
2025
+ if (v < -1e-12 || u + v > 1 + 1e-12) return null;
2026
+ const tHit = (e2x * qx + e2y * qy + e2z * qz) * invDet;
2027
+ return tHit > 1e-10 ? tHit : null;
2028
+ };
2029
+
2030
+ const pointInside = (p) => {
2031
+ // Three-axis majority vote with jitter
2032
+ const dirs = [
2033
+ [1, 0, 0], [0, 1, 0], [0, 0, 1],
2034
+ ];
2035
+ let votes = 0;
2036
+ for (let k = 0; k < dirs.length; k++) {
2037
+ const dir = dirs[k];
2038
+ const offset = [p[0] + (k + 1) * jitter, p[1] + (k + 2) * jitter, p[2] + (k + 3) * jitter];
2039
+ let hits = 0;
2040
+ for (let i = 0; i < faces.length; i++) {
2041
+ const th = rayTri(offset, dir, faces[i]);
2042
+ if (th !== null) hits++;
2043
+ }
2044
+ if ((hits % 2) === 1) votes++;
2045
+ }
2046
+ return votes >= 2; // at least 2 of 3 say inside
2047
+ };
2048
+
2049
+ // Compute slightly jittered centroids to avoid t≈0 self-hits
2050
+ const triProbe = (t) => {
2051
+ const [A, B, C] = faces[t];
2052
+ const px = (A[0] + B[0] + C[0]) / 3 + jitter;
2053
+ const py = (A[1] + B[1] + C[1]) / 3 + jitter;
2054
+ const pz = (A[2] + B[2] + C[2]) / 3 + jitter;
2055
+ return [px, py, pz];
2056
+ };
2057
+
2058
+ const keepTri = new Uint8Array(triCount);
2059
+ for (let t = 0; t < triCount; t++) keepTri[t] = 1;
2060
+
2061
+ let removed = 0;
2062
+ for (let t = 0; t < triCount; t++) {
2063
+ const p = triProbe(t);
2064
+ if (pointInside(p)) { keepTri[t] = 0; removed++; }
2065
+ }
2066
+
2067
+ if (removed === 0) return 0;
2068
+
2069
+ // Rebuild compact mesh
2070
+ const nv = (vp.length / 3) | 0;
2071
+ const usedVert = new Uint8Array(nv);
2072
+ const newTV = [];
2073
+ const newIDs = [];
2074
+ for (let t = 0; t < triCount; t++) {
2075
+ if (!keepTri[t]) continue;
2076
+ const b = t * 3;
2077
+ const a = tv[b + 0] >>> 0;
2078
+ const b1 = tv[b + 1] >>> 0;
2079
+ const c = tv[b + 2] >>> 0;
2080
+ newTV.push(a, b1, c);
2081
+ newIDs.push(ids[t]);
2082
+ usedVert[a] = 1; usedVert[b1] = 1; usedVert[c] = 1;
2083
+ }
2084
+
2085
+ const oldToNew = new Int32Array(nv);
2086
+ for (let i = 0; i < nv; i++) oldToNew[i] = -1;
2087
+ const newVP = [];
2088
+ let write = 0;
2089
+ for (let i = 0; i < nv; i++) {
2090
+ if (!usedVert[i]) continue;
2091
+ oldToNew[i] = write++;
2092
+ newVP.push(vp[i * 3 + 0], vp[i * 3 + 1], vp[i * 3 + 2]);
2093
+ }
2094
+ for (let i = 0; i < newTV.length; i++) newTV[i] = oldToNew[newTV[i]];
2095
+
2096
+ this._vertProperties = newVP;
2097
+ this._triVerts = newTV;
2098
+ this._triIDs = newIDs;
2099
+ this._vertKeyToIndex = new Map();
2100
+ for (let i = 0; i < this._vertProperties.length; i += 3) {
2101
+ const x = this._vertProperties[i], y = this._vertProperties[i + 1], z = this._vertProperties[i + 2];
2102
+ this._vertKeyToIndex.set(`${x},${y},${z}`, (i / 3) | 0);
2103
+ }
2104
+ this._dirty = true;
2105
+ this._faceIndex = null;
2106
+ // Fix orientation just in case
2107
+ this.fixTriangleWindingsByAdjacency();
2108
+ return removed;
2109
+ }
2110
+
2111
+ /**
2112
+ * Remove internal triangles using solid-angle (winding number) test.
2113
+ * Computes sum of solid angles of all triangles at each triangle's centroid.
2114
+ * If |sumOmega| > threshold (≈ 2π), marks that triangle as inside and removes it.
2115
+ * Robust to self-intersections and coplanar cases; does not require Manifold.
2116
+ * @param {object} [options]
2117
+ * @param {number} [options.offsetScale=1e-5] centroid offset scale relative to bounding box diagonal
2118
+ * @param {number} [options.crossingTolerance=0.05] tolerance for deciding inside/outside crossings
2119
+ */
2120
+ export function removeInternalTrianglesByWinding({ offsetScale = 1e-5, crossingTolerance = 0.05 } = {}) {
2121
+ // Ensure local edge orientation is consistent to get meaningful normals
2122
+ try { this.fixTriangleWindingsByAdjacency(); } catch { }
2123
+ const vp = this._vertProperties;
2124
+ const tv = this._triVerts;
2125
+ const ids = this._triIDs;
2126
+ const triCount = (tv.length / 3) | 0;
2127
+ if (triCount === 0) return 0;
2128
+
2129
+ // Bounding box for epsilon offset scaling
2130
+ let minX = Infinity, minY = Infinity, minZ = Infinity;
2131
+ let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
2132
+ for (let i = 0; i < vp.length; i += 3) {
2133
+ const x = vp[i], y = vp[i + 1], z = vp[i + 2];
2134
+ if (x < minX) minX = x; if (x > maxX) maxX = x;
2135
+ if (y < minY) minY = y; if (y > maxY) maxY = y;
2136
+ if (z < minZ) minZ = z; if (z > maxZ) maxZ = z;
2137
+ }
2138
+ const diag = Math.hypot(maxX - minX, maxY - minY, maxZ - minZ) || 1;
2139
+ const eps = offsetScale * diag;
2140
+
2141
+ // Prepare faces and normals
2142
+ const faces = new Array(triCount);
2143
+ const centroids = new Array(triCount);
2144
+ const normals = new Array(triCount);
2145
+ for (let t = 0; t < triCount; t++) {
2146
+ const b = t * 3;
2147
+ const i0 = tv[b + 0] >>> 0;
2148
+ const i1 = tv[b + 1] >>> 0;
2149
+ const i2 = tv[b + 2] >>> 0;
2150
+ const ax = vp[i0 * 3 + 0], ay = vp[i0 * 3 + 1], az = vp[i0 * 3 + 2];
2151
+ const bx = vp[i1 * 3 + 0], by = vp[i1 * 3 + 1], bz = vp[i1 * 3 + 2];
2152
+ const cx = vp[i2 * 3 + 0], cy = vp[i2 * 3 + 1], cz = vp[i2 * 3 + 2];
2153
+ faces[t] = [[ax, ay, az], [bx, by, bz], [cx, cy, cz]];
2154
+ centroids[t] = [(ax + bx + cx) / 3, (ay + by + cy) / 3, (az + bz + cz) / 3];
2155
+ const ux = bx - ax, uy = by - ay, uz = bz - az;
2156
+ const vx = cx - ax, vy = cy - ay, vz = cz - az;
2157
+ let nx = uy * vz - uz * vy;
2158
+ let ny = uz * vx - ux * vz;
2159
+ let nz = ux * vy - uy * vx;
2160
+ const nl = Math.hypot(nx, ny, nz);
2161
+ if (nl < 1e-18) {
2162
+ normals[t] = [0, 0, 0];
2163
+ } else {
2164
+ normals[t] = [nx / nl, ny / nl, nz / nl];
2165
+ }
2166
+ }
2167
+
2168
+ // Oriented solid angle of triangle ABC as seen from point P
2169
+ const solidAngle = (P, A, B, C) => {
2170
+ const ax = A[0] - P[0], ay = A[1] - P[1], az = A[2] - P[2];
2171
+ const bx = B[0] - P[0], by = B[1] - P[1], bz = B[2] - P[2];
2172
+ const cx = C[0] - P[0], cy = C[1] - P[1], cz = C[2] - P[2];
2173
+ const la = Math.hypot(ax, ay, az), lb = Math.hypot(bx, by, bz), lc = Math.hypot(cx, cy, cz);
2174
+ if (la < 1e-18 || lb < 1e-18 || lc < 1e-18) return 0;
2175
+ const dotAB = ax * bx + ay * by + az * bz;
2176
+ const dotBC = bx * cx + by * cy + bz * cz;
2177
+ const dotCA = cx * ax + cy * ay + cz * az;
2178
+ const crossx = ay * bz - az * by;
2179
+ const crossy = az * bx - ax * bz;
2180
+ const crossz = ax * by - ay * bx;
2181
+ const triple = crossx * cx + crossy * cy + crossz * cz; // a·(b×c)
2182
+ const denom = la * lb * lc + dotAB * lc + dotBC * la + dotCA * lb;
2183
+ return 2 * Math.atan2(triple, denom);
2184
+ };
2185
+
2186
+ // Generalized winding number w(P) in [−1,1]; normalized by 4π
2187
+ const winding = (P) => {
2188
+ let omega = 0;
2189
+ for (let u = 0; u < triCount; u++) {
2190
+ const [A, B, C] = faces[u];
2191
+ omega += solidAngle(P, A, B, C);
2192
+ }
2193
+ return omega / (4 * Math.PI);
2194
+ };
2195
+
2196
+ const keepTri = new Uint8Array(triCount);
2197
+ for (let t = 0; t < triCount; t++) keepTri[t] = 1;
2198
+ let removed = 0;
2199
+ const tau = Math.max(0, Math.min(0.49, crossingTolerance));
2200
+
2201
+ for (let t = 0; t < triCount; t++) {
2202
+ const N = normals[t];
2203
+ if (!N || (N[0] === 0 && N[1] === 0 && N[2] === 0)) { continue; } // keep degenerate-orientation tris
2204
+ const C = centroids[t];
2205
+ const Pplus = [C[0] + N[0] * eps, C[1] + N[1] * eps, C[2] + N[2] * eps];
2206
+ const Pminus = [C[0] - N[0] * eps, C[1] - N[1] * eps, C[2] - N[2] * eps];
2207
+ const wPlus = winding(Pplus);
2208
+ const wMinus = winding(Pminus);
2209
+ const a = wPlus - 0.5;
2210
+ const b = wMinus - 0.5;
2211
+ const crosses = (a < -tau && b > tau) || (a > tau && b < -tau) || (a * b < -tau * tau);
2212
+ if (!crosses) { keepTri[t] = 0; removed++; }
2213
+ }
2214
+
2215
+ if (removed === 0) return 0;
2216
+
2217
+ // Rebuild compact mesh
2218
+ const nv = (vp.length / 3) | 0;
2219
+ const usedVert = new Uint8Array(nv);
2220
+ const newTV = [];
2221
+ const newIDs = [];
2222
+ for (let t = 0; t < triCount; t++) {
2223
+ if (!keepTri[t]) continue;
2224
+ const b = t * 3;
2225
+ const a = tv[b + 0] >>> 0;
2226
+ const b1 = tv[b + 1] >>> 0;
2227
+ const c = tv[b + 2] >>> 0;
2228
+ newTV.push(a, b1, c);
2229
+ newIDs.push(ids[t]);
2230
+ usedVert[a] = 1; usedVert[b1] = 1; usedVert[c] = 1;
2231
+ }
2232
+
2233
+ const oldToNew = new Int32Array(nv);
2234
+ for (let i = 0; i < nv; i++) oldToNew[i] = -1;
2235
+ const newVP = [];
2236
+ let write = 0;
2237
+ for (let i = 0; i < nv; i++) {
2238
+ if (!usedVert[i]) continue;
2239
+ oldToNew[i] = write++;
2240
+ newVP.push(vp[i * 3 + 0], vp[i * 3 + 1], vp[i * 3 + 2]);
2241
+ }
2242
+ for (let i = 0; i < newTV.length; i++) newTV[i] = oldToNew[newTV[i]];
2243
+
2244
+ this._vertProperties = newVP;
2245
+ this._triVerts = newTV;
2246
+ this._triIDs = newIDs;
2247
+ this._vertKeyToIndex = new Map();
2248
+ for (let i = 0; i < this._vertProperties.length; i += 3) {
2249
+ const x = this._vertProperties[i], y = this._vertProperties[i + 1], z = this._vertProperties[i + 2];
2250
+ this._vertKeyToIndex.set(`${x},${y},${z}`, (i / 3) | 0);
2251
+ }
2252
+ this._dirty = true;
2253
+ this._faceIndex = null;
2254
+ this.fixTriangleWindingsByAdjacency();
2255
+ return removed;
2256
+ }
2257
+
2258
+ /**
2259
+ * Reassign tiny disconnected islands within the same face label to the
2260
+ * largest adjacent face by surface area.
2261
+ *
2262
+ * This targets defects where a face name/ID is applied to multiple
2263
+ * disconnected triangle groups; small groups are relabeled.
2264
+ *
2265
+ * @param {number} size area threshold; components below this are reassigned
2266
+ * @returns {number} number of triangles reassigned
2267
+ */
2268
+ export function cleanupTinyFaceIslands(size) {
2269
+ const maxArea = Number(size);
2270
+ if (!Number.isFinite(maxArea) || maxArea <= 0) return 0;
2271
+
2272
+ const tv = this._triVerts;
2273
+ const vp = this._vertProperties;
2274
+ const ids = this._triIDs;
2275
+ const triCount = (tv?.length || 0) / 3 | 0;
2276
+ if (!triCount || !vp || vp.length < 9 || !ids || ids.length < triCount) return 0;
2277
+
2278
+ const triArea = (i0, i1, i2) => {
2279
+ const x0 = vp[i0 * 3 + 0], y0 = vp[i0 * 3 + 1], z0 = vp[i0 * 3 + 2];
2280
+ const x1 = vp[i1 * 3 + 0], y1 = vp[i1 * 3 + 1], z1 = vp[i1 * 3 + 2];
2281
+ const x2 = vp[i2 * 3 + 0], y2 = vp[i2 * 3 + 1], z2 = vp[i2 * 3 + 2];
2282
+ const ux = x1 - x0, uy = y1 - y0, uz = z1 - z0;
2283
+ const vx = x2 - x0, vy = y2 - y0, vz = z2 - z0;
2284
+ const cx = uy * vz - uz * vy;
2285
+ const cy = uz * vx - ux * vz;
2286
+ const cz = ux * vy - uy * vx;
2287
+ return 0.5 * Math.hypot(cx, cy, cz);
2288
+ };
2289
+
2290
+ // Per-triangle areas and face groupings.
2291
+ const areas = new Float64Array(triCount);
2292
+ const faceToTris = new Map(); // faceId -> tri indices[]
2293
+ const faceArea = new Map(); // faceId -> total area
2294
+ for (let t = 0; t < triCount; t++) {
2295
+ const base = t * 3;
2296
+ const i0 = tv[base + 0] >>> 0;
2297
+ const i1 = tv[base + 1] >>> 0;
2298
+ const i2 = tv[base + 2] >>> 0;
2299
+ const a = triArea(i0, i1, i2);
2300
+ areas[t] = a;
2301
+ const id = ids[t] >>> 0;
2302
+ let tris = faceToTris.get(id);
2303
+ if (!tris) { tris = []; faceToTris.set(id, tris); }
2304
+ tris.push(t);
2305
+ faceArea.set(id, (faceArea.get(id) || 0) + a);
2306
+ }
2307
+
2308
+ // Build edge -> triangles map, then triangle adjacency and boundary neighbors.
2309
+ const nv = (vp.length / 3) | 0;
2310
+ const NV = BigInt(Math.max(1, nv));
2311
+ const eKey = (a, b) => {
2312
+ const A = BigInt(a), B = BigInt(b);
2313
+ return A < B ? (A * NV + B) : (B * NV + A);
2314
+ };
2315
+
2316
+ const edgeToTris = new Map(); // key -> tri indices[]
2317
+ for (let t = 0; t < triCount; t++) {
2318
+ const base = t * 3;
2319
+ const i0 = tv[base + 0] >>> 0;
2320
+ const i1 = tv[base + 1] >>> 0;
2321
+ const i2 = tv[base + 2] >>> 0;
2322
+ const edges = [[i0, i1], [i1, i2], [i2, i0]];
2323
+ for (let k = 0; k < 3; k++) {
2324
+ const a = edges[k][0], b = edges[k][1];
2325
+ const key = eKey(a, b);
2326
+ let arr = edgeToTris.get(key);
2327
+ if (!arr) { arr = []; edgeToTris.set(key, arr); }
2328
+ arr.push(t);
2329
+ }
2330
+ }
2331
+
2332
+ const triAdj = new Array(triCount);
2333
+ const triNeighborFaces = new Array(triCount);
2334
+ for (let t = 0; t < triCount; t++) {
2335
+ triAdj[t] = [];
2336
+ triNeighborFaces[t] = new Set();
2337
+ }
2338
+
2339
+ for (const [, tris] of edgeToTris.entries()) {
2340
+ if (tris.length !== 2) continue;
2341
+ const a = tris[0] | 0;
2342
+ const b = tris[1] | 0;
2343
+ triAdj[a].push(b);
2344
+ triAdj[b].push(a);
2345
+ const idA = ids[a] >>> 0;
2346
+ const idB = ids[b] >>> 0;
2347
+ if (idA !== idB) {
2348
+ triNeighborFaces[a].add(idB);
2349
+ triNeighborFaces[b].add(idA);
2350
+ }
2351
+ }
2352
+
2353
+ // Tokenized visited array avoids clearing large buffers per face.
2354
+ const seenToken = new Int32Array(triCount);
2355
+ let token = 1;
2356
+
2357
+ let reassigned = 0;
2358
+
2359
+ for (const [faceId, tris] of faceToTris.entries()) {
2360
+ if (!tris || tris.length < 2) continue;
2361
+
2362
+ token++;
2363
+ const components = [];
2364
+ const stack = [];
2365
+
2366
+ for (let i = 0; i < tris.length; i++) {
2367
+ const seed = tris[i] | 0;
2368
+ if (seenToken[seed] === token) continue;
2369
+ seenToken[seed] = token;
2370
+ stack.length = 0;
2371
+ stack.push(seed);
2372
+
2373
+ const compTris = [];
2374
+ let compArea = 0;
2375
+
2376
+ while (stack.length) {
2377
+ const t = stack.pop() | 0;
2378
+ compTris.push(t);
2379
+ compArea += areas[t];
2380
+ const nbrs = triAdj[t];
2381
+ for (let j = 0; j < nbrs.length; j++) {
2382
+ const u = nbrs[j] | 0;
2383
+ if (seenToken[u] === token) continue;
2384
+ if ((ids[u] >>> 0) !== faceId) continue;
2385
+ seenToken[u] = token;
2386
+ stack.push(u);
2387
+ }
2388
+ }
2389
+
2390
+ components.push({ tris: compTris, area: compArea });
2391
+ }
2392
+
2393
+ if (components.length <= 1) continue;
2394
+
2395
+ for (let c = 0; c < components.length; c++) {
2396
+ const comp = components[c];
2397
+ if (!comp || !(comp.area < maxArea)) continue;
2398
+
2399
+ const neighborIds = new Set();
2400
+ for (let i = 0; i < comp.tris.length; i++) {
2401
+ const t = comp.tris[i] | 0;
2402
+ const nbrFaces = triNeighborFaces[t];
2403
+ for (const nid of nbrFaces) {
2404
+ if ((nid >>> 0) === faceId) continue;
2405
+ neighborIds.add(nid >>> 0);
2406
+ }
2407
+ }
2408
+ if (neighborIds.size === 0) continue;
2409
+
2410
+ let bestId = null;
2411
+ let bestArea = -Infinity;
2412
+ for (const nid of neighborIds) {
2413
+ const a = faceArea.get(nid) || 0;
2414
+ if (a > bestArea) {
2415
+ bestArea = a;
2416
+ bestId = nid;
2417
+ }
2418
+ }
2419
+ if (bestId === null) continue;
2420
+
2421
+ for (let i = 0; i < comp.tris.length; i++) {
2422
+ const t = comp.tris[i] | 0;
2423
+ ids[t] = bestId;
2424
+ reassigned++;
2425
+ }
2426
+
2427
+ // Keep face area accounting roughly correct for subsequent choices.
2428
+ faceArea.set(faceId, (faceArea.get(faceId) || 0) - comp.area);
2429
+ faceArea.set(bestId, (faceArea.get(bestId) || 0) + comp.area);
2430
+ }
2431
+ }
2432
+
2433
+ if (reassigned > 0) {
2434
+ this._dirty = true;
2435
+ this._faceIndex = null;
2436
+ }
2437
+
2438
+ return reassigned;
2439
+ }
2440
+
2441
+ // Merge faces whose area is below a threshold into their largest adjacent neighbor.
2442
+ export function mergeTinyFaces(maxArea = 0.001) {
2443
+ if (!Number.isFinite(maxArea) || maxArea <= 0) return this;
2444
+ if (typeof this.getFaceNames !== 'function' || typeof this.getBoundaryEdgePolylines !== 'function') return this;
2445
+ const faceNames = this.getFaceNames() || [];
2446
+ if (!Array.isArray(faceNames) || faceNames.length === 0) return this;
2447
+
2448
+ const areaCache = new Map();
2449
+ const areaOf = (name) => {
2450
+ if (areaCache.has(name)) return areaCache.get(name);
2451
+ let area = 0;
2452
+ try {
2453
+ const tris = this.getFace(name);
2454
+ if (Array.isArray(tris)) {
2455
+ for (const tri of tris) {
2456
+ const p1 = tri?.p1, p2 = tri?.p2, p3 = tri?.p3;
2457
+ if (!p1 || !p2 || !p3) continue;
2458
+ const ax = p2[0] - p1[0], ay = p2[1] - p1[1], az = p2[2] - p1[2];
2459
+ const bx = p3[0] - p1[0], by = p3[1] - p1[1], bz = p3[2] - p1[2];
2460
+ const cx = ay * bz - az * by;
2461
+ const cy = az * bx - ax * bz;
2462
+ const cz = ax * by - ay * bx;
2463
+ area += 0.5 * Math.hypot(cx, cy, cz);
2464
+ }
2465
+ }
2466
+ } catch { area = 0; }
2467
+ areaCache.set(name, area);
2468
+ return area;
2469
+ };
2470
+
2471
+ const boundaries = this.getBoundaryEdgePolylines() || [];
2472
+ const neighbors = new Map();
2473
+ for (const poly of boundaries) {
2474
+ const a = poly?.faceA;
2475
+ const b = poly?.faceB;
2476
+ if (!a || !b) continue;
2477
+ if (!neighbors.has(a)) neighbors.set(a, new Set());
2478
+ if (!neighbors.has(b)) neighbors.set(b, new Set());
2479
+ neighbors.get(a).add(b);
2480
+ neighbors.get(b).add(a);
2481
+ }
2482
+
2483
+ let merged = 0;
2484
+ for (const name of faceNames) {
2485
+ const area = areaOf(name);
2486
+ if (!(area < maxArea)) continue;
2487
+ const adj = neighbors.get(name);
2488
+ if (!adj || adj.size === 0) continue;
2489
+ let best = null;
2490
+ let bestArea = -Infinity;
2491
+ for (const n of adj) {
2492
+ const a = areaOf(n);
2493
+ if (a > bestArea) { bestArea = a; best = n; }
2494
+ }
2495
+ if (best) {
2496
+ this.renameFace(name, best);
2497
+ merged++;
2498
+ }
2499
+ }
2500
+ if (merged > 0) {
2501
+ try {
2502
+ this._faceIndex = null;
2503
+ this._dirty = true;
2504
+ // Rebuild now so the caller gets a clean, chainable solid.
2505
+ if (typeof this._manifoldize === 'function') {
2506
+ this._manifoldize();
2507
+ if (typeof this._ensureFaceIndex === 'function') this._ensureFaceIndex();
2508
+ }
2509
+ } catch { }
2510
+ }
2511
+ return this;
2512
+ }