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.
- package/LICENSE.md +32 -0
- package/README.md +154 -0
- package/dist-kernel/brep-kernel.js +74699 -0
- package/package.json +58 -0
- package/src/BREP/AssemblyComponent.js +42 -0
- package/src/BREP/BREP.js +43 -0
- package/src/BREP/BetterSolid.js +805 -0
- package/src/BREP/Edge.js +103 -0
- package/src/BREP/Extrude.js +403 -0
- package/src/BREP/Face.js +187 -0
- package/src/BREP/MeshRepairer.js +634 -0
- package/src/BREP/OffsetShellSolid.js +614 -0
- package/src/BREP/PointCloudWrap.js +302 -0
- package/src/BREP/Revolve.js +345 -0
- package/src/BREP/SolidMethods/authoring.js +112 -0
- package/src/BREP/SolidMethods/booleanOps.js +230 -0
- package/src/BREP/SolidMethods/chamfer.js +122 -0
- package/src/BREP/SolidMethods/edgeResolution.js +25 -0
- package/src/BREP/SolidMethods/fillet.js +792 -0
- package/src/BREP/SolidMethods/index.js +72 -0
- package/src/BREP/SolidMethods/io.js +105 -0
- package/src/BREP/SolidMethods/lifecycle.js +103 -0
- package/src/BREP/SolidMethods/manifoldOps.js +375 -0
- package/src/BREP/SolidMethods/meshCleanup.js +2512 -0
- package/src/BREP/SolidMethods/meshQueries.js +264 -0
- package/src/BREP/SolidMethods/metadata.js +106 -0
- package/src/BREP/SolidMethods/metrics.js +51 -0
- package/src/BREP/SolidMethods/transforms.js +361 -0
- package/src/BREP/SolidMethods/visualize.js +508 -0
- package/src/BREP/SolidShared.js +26 -0
- package/src/BREP/Sweep.js +1596 -0
- package/src/BREP/Tube.js +857 -0
- package/src/BREP/Vertex.js +43 -0
- package/src/BREP/applyBooleanOperation.js +704 -0
- package/src/BREP/boundsUtils.js +48 -0
- package/src/BREP/chamfer.js +551 -0
- package/src/BREP/edgePolylineUtils.js +85 -0
- package/src/BREP/fillets/common.js +388 -0
- package/src/BREP/fillets/fillet.js +1422 -0
- package/src/BREP/fillets/filletGeometry.js +15 -0
- package/src/BREP/fillets/inset.js +389 -0
- package/src/BREP/fillets/offsetHelper.js +143 -0
- package/src/BREP/fillets/outset.js +88 -0
- package/src/BREP/helix.js +193 -0
- package/src/BREP/meshToBrep.js +234 -0
- package/src/BREP/primitives.js +279 -0
- package/src/BREP/setupManifold.js +71 -0
- package/src/BREP/threadGeometry.js +1120 -0
- package/src/BREP/triangleUtils.js +8 -0
- package/src/BREP/triangulate.js +608 -0
- package/src/FeatureRegistry.js +183 -0
- package/src/PartHistory.js +1132 -0
- package/src/UI/AccordionWidget.js +292 -0
- package/src/UI/CADmaterials.js +850 -0
- package/src/UI/EnvMonacoEditor.js +522 -0
- package/src/UI/FloatingWindow.js +396 -0
- package/src/UI/HistoryWidget.js +457 -0
- package/src/UI/MainToolbar.js +131 -0
- package/src/UI/ModelLibraryView.js +194 -0
- package/src/UI/OrthoCameraIdle.js +206 -0
- package/src/UI/PluginsWidget.js +280 -0
- package/src/UI/SceneListing.js +606 -0
- package/src/UI/SelectionFilter.js +629 -0
- package/src/UI/ViewCube.js +389 -0
- package/src/UI/assembly/AssemblyConstraintCollectionWidget.js +329 -0
- package/src/UI/assembly/AssemblyConstraintControlsWidget.js +282 -0
- package/src/UI/assembly/AssemblyConstraintsWidget.css +292 -0
- package/src/UI/assembly/AssemblyConstraintsWidget.js +1373 -0
- package/src/UI/assembly/constraintFaceUtils.js +115 -0
- package/src/UI/assembly/constraintHighlightUtils.js +70 -0
- package/src/UI/assembly/constraintLabelUtils.js +31 -0
- package/src/UI/assembly/constraintPointUtils.js +64 -0
- package/src/UI/assembly/constraintSelectionUtils.js +185 -0
- package/src/UI/assembly/constraintStatusUtils.js +142 -0
- package/src/UI/componentSelectorModal.js +240 -0
- package/src/UI/controls/CombinedTransformControls.js +386 -0
- package/src/UI/dialogs.js +351 -0
- package/src/UI/expressionsManager.js +100 -0
- package/src/UI/featureDialogWidgets/booleanField.js +25 -0
- package/src/UI/featureDialogWidgets/booleanOperationField.js +97 -0
- package/src/UI/featureDialogWidgets/buttonField.js +45 -0
- package/src/UI/featureDialogWidgets/componentSelectorField.js +102 -0
- package/src/UI/featureDialogWidgets/defaultField.js +23 -0
- package/src/UI/featureDialogWidgets/fileField.js +66 -0
- package/src/UI/featureDialogWidgets/index.js +34 -0
- package/src/UI/featureDialogWidgets/numberField.js +165 -0
- package/src/UI/featureDialogWidgets/optionsField.js +33 -0
- package/src/UI/featureDialogWidgets/referenceSelectionField.js +208 -0
- package/src/UI/featureDialogWidgets/stringField.js +24 -0
- package/src/UI/featureDialogWidgets/textareaField.js +28 -0
- package/src/UI/featureDialogWidgets/threadDesignationField.js +160 -0
- package/src/UI/featureDialogWidgets/transformField.js +252 -0
- package/src/UI/featureDialogWidgets/utils.js +43 -0
- package/src/UI/featureDialogWidgets/vec3Field.js +133 -0
- package/src/UI/featureDialogs.js +1414 -0
- package/src/UI/fileManagerWidget.js +615 -0
- package/src/UI/history/HistoryCollectionWidget.js +1294 -0
- package/src/UI/history/historyCollectionWidget.css.js +257 -0
- package/src/UI/history/historyDisplayInfo.js +133 -0
- package/src/UI/mobile.js +28 -0
- package/src/UI/objectDump.js +442 -0
- package/src/UI/pmi/AnnotationCollectionWidget.js +120 -0
- package/src/UI/pmi/AnnotationHistory.js +353 -0
- package/src/UI/pmi/AnnotationRegistry.js +90 -0
- package/src/UI/pmi/BaseAnnotation.js +269 -0
- package/src/UI/pmi/LabelOverlay.css +102 -0
- package/src/UI/pmi/LabelOverlay.js +191 -0
- package/src/UI/pmi/PMIMode.js +1550 -0
- package/src/UI/pmi/PMIViewsWidget.js +1098 -0
- package/src/UI/pmi/annUtils.js +729 -0
- package/src/UI/pmi/dimensions/AngleDimensionAnnotation.js +647 -0
- package/src/UI/pmi/dimensions/ExplodeBodyAnnotation.js +507 -0
- package/src/UI/pmi/dimensions/HoleCalloutAnnotation.js +462 -0
- package/src/UI/pmi/dimensions/LeaderAnnotation.js +403 -0
- package/src/UI/pmi/dimensions/LinearDimensionAnnotation.js +532 -0
- package/src/UI/pmi/dimensions/NoteAnnotation.js +110 -0
- package/src/UI/pmi/dimensions/RadialDimensionAnnotation.js +659 -0
- package/src/UI/pmi/pmiStyle.js +44 -0
- package/src/UI/sketcher/SketchMode3D.js +4095 -0
- package/src/UI/sketcher/dimensions.js +674 -0
- package/src/UI/sketcher/glyphs.js +236 -0
- package/src/UI/sketcher/highlights.js +60 -0
- package/src/UI/toolbarButtons/aboutButton.js +5 -0
- package/src/UI/toolbarButtons/exportButton.js +609 -0
- package/src/UI/toolbarButtons/flatPatternButton.js +307 -0
- package/src/UI/toolbarButtons/importButton.js +160 -0
- package/src/UI/toolbarButtons/inspectorToggleButton.js +12 -0
- package/src/UI/toolbarButtons/metadataButton.js +1063 -0
- package/src/UI/toolbarButtons/orientToFaceButton.js +114 -0
- package/src/UI/toolbarButtons/registerDefaultButtons.js +46 -0
- package/src/UI/toolbarButtons/saveButton.js +99 -0
- package/src/UI/toolbarButtons/scriptRunnerButton.js +302 -0
- package/src/UI/toolbarButtons/testsButton.js +26 -0
- package/src/UI/toolbarButtons/undoRedoButtons.js +25 -0
- package/src/UI/toolbarButtons/wireframeToggleButton.js +5 -0
- package/src/UI/toolbarButtons/zoomToFitButton.js +5 -0
- package/src/UI/triangleDebuggerWindow.js +945 -0
- package/src/UI/viewer.js +4228 -0
- package/src/assemblyConstraints/AssemblyConstraintHistory.js +1576 -0
- package/src/assemblyConstraints/AssemblyConstraintRegistry.js +120 -0
- package/src/assemblyConstraints/BaseAssemblyConstraint.js +66 -0
- package/src/assemblyConstraints/constraintExpressionUtils.js +35 -0
- package/src/assemblyConstraints/constraintUtils/parallelAlignment.js +676 -0
- package/src/assemblyConstraints/constraints/AngleConstraint.js +485 -0
- package/src/assemblyConstraints/constraints/CoincidentConstraint.js +194 -0
- package/src/assemblyConstraints/constraints/DistanceConstraint.js +616 -0
- package/src/assemblyConstraints/constraints/FixedConstraint.js +78 -0
- package/src/assemblyConstraints/constraints/ParallelConstraint.js +252 -0
- package/src/assemblyConstraints/constraints/TouchAlignConstraint.js +961 -0
- package/src/core/entities/HistoryCollectionBase.js +72 -0
- package/src/core/entities/ListEntityBase.js +109 -0
- package/src/core/entities/schemaProcesser.js +121 -0
- package/src/exporters/sheetMetalFlatPattern.js +659 -0
- package/src/exporters/sheetMetalUnfold.js +862 -0
- package/src/exporters/step.js +1135 -0
- package/src/exporters/threeMF.js +575 -0
- package/src/features/assemblyComponent/AssemblyComponentFeature.js +780 -0
- package/src/features/boolean/BooleanFeature.js +94 -0
- package/src/features/chamfer/ChamferFeature.js +116 -0
- package/src/features/datium/DatiumFeature.js +80 -0
- package/src/features/edgeFeatureUtils.js +41 -0
- package/src/features/extrude/ExtrudeFeature.js +143 -0
- package/src/features/fillet/FilletFeature.js +197 -0
- package/src/features/helix/HelixFeature.js +405 -0
- package/src/features/hole/HoleFeature.js +1050 -0
- package/src/features/hole/screwClearance.js +86 -0
- package/src/features/hole/threadDesignationCatalog.js +149 -0
- package/src/features/imageHeightSolid/ImageHeightmapSolidFeature.js +463 -0
- package/src/features/imageToFace/ImageToFaceFeature.js +727 -0
- package/src/features/imageToFace/imageEditor.js +1270 -0
- package/src/features/imageToFace/traceUtils.js +971 -0
- package/src/features/import3dModel/Import3dModelFeature.js +151 -0
- package/src/features/loft/LoftFeature.js +605 -0
- package/src/features/mirror/MirrorFeature.js +151 -0
- package/src/features/offsetFace/OffsetFaceFeature.js +370 -0
- package/src/features/offsetShell/OffsetShellFeature.js +89 -0
- package/src/features/overlapCleanup/OverlapCleanupFeature.js +85 -0
- package/src/features/pattern/PatternFeature.js +275 -0
- package/src/features/patternLinear/PatternLinearFeature.js +120 -0
- package/src/features/patternRadial/PatternRadialFeature.js +186 -0
- package/src/features/plane/PlaneFeature.js +154 -0
- package/src/features/primitiveCone/primitiveConeFeature.js +99 -0
- package/src/features/primitiveCube/primitiveCubeFeature.js +70 -0
- package/src/features/primitiveCylinder/primitiveCylinderFeature.js +91 -0
- package/src/features/primitivePyramid/primitivePyramidFeature.js +72 -0
- package/src/features/primitiveSphere/primitiveSphereFeature.js +62 -0
- package/src/features/primitiveTorus/primitiveTorusFeature.js +109 -0
- package/src/features/remesh/RemeshFeature.js +97 -0
- package/src/features/revolve/RevolveFeature.js +111 -0
- package/src/features/selectionUtils.js +118 -0
- package/src/features/sheetMetal/SheetMetalContourFlangeFeature.js +1656 -0
- package/src/features/sheetMetal/SheetMetalCutoutFeature.js +1056 -0
- package/src/features/sheetMetal/SheetMetalFlangeFeature.js +1568 -0
- package/src/features/sheetMetal/SheetMetalHemFeature.js +43 -0
- package/src/features/sheetMetal/SheetMetalObject.js +141 -0
- package/src/features/sheetMetal/SheetMetalTabFeature.js +176 -0
- package/src/features/sheetMetal/UNFOLD_NEUTRAL_REQUIREMENTS.md +153 -0
- package/src/features/sheetMetal/contour-flange-rebuild-spec.md +261 -0
- package/src/features/sheetMetal/profileUtils.js +25 -0
- package/src/features/sheetMetal/sheetMetalCleanup.js +9 -0
- package/src/features/sheetMetal/sheetMetalFaceTypes.js +146 -0
- package/src/features/sheetMetal/sheetMetalMetadata.js +165 -0
- package/src/features/sheetMetal/sheetMetalPipeline.js +169 -0
- package/src/features/sheetMetal/sheetMetalProfileUtils.js +216 -0
- package/src/features/sheetMetal/sheetMetalTabUtils.js +29 -0
- package/src/features/sheetMetal/sheetMetalTree.js +210 -0
- package/src/features/sketch/SketchFeature.js +955 -0
- package/src/features/sketch/sketchSolver2D/ConstraintEngine.js +800 -0
- package/src/features/sketch/sketchSolver2D/constraintDefinitions.js +704 -0
- package/src/features/sketch/sketchSolver2D/mathHelpersMod.js +307 -0
- package/src/features/spline/SplineEditorSession.js +988 -0
- package/src/features/spline/SplineFeature.js +1388 -0
- package/src/features/spline/splineUtils.js +218 -0
- package/src/features/sweep/SweepFeature.js +110 -0
- package/src/features/transform/TransformFeature.js +152 -0
- package/src/features/tube/TubeFeature.js +635 -0
- package/src/fs.proxy.js +625 -0
- package/src/idbStorage.js +254 -0
- package/src/index.js +12 -0
- package/src/main.js +15 -0
- package/src/metadataManager.js +64 -0
- package/src/path.proxy.js +277 -0
- package/src/plugins/ghLoader.worker.js +151 -0
- package/src/plugins/pluginManager.js +286 -0
- package/src/pmi/PMIViewsManager.js +134 -0
- package/src/services/componentLibrary.js +198 -0
- package/src/tests/ConsoleCapture.js +189 -0
- package/src/tests/S7-diagnostics-2025-12-23T18-37-23-570Z.json +630 -0
- package/src/tests/browserTests.js +597 -0
- package/src/tests/debugBoolean.js +225 -0
- package/src/tests/partFiles/badBoolean.json +957 -0
- package/src/tests/partFiles/extrudeTest.json +88 -0
- package/src/tests/partFiles/filletFail.json +58 -0
- package/src/tests/partFiles/import_TEst.part.part.json +646 -0
- package/src/tests/partFiles/sheetMetalHem.BREP.json +734 -0
- package/src/tests/test_boolean_subtract.js +27 -0
- package/src/tests/test_chamfer.js +17 -0
- package/src/tests/test_extrudeFace.js +24 -0
- package/src/tests/test_fillet.js +17 -0
- package/src/tests/test_fillet_nonClosed.js +45 -0
- package/src/tests/test_filletsMoreDifficult.js +46 -0
- package/src/tests/test_history_features_basic.js +149 -0
- package/src/tests/test_hole.js +282 -0
- package/src/tests/test_mirror.js +16 -0
- package/src/tests/test_offsetShellGrouping.js +85 -0
- package/src/tests/test_plane.js +4 -0
- package/src/tests/test_primitiveCone.js +11 -0
- package/src/tests/test_primitiveCube.js +7 -0
- package/src/tests/test_primitiveCylinder.js +8 -0
- package/src/tests/test_primitivePyramid.js +9 -0
- package/src/tests/test_primitiveSphere.js +17 -0
- package/src/tests/test_primitiveTorus.js +21 -0
- package/src/tests/test_pushFace.js +126 -0
- package/src/tests/test_sheetMetalContourFlange.js +125 -0
- package/src/tests/test_sheetMetal_features.js +80 -0
- package/src/tests/test_sketch_openLoop.js +45 -0
- package/src/tests/test_solidMetrics.js +58 -0
- package/src/tests/test_stlLoader.js +1889 -0
- package/src/tests/test_sweepFace.js +55 -0
- package/src/tests/test_tube.js +45 -0
- package/src/tests/test_tube_closedLoop.js +67 -0
- package/src/tests/tests.js +493 -0
- package/src/tools/assemblyConstraintDialogCapturePage.js +56 -0
- package/src/tools/dialogCapturePageFactory.js +227 -0
- package/src/tools/featureDialogCapturePage.js +47 -0
- package/src/tools/pmiAnnotationDialogCapturePage.js +60 -0
- package/src/utils/axisHelpers.js +99 -0
- package/src/utils/deepClone.js +69 -0
- package/src/utils/geometryTolerance.js +37 -0
- package/src/utils/normalizeTypeString.js +8 -0
- 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
|
+
}
|