Coverage for trimesh/boolean.py: 82%

45 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-06-24 04:40 +0000

1""" 

2boolean.py 

3------------- 

4 

5Do boolean operations on meshes using either Blender or Manifold. 

6""" 

7 

8import numpy as np 

9 

10from . import interfaces 

11from .exceptions import ExceptionWrapper 

12from .iteration import reduce_cascade 

13from .typed import BooleanEngineType, BooleanOperationType, Callable, Sequence 

14 

15try: 

16 from manifold3d import Manifold, Mesh 

17except BaseException as E: 

18 Mesh = ExceptionWrapper(E) 

19 Manifold = ExceptionWrapper(E) 

20 

21 

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. 

30 

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

43 

44 Returns 

45 ---------- 

46 difference 

47 A `Trimesh` that contains `meshes[0] - meshes[1:]` 

48 """ 

49 

50 return _engines[engine]( 

51 meshes, operation="difference", check_volume=check_volume, **kwargs 

52 ) 

53 

54 

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. 

63 

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

76 

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 ) 

85 

86 

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. 

95 

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

108 

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 ) 

117 

118 

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. 

127 

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!") 

143 

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 ] 

154 

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}'") 

173 

174 # Convert back to trimesh meshes 

175 from . import Trimesh 

176 

177 result_mesh = result_manifold.to_mesh() 

178 

179 return Trimesh( 

180 vertices=result_mesh.vert_properties, faces=result_mesh.tri_verts, process=False 

181 ) 

182 

183 

184# which backend boolean engines do we have 

185_engines: dict[str, Callable] = {} 

186 

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 

193 

194 

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`")) 

201 

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) 

209 

210engines_available = { 

211 k for k, v in _engines.items() if not isinstance(v, ExceptionWrapper) 

212}