
Security News
arXiv Is Rate Limiting Authors Following a Flood of AI Slop Submissions
arXiv now limits authors to two submissions a month as AI slop overwhelms moderators, delays good papers, and sparks debate over applying the limit to everyone.
trimesh-boolean
Advanced tools
Triangle mesh boolean operations — supports open surfaces, terrain intersection, and mesh repair
Triangle mesh boolean operations for JavaScript — supports open surfaces.
Unlike every other mesh boolean package in the npm ecosystem, trimesh-boolean works on open (non-watertight) meshes like terrain surfaces, DTMs, and partial shells. It also works on closed solids.
| Package | Open surfaces? | Approach |
|---|---|---|
| three-bvh-csg | No | BVH-accelerated BSP |
| three-csg-ts | No | BSP tree (TS) |
| manifold-3d | No | WASM C++ |
| trimesh-boolean | Yes | Moller intersection + fan triangulation + shared Steiner points + boundary topology classification |
Every existing package requires closed, manifold input. If you're working with terrain surfaces, geological models, or any open mesh — they won't work. trimesh-boolean will.
npm install trimesh-boolean
Or use the CDN for browser scripts:
<script src="https://unpkg.com/trimesh-boolean/build/trimesh-boolean.min.js"></script>
<!-- Exposes window.TrimeshBoolean -->
Links: npm · GitHub · Live Demo
The live demo is built and deployed automatically on every push to main or master by .github/workflows/pages.yml (npm run build:docs in CI). You do not need to commit the docs/ folder for the site to update.
One-time repo setting: GitHub → Settings → Pages → Build and deployment → Source: GitHub Actions (not “Deploy from a branch”). After that, each push runs the workflow and refreshes the demo.
To preview the same build locally:
npm run build:docs
Edit demos only under examples/ (Vite root). Local dev: npm run dev.
import { boolean, splitMeshPair, mergeSplitGroups,
splitToComponents, mergeSmallComponents, mergeComponents,
selectSplits, repairMesh, intersectMeshPair } from 'trimesh-boolean';
// Triangle soup: simple {v0, v1, v2} objects
var meshA = [
{ v0: {x:0,y:0,z:0}, v1: {x:2,y:0,z:0}, v2: {x:1,y:2,z:0} },
// ... more triangles
];
var meshB = [/* ... */];
// One-shot boolean operation
var result = boolean(meshA, meshB, 'subtract');
// result = { soup: Triangle[], points: Vertex[], triangles: WeldedTri[] }
// Split-and-pick workflow (inspect groups before merging)
var split = splitMeshPair(meshA, meshB);
// split.groups = { aInside, aOutside, bInside, bOutside }
// split.segments = TaggedSegment[]
var merged = mergeSplitGroups(split.groups, 'union');
// Custom surface workflow — user picks individual components
var comps = splitToComponents(split.groups);
// comps = [{ mesh:"A", side:"inside", index:0, soup:[...], triCount:599 }, ...]
comps = mergeSmallComponents(comps, 50); // collapse tiny fragments
var picked = mergeComponents([
{ soup: comps[0].soup }, // keep A-inside #0
{ soup: comps[2].soup, flip: true } // keep B-outside #0, flip normals
]);
// ── BMS Pipeline (v0.4.0) — shared Steiner points + boundary topology ──
import { bmsBooleanOp } from 'trimesh-boolean';
var bms = bmsBooleanOp(meshA, meshB, null, { preRepair: true });
// bms.groups = { aInside, aOutside, bInside, bOutside }
// bms.segments = pool-vertex intersection segments
// bms.polylines = chained intersection polylines
// bms.meshEdgePolys = { A: {...}, B: {...} } — per-mesh boundary polygons
// bms.pool = shared vertex pool
// BMS + splitToComponents for per-region analysis
var comps = splitToComponents(bms.groups);
// 9 components for double-crossing terrain + convoluted block
// Just intersection segments (no boolean)
var segments = intersectMeshPair(meshA, meshB);
// segments = [{ p0: {x,y,z}, p1: {x,y,z} }, ...]
// Mesh repair
var repaired = await repairMesh(meshA, {
closeMode: 'stitch',
snapTolerance: 0.01
});
import { booleanFromMeshes, meshToSoup, soupToMesh } from 'trimesh-boolean/three';
// Boolean on Three.js meshes directly
var resultMesh = booleanFromMeshes(threeGroupA, threeGroupB, 'subtract');
scene.add(resultMesh);
// Or convert manually
var soup = meshToSoup(threeMesh);
var mesh = soupToMesh(soup, { color: 0xff0000 });
boolean(soupA, soupB, operation)Perform a boolean operation on two triangle soups.
Triangle[] — arrays of { v0, v1, v2 } where each vertex is { x, y, z }"subtract" | "union" | "intersect"{ soup, points, triangles } or nullsplitMeshPair(soupA, soupB)Split two meshes into 4 inside/outside groups without combining them. This is the "split-and-pick" workflow: compute groups, then the caller decides which to keep.
Triangle[]{ groups: { aInside, aOutside, bInside, bOutside }, segments } or nullmergeSplitGroups(groups, operation)Merge split groups into a single result based on the operation type.
{ aInside, aOutside, bInside, bOutside } from splitMeshPair"subtract" | "union" | "intersect"{ soup, points, triangles } or nullsplitToComponents(groups)Decompose each of the 4 split groups into connected components (disconnected mesh regions). Useful for multi-crossing surfaces where a single group contains multiple spatially separated zones.
{ aInside, aOutside, bInside, bOutside } from splitMeshPair[{ mesh: "A"|"B", side: "inside"|"outside", index: number, soup: Triangle[], triCount: number }, ...] — sorted largest-first within each groupmergeSmallComponents(comps, threshold?)Merge small fragment components into their nearest same-group (mesh + side) larger component by centroid proximity. Cleans up tiny classification artifacts.
splitToComponents50)mergeComponents(picks)Merge an arbitrary selection of component soups into a single welded result. Works with the output of splitToComponents — pass in the components the user has selected.
[{ soup: Triangle[], flip?: boolean }, ...]{ soup, points, triangles } or nullselectSplits(groups, selections)Select specific split groups by name, with optional normal flipping.
{ aInside, aOutside, bInside, bOutside } from splitMeshPair{ aInside?: boolean|"flip", aOutside?: boolean|"flip", bInside?: boolean|"flip", bOutside?: boolean|"flip" }{ soup, points, triangles } or nullThe BMS (Brent's Mega Soup) pipeline is a new boolean pipeline designed for open surfaces. It solves the core problems of the original pipeline: shared Steiner points, identity-based segment chaining, fan triangulation with guaranteed constraint edges, and boundary topology classification.
bmsBooleanOp(soupA, soupB, operation?, options?)Run the full BMS pipeline. Both meshes are split into a unified mega soup where intersection points are shared by object reference (not string matching).
"subtract" | "union" | "intersect" — omit to get groups onlyboolean — resolve T-junctions + weld before splittingnumber — vertex pool merge tolerance{ groups, segments, polylines, meshEdgePolys, megaSoup, pool }bmsIntersect(trisA, trisB, options?)Compute intersections with a shared vertex pool. Every segment endpoint goes through the pool — both meshes get the exact same object reference at each intersection location.
bmsSplit(trisA, trisB, intersectResult)Re-triangulate crossed triangles using fan triangulation with pool vertex references. Produces a tagged mega soup: [{v0, v1, v2, mesh: "A"|"B", origIdx}].
bmsChain(segments)Chain intersection segments using pool vertex identity (integer ID lookup, not distance threshold). Two segments sharing a pool vertex connect by definition.
bmsClosePolylines(polylines, trisA, trisB, megaSoup?, segments?)Build mesh edge polygons connecting intersection polylines via graph-walks and boundary edge-walks. Graph-walks avoid crossing intersection lines (barrier-aware BFS).
bmsClassify(megaSoup, closedPolylines, segments, trisA, trisB)Barrier flood-fill classification. Intersection segment edges block propagation between regions. Boundary topology determines inside/outside: the component touching the mesh's open boundary = outside.
createVertexPool(tolerance)Create a shared vertex pool with spatial hash deduplication. Points within tolerance get merged to the same object reference.
intersectMeshPair(trisA, trisB)Find all intersection segments between two meshes.
Segment[] — [{ p0, p1 }, ...]intersectMeshPairTagged(trisA, trisB)Like intersectMeshPair but each segment carries source triangle indices.
[{ p0, p1, idxA, idxB }, ...]repairMesh(soup, config?, onProgress?)High-level async mesh repair pipeline.
"none" | "weld" | "stitch" (default: "none")0)1.0)true)0.01)true)false)1e-4)Promise<{ soup, points, triangles }>Individual repair steps, usable standalone:
| Function | Description |
|---|---|
deduplicateSeamVertices(tris, tol?) | Merge coincident seam vertices |
resolveTJunctions(soup, tol?, maxPasses?) | Split edges at T-junction vertices |
weldVertices(tris, tolerance) | Merge vertices within tolerance → indexed mesh |
weldedToSoup(weldedTris) | Convert indexed mesh back to soup |
removeDegenerateTriangles(tris, minArea?, sliverRatio?) | Remove zero-area and sliver triangles |
extractBoundaryLoops(tris) | Find open boundary loops |
triangulateLoop(loop) | Triangulate a 3D polygon loop |
capBoundaryLoops(tris) | Cap all boundary loops |
stitchByProximity(tris, tolerance?) | Connect nearby boundary edges |
cleanCrossingTriangles(tris) | Remove over-shared edge duplicates |
removeOverlappingTriangles(tris, tol?) | Remove anti-parallel internal walls |
forceCloseIndexedMesh(points, triangles) | Force-close an indexed mesh |
fillOpenEdgeLoops(soup) | Fill closed loops of open edges with fan triangles |
weldBoundaryVertices(tris, tolerance) | Weld boundary-only vertices |
| Function | Description |
|---|---|
findConnectedComponents(soup) | Split a soup into connected components via shared edges |
splitToComponents(groups) | Decompose 4 split groups into per-component list |
mergeSmallComponents(comps, threshold?) | Merge small fragments into nearest same-group neighbor |
mergeComponents(picks) | Merge user-selected component soups into welded result |
selectSplits(groups, selections) | Select specific groups with optional flip |
| Function | Description |
|---|---|
triNormal(tri) | Unit face normal of a triangle |
ensureZUpNormals(tris) | Flip downward-facing triangles to Z-up |
flipAllNormals(tris) | Reverse all triangle winding |
classifyNormalDirection(tris, isClosed, volume) | Classify dominant normal direction |
computeSignedVolume(tris) | Signed volume via divergence theorem |
computeProjectedArea(tris, plane) | Projected footprint area |
compute3DSurfaceArea(tris) | True 3D surface area |
| Function | Description |
|---|---|
triTriIntersection(triA, triB) | Moller tri-tri intersection → segment |
triTriIntersectionDetailed(triA, triB) | With signed distances |
chainSegments(segments, threshold) | Chain segments into polylines |
simplifyPolyline(points, spacing) | Distance-based simplification |
buildSpatialGrid(tris, cellSize) | Build XY spatial hash (for Z-ray) |
buildSpatialGridOnAxes(tris, cellSize, getA, getB) | Build spatial hash on arbitrary axes |
queryGrid(grid, bb, cellSize) | Query XY spatial hash |
queryGridOnAxes(grid, a, b, cellSize) | Query arbitrary-axis spatial hash |
| Function | Description |
|---|---|
classifyPointMultiAxis(point, otherTris, grids) | Classify inside/outside via 3-axis majority vote |
classifyByFloodFill(tris, crossedMap, otherTris, otherGrids) | BFS flood-fill classification with multi-axis seeds |
fanTriangulate(tri, segments) | Fan triangulation of crossed triangle (primary method) |
retriangulateWithSteinerPoints(tri, segments) | CDT split of crossed triangle with Steiner points (fallback) |
buildCurtainAndCap(tris, floorOffset) | Extrude boundary to floor + cap |
generateClosingTriangles(tris, maxDist) | Iteratively close boundary gaps |
Internal (non-exported) functions used by the boolean pipeline:
| Function | Description |
|---|---|
splitStraddlingAndClassify(tris, classifications, crossedMap, otherTris, otherGrids, otherIdxKey) | Split crossed tris via fan triangulation, classify via border-segment priority + half-space fallback, enforce constraints across segment edges |
halfSpaceTest(point) | Classify a point against the nearest intersection segment using the other mesh's triangle normal |
segHalfSpace(point, seg) | Classify a point against a specific segment's other-mesh triangle plane |
| Function | Description |
|---|---|
dist3(a, b) | 3D Euclidean distance |
distSq3(a, b) | Squared 3D distance |
triangleArea3D(tri) | Triangle area via cross product |
computeBounds(points) | Axis-aligned bounding box |
cross(a, b) | 3D cross product |
lerpVert(a, b, t) | Linear vertex interpolation |
countOpenEdges(tris) | Count boundary and non-manifold edges |
The library includes real-world mining surface data from the Kirra application. Kirra surfaces use the format { vertices: [{x,y,z}, ...] } per triangle. To convert to trimesh-boolean soup:
// Convert Kirra triangles to trimesh-boolean soup
function kirraToSoup(surface) {
var soup = [];
for (var i = 0; i < surface.triangles.length; i++) {
var verts = surface.triangles[i].vertices;
if (verts && verts.length >= 3) {
soup.push({ v0: verts[0], v1: verts[1], v2: verts[2] });
}
}
return soup;
}
Pre-extracted Kirra surfaces (terrain, cylinder, cup, convoluted block) are included in examples/public/kirra-surfaces.json with UTM coordinates centroid-subtracted for demo use. The convoluted block is a 32-triangle open surface that crosses the terrain twice — the hardest test case for multi-crossing classification.
The Three.js demo (examples/index.html) also has Import KAP / Export KAP: import reads surfaces.json from a Kirra .kap ZIP into the Mesh A/B dropdowns; export writes a minimal Kirra-compatible archive (manifest + two surfaces for the current Mesh A and B) for round-tripping or testing in Kirra.
The library uses plain JavaScript objects — no classes, no Three.js types:
// Vertex
{ x: number, y: number, z: number }
// Triangle (soup format)
{ v0: Vertex, v1: Vertex, v2: Vertex }
// Welded triangle (indexed format)
{ vertices: [Vertex, Vertex, Vertex] }
// Segment
{ p0: Vertex, p1: Vertex }
Find all triangle-triangle intersection segments between mesh A and mesh B using the Moller algorithm.
idxA, idxB)crossedMap — which triangles are "crossed" by the other meshBuild 3 spatial hash grids per mesh (XY, YZ, XZ) for accelerated ray casting along Z, X, and Y axes.
2x the average edge length of each meshBFS from non-crossed seed triangles to classify connected regions as inside/outside via multi-axis majority vote.
Split every crossed triangle at its intersection segment endpoints using fan triangulation (CDT fallback for edge cases).
Classify every triangle (non-crossed and crossed sub-triangles) using a multi-stage approach:
Step D0 — Border-segment priority: If a crossed sub-triangle shares an edge with an intersection segment, classify its centroid directly against that specific segment's other-mesh triangle plane. This prevents "nearest segment" from picking a wrong crossing in multi-crossing scenarios.
Steps D1-D4 — Fallback chain: half-space test against nearest segment, vertex adjacency inheritance, sub-triangle centroid half-space, ray-cast centroid (last resort).
Step E3 — Constraint enforcement: After initial classification and adjacency propagation, iterate through all segment edges. Sub-triangles sharing a segment edge MUST have opposite classifications (one inside, one outside). Any violations are corrected using the specific segment's plane.
Merge coincident vertices along the intersection boundary to produce a clean seam.
1e-4Ensure consistent winding order across the result mesh.
Combine inside/outside groups based on the requested operation.
| Operation | Formula | Groups kept |
|---|---|---|
subtract | A \ B | A-outside-B + B-inside-A (flipped) |
union | A ∪ B | A-outside-B + B-outside-A |
intersect | A ∩ B | A-inside-B + B-inside-A |
Weld the combined triangle soup into an indexed mesh and return { soup, points, triangles }.
Optional peer dependency:
>=0.150.0 — Only needed for trimesh-boolean/three adapterMIT
FAQs
Triangle mesh boolean operations — supports open surfaces, terrain intersection, and mesh repair
The npm package trimesh-boolean receives a total of 84 weekly downloads. As such, trimesh-boolean popularity was classified as not popular.
We found that trimesh-boolean demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.

Security News
arXiv now limits authors to two submissions a month as AI slop overwhelms moderators, delays good papers, and sparks debate over applying the limit to everyone.

Research
/Security News
A new GhostAction wave hits hundreds of GitHub repos, expanding CI/CD secret theft to cloud and AI credentials in source code and git history.

Research
/Security News
Tensorlake npm SDK version 0.5.144 was compromised in a ChainDrop / Shai-Hulud attack, delivering credential-stealing malware.