Coverage for trimesh/boolean.py: 82%
45 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-24 04:40 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-24 04:40 +0000
1"""
2boolean.py
3-------------
5Do boolean operations on meshes using either Blender or Manifold.
6"""
8import numpy as np
10from . import interfaces
11from .exceptions import ExceptionWrapper
12from .iteration import reduce_cascade
13from .typed import BooleanEngineType, BooleanOperationType, Callable, Sequence
15try:
16 from manifold3d import Manifold, Mesh
17except BaseException as E:
18 Mesh = ExceptionWrapper(E)
19 Manifold = ExceptionWrapper(E)
22def difference(
23 meshes: Sequence,
24 engine: BooleanEngineType = None,
25 check_volume: bool = True,
26 **kwargs,
27):
28 """
29 Compute the boolean difference between a mesh an n other meshes.
31 Parameters
32 ----------
33 meshes : sequence of trimesh.Trimesh
34 Meshes to be processed.
35 engine
36 Which backend to use, i.e. 'blender' or 'manifold'
37 check_volume
38 Raise an error if not all meshes are watertight
39 positive volumes. Advanced users may want to ignore
40 this check as it is expensive.
41 kwargs
42 Passed through to the `engine`.
44 Returns
45 ----------
46 difference
47 A `Trimesh` that contains `meshes[0] - meshes[1:]`
48 """
50 return _engines[engine](
51 meshes, operation="difference", check_volume=check_volume, **kwargs
52 )
55def union(
56 meshes: Sequence,
57 engine: BooleanEngineType = None,
58 check_volume: bool = True,
59 **kwargs,
60):
61 """
62 Compute the boolean union between a mesh an n other meshes.
64 Parameters
65 ----------
66 meshes : list of trimesh.Trimesh
67 Meshes to be processed
68 engine : str
69 Which backend to use, i.e. 'blender' or 'manifold'
70 check_volume
71 Raise an error if not all meshes are watertight
72 positive volumes. Advanced users may want to ignore
73 this check as it is expensive.
74 kwargs
75 Passed through to the `engine`.
77 Returns
78 ----------
79 union
80 A `Trimesh` that contains the union of all passed meshes.
81 """
82 return _engines[engine](
83 meshes, operation="union", check_volume=check_volume, **kwargs
84 )
87def intersection(
88 meshes: Sequence,
89 engine: BooleanEngineType = None,
90 check_volume: bool = True,
91 **kwargs,
92):
93 """
94 Compute the boolean intersection between a mesh and other meshes.
96 Parameters
97 ----------
98 meshes : list of trimesh.Trimesh
99 Meshes to be processed
100 engine : str
101 Which backend to use, i.e. 'blender' or 'manifold'
102 check_volume
103 Raise an error if not all meshes are watertight
104 positive volumes. Advanced users may want to ignore
105 this check as it is expensive.
106 kwargs
107 Passed through to the `engine`.
109 Returns
110 ----------
111 intersection
112 A `Trimesh` that contains the intersection geometry.
113 """
114 return _engines[engine](
115 meshes, operation="intersection", check_volume=check_volume, **kwargs
116 )
119def boolean_manifold(
120 meshes: Sequence,
121 operation: BooleanOperationType,
122 check_volume: bool = True,
123 **kwargs,
124):
125 """
126 Run an operation on a set of meshes using the Manifold engine.
128 Parameters
129 ----------
130 meshes : list of trimesh.Trimesh
131 Meshes to be processed
132 operation
133 Which boolean operation to do.
134 check_volume
135 Raise an error if not all meshes are watertight
136 positive volumes. Advanced users may want to ignore
137 this check as it is expensive.
138 kwargs
139 Passed through to the `engine`.
140 """
141 if check_volume and not all(m.is_volume for m in meshes):
142 raise ValueError("Not all meshes are volumes!")
144 # Convert to manifold meshes
145 manifolds = [
146 Manifold(
147 mesh=Mesh(
148 vert_properties=np.array(mesh.vertices, dtype=np.float32),
149 tri_verts=np.array(mesh.faces, dtype=np.uint32),
150 )
151 )
152 for mesh in meshes
153 ]
155 # Perform operations
156 if operation == "difference":
157 if len(meshes) < 2:
158 raise ValueError("Difference only defined over two meshes.")
159 elif len(meshes) == 2:
160 # apply the single difference
161 result_manifold = manifolds[0] - manifolds[1]
162 elif len(meshes) > 2:
163 # union all the meshes to be subtracted from the final result
164 unioned = reduce_cascade(lambda a, b: a + b, manifolds[1:])
165 # apply the difference
166 result_manifold = manifolds[0] - unioned
167 elif operation == "union":
168 result_manifold = reduce_cascade(lambda a, b: a + b, manifolds)
169 elif operation == "intersection":
170 result_manifold = reduce_cascade(lambda a, b: a ^ b, manifolds)
171 else:
172 raise ValueError(f"Invalid boolean operation: '{operation}'")
174 # Convert back to trimesh meshes
175 from . import Trimesh
177 result_mesh = result_manifold.to_mesh()
179 return Trimesh(
180 vertices=result_mesh.vert_properties, faces=result_mesh.tri_verts, process=False
181 )
184# which backend boolean engines do we have
185_engines: dict[str, Callable] = {}
187if isinstance(Manifold, ExceptionWrapper):
188 # manifold isn't available so use the import error
189 _engines["manifold"] = Manifold
190else:
191 # manifold3d is the preferred option
192 _engines["manifold"] = boolean_manifold
195if interfaces.blender.exists:
196 # we have `blender` in the path which we can call with subprocess
197 _engines["blender"] = interfaces.blender.boolean
198else:
199 # failing that add a helpful error message
200 _engines["blender"] = ExceptionWrapper(ImportError("`blender` is not in `PATH`"))
202# pick the first value that isn't an ExceptionWrapper.
203_engines[None] = next(
204 (v for v in _engines.values() if not isinstance(v, ExceptionWrapper)),
205 ExceptionWrapper(
206 ImportError("No boolean backend: `pip install manifold3d` or install `blender`")
207 ),
208)
210engines_available = {
211 k for k, v in _engines.items() if not isinstance(v, ExceptionWrapper)
212}