Self-hosted Express + Postgres app for organizing STL/3MF/OBJ/etc. with: - Model/file library, tags, collections, print settings - URL import: Thingiverse (API), Printables (GraphQL), MakerWorld (FlareSolverr + token), cubee3d/other (metadata + manual files) - In-browser 3D viewer; 3MF rendered via Python/trimesh GLB conversion - Auto thumbnails, license/commercial-use tracking, in-app editing - Settings (API tokens / FlareSolverr) stored in DB with .env fallback Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
44 lines
1.3 KiB
Python
44 lines
1.3 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
|
|
|
|
|
|
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]
|
|
scene = trimesh.load(in_path, force="scene")
|
|
geoms = list(scene.geometry.values())
|
|
if not geoms:
|
|
print(json.dumps({"error": "no geometry found in 3MF"}))
|
|
return 1
|
|
|
|
scene.export(out_path, file_type="glb")
|
|
|
|
combined = trimesh.util.concatenate(geoms)
|
|
ext = combined.bounds[1] - combined.bounds[0]
|
|
summary = {
|
|
"glb": out_path,
|
|
"bbox": {"x": round(float(ext[0]), 3), "y": round(float(ext[1]), 3), "z": round(float(ext[2]), 3)},
|
|
"faces": int(sum(len(g.faces) for g in geoms)),
|
|
}
|
|
try:
|
|
# trimesh volume is in model units (mm) -> cm^3 to match the rest of the app
|
|
summary["volume_cm3"] = round(float(combined.volume) / 1000.0, 3)
|
|
except Exception:
|
|
summary["volume_cm3"] = None
|
|
|
|
print(json.dumps(summary))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|