🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

trimesh-boolean

Package Overview
Dependencies
Maintainers
1
Versions
24
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

trimesh-boolean

Triangle mesh boolean operations — supports open surfaces, terrain intersection, and mesh repair

Source
npmnpm
Version
0.6.0
Version published
Weekly downloads
287
-65.04%
Maintainers
1
Weekly downloads
 
Created
Source

trimesh-boolean

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.

Why trimesh-boolean?

PackageOpen surfaces?Approach
three-bvh-csgNoBVH-accelerated BSP
three-csg-tsNoBSP tree (TS)
manifold-3dNoWASM C++
trimesh-booleanYesMoller intersection + fan triangulation + shared Steiner points + hybrid boundary/barrier-normal classification + heffalump per-triangle classifier

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.

Install

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

Live demo (GitHub Pages)

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 → SettingsPagesBuild and deploymentSource: 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.

Quick Start

import { boolean, splitMeshPair, mergeSplitGroups,
         splitToComponents, mergeSmallComponents, mergeComponents,
         selectSplits, repairMesh, intersectMeshPair,
         heffalumpClassify, shouldUseHeffalump,
         reclassifyTriangles, reclassifyAtPoint, reclassifyRegion } 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.5.0) — hybrid classification + per-component walks ──
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.componentWalks = per-component boundary walk segments
// bms.pool = shared vertex pool

// BMS + splitToComponents for per-region analysis
var comps = splitToComponents(bms.groups);
// 9 components for double-crossing terrain + convoluted block

// ── Heffalump Classifier (v0.5.5) — per-triangle classification for defective meshes ──
import { heffalumpClassify, shouldUseHeffalump } from 'trimesh-boolean';

// Auto-detect if meshes have non-manifold edges
if (shouldUseHeffalump(meshA, meshB)) {
  // heffalumpClassify classifies each triangle individually
  // Works on meshes with fragmented boundaries, cracks, and holes
  var hResult = heffalumpClassify(megaSoup, segments, meshA, meshB);
  // hResult.aInside, hResult.aOutside, hResult.bInside, hResult.bOutside
  // hResult.componentWalks — boundary walk segments for visualization
}

// Manual reclassification — fix misclassified triangles interactively
import { reclassifyAtPoint } from 'trimesh-boolean';
var fix = reclassifyAtPoint(groups, clickX, clickY, clickZ, 0.01);
// fix = { moved: true, mesh: "A", from: "inside", to: "outside" }

// 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
});

Three.js Adapter

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 });

API Reference

boolean(soupA, soupB, operation)

Perform a boolean operation on two triangle soups.

  • soupA / soupB: Triangle[] — arrays of { v0, v1, v2 } where each vertex is { x, y, z }
  • operation: "subtract" | "union" | "intersect"
  • Returns: { soup, points, triangles } or null

