Surface Boolean Without CSG — Closing the Seam After Split-and-Classify

Hello ThreeJS gang,

I have been down the route of robust AI conversations with multiple AIs, and I am still struggling to find a resolution. I am like 99.5% there and really just seeking some advice on some out there options or solutions that others might have had luck with.

I just can’t get this surface to close.

There is a link to the current beta app on my GitHub.

Context:
I’m building a mining/blasting app (vanilla JS + Three.js r170) that needs a Maptek Style - Vulcan TRIBOOL - style surface boolean — two overlapping triangulated surfaces (e.g. terrain + pit design), split along their intersection, user picks which regions to keep, merge into one surface, NOT a solid CSG — these are open triangle meshes (DTM surfaces). CSG does work to an extent on merging the mesh, but not a solid union as such. I can’t close the mesh terrain meshes prior to as they are convoluted and would cross.

The Journey (4 commits of pain)

Commit 1 — Initial Implementation:
Built the full pipeline from scratch:

  • Moller tri-tri intersection to find intersection segments between surfaces
  • Tag each segment with source triangle indices on both surfaces
  • Split crossed triangles along intersection segments (project to triangle-local 2D frame to handle steep/vertical pit walls correctly)
  • Classify non-crossed triangles as above/below the other surface via centroid Z interpolation against a spatial grid
  • Interactive 3D pick UI — user clicks regions to keep/remove, then Apply merges kept triangles

Commit 2 — Refinement:
The splitting worked but classification was fragile:

  • Triangles outside the other surface’s XY extent got classified as “outside” — disconnected from their actual connected region. Fixed with adjacency
    flood-fill to propagate above/below labels into outside regions.
  • Added a secondary straddling triangle refinement pass: after primary tri-tri split, some large triangles still straddle the boundary. Detect via
    per-vertex Z comparison against the other surface, split at Z-crossings with binary search for coverage-boundary cases.
  • Section plane tool added for debugging.

Commit 3 — Normals + More Closing Attempts:
Added surface normal tools (flip/align/Z-up) since boolean results often have inconsistent winding. Added stitchByProximity() — match boundary edges whose
endpoints are within tolerance, fill with triangles.

Commit 4 — The Seam Problem (current state):
The boolean split itself works well. The problem is closing the seam where the two surfaces were cut and merged. After applying:

  • ~170 boundary (open) edges remain
  • ~35 non-manifold (over-shared) edges from splitting artifacts

I’ve thrown every AI option at it:

  1. weldVertices(tris, tolerance) — spatial-grid O(n) vertex welding, snaps coincident points
  2. weldBoundaryVertices(tris, tolerance) — higher-tolerance weld targeting only boundary-edge endpoints (union-find clustering)
  3. cleanCrossingTriangles(tris) — removes duplicate triangles causing over-shared edges (fingerprint by sorted vertex key)
  4. removeOverlappingTriangles(tris, tolerance) — spatial grid finds triangles with close centroids + anti-parallel or parallel normals, removes internal
    wall pairs
  5. stitchByProximity(tris, tolerance) — for each boundary edge, find nearest boundary edge within tolerance, fill the quad with 2 triangles
  6. generateClosingTriangles(tris, maxDist) — for each remaining boundary edge, find nearest vertex that can form a valid triangle
  7. forceCloseIndexedMesh(points, triangles) — iterative pass (up to 30 iterations) on indexed mesh: for each boundary edge, find nearest point, add
    triangle, update edge counts
  8. buildCurtainAndCap(tris, floorOffset) — extrude remaining open edges vertically to a floor + earcut bottom cap
  9. capBoundaryLoops(tris) — extract boundary loops, triangulate each loop with Constrainautor/Delaunator

The pipeline order is: collect → weld → clean crossings → remove overlaps → weld boundary → stitch → generate closing tris → curtain+cap → re-weld →
force-close → log stats.

The Core Problem

The two surfaces meet along the intersection polyline, but after splitting, surface A’s boundary vertices and surface B’s boundary vertices along that seam are close but not identical. They were computed independently by different triangle splits. Even after welding with tolerance, the seam has:

  • Small gaps where vertex positions don’t quite match
  • Occasional T-junctions (vertex of one surface lands mid-edge on the other)
  • Non-manifold edges from overlapping split fragments

What I think I need is a way to enforce that both surfaces share exactly the same vertices along the intersection polyline during the split phase, rather than trying to fix it after the fact. Currently, each surface’s triangles are split independently — they each compute their own crossing points on their own edges, which produces slightly different vertex positions at the seam.

Question for the Community

Has anyone solved this “shared intersection vertices” problem for triangle mesh booleans without full CSG? Specifically:

  1. During the split phase, is there a robust way to ensure both meshes produce identical vertices along the intersection? I’m thinking: compute intersection polyline vertices once, then snap both surfaces’ split points to those shared vertices.
  2. After the merge, what’s the most reliable approach to close a seam between two open mesh regions that almost share a boundary? My stitch-by-proximity
    and force-close approaches work for ~80% of edges but leave stubborn gaps.
  3. Is there a known algorithm for “zip-closing” two boundary loops that roughly overlap? Like a two-pointer greedy walk that pairs vertices and fills with triangles?

Any pointers to papers, libraries, or Three.js examples appreciated. The meshes are 10k–50k triangles typically, so performance matters but isn’t the primary concern — correctness is.

I’m not aware of any, but if you have ordered vertices of both edges, distance-based stitching should be straightforward. See my attempt of stitching edges with random number of vertices.

However, if edges are too rigged, this approach may generate wrong triangles – the two arrows point to almost, but not yet wrong triangles. If such cases are possible with your data, maybe angle-based stitching would give better results.

4 Likes

Of course, your circumstances and restrictions are different, but my examples may still be helpful to you.

In one case, I calculated the vertices at the boundaries of geometries so that they fit.

Inner Geometry (Triangulation)

sandbox => InnerGeometryTHREEi_01


In another case, I created a separate connection geometry. This is deliberately quite large in the example, but smaller ones can also be implemented.

Single-branched geometry organically shaped in the web editor

2 Likes

If I understand the problem correctly, there are two closely spaced fronts between which triangles must be formed.

Possibly limited by a vertical cut?

It may also be possible to adapt E. Hartmann’s triangulation method here.

Page 82

I have already adapted it specifically to a few scenarios.
I don’t usually start with a hexagon, but rather with a simple triangle.

In this example, you can clearly see the progress of the formation of triangles.
Triangulation sphere with holes see the example TriangulationSphereWithHoles

I would first determine the smallest distance between two adjacent vertices in both fronts. Possibly also the minimum distance between the two fronts. This measurement is then taken as the approximate triangle length of the triangulation.

You need at least an approximate normal for the current point. With a sphere, this is trivial, of course, but it was also possible with SDFs.
Generation of SDFGeometry online and locally

2 Likes

This looks promising. Thanks.

1 Like

Here is the online demo: https://codepen.io/boytchev/full/OPXKodL.

It does not construct the triangles, only draws the red segments for illustration. The triangles are easy to make, if needed.