"""model_3d — generate an STL file from an OpenSCAD script.
Requires OpenSCAD installed on the system:
Arch: sudo pacman -S openscad
Debian: sudo apt install openscad
"""
import asyncio
import shutil
from pathlib import Path
from .base import Tool, ToolResult
class Model3DTool(Tool):
name = "model_3d"
description = (
"Generate a binary STL file from an existing OpenSCAD (.scad) script. "
"The script must already be written to disk; this tool only compiles it.\n\n"
"Workflow:\n"
"1. Write your .scad script with the filesystem tool.\n"
"2. Call model_3d with the scad_path and desired output_path.\n"
"3. The resulting STL is saved to output_path.\n\n"
"Use content_publish on the STL to show the user an interactive 3D viewer."
)
parameters = {
"type": "object",
"properties": {
"scad_path": {
"type": "string",
"description": (
"Absolute or relative path to the existing .scad file. "
"The file must already exist."
),
},
"output_path": {
"type": "string",
"description": (
"Absolute or relative path where the STL should be written. "
"Parent directories are created automatically."
),
},
},
"required": ["scad_path", "output_path"],
}
async def execute(self, params: dict) -> ToolResult:
if not shutil.which("openscad"):
return ToolResult(
success=False,
output="OpenSCAD is not installed on this system.",
error="openscad_not_found",
)
scad_path = Path(params["scad_path"]).expanduser().resolve()
output_path = Path(params["output_path"]).expanduser().resolve()
if not scad_path.exists():
return ToolResult(
success=False,
output=f"SCAD file not found: {scad_path}",
error="scad_not_found",
)
if not scad_path.is_file():
return ToolResult(
success=False,
output=f"Path is not a file: {scad_path}",
error="not_a_file",
)
output_path.parent.mkdir(parents=True, exist_ok=True)
proc = await asyncio.create_subprocess_exec(
"openscad",
"--export-format", "binstl",
"-o", str(output_path),
str(scad_path),
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
stdout, stderr = await proc.communicate()
if proc.returncode != 0:
err = (stderr.decode(errors="replace") or "OpenSCAD exited with an error.").strip()
return ToolResult(
success=False,
output=f"OpenSCAD failed to compile STL:\n{err}",
error="openscad_compile_error",
)
if not output_path.exists():
return ToolResult(
success=False,
output="OpenSCAD completed but no STL file was produced.",
error="no_output",
)
size_kb = output_path.stat().st_size / 1024
return ToolResult(
success=True,
output=(
f"Generated: {output_path.name}\n"
f"Path: {output_path}\n"
f"Size: {size_kb:.1f} KB"
),
metadata={"output_path": str(output_path), "size_kb": round(size_kb, 1)},
)