splitMeshPair(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.

  • soupA / soupB: Triangle[]
  • Returns: { groups: { aInside, aOutside, bInside, bOutside }, segments } or null

mergeSplitGroups(groups, operation)

Merge split groups into a single result based on the operation type.

  • groups: { aInside, aOutside, bInside, bOutside } from splitMeshPair
  • operation: "subtract" | "union" | "intersect"
  • Returns: { soup, points, triangles } or null

splitToComponents(groups, options?)

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.

  • groups: { aInside, aOutside, bInside, bOutside } from splitMeshPair
  • options.pooled: boolean — route each group through the integer-id findConnectedComponentsPooled fast path (identical result, far less string hashing at scale). Default off.
  • options.tolerance: number — vertex-weld quantization for the pooled path (default 1e-6)
  • Returns: [{ mesh: "A"|"B", side: "inside"|"outside", index: number, soup: Triangle[], triCount: number }, ...] — sorted largest-first within each group

Scaling at millions of triangles

findConnectedComponents and the default splitToComponents key their edge map on toFixed(6) strings; at millions of triangles that string hashing dominates time and heap. Three opt-in, back-compatible integer-id paths avoid it (the default string paths are unchanged):

  • findConnectedComponentsPooled(soup, options?) — same shared-edge adjacency and largest-first ordering as findConnectedComponents, but vertices are hashed to integer ids so the edge map uses integer keys. Identical result on clean input. options.tolerance defaults to 1e-6.
  • splitToComponents(groups, { pooled: true }) — the same, applied to all four groups.
  • connectedComponentsIndexed(tris) / decomposeIndexedGroups(indexed, smallThreshold?) — decompose an already-indexed result (indexGroups(...) or bmsBooleanOp(..., { indexed: true }).indexed) with no soup and no re-hashing. Connectivity here is shared-vertex (union-find over pool ids), which is coarser than the soup path's shared-edge relation — on a clean seam-welded result they agree; on meshes with genuine vertex-only touches the indexed decompose yields fewer, larger components. decomposeIndexedGroups returns [{ mesh, side, group, index, points, triangles, triCount }, ...] where triangles are [i,j,k] triples into the shared points.
import { bmsBooleanOp, splitToComponents, decomposeIndexedGroups } from 'trimesh-boolean';

// Soup, pooled edge-based components (drop-in accelerator):
var bms = bmsBooleanOp(meshA, meshB, null, { indexed: true });
var comps = splitToComponents(bms.groups, { pooled: true });

// Or decompose the indexed twin directly — no soup:
var indexedComps = decomposeIndexedGroups(bms.indexed);
// indexedComps[0] = { mesh, side, group, index, points, triangles: [[i,j,k], ...], triCount }

mergeSmallComponents(comps, threshold?)

Merge small fragment components into their nearest same-group (mesh + side) larger component by centroid proximity. Cleans up tiny classification artifacts.

  • comps: Component array from splitToComponents
  • threshold: Triangle count below which a component is merged (default: 50)
  • Returns: Reduced component array with fragments absorbed

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.

  • picks: [{ soup: Triangle[], flip?: boolean }, ...]
  • Returns: { soup, points, triangles } or null

selectSplits(groups, selections)

Select specific split groups by name, with optional normal flipping.

  • groups: { aInside, aOutside, bInside, bOutside } from splitMeshPair
  • selections: { aInside?: boolean|"flip", aOutside?: boolean|"flip", bInside?: boolean|"flip", bOutside?: boolean|"flip" }
  • Returns: { soup, points, triangles } or null

BMS Pipeline (v0.4.0+)

The BMS (Brent's Mega Soup) pipeline is a 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, hybrid classification (v0.5.0), and per-triangle heffalump classification for defective meshes (v0.5.5).

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).

  • operation: "subtract" | "union" | "intersect" — omit to get groups only
  • options.classifier (v0.5.8): "auto" (default) | "hybrid" | "heffalump". Auto censuses the inputs (non-manifold → heffalump + pre-repair), runs the hybrid classifier, verifies partition / chain-closure / barrier post-conditions, and on any failure re-runs only the classification stage with the heffalump on the existing mega soup. No caller needs to know what a heffalump is anymore.
  • options.preRepair: boolean — resolve T-junctions + weld before splitting. Default: auto-enabled when the census finds non-manifold edges.
  • options.tolerance: number — vertex pool merge tolerance
  • options.indexed: boolean — also attach result.indexed, a compact indexed twin of the groups (one shared points pool + per-group [i,j,k] index triples). Back-compatible: the soup groups are unchanged. See Indexed Group Output below.
  • Returns: { groups, segments, polylines, meshEdgePolys, componentWalks, megaSoup, pool, classifier, verification }classifier reports the path per mesh (e.g. { A: "hybrid", B: "heffalump (partition)" }); verification carries the post-condition check results. With { indexed: true }, also carries indexed (see below).

verifyBmsClassification(megaSoup, triSides, segments, polylines, trisA, trisB) (v0.5.8)

The auto-classifier's post-condition checks, exported standalone: partition (both meshes must have non-empty inside AND outside groups when intersection segments exist), chain closure (every intersection polyline closes or ends on a mesh boundary), and barrier constraint (same-mesh triangles sharing a barrier edge classify to opposite sides). Returns { ok, failures, counts }.

Indexed Group Output

The pipeline's native output is triangle soup — every triangle carries its own three {x,y,z} vertex objects. That form is simple to consume, but it stores each shared vertex many times over (~5–10× heavier than an indexed mesh). For a multi-million-triangle boolean result you'll want the indexed twin: one shared vertex pool plus per-group [i,j,k] index triples. The pool is shared across all four groups, so the seam between aInside/aOutside (and bInside/bOutside) welds automatically — no re-dedupe needed.

This is purely additive: the soup groups are never modified. Opt in on bmsBooleanOp with { indexed: true }, or call indexGroups directly on any soup groups.

indexGroups(groups, tolerance?)

Convert soup split groups into a compact indexed representation.

  • groups: { aInside?, aOutside?, bInside?, bOutside? } — soup groups ({ v0, v1, v2 } triangles), as returned by splitMeshPair, bmsBooleanOp, etc.
  • tolerance: number — vertex-weld quantization in world units (default: 1e-4)
  • Returns: { points, groups } where points is a shared Vertex[] pool and groups is { aInside, aOutside, bInside, bOutside }, each an array of [i, j, k] index triples into points
import { bmsBooleanOp, indexGroups, splitMeshPair } from 'trimesh-boolean';

// Opt in on the pipeline result...
var bms = bmsBooleanOp(meshA, meshB, 'subtract', { indexed: true });
// bms.indexed = { points: Vertex[], groups: { aInside: [i,j,k][], ... } }

// ...or index any soup groups after the fact:
var split = splitMeshPair(meshA, meshB);
var indexed = indexGroups(split.groups, 1e-4);

indexGroupsToTypedArrays(indexed)

Flatten indexed groups into typed arrays for transfer or GPU upload. Positions are the same shared pool across all groups (indices are global), so a BufferGeometry can point every group's index buffer at one position buffer.

  • indexed: the return value of indexGroups
  • Returns: { positions, index } where positions is a Float64Array of [x,y,z,...] and index is { aInside, aOutside, bInside, bOutside }, each a Uint32Array of flattened triangle indices
import { indexGroupsToTypedArrays } from 'trimesh-boolean';

var flat = indexGroupsToTypedArrays(bms.indexed);
// flat.positions -> Float64Array, flat.index.aInside -> Uint32Array, ...

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}].

