npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

trimesh-boolean

v0.3.0

Published

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

Readme

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?

| 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 + half-space 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.

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

Quick Start

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

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

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
  • Returns: [{ mesh: "A"|"B", side: "inside"|"outside", index: number, soup: Triangle[], triCount: number }, ...] — sorted largest-first within each group

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

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:

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

Component Functions

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

Normal Functions

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

Intersection Functions

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

Boolean Internals (Advanced)

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

Utility Functions

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

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

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.

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

Step 9: Weld & Return

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

Mesh Repair Algorithm

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

Dependencies

Optional peer dependency:

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

License

MIT