| 479 | |
| 480 | @router.get("/export") |
| 481 | def export_mesh(path: str, format: str): |
| 482 | if format not in ("obj", "stl", "ply"): |
| 483 | raise HTTPException(400, "Supported formats: obj, stl, ply") |
| 484 | |
| 485 | input_path = (WORKSPACE_DIR / path).resolve() |
| 486 | if not str(input_path).startswith(str(WORKSPACE_DIR.resolve())): |
| 487 | raise HTTPException(400, "Invalid path") |
| 488 | if not input_path.exists(): |
| 489 | raise HTTPException(404, f"File not found: {path}") |
| 490 | |
| 491 | loaded = trimesh.load(str(input_path)) |
| 492 | if isinstance(loaded, trimesh.Scene): |
| 493 | geoms = list(loaded.geometry.values()) |
| 494 | mesh = trimesh.util.concatenate(geoms) if len(geoms) > 1 else geoms[0] |
| 495 | else: |
| 496 | mesh = loaded |
| 497 | |
| 498 | data = mesh.export(file_type=format) |
| 499 | stem = input_path.stem |
| 500 | mime = "text/plain" if format == "obj" else "application/octet-stream" |
| 501 | # trimesh exports ply as bytes even in text mode — octet-stream is fine for all binary formats |
| 502 | return Response( |
| 503 | content=data, |
| 504 | media_type=mime, |
| 505 | headers={"Content-Disposition": f'attachment; filename="{stem}.{format}"'}, |
| 506 | ) |