Skip to content
rmoscoloniPublic

About

Repair and boolean-cut triangle meshes (STL) - Qt GUI + CLI

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

2 Commits

Folders and files

Repository files navigation

boolcut

STL Boolean Studio - boolean cutting for triangle meshes.

Repair-and-boolean pipeline for triangle meshes, built around the workflow of trimming a CATIA/scanner STL against a box: inspect, make watertight, run the boolean, then drop the sliver bodies the boolean leaves behind.

Ships as a Qt desktop app with an embedded 3D viewer, plus a CLI that runs the identical pipeline for scripting and batch work.


Install

Windows / PowerShell:

.\setup.ps1

Linux / macOS / Git Bash:

./setup.sh

Either script creates .venv and installs requirements.txt. Python 3.10+ (developed and tested on 3.11).

Run

.\run.ps1                              # GUI
.venv\Scripts\python.exe app.py        # GUI, equivalent
.venv/bin/python app.py                # GUI

CLI:

.venv/Scripts/python.exe -m stlstudio.cli c2.stl box.stl -o b_minus_a -w cavity.stl

Operations

-o / the GUI dropdown. A and B are the two loaded meshes, in order.

key meaning
union A ∪ B, merge both into one solid
intersection A ∩ B, keep only the overlap — trims A to B
a_minus_b A − B, cut B out of A
b_minus_a B − A, cut A out of B — the cavity / mould case

b_minus_a is the default because it is the one that needs a tool: cutting a scanned part out of a stock block.

The pipeline

1. Inspect. Face and vertex counts, watertightness, winding consistency, body count, volume, area, bounds, open edges grouped into boundary loops, and degenerate faces. The GUI flags a mesh that is not ready for a boolean.

2. Repair (core.repair), because manifold3d needs closed solids:

  • drop stray disconnected bodies, keeping the largest
  • merge coincident vertices, drop degenerate and duplicate faces
  • cap every open boundary loop
  • orient normals outward

Hole capping projects each boundary loop onto its own best-fit plane (SVD) and triangulates it with mapbox-earcut, which reuses only existing boundary vertices. No new points are introduced, so a ragged scan boundary is preserved exactly rather than flattened. trimesh.repair.fill_holes handles only triangle- and quad-sized gaps and silently leaves a 2000-vertex loop open, which is why it is not used here.

3. Boolean via manifold3d through trimesh — about 1 s on 800k faces.

4. Body filtering. See below.

5. Verify. Volume accounting and a watertightness check on the result. The CLI exits non-zero if the result is not watertight.

Sliver bodies

A boolean between nearly-coincident surfaces leaves detached wafers. On the reference case, box − c2 returns two bodies: the cavity block, and a 0.2534 mm-thick slab spanning the whole footprint where the box floor sat just below the part.

Two independent tests catch these, because either alone misses real cases:

  • volume — under --min-volume-frac of the total (default 0.01)
  • thickness — smallest bounding-box dimension under --min-thickness (default 0.5, in model units)

The thickness test is the one that matters. That wafer carries 2.09 % of the volume, so a 1 % volume threshold sails right past it, but at 0.25 mm it is thinner than any nozzle will print.

The largest body is never dropped. Set either threshold to 0 to disable it, --keep-slivers to disable both, or --keep-largest to ignore the heuristics and keep only body 0.

In the GUI every body is listed with faces, volume, share and minimum thickness, with the flagged ones pre-unticked — nothing is removed without showing you what and why.

Viewer

Embedded PyVista/VTK. A is blue, B is orange, result is green. Operands default to 35 % opacity so you can see through a stock block to the part inside, and are auto-hidden once a result exists; the show checkboxes bring them back. Left drag orbits, scroll zooms, middle drag pans.

Reference results

c2.stl (811,512 faces, open along the bottom, one stray triangle) against box.stl (12 faces, watertight), after repairing c2 to 68714.218 mm³:

operation volume bodies note
intersection 65328.578 1 part trimmed to the box
a_minus_b 3385.640 1 the trimmed-off rim
union 192455.640 1 = 189070.000 + 3385.640
b_minus_a 121151.674 1 cavity, after dropping the wafer

Reproduce with _tests/smoke_test.py (GUI path, asserts the b_minus_a volume and body count) or by running the CLI for each op.

Tests

.venv/Scripts/python.exe _tests/test_core.py    # fast, synthetic cubes, no files needed
.venv/Scripts/python.exe _tests/smoke_test.py   # full GUI run on the c2/box reference pair

test_core.py covers the four booleans, hole capping, stray-body removal, inverted normals and the sliver filter. smoke_test.py skips itself if _origin/c2.stl is missing.

Sample data

_origin/box.stl (12 faces) is tracked in the repo. The reference part c2.stl (~40 MB) and c2_trimmed.stl are git-ignored because of their size; place your own scanner/CATIA STL in _origin/ to reproduce the results above.

Layout

app.py                 GUI entry point
_tests/                test_core.py (synthetic, fast) and smoke_test.py (GUI end-to-end)
_origin/               sample meshes
requirements.txt
setup.ps1 / setup.sh   create .venv and install
run.ps1                launch the GUI from .venv
stlstudio/
  core.py              analysis, repair, capping, booleans, body filtering (no GUI imports)
  gui.py               Qt window, worker thread, PyVista viewer
  cli.py               headless runner

core.py has no GUI dependency — import it directly to build your own batch scripts:

import trimesh
from stlstudio import core

part = core.repair(trimesh.load("c2.stl", force="mesh"))
stock = trimesh.load("box.stl", force="mesh")
cavity = core.boolean(part, stock, "b_minus_a")

parts = core.bodies(cavity)
keep = [i for i in range(len(parts)) if i not in core.spurious_indices(parts)]
core.keep_bodies(parts, keep).export("cavity.stl")

Dependencies

package why
trimesh mesh container, IO, boolean front-end
manifold3d the boolean engine
mapbox-earcut hole capping without inventing vertices
networkx boundary-loop tracing
numpy, scipy numerics and trimesh accelerators
PySide6, qtpy GUI toolkit
pyvista, pyvistaqt VTK 3D viewer embedded in Qt

Notes

  • Heavy work runs on a QThread, so the window stays responsive; the log streams progress live.
  • Repair of an 800k-face mesh takes ~5 s. It is not trimesh.repair.fix_normals bound: caps are wound directly from the existing boundary half-edges, which is milliseconds against the ~80 s that fix_normals costs on a mesh that size. fix_normals remains a fallback for genuinely inconsistent winding.
  • A capped bottom becomes interior surface in a cavity result. If the underside should stay open, run with --no-cap — but the boolean then needs closed input by some other route.
  • Units are whatever the STL carries; STL is unitless, so --min-thickness is in the same units as the file (mm for these).

License

MIT - see LICENSE.

About

Repair and boolean-cut triangle meshes (STL) - Qt GUI + CLI

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages