File indexing completed on 2026-11-09 10:47:17
0001
0002 """
0003 intersect_stl.py
0004
0005 Compute the portion of the first STL mesh that lies inside
0006 the second STL mesh, and write it to a new STL file.
0007
0008 Usage:
0009 python intersect_stl.py first.stl second.stl intersection.stl
0010
0011 Optional:
0012 python intersect_stl.py first.stl second.stl intersection.stl --engine manifold
0013 """
0014
0015 import argparse
0016 import sys
0017 from pathlib import Path
0018
0019 import trimesh
0020
0021
0022 def load_mesh(path):
0023 mesh = trimesh.load_mesh(path, force="mesh", process=True)
0024
0025 if not isinstance(mesh, trimesh.Trimesh):
0026 raise ValueError("{} did not load as a single mesh.".format(path))
0027
0028 return mesh
0029
0030
0031 def combine_result(result):
0032 """
0033 Boolean operations can return a Trimesh, Scene, or collection
0034 depending on the trimesh version/backend. Convert everything
0035 to one Trimesh.
0036 """
0037 if result is None:
0038 return None
0039
0040 if isinstance(result, trimesh.Trimesh):
0041 return result
0042
0043 if isinstance(result, trimesh.Scene):
0044 meshes = [
0045 geom for geom in result.geometry.values()
0046 if isinstance(geom, trimesh.Trimesh)
0047 ]
0048 if not meshes:
0049 return None
0050 return trimesh.util.concatenate(meshes)
0051
0052 if isinstance(result, (list, tuple)):
0053 meshes = [
0054 geom for geom in result
0055 if isinstance(geom, trimesh.Trimesh)
0056 ]
0057 if not meshes:
0058 return None
0059 return trimesh.util.concatenate(meshes)
0060
0061 raise TypeError(
0062 "Unsupported boolean result type: {}".format(type(result))
0063 )
0064
0065
0066 def meshes_intersection(mesh_a, mesh_b, engine=None):
0067 """
0068 Calculate the geometric intersection of mesh_a and mesh_b.
0069 """
0070 result = trimesh.boolean.intersection(
0071 [mesh_a, mesh_b],
0072 engine=engine
0073 )
0074
0075 return combine_result(result)
0076
0077
0078 def main():
0079 parser = argparse.ArgumentParser(
0080 description=(
0081 "Write the portion of the first STL mesh that "
0082 "intersects the second STL mesh."
0083 )
0084 )
0085
0086 parser.add_argument(
0087 "input_a",
0088 help="First STL mesh"
0089 )
0090
0091 parser.add_argument(
0092 "input_b",
0093 help="Second STL mesh"
0094 )
0095
0096 parser.add_argument(
0097 "output",
0098 help="Output STL containing the intersection"
0099 )
0100
0101 parser.add_argument(
0102 "--engine",
0103 default=None,
0104 help="Boolean engine: manifold, blender, or scad. Default: auto."
0105 )
0106
0107 args = parser.parse_args()
0108
0109 mesh_a = load_mesh(args.input_a)
0110 mesh_b = load_mesh(args.input_b)
0111
0112 if not mesh_a.is_watertight:
0113 print(
0114 "Warning: first mesh is not watertight.",
0115 file=sys.stderr
0116 )
0117
0118 if not mesh_b.is_watertight:
0119 print(
0120 "Warning: second mesh is not watertight.",
0121 file=sys.stderr
0122 )
0123
0124 try:
0125 result = meshes_intersection(
0126 mesh_a,
0127 mesh_b,
0128 engine=args.engine
0129 )
0130 except Exception as exc:
0131 print(
0132 "Boolean intersection failed: {}".format(exc),
0133 file=sys.stderr
0134 )
0135 print(
0136 "Make sure both meshes are closed/watertight and that "
0137 "a boolean backend such as manifold3d is installed.",
0138 file=sys.stderr
0139 )
0140 sys.exit(2)
0141
0142
0143 if result is None or len(result.faces) == 0:
0144 print("The meshes do not have a volumetric intersection.")
0145 print("No output mesh was written.")
0146 sys.exit(1)
0147
0148
0149 result.remove_duplicate_faces()
0150 result.remove_degenerate_faces()
0151 result.remove_unreferenced_vertices()
0152 result.merge_vertices()
0153
0154 output_path = Path(args.output)
0155 result.export(str(output_path))
0156
0157 print("Intersection mesh written to:")
0158 print(" {}".format(output_path))
0159 print("Vertices: {}".format(len(result.vertices)))
0160 print("Faces: {}".format(len(result.faces)))
0161 print("Volume: {}".format(result.volume))
0162
0163
0164 if __name__ == "__main__":
0165 main()