| from __future__ import annotations |
|
|
| import argparse |
| import json |
| import math |
| import re |
| import sys |
| from pathlib import Path |
| from typing import Any |
|
|
| import cadquery as cq |
| from cadquery import exporters |
|
|
|
|
| BLOCKED_TOKENS = [ |
| "__", |
| "open(", |
| "exec(", |
| "eval(", |
| "compile(", |
| "input(", |
| "globals(", |
| "getattr(", |
| "setattr(", |
| "delattr(", |
| "subprocess", |
| "socket", |
| "requests", |
| "urllib", |
| "shutil", |
| "pickle", |
| "pathlib", |
| "os.", |
| "sys.", |
| ] |
|
|
| ALLOWED_IMPORT_LINES = { |
| "import cadquery as cq", |
| "from cadquery import exporters", |
| "import math", |
| } |
|
|
|
|
| def clean_code(code: str) -> str: |
| value = code.strip() |
| value = re.sub(r"^```(?:python|py)?", "", value, flags=re.IGNORECASE).strip() |
| value = re.sub(r"```$", "", value).strip() |
| lines = [] |
| for line in value.splitlines(): |
| stripped = line.strip() |
| if stripped in ALLOWED_IMPORT_LINES: |
| continue |
| if stripped.startswith("import ") or stripped.startswith("from "): |
| raise ValueError(f"Unsupported import line: {stripped}") |
| lines.append(line) |
| return "\n".join(lines).strip() |
|
|
|
|
| def validate_code(code: str) -> None: |
| lowered = code.lower() |
| for token in BLOCKED_TOKENS: |
| if token in lowered: |
| raise ValueError(f"Blocked Python token in CadQuery code: {token}") |
|
|
|
|
| def exportable_object(namespace: dict[str, Any], captured: list[Any]) -> Any: |
| if captured: |
| return captured[-1] |
| for name in ["fixture", "result", "model", "solid", "body", "part"]: |
| if name in namespace: |
| return namespace[name] |
| raise ValueError("CadQuery code must assign the final object to fixture/result/model/solid/body/part or call show_object(obj).") |
|
|
|
|
| def normalize_export_object(obj: Any) -> Any: |
| if hasattr(obj, "toCompound"): |
| return obj.toCompound() |
| return obj |
|
|
|
|
| def object_bbox(obj: Any): |
| obj = normalize_export_object(obj) |
| shape = obj.val() if hasattr(obj, "val") else obj |
| return shape.BoundingBox() |
|
|
|
|
| def main() -> None: |
| parser = argparse.ArgumentParser(description="Run constrained CadQuery code and export STL.") |
| parser.add_argument("--out-dir", required=True) |
| parser.add_argument("--name", default="generated_cadquery") |
| args = parser.parse_args() |
|
|
| payload = json.loads(sys.stdin.read() or "{}") |
| raw_code = str(payload.get("code", "")) |
| code = clean_code(raw_code) |
| validate_code(code) |
|
|
| out_dir = Path(args.out_dir) |
| out_dir.mkdir(parents=True, exist_ok=True) |
| safe_name = re.sub(r"[^a-zA-Z0-9_-]+", "_", args.name).strip("_") or "generated_cadquery" |
| stl_path = out_dir / f"{safe_name}.stl" |
|
|
| captured: list[Any] = [] |
|
|
| def show_object(obj: Any, *args: Any, **kwargs: Any) -> None: |
| captured.append(obj) |
|
|
| safe_builtins = { |
| "abs": abs, |
| "bool": bool, |
| "dict": dict, |
| "enumerate": enumerate, |
| "float": float, |
| "int": int, |
| "len": len, |
| "list": list, |
| "max": max, |
| "min": min, |
| "pow": pow, |
| "range": range, |
| "round": round, |
| "set": set, |
| "str": str, |
| "sum": sum, |
| "tuple": tuple, |
| "zip": zip, |
| } |
| namespace: dict[str, Any] = { |
| "__builtins__": safe_builtins, |
| "cq": cq, |
| "Assembly": cq.Assembly, |
| "Color": cq.Color, |
| "exporters": exporters, |
| "math": math, |
| "show_object": show_object, |
| } |
| safe_builtins["locals"] = lambda: namespace |
| exec(code, namespace, namespace) |
| obj = exportable_object(namespace, captured) |
| export_obj = normalize_export_object(obj) |
| exporters.export(export_obj, str(stl_path)) |
| bbox = object_bbox(obj) |
| print( |
| json.dumps( |
| { |
| "name": safe_name, |
| "stl_path": str(stl_path), |
| "bounding_box": { |
| "xlen": bbox.xlen, |
| "ylen": bbox.ylen, |
| "zlen": bbox.zlen, |
| "xmin": bbox.xmin, |
| "xmax": bbox.xmax, |
| "ymin": bbox.ymin, |
| "ymax": bbox.ymax, |
| "zmin": bbox.zmin, |
| "zmax": bbox.zmax, |
| }, |
| "cadquery_features": payload.get("features", []), |
| "code": code, |
| } |
| ) |
| ) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|