Since v0.5.8 a fan-sliver guard detects extreme triangle/chain size mismatches (a giant face crossed by a dense intersection chain) and switches that face from corner fans to a chain-constrained CDT seeded with graded interior Steiner points — bounded aspect ratio, no needle "spurs", and no T-junctions (the added points are strictly interior).

bmsChain(segments)

Chain intersection segments using pool vertex identity (integer ID lookup, not distance threshold). Two segments sharing a pool vertex connect by definition.

chainedOpenEdge(tris, megaSoup?, segments?)

Walk the complete open boundary of a mesh as a closed polygon. Uses a half-edge structure to correctly navigate bowtie (non-manifold) vertices. Returns the largest loop as an ordered array of {key, vertex}, with the first vertex repeated at the end.

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)

Hybrid classification (v0.5.0). Barrier flood-fill produces connected components per mesh. Classification method depends on mesh type:

  • Open meshes: boundary topology — component touching mesh open boundary = outside, enclosed by barriers = inside.
  • Closed meshes: barrier-normal — dot product of component direction vs other mesh's face normal at barrier edges.

Also extracts per-component boundary walk segments (componentWalks) for visualization.

Self-Intersection Fold Resolver (v0.6.0)

Resolves a self-intersecting, coincident-folded, or non-orientable closed mesh into a clean solid — the case orientSolid alone cannot fix, because a folded component has no consistent winding to orient toward. It re-cuts the coincident and self-crossing triangles into a conforming mesh arrangement, then classifies the resulting cells by generalized winding number (Zhou et al. 2016 + Jacobson et al. 2013).

This is a purely additive feature: every function below is a new export, and no existing API, default, or output changed. The A-vs-B bmsBooleanOp / boolean paths are byte-for-byte unchanged (the new edge-Steiner split path only activates when a self-arrangement populates intersectResult.edgePointsA/B, and the new orientSolid branch only runs under options.coherenceOnly).

