#!/usr/bin/env python3 """Convert a 3MF (incl. Bambu/MakerWorld production-extension files) to GLB. Usage: convert_3mf.py [--plate N] Prints a JSON summary {glb, plate, volume_cm3, bbox, faces} to stdout. Negative/modifier/support parts are left out, parts are coloured by their filament when the project says so, and --plate keeps just that plate. """ import argparse import json import trimesh from trimesh.visual import TextureVisuals from trimesh.visual.material import PBRMaterial import threemf_stream def mesh_in(vertices, faces, colour): mesh = trimesh.Trimesh(vertices=vertices, faces=faces, process=False) if colour: rgb = [int(colour[i:i + 2], 16) for i in (1, 3, 5)] mesh.visual = TextureVisuals(material=PBRMaterial( baseColorFactor=rgb + [255], metallicFactor=0.1, roughnessFactor=0.7)) return mesh def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("input") parser.add_argument("output") parser.add_argument("--plate", type=int) args = parser.parse_args() try: meshes, placements = threemf_stream.load(args.input) project = threemf_stream.read_project(args.input) placements = threemf_stream.printable(project, placements) if args.plate is not None: placements = threemf_stream.on_plate(project, placements, args.plate) 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, colour) is stored once; repeated placements become instances of it. scene = trimesh.Scene() added = set() for i, p in enumerate(placements): colour = threemf_stream.filament_colour(project, p) geom = f"{p.key}|{colour or ''}" node = f"part{i}" if geom in added: scene.graph.update(frame_from=scene.graph.base_frame, frame_to=node, matrix=p.transform, geometry=geom) else: v, f = meshes[p.key] scene.add_geometry(mesh_in(v, f, colour), node_name=node, geom_name=geom, transform=p.transform) added.add(geom) scene.export(args.output, file_type="glb") stats = threemf_stream.measure(meshes, placements) ext = stats["extents"] print(json.dumps({ "glb": args.output, "plate": args.plate, "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__": raise SystemExit(main())