File indexing completed on 2026-11-09 10:47:17
0001
0002
0003 """
0004 merge_stl.py
0005
0006 Perform a boolean union of two overlapping STL meshes and write
0007 the result to a new STL file.
0008
0009 Usage:
0010 python merge_stl.py mesh1.stl mesh2.stl merged.stl
0011
0012 Optional:
0013 --engine manifold
0014 --engine blender
0015 --engine scad
0016 """
0017
0018 import argparse
0019 import sys
0020 from pathlib import Path
0021
0022 import trimesh
0023
0024
0025 def load_mesh(path):
0026 """Load an STL file as a Trimesh."""
0027 mesh = trimesh.load_mesh(
0028 path,
0029 force="mesh",
0030 process=True
0031 )
0032
0033 if not isinstance(mesh, trimesh.Trimesh):
0034 raise ValueError(
0035 "{} did not load as a single mesh.".format(path)
0036 )
0037
0038 return mesh
0039
0040
0041 def combine_result(result):
0042 """
0043 Convert a boolean result into a single Trimesh.
0044 """
0045 if result is None:
0046 return None
0047
0048 if isinstance(result, trimesh.Trimesh):
0049 return result
0050
0051 if isinstance(result, trimesh.Scene):
0052 meshes = [
0053 geom
0054 for geom in result.geometry.values()
0055 if isinstance(geom, trimesh.Trimesh)
0056 ]
0057
0058 if not meshes:
0059 return None
0060
0061 return trimesh.util.concatenate(meshes)
0062
0063 if isinstance(result, (list, tuple)):
0064 meshes = [
0065 geom
0066 for geom in result
0067 if isinstance(geom, trimesh.Trimesh)
0068 ]
0069
0070 if not meshes:
0071 return None
0072
0073 return trimesh.util.concatenate(meshes)
0074
0075 raise TypeError(
0076 "Unsupported boolean result type: {}".format(type(result))
0077 )
0078
0079
0080 def merge_meshes(mesh_a, mesh_b, engine=None):
0081 """Perform boolean union of two meshes."""
0082 result = trimesh.boolean.union(
0083 [mesh_a, mesh_b],
0084 engine=engine
0085 )
0086
0087 return combine_result(result)
0088
0089
0090 def main():
0091 parser = argparse.ArgumentParser(
0092 description=(
0093 "Merge two STL meshes using a boolean union."
0094 )
0095 )
0096
0097 parser.add_argument(
0098 "input_a",
0099 help="First STL mesh"
0100 )
0101
0102 parser.add_argument(
0103 "input_b",
0104 help="Second STL mesh"
0105 )
0106
0107 parser.add_argument(
0108 "output",
0109 help="Output STL mesh"
0110 )
0111
0112 parser.add_argument(
0113 "--engine",
0114 default=None,
0115 help=(
0116 "Boolean engine: manifold, blender, or scad. "
0117 "Default: auto"
0118 )
0119 )
0120
0121 args = parser.parse_args()
0122
0123
0124 try:
0125 mesh_a = load_mesh(args.input_a)
0126 mesh_b = load_mesh(args.input_b)
0127 except Exception as exc:
0128 print(
0129 "Error loading mesh: {}".format(exc),
0130 file=sys.stderr
0131 )
0132 sys.exit(1)
0133
0134 print("Mesh A:")
0135 print(" vertices: {}".format(len(mesh_a.vertices)))
0136 print(" faces: {}".format(len(mesh_a.faces)))
0137 print(" watertight: {}".format(mesh_a.is_watertight))
0138 print(" volume: {}".format(abs(mesh_a.volume)))
0139
0140 print()
0141 print("Mesh B:")
0142 print(" vertices: {}".format(len(mesh_b.vertices)))
0143 print(" faces: {}".format(len(mesh_b.faces)))
0144 print(" watertight: {}".format(mesh_b.is_watertight))
0145 print(" volume: {}".format(abs(mesh_b.volume)))
0146
0147 if not mesh_a.is_watertight or not mesh_b.is_watertight:
0148 print()
0149 print(
0150 "WARNING: one or both meshes are not watertight. "
0151 "Boolean union may fail or produce invalid geometry.",
0152 file=sys.stderr
0153 )
0154
0155
0156 print()
0157 print("Calculating boolean union...")
0158
0159 try:
0160 result = merge_meshes(
0161 mesh_a,
0162 mesh_b,
0163 engine=args.engine
0164 )
0165 except Exception as exc:
0166 print(
0167 "Boolean union failed: {}".format(exc),
0168 file=sys.stderr
0169 )
0170 sys.exit(2)
0171
0172 if result is None or len(result.faces) == 0:
0173 print(
0174 "Boolean union produced an empty mesh.",
0175 file=sys.stderr
0176 )
0177 sys.exit(2)
0178
0179
0180
0181
0182 result.update_faces(result.unique_faces())
0183 result.remove_unreferenced_vertices()
0184
0185
0186 output_path = Path(args.output)
0187
0188 try:
0189 result.export(str(output_path))
0190 except Exception as exc:
0191 print(
0192 "Error writing output: {}".format(exc),
0193 file=sys.stderr
0194 )
0195 sys.exit(1)
0196
0197 print()
0198 print("Merged mesh:")
0199 print(" vertices: {}".format(len(result.vertices)))
0200 print(" faces: {}".format(len(result.faces)))
0201 print(" watertight: {}".format(result.is_watertight))
0202 print(" volume: {}".format(abs(result.volume)))
0203 print()
0204 print("Wrote:")
0205 print(" {}".format(output_path))
0206
0207
0208 if __name__ == "__main__":
0209 main()