Initial commit: step2urdf tool with handtuned JSON import.
Includes FastAPI backend, vendored step2urdf frontend, and A7 handtuned arm JSON for URDF generation.
This commit is contained in:
+155
@@ -0,0 +1,155 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import FastAPI, File, HTTPException, UploadFile
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import FileResponse, RedirectResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from app.config import settings
|
||||
from app.models import ExportRequest, ProposeRequest
|
||||
from app.services.mesh_export import geometry_backend_status
|
||||
from app.services.pipeline import create_job_from_upload, export_package, load_parse, propose_draft
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
LEGACY_STATIC = ROOT / "app" / "static"
|
||||
FRONTEND_DIST = ROOT / "frontend" / "dist"
|
||||
|
||||
app = FastAPI(title="step2urdf-tool", version="0.2.0")
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=[
|
||||
"http://127.0.0.1:5678",
|
||||
"http://localhost:5678",
|
||||
"http://127.0.0.1:8787",
|
||||
"http://localhost:8787",
|
||||
],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
|
||||
@app.get("/api/health")
|
||||
async def health():
|
||||
return {
|
||||
"ok": True,
|
||||
"zhipu_configured": bool(settings.zhipu_api_key),
|
||||
"zhipu_model": settings.zhipu_model,
|
||||
"geometry": geometry_backend_status(),
|
||||
"primary_ui": "step2urdf frontend (Vite :5678 in dev)",
|
||||
}
|
||||
|
||||
|
||||
@app.post("/api/parse")
|
||||
async def parse_step(file: UploadFile = File(...)):
|
||||
"""Optional server-side STEP text parse (legacy / debug). Geometry stays in the browser."""
|
||||
suffix = Path(file.filename or "model.STEP").suffix or ".STEP"
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp:
|
||||
tmp_path = Path(tmp.name)
|
||||
shutil.copyfileobj(file.file, tmp)
|
||||
try:
|
||||
result = create_job_from_upload(tmp_path, file.filename or "model.STEP")
|
||||
finally:
|
||||
tmp_path.unlink(missing_ok=True)
|
||||
return result
|
||||
|
||||
|
||||
@app.get("/api/jobs/{job_id}")
|
||||
async def get_job(job_id: str):
|
||||
try:
|
||||
return load_parse(job_id)
|
||||
except FileNotFoundError as e:
|
||||
raise HTTPException(404, str(e)) from e
|
||||
|
||||
|
||||
@app.post("/api/propose")
|
||||
async def propose(req: ProposeRequest):
|
||||
"""Zhipu-assisted link/joint proposal.
|
||||
|
||||
Preferred body (from step2urdf UI)::
|
||||
|
||||
{"part_names": [...], "profile": "a7", "robot_name": "A7", "extra_hint": "..."}
|
||||
|
||||
Legacy: ``job_id`` from ``/api/parse``.
|
||||
"""
|
||||
names = list(req.part_names or [])
|
||||
if req.solid_names:
|
||||
# Deduplicate while preserving order
|
||||
seen: set[str] = set()
|
||||
merged: list[str] = []
|
||||
for n in names + list(req.solid_names):
|
||||
if n and n not in seen:
|
||||
seen.add(n)
|
||||
merged.append(n)
|
||||
names = merged
|
||||
|
||||
try:
|
||||
draft = await propose_draft(
|
||||
req.job_id,
|
||||
profile=req.profile,
|
||||
robot_name=req.robot_name,
|
||||
extra_hint=req.extra_hint,
|
||||
part_names=names or None,
|
||||
solids_geom=list(req.solids or []),
|
||||
)
|
||||
except FileNotFoundError as e:
|
||||
raise HTTPException(404, str(e)) from e
|
||||
except ValueError as e:
|
||||
raise HTTPException(400, str(e)) from e
|
||||
except Exception as e: # noqa: BLE001
|
||||
raise HTTPException(502, f"Zhipu/propose failed: {e}") from e
|
||||
return draft
|
||||
|
||||
|
||||
@app.post("/api/export")
|
||||
async def export(req: ExportRequest):
|
||||
"""Legacy server-side URDF write. Prefer Export URDF in the step2urdf UI."""
|
||||
try:
|
||||
meta = export_package(req.job_id, req.draft, include_meshes=req.include_meshes)
|
||||
except FileNotFoundError as e:
|
||||
raise HTTPException(404, str(e)) from e
|
||||
return meta
|
||||
|
||||
|
||||
@app.get("/api/jobs/{job_id}/download/{robot_name}")
|
||||
async def download_urdf(job_id: str, robot_name: str):
|
||||
path = settings.jobs_dir / job_id / "export" / robot_name / f"{robot_name}.urdf"
|
||||
if not path.exists():
|
||||
raise HTTPException(404, "URDF not found; export first")
|
||||
return FileResponse(path, filename=f"{robot_name}.urdf", media_type="application/xml")
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
if FRONTEND_DIST.is_dir() and (FRONTEND_DIST / "index.html").exists():
|
||||
return FileResponse(FRONTEND_DIST / "index.html")
|
||||
return RedirectResponse(url="/legacy/")
|
||||
|
||||
|
||||
# Built step2urdf UI (after `pnpm build` in frontend/)
|
||||
if FRONTEND_DIST.is_dir():
|
||||
app.mount("/assets", StaticFiles(directory=str(FRONTEND_DIST / "assets")), name="frontend-assets")
|
||||
|
||||
# Previous MVP static UI kept under /legacy only
|
||||
if LEGACY_STATIC.is_dir():
|
||||
app.mount("/legacy", StaticFiles(directory=str(LEGACY_STATIC), html=True), name="legacy")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
import uvicorn
|
||||
|
||||
uvicorn.run(
|
||||
"app.main:app",
|
||||
host=settings.host,
|
||||
port=settings.port,
|
||||
reload=True,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user