| 13 | |
| 14 | @router.get("/{fmt}") |
| 15 | def export_mesh(fmt: str, path: str): |
| 16 | if fmt not in SUPPORTED: |
| 17 | raise HTTPException(400, f"Unsupported format: {fmt}. Supported: {', '.join(SUPPORTED)}") |
| 18 | |
| 19 | full_path = (WORKSPACE_DIR / path).resolve() |
| 20 | if not str(full_path).startswith(str(WORKSPACE_DIR.resolve())): |
| 21 | raise HTTPException(400, "Invalid path") |
| 22 | if not full_path.exists(): |
| 23 | raise HTTPException(404, f"File not found: {path}") |
| 24 | |
| 25 | # GLB — serve directly, no conversion needed |
| 26 | if fmt == "glb": |
| 27 | return FileResponse(str(full_path), media_type="model/gltf-binary") |
| 28 | |
| 29 | # Load and flatten scene to a single mesh |
| 30 | loaded = trimesh.load(str(full_path)) |
| 31 | if isinstance(loaded, trimesh.Scene): |
| 32 | geoms = list(loaded.geometry.values()) |
| 33 | mesh = trimesh.util.concatenate(geoms) if len(geoms) > 1 else geoms[0] |
| 34 | else: |
| 35 | mesh = loaded |
| 36 | |
| 37 | buf = io.BytesIO() |
| 38 | if fmt == "stl": |
| 39 | mesh.export(buf, file_type="stl") |
| 40 | media_type = "model/stl" |
| 41 | elif fmt == "ply": |
| 42 | mesh.export(buf, file_type="ply") |
| 43 | media_type = "application/octet-stream" |
| 44 | else: # obj |
| 45 | mesh.export(buf, file_type="obj") |
| 46 | media_type = "text/plain" |
| 47 | |
| 48 | buf.seek(0) |
| 49 | return Response(content=buf.read(), media_type=media_type) |