neuralcad / core /executor.py
CallMeDaniel's picture
feat: add 3MF export and /api/models/{name}.3mf endpoint
38cfebe
"""
Safe CadQuery code execution engine.
Executes LLM-generated CadQuery code in a sandboxed namespace,
validates the result, and exports to STEP/STL.
"""
import io
import sys
import traceback
from pathlib import Path
from typing import Any, Optional
import cadquery as cq
from pydantic import BaseModel, Field
class ExecutionResult(BaseModel):
"""Result of executing a CadQuery script."""
model_config = {"arbitrary_types_allowed": True}
success: bool
result: Optional[Any] = Field(default=None, exclude=True) # cq.Workplane
code: str = Field(default="", exclude=True)
error: Optional[str] = None
stdout: str = Field(default="", exclude=True)
volume: float = Field(default=0.0, serialization_alias="volume_mm3")
bounding_box: tuple = Field(default=(), serialization_alias="bounding_box_mm")
face_count: int = 0
edge_count: int = 0
def summary(self) -> str:
if not self.success:
return f"FAILED: {self.error}"
bb = self.bounding_box
return (
f"OK | Volume: {self.volume:.1f} mm³ | "
f"BBox: {bb[0]:.1f}×{bb[1]:.1f}×{bb[2]:.1f} mm | "
f"Faces: {self.face_count} | Edges: {self.edge_count}"
)
# Allowed imports in the sandboxed namespace
SAFE_NAMESPACE = {
"cq": cq,
"cadquery": cq,
"math": __import__("math"),
"__builtins__": {
"range": range,
"len": len,
"abs": abs,
"min": min,
"max": max,
"round": round,
"int": int,
"float": float,
"tuple": tuple,
"list": list,
"True": True,
"False": False,
"None": None,
"print": print,
"enumerate": enumerate,
"zip": zip,
},
}
def sanitize_code(code: str) -> str:
"""Clean up LLM output — strip markdown fences, trailing whitespace,
and redundant import statements (already in namespace)."""
code = code.strip()
# Remove markdown code fences if present
if code.startswith("```python"):
code = code[len("```python"):]
elif code.startswith("```"):
code = code[3:]
if code.endswith("```"):
code = code[:-3]
# Strip import lines for modules already in namespace
lines = code.strip().splitlines()
cleaned = []
for line in lines:
stripped = line.strip()
# Keep the line unless it's a redundant cadquery/math import
if stripped.startswith("import cadquery") or stripped.startswith("from cadquery"):
continue
if stripped == "import math":
continue
cleaned.append(line)
return "\n".join(cleaned).strip()
def execute_cadquery(code: str) -> ExecutionResult:
"""
Execute a CadQuery script string and return the result.
The script must assign its output to a variable called `result`.
"""
code = sanitize_code(code)
# Capture stdout
old_stdout = sys.stdout
sys.stdout = captured = io.StringIO()
namespace = dict(SAFE_NAMESPACE)
try:
exec(code, namespace) # noqa: S102
except Exception:
sys.stdout = old_stdout
return ExecutionResult(
success=False,
code=code,
error=traceback.format_exc(),
stdout=captured.getvalue(),
)
sys.stdout = old_stdout
stdout_text = captured.getvalue()
# Extract the result
result_obj = namespace.get("result")
if result_obj is None:
return ExecutionResult(
success=False,
code=code,
error="Script did not assign a value to `result`.",
stdout=stdout_text,
)
if not isinstance(result_obj, cq.Workplane):
return ExecutionResult(
success=False,
code=code,
error=f"Expected cq.Workplane, got {type(result_obj).__name__}",
stdout=stdout_text,
)
# Extract geometry metadata
try:
shape = result_obj.val()
bb = result_obj.val().BoundingBox()
bbox_dims = (bb.xlen, bb.ylen, bb.zlen)
volume = shape.Volume()
faces = len(result_obj.faces().vals())
edges = len(result_obj.edges().vals())
except Exception as e:
return ExecutionResult(
success=False,
code=code,
error=f"Geometry extraction failed: {e}",
stdout=stdout_text,
)
return ExecutionResult(
success=True,
result=result_obj,
code=code,
stdout=stdout_text,
volume=volume,
bounding_box=bbox_dims,
face_count=faces,
edge_count=edges,
)
def export_step(result: cq.Workplane, path: str | Path) -> Path:
"""Export a CadQuery workplane to STEP format."""
path = Path(path)
cq.exporters.export(result, str(path), exportType="STEP")
return path
def export_stl(result: cq.Workplane, path: str | Path, tolerance: float = 0.01) -> Path:
"""Export a CadQuery workplane to STL format."""
path = Path(path)
cq.exporters.export(result, str(path), exportType="STL", tolerance=tolerance)
return path
def export_3mf(result: cq.Workplane, path: str | Path) -> Path:
"""Export a CadQuery workplane to 3MF format (slicer-ready)."""
path = Path(path)
cq.exporters.export(result, str(path), exportType="3MF")
return path
def export_all(result: cq.Workplane, base_path: str | Path) -> dict[str, Path]:
"""Export to STEP, STL, and 3MF."""
base = Path(base_path)
return {
"step": export_step(result, base.with_suffix(".step")),
"stl": export_stl(result, base.with_suffix(".stl")),
"3mf": export_3mf(result, base.with_suffix(".3mf")),
}