bmsSelfResolveIndexed(indexed, options?)

Main entry. Takes and returns indexed geometry — the efficient form for large meshes.

import { bmsSelfResolveIndexed } from 'trimesh-boolean';

var result = bmsSelfResolveIndexed(
  { positions: Float64Array, index: Uint32Array },   // xyz-per-vertex + triangle indices
  { weldTolerance: /* default avgEdge · 0.012 */ }   // all options opt-in
);
// result = { positions: Float64Array, index: Uint32Array, diagnostics }

diagnostics reports what happened, honestly:

  • classifier"cell-complex" when the mesh resolved watertight, "patch" when it fell back (see below).
  • arrangementOpenEdges, preOrientSeamViolations — residual non-orientable seams, if any.
  • cellComplex.leakedFaceFraction, seamOpenBefore / seamOpenAfter — per-mesh, where inside and outside could not be separated.

Genuinely-degenerate input — coincident folds whose seam glues inside to outside as a single topological cell — cannot be made watertight by any winding method. On those, the resolver falls back to a volume-accurate result (typical volume error < 0.01%) rather than tearing or collapsing the mesh, and reports classifier: "patch". It never ships a proximity-stitch collapse.

bmsSelfResolve(soup, options?)

Soup-form entry ({v0,v1,v2}[] in, { soup, diagnostics } out) for when you are not already indexed.

Pipeline internals (advanced)

Exposed for composition and testing; you normally only need the two entries above.

  • coplanarOverlap, emitCoplanarSegments — coincident (coplanar) overlap detection and segment emission. This is the fold case the classic Möller test near-parallel-rejects, so ordinary intersection never sees it.
  • bmsSelfIntersect, bmsSelfArrange — self-intersection detection and the conforming self-arrangement (shared-pool edge-Steiner splits → no T-junctions).
  • conditionArrangement — opt-in seam snap-round + hole-fill; runs only when the cell complex detects a leak, and is a no-op on watertight input.
  • solidAngle, windingNumber, windingNumberIndexed, extractByWinding, extractByWindingPatches — the generalized winding-number field and surface extraction.
  • extractByCellComplex — the volumetric cell-complex classifier (radial edge fans → cells → winding propagation → manifold-by-construction extraction).
  • dedupCoincidentTriangles — exact coincident-face deduplication.

Heffalump Classifier (v0.5.5)

The heffalump classifier is a barrier-only classification strategy for meshes with defective topology (non-manifold edges, fragmented boundaries, cracks, holes). Instead of relying on boundary walks (which break when the boundary is fragmented), it classifies each triangle individually:

  • Closed other mesh: ray-cast each triangle's centroid through the other mesh (odd crossings = inside)
  • Open other mesh: find nearest surface triangle and check which side the centroid is on (normal direction = outside)

heffalumpClassify(megaSoup, segments, trisA, trisB, opts?)

Barrier-only classification for meshes with defective topology. Classifies each triangle individually rather than by flood-filled components.

  • megaSoup: Split triangles with mesh tags from bmsSplit
  • segments: Intersection segments with pool vertex endpoints
  • trisA / trisB: Original mesh triangles
  • opts.snapThreshold (v0.5.8, default 0.9): per-component majority-snap ratio. After the per-triangle vote, if ≥ this fraction of a component agrees, the stragglers snap to the majority — this erases lone flipped "spur" triangles whose centroids hug the other surface. Genuinely mixed components (the barrier-gap case) are nowhere near unanimous and stay per-triangle.
  • opts.maxSnapStragglers (v0.5.9, default 8): absolute-count gate on the snap. The snap only collapses a tiny minority — it never bulldozes a large, legitimate region that happens to read as a low ratio (e.g. a terrain area inside a prism that is flood-fill-connected to the outside).
  • Returns: { aInside, aOutside, bInside, bOutside, componentWalks }

shouldUseHeffalump(trisA, trisB)

Detect whether a mesh pair needs the heffalump classifier. Returns true if either mesh has non-manifold (over-shared) edges.

  • Returns: boolean

reclassifyTriangles(groups, mesh, fromSide, triIndices)

