trimesh's 3MF loader re-reads a whole production-extension part file for every component that points into it. A Bambu Studio project with 884 components across four ~50 MB part files grew past 3 GB within seconds and failed both the preview conversion and the geometry fingerprint. - Add tools/threemf_stream.py: parses each part once with lxml iterparse, resolves each component to the single object it names, and returns each mesh once plus its placements. The same project now converts in ~8 s at ~380 MB peak (fingerprinting ~190 MB). - convert_3mf.py and fingerprint.py use it; the GLB stores each object once and instances repeats. Stats are computed per placement (mirrored placements keep a positive volume), without concatenating meshes. - Run sidecar scripts through one queue (lib/python.js) so a preview and background fingerprinting never load big meshes at the same time; previews go first, and 3MFs are fingerprinted one per run. - Surface the converter's JSON error / timeout instead of "Command failed". - Tests with a Bambu-style component fixture (shared part, mirrored placement, decoy object) and STL/3MF fingerprint parity. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
57 lines
1.8 KiB
Python
57 lines
1.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Convert a 3MF (incl. Bambu/MakerWorld production-extension files) to GLB.
|
|
|
|
Usage: convert_3mf.py <input.3mf> <output.glb>
|
|
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 <in.3mf> <out.glb>"}))
|
|
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())
|