Pavan Siddapuram/ SE
← back to writing
FEB 2026· 2 MIN READ

CSG in the browser: a field report

Constructive solid geometry is one of those topics that looks settled in the textbook and falls apart the instant you try to ship it. Union, intersection, difference — three operations on watertight meshes. How hard could it be?

The honest answer

Hard. Real meshes are not watertight. Vertices that should coincide miss each other by the seventh decimal place. Coplanar faces produce degenerate triangles. The epsilon you pick to consider two points equal is the most consequential constant in the codebase.

For our 3D CAD platform we needed booleans that ran in the browser, on user-supplied STL files, at interactive rates. The constraints made the choice obvious: no server round-trip, no native binaries, no waiting.

BVH or bust

The naive algorithm is O(n²) in triangle pairs. For meshes with hundreds of thousands of triangles this is a non-starter. A bounding volume hierarchy collapses it to something tractable — usually O(n log n) for the broad phase — and lets you skip past the 99% of triangle pairs that obviously do not intersect.

The BVH is not an optimization. It is the only reason the feature exists.

Decimation, in three stages

Even with fast booleans, the resulting mesh is often absurd: two million triangles where fifty thousand would suffice. We decimate in three passes — quadric error metric for the bulk reduction, edge collapse for topology preservation, and a final cleanup pass that welds near-coincident vertices and removes degenerate faces.

The visible difference between 2M triangles and 50K, after decimation, is essentially zero. The performance difference is everything.

Workers, and the cost of copying

Booleans block the main thread. So we run them in a Web Worker. But the obvious approach — postMessage with the mesh data — copies the buffer, which for large meshes costs more than the boolean itself.

The fix is Transferable objects. You hand the buffer to the worker and lose access to it on the main thread. Zero copy, instant transfer. The mental model is unfamiliar — JavaScript developers are not used to ownership — but once you internalize it, the rest of the architecture follows.

We hold 60fps on meshes that, a year ago, would have crashed the tab.