Move triangles between inside/outside groups by index. Useful for fixing misclassified triangles programmatically.

  • groups: { aInside, aOutside, bInside, bOutside }
  • mesh: "A" | "B"
  • fromSide: "inside" | "outside" — triangles are moved to the opposite side
  • triIndices: number[] — indices within the source array to move
  • Returns: Updated groups (mutated in place)

reclassifyAtPoint(groups, cx, cy, cz, tolerance?)

Reclassify a single triangle identified by its centroid coordinates. Useful for click-to-toggle in a 3D viewer.

  • cx, cy, cz: Centroid coordinates to match
  • tolerance: Match tolerance (default: 0.01)
  • Returns: { moved: boolean, mesh?: string, from?: string, to?: string }

reclassifyRegion(groups, mesh, fromSide, seedIdx)

Reclassify a connected region — flood fill from a seed triangle to move all connected same-side triangles together.

  • mesh: "A" | "B"
  • fromSide: "inside" | "outside"
  • seedIdx: Index of the seed triangle in the source array
  • Returns: number — count of triangles moved

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.

  • Returns: Segment[][{ p0, p1 }, ...]

intersectMeshPairTagged(trisA, trisB)

Like intersectMeshPair but each segment carries source triangle indices.

  • Returns: [{ p0, p1, idxA, idxB }, ...]

repairMesh(soup, config?, onProgress?)

High-level async mesh repair pipeline.

  • config.closeMode: "none" | "weld" | "stitch" (default: "none")
  • config.snapTolerance: Weld tolerance in metres (default: 0)
  • config.stitchTolerance: Stitch tolerance (default: 1.0)
  • config.removeDegenerate: Remove degenerate/sliver triangles (default: true)
  • config.sliverRatio: Sliver aspect ratio threshold (default: 0.01)
  • config.cleanCrossings: Remove over-shared edge duplicates (default: true)
  • config.removeOverlapping: Remove anti-parallel internal wall triangles (default: false)
  • config.overlapTolerance: Overlap detection tolerance (default: 1e-4)
  • Returns: Promise<{ soup, points, triangles }>

Repair Functions

Individual repair steps, usable standalone:

FunctionDescription
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 (parallel)
capBoundaryLoopsSequential(tris)Cap all boundary loops (sequential)
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
soupToIndexed(tris, tolerance)Alias for weldVertices — convert soup to indexed mesh
indexedToSoup(weldedTris)Alias for weldedToSoup — convert indexed mesh back to soup

Component Functions

FunctionDescription
findConnectedComponents(soup)Split a soup into connected components via shared edges
findConnectedComponentsPooled(soup, options?)Integer-id twin of the above — same result, no toFixed string keys (see Scaling)
splitToComponents(groups, options?)Decompose 4 split groups into per-component list ({ pooled: true } for the fast path)
connectedComponentsIndexed(tris)Components of an already-indexed mesh via shared-vertex union-find
decomposeIndexedGroups(indexed, smallThreshold?)Decompose indexed groups (shared pool + [i,j,k]) — soup-free
mergeSmallComponents(comps, threshold?)Merge small fragments into nearest same-group neighbor
mergeSmallIndexedComponents(comps, threshold)Fold small indexed components into the largest
mergeComponents(picks)Merge user-selected component soups into welded result
selectSplits(groups, selections)Select specific groups with optional flip

Normal Functions

FunctionDescription
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

Intersection Functions

FunctionDescription
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
computeBBox(tris)Compute axis-aligned bounding box of a triangle array
triBBox(tri)Compute bounding box of a single triangle
bboxOverlap(a, b)Test if two bounding boxes overlap
estimateAvgEdge(tris)Estimate average edge length of a triangle array

Boolean Internals (Advanced)

FunctionDescription
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:

FunctionDescription
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

Utility Functions

FunctionDescription
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
vKey(v)Vertex to string key for spatial hashing
edgeKey(k1, k2)Canonical edge key from two vertex keys
indexGroups(groups, tolerance?)Soup groups → shared points pool + per-group [i,j,k] index triples (see Indexed Group Output)
indexGroupsToTypedArrays(indexed)Flatten indexed groups → Float64Array positions + per-group Uint32Array indices

