Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-11-09 10:47:17

0001 #!/usr/bin/env python3
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     # Load meshes
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     # Boolean union
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     # Basic cleanup
0180 #    result.remove_duplicate_faces()
0181 #    result.remove_degenerate_faces()
0182     result.update_faces(result.unique_faces())
0183     result.remove_unreferenced_vertices()
0184 
0185     # Write output
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()