#!/usr/bin/env python3 """Convert a 3MF (incl. Bambu/MakerWorld production-extension files) to GLB. Usage: convert_3mf.py Prints a JSON summary {glb, volume_cm3, bbox, faces} to stdout. """ import sys import json import trimesh import threemf_stream def main(): if len(sys.argv) != 3: print(json.dumps({"error": "usage: convert_3mf.py "})) return 2 in_path, out_path = sys.argv[1], sys.argv[2] try: meshes, placements = threemf_stream.load(in_path) except Exception as exc: print(json.dumps({"error": f"could not read 3MF: {str(exc)[:200]}"})) return 1 if not placements: print(json.dumps({"error": "no geometry found in 3MF"})) return 1 # Each object is stored once; repeated placements become instances of it. scene = trimesh.Scene() added = set() for i, (key, transform) in enumerate(placements): node = f"part{i}" if key in added: scene.graph.update(frame_from=scene.graph.base_frame, frame_to=node, matrix=transform, geometry=key) else: v, f = meshes[key] scene.add_geometry(trimesh.Trimesh(vertices=v, faces=f, process=False), node_name=node, geom_name=key, transform=transform) added.add(key) scene.export(out_path, file_type="glb") stats = threemf_stream.measure(meshes, placements) ext = stats["extents"] print(json.dumps({ "glb": out_path, "bbox": {"x": round(ext[0], 3), "y": round(ext[1], 3), "z": round(ext[2], 3)}, "faces": stats["faces"], # mm^3 -> cm^3 to match the rest of the app "volume_cm3": round(stats["volume"] / 1000.0, 3), })) return 0 if __name__ == "__main__": sys.exit(main())