Working with Kirra Surface Data

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.

Data Model

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 }

How It Works

Boolean Algorithm — Step by Step

Step 1: Intersection Detection

Find all triangle-triangle intersection segments between mesh A and mesh B using the Moller algorithm.

  • Each segment records its source triangle indices (idxA, idxB)
  • Builds a crossedMap — which triangles are "crossed" by the other mesh
  • Issue: Moller can miss near-coplanar or edge-on intersections. Deterministic jitter offsets mitigate this.

Step 2: Spatial Grid Construction

Build 3 spatial hash grids per mesh (XY, YZ, XZ) for accelerated ray casting along Z, X, and Y axes.

  • Cell size is 2x the average edge length of each mesh
  • Three grids enable multi-axis ray casting (no single-axis blind spots)
  • Issue: Very small or very large meshes (sub-mm edges, or UTM 600000+) can cause hash collisions. The library centroid-shifts large coordinates internally.

Step 3: Flood-Fill Classification

BFS from non-crossed seed triangles to classify connected regions as inside/outside via multi-axis majority vote.

  • Seed triangles ray-cast along +Z, +X, +Y; 2+ inside votes = inside
  • Classification propagates to all connected non-crossed triangles in the same component
  • Issue (open surfaces): Ray-casting against an open mesh is unreliable — rays can escape through open edges and give wrong inside/outside results. For open surfaces, Step 5 corrects these errors using the half-space test.

Step 4: Fan Triangulation

Split every crossed triangle at its intersection segment endpoints using fan triangulation (CDT fallback for edge cases).

  • Chain intersection segments into ordered polylines
  • Identify boundary entry/exit points via barycentric coordinates
  • Fan from each original vertex to sequential chain points — every sub-triangle has at least 1 original vertex and 1 edge on the intersection boundary
  • Eliminates "pocket triangles" (all-Steiner sub-triangles) that plagued CDT — reduced from 93 to 5 across test meshes
  • CDT fallback for: multiple disconnected polylines, entry/exit on same edge, chain endpoint at original vertex

Step 5: Half-Space Classification

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.

  • Calibration: The normal convention (outward vs inward) is auto-detected by sampling points near the intersection, offsetting along the triangle normal, and ray-casting.
  • Why this works for open surfaces: Unlike ray-casting (which counts boundary crossings and fails for non-watertight meshes), the half-space test only needs local geometry at the intersection.
  • Confidence propagation: Confident classifications propagate to adjacent non-confident triangles via edge adjacency.
  • Known limitation: Multi-crossing open surfaces can still have spill where the fallback path picks the wrong nearest segment. See KNOWN_ISSUES.md for details.

Step 6: Seam Deduplication

Merge coincident vertices along the intersection boundary to produce a clean seam.

  • Tolerance-based deduplication at 1e-4
  • Applied independently to each of the 4 groups (aInside, aOutside, bInside, bOutside)

Step 7: Normal Propagation

Ensure consistent winding order across the result mesh.

  • BFS-based winding propagation from a seed triangle
  • Falls back to Z-up normal enforcement if BFS doesn't reach all triangles

Step 8: Group Combination

Combine inside/outside groups based on the requested operation.

OperationFormulaGroups kept
subtractA \ BA-outside-B + B-inside-A (flipped)
unionA ∪ BA-outside-B + B-outside-A
intersectA ∩ BA-inside-B + B-inside-A

Step 9: Weld & Return

Weld the combined triangle soup into an indexed mesh and return { soup, points, triangles }.

Mesh Repair Algorithm

  • Vertex deduplication — spatial-grid merge of coincident vertices
  • T-junction resolution — detect and split edges at mid-edge vertices
  • Welding — merge vertices within tolerance
  • Degenerate removal — filter zero-area and sliver triangles
  • Stitching — connect nearby boundary edges
  • Capping — triangulate boundary loops
  • Force-close — fill remaining gaps iteratively

Dependencies

Optional peer dependency:

  • three >=0.150.0 — Only needed for trimesh-boolean/three adapter

License

MIT

Keywords

mesh

FAQs

Package last updated on 19 Jul 2026

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts