Spaces:
Sleeping
Sleeping
Commit ·
cdccbe3
1
Parent(s): 1923201
refactor: add ToolConfig pydantic model, replace tool_config dicts
Browse filesCo-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- agents/tools.py +2 -2
- core/cam.py +26 -18
- tests/test_cam.py +48 -12
agents/tools.py
CHANGED
|
@@ -97,11 +97,11 @@ class GenerateGcodeTool(BaseTool):
|
|
| 97 |
args_schema: Type[BaseModel] = GenerateGcodeInput
|
| 98 |
|
| 99 |
def _run(self, operations: list[str], tool_diameter: float = 6.0, post_processor: str = "grbl") -> str:
|
| 100 |
-
from core.cam import generate_gcode
|
| 101 |
shape = get_last_shape()
|
| 102 |
if shape is None:
|
| 103 |
return json.dumps({"success": False, "error": "No shape available. Run Execute CadQuery Code first."})
|
| 104 |
-
tool_config =
|
| 105 |
result = generate_gcode(
|
| 106 |
shape=shape, operations=operations,
|
| 107 |
tool_config=tool_config, post_processor=post_processor,
|
|
|
|
| 97 |
args_schema: Type[BaseModel] = GenerateGcodeInput
|
| 98 |
|
| 99 |
def _run(self, operations: list[str], tool_diameter: float = 6.0, post_processor: str = "grbl") -> str:
|
| 100 |
+
from core.cam import generate_gcode, ToolConfig
|
| 101 |
shape = get_last_shape()
|
| 102 |
if shape is None:
|
| 103 |
return json.dumps({"success": False, "error": "No shape available. Run Execute CadQuery Code first."})
|
| 104 |
+
tool_config = ToolConfig(diameter=tool_diameter, h_feed=800, v_feed=200, speed=18000)
|
| 105 |
result = generate_gcode(
|
| 106 |
shape=shape, operations=operations,
|
| 107 |
tool_config=tool_config, post_processor=post_processor,
|
core/cam.py
CHANGED
|
@@ -9,12 +9,20 @@ from __future__ import annotations
|
|
| 9 |
from pydantic import BaseModel, Field
|
| 10 |
|
| 11 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
class CAMResult(BaseModel):
|
| 13 |
"""Result of G-code generation from a CadQuery shape."""
|
| 14 |
success: bool
|
| 15 |
gcode: str | None = None
|
| 16 |
operations: list[str] = Field(default_factory=list)
|
| 17 |
-
tool_config:
|
| 18 |
post_processor: str = "grbl"
|
| 19 |
error: str | None = None
|
| 20 |
|
|
@@ -29,25 +37,25 @@ class CAMPlan(BaseModel):
|
|
| 29 |
tool_speed: float = Field(default=18000, description="Spindle speed RPM")
|
| 30 |
post_processor: str = Field(default="grbl", description="G-code format")
|
| 31 |
|
| 32 |
-
def to_tool_config(self) ->
|
| 33 |
-
"""Convert to
|
| 34 |
-
return
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
|
| 41 |
|
| 42 |
from config.settings import settings
|
| 43 |
|
| 44 |
|
| 45 |
-
def _get_default_tool_config() ->
|
| 46 |
"""Load default roughing tool config from config.yaml cam section."""
|
| 47 |
roughing = settings.cam.tools.get("roughing")
|
| 48 |
if roughing:
|
| 49 |
-
return roughing.model_dump()
|
| 50 |
-
return
|
| 51 |
|
| 52 |
|
| 53 |
def _get_default_post_processor() -> str:
|
|
@@ -63,7 +71,7 @@ def _get_stock_offset() -> float:
|
|
| 63 |
def generate_gcode(
|
| 64 |
shape,
|
| 65 |
operations: list[str],
|
| 66 |
-
tool_config:
|
| 67 |
post_processor: str | None = None,
|
| 68 |
stock_offset_mm: float | None = None,
|
| 69 |
) -> CAMResult:
|
|
@@ -73,7 +81,7 @@ def generate_gcode(
|
|
| 73 |
shape: A cq.Workplane object from the executor.
|
| 74 |
operations: List of operation names: "adaptive", "pocket", "profile",
|
| 75 |
"face", "drill", "surface", "waterline".
|
| 76 |
-
tool_config:
|
| 77 |
post_processor: Post-processor name (e.g. "grbl", "linuxcnc"). Uses config default if None.
|
| 78 |
stock_offset_mm: Stock offset from bounding box in mm. Uses config default if None.
|
| 79 |
|
|
@@ -105,10 +113,10 @@ def generate_gcode(
|
|
| 105 |
|
| 106 |
try:
|
| 107 |
tool = Endmill(
|
| 108 |
-
diameter=tool_config.
|
| 109 |
-
h_feed=tool_config.
|
| 110 |
-
v_feed=tool_config.
|
| 111 |
-
speed=tool_config.
|
| 112 |
)
|
| 113 |
|
| 114 |
so = stock_offset_mm
|
|
|
|
| 9 |
from pydantic import BaseModel, Field
|
| 10 |
|
| 11 |
|
| 12 |
+
class ToolConfig(BaseModel):
|
| 13 |
+
"""CNC tool configuration for G-code generation."""
|
| 14 |
+
diameter: float = 6.0
|
| 15 |
+
h_feed: float = 800
|
| 16 |
+
v_feed: float = 200
|
| 17 |
+
speed: float = 18000
|
| 18 |
+
|
| 19 |
+
|
| 20 |
class CAMResult(BaseModel):
|
| 21 |
"""Result of G-code generation from a CadQuery shape."""
|
| 22 |
success: bool
|
| 23 |
gcode: str | None = None
|
| 24 |
operations: list[str] = Field(default_factory=list)
|
| 25 |
+
tool_config: ToolConfig = Field(default_factory=ToolConfig)
|
| 26 |
post_processor: str = "grbl"
|
| 27 |
error: str | None = None
|
| 28 |
|
|
|
|
| 37 |
tool_speed: float = Field(default=18000, description="Spindle speed RPM")
|
| 38 |
post_processor: str = Field(default="grbl", description="G-code format")
|
| 39 |
|
| 40 |
+
def to_tool_config(self) -> ToolConfig:
|
| 41 |
+
"""Convert to ToolConfig for generate_gcode()."""
|
| 42 |
+
return ToolConfig(
|
| 43 |
+
diameter=self.tool_diameter,
|
| 44 |
+
h_feed=self.tool_h_feed,
|
| 45 |
+
v_feed=self.tool_v_feed,
|
| 46 |
+
speed=self.tool_speed,
|
| 47 |
+
)
|
| 48 |
|
| 49 |
|
| 50 |
from config.settings import settings
|
| 51 |
|
| 52 |
|
| 53 |
+
def _get_default_tool_config() -> ToolConfig:
|
| 54 |
"""Load default roughing tool config from config.yaml cam section."""
|
| 55 |
roughing = settings.cam.tools.get("roughing")
|
| 56 |
if roughing:
|
| 57 |
+
return ToolConfig(**roughing.model_dump())
|
| 58 |
+
return ToolConfig()
|
| 59 |
|
| 60 |
|
| 61 |
def _get_default_post_processor() -> str:
|
|
|
|
| 71 |
def generate_gcode(
|
| 72 |
shape,
|
| 73 |
operations: list[str],
|
| 74 |
+
tool_config: ToolConfig | None = None,
|
| 75 |
post_processor: str | None = None,
|
| 76 |
stock_offset_mm: float | None = None,
|
| 77 |
) -> CAMResult:
|
|
|
|
| 81 |
shape: A cq.Workplane object from the executor.
|
| 82 |
operations: List of operation names: "adaptive", "pocket", "profile",
|
| 83 |
"face", "drill", "surface", "waterline".
|
| 84 |
+
tool_config: ToolConfig with diameter, h_feed, v_feed, speed. Uses config defaults if None.
|
| 85 |
post_processor: Post-processor name (e.g. "grbl", "linuxcnc"). Uses config default if None.
|
| 86 |
stock_offset_mm: Stock offset from bounding box in mm. Uses config default if None.
|
| 87 |
|
|
|
|
| 113 |
|
| 114 |
try:
|
| 115 |
tool = Endmill(
|
| 116 |
+
diameter=tool_config.diameter,
|
| 117 |
+
h_feed=tool_config.h_feed,
|
| 118 |
+
v_feed=tool_config.v_feed,
|
| 119 |
+
speed=tool_config.speed,
|
| 120 |
)
|
| 121 |
|
| 122 |
so = stock_offset_mm
|
tests/test_cam.py
CHANGED
|
@@ -1,6 +1,27 @@
|
|
| 1 |
"""Tests for core/cam.py — CAM engine."""
|
| 2 |
|
| 3 |
-
from core.cam import CAMResult
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
|
| 6 |
class TestCAMResult:
|
|
@@ -9,7 +30,7 @@ class TestCAMResult:
|
|
| 9 |
success=True,
|
| 10 |
gcode="G21 G90\nG00 X0 Y0 Z10\nM30",
|
| 11 |
operations=["pocket", "profile"],
|
| 12 |
-
tool_config=
|
| 13 |
post_processor="grbl",
|
| 14 |
)
|
| 15 |
assert r.success is True
|
|
@@ -26,16 +47,20 @@ class TestCAMResult:
|
|
| 26 |
def test_model_dump(self):
|
| 27 |
r = CAMResult(
|
| 28 |
success=True, gcode="G21 G90\nM30",
|
| 29 |
-
operations=["pocket"], tool_config=
|
| 30 |
)
|
| 31 |
d = r.model_dump()
|
| 32 |
assert d["success"] is True
|
| 33 |
assert d["gcode"] == "G21 G90\nM30"
|
| 34 |
assert d["operations"] == ["pocket"]
|
| 35 |
-
assert d["tool_config"] == {"diameter": 6}
|
| 36 |
assert d["post_processor"] == "grbl"
|
| 37 |
assert d["error"] is None
|
| 38 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
|
| 40 |
from unittest.mock import patch, MagicMock
|
| 41 |
from core.cam import generate_gcode
|
|
@@ -48,7 +73,7 @@ class TestGenerateGcode:
|
|
| 48 |
result = generate_gcode(
|
| 49 |
shape=mock_shape,
|
| 50 |
operations=["pocket"],
|
| 51 |
-
tool_config=
|
| 52 |
post_processor="grbl",
|
| 53 |
)
|
| 54 |
assert result.success is False
|
|
@@ -71,13 +96,25 @@ class TestGenerateGcode:
|
|
| 71 |
tool_config=None,
|
| 72 |
)
|
| 73 |
assert result.tool_config is not None
|
| 74 |
-
assert
|
|
|
|
| 75 |
|
| 76 |
def test_uses_default_post_processor(self):
|
| 77 |
mock_shape = MagicMock()
|
| 78 |
result = generate_gcode(shape=mock_shape, operations=["pocket"])
|
| 79 |
assert result.post_processor == "grbl"
|
| 80 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
|
| 82 |
from core.cam import CAMPlan
|
| 83 |
|
|
@@ -107,9 +144,8 @@ class TestCAMPlan:
|
|
| 107 |
def test_to_tool_config(self):
|
| 108 |
plan = CAMPlan(operations=["pocket"])
|
| 109 |
config = plan.to_tool_config()
|
| 110 |
-
assert config
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
}
|
|
|
|
| 1 |
"""Tests for core/cam.py — CAM engine."""
|
| 2 |
|
| 3 |
+
from core.cam import CAMResult, ToolConfig
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class TestToolConfig:
|
| 7 |
+
def test_defaults(self):
|
| 8 |
+
tc = ToolConfig()
|
| 9 |
+
assert tc.diameter == 6.0
|
| 10 |
+
assert tc.h_feed == 800
|
| 11 |
+
assert tc.v_feed == 200
|
| 12 |
+
assert tc.speed == 18000
|
| 13 |
+
|
| 14 |
+
def test_custom_values(self):
|
| 15 |
+
tc = ToolConfig(diameter=3.0, h_feed=400, v_feed=100, speed=24000)
|
| 16 |
+
assert tc.diameter == 3.0
|
| 17 |
+
assert tc.h_feed == 400
|
| 18 |
+
assert tc.v_feed == 100
|
| 19 |
+
assert tc.speed == 24000
|
| 20 |
+
|
| 21 |
+
def test_model_dump(self):
|
| 22 |
+
tc = ToolConfig(diameter=10.0, h_feed=1000, v_feed=300, speed=12000)
|
| 23 |
+
d = tc.model_dump()
|
| 24 |
+
assert d == {"diameter": 10.0, "h_feed": 1000, "v_feed": 300, "speed": 12000}
|
| 25 |
|
| 26 |
|
| 27 |
class TestCAMResult:
|
|
|
|
| 30 |
success=True,
|
| 31 |
gcode="G21 G90\nG00 X0 Y0 Z10\nM30",
|
| 32 |
operations=["pocket", "profile"],
|
| 33 |
+
tool_config=ToolConfig(diameter=6, h_feed=800),
|
| 34 |
post_processor="grbl",
|
| 35 |
)
|
| 36 |
assert r.success is True
|
|
|
|
| 47 |
def test_model_dump(self):
|
| 48 |
r = CAMResult(
|
| 49 |
success=True, gcode="G21 G90\nM30",
|
| 50 |
+
operations=["pocket"], tool_config=ToolConfig(diameter=6),
|
| 51 |
)
|
| 52 |
d = r.model_dump()
|
| 53 |
assert d["success"] is True
|
| 54 |
assert d["gcode"] == "G21 G90\nM30"
|
| 55 |
assert d["operations"] == ["pocket"]
|
| 56 |
+
assert d["tool_config"] == {"diameter": 6.0, "h_feed": 800, "v_feed": 200, "speed": 18000}
|
| 57 |
assert d["post_processor"] == "grbl"
|
| 58 |
assert d["error"] is None
|
| 59 |
|
| 60 |
+
def test_default_tool_config_is_toolconfig(self):
|
| 61 |
+
r = CAMResult(success=True)
|
| 62 |
+
assert isinstance(r.tool_config, ToolConfig)
|
| 63 |
+
|
| 64 |
|
| 65 |
from unittest.mock import patch, MagicMock
|
| 66 |
from core.cam import generate_gcode
|
|
|
|
| 73 |
result = generate_gcode(
|
| 74 |
shape=mock_shape,
|
| 75 |
operations=["pocket"],
|
| 76 |
+
tool_config=ToolConfig(diameter=6, h_feed=800, v_feed=200, speed=18000),
|
| 77 |
post_processor="grbl",
|
| 78 |
)
|
| 79 |
assert result.success is False
|
|
|
|
| 96 |
tool_config=None,
|
| 97 |
)
|
| 98 |
assert result.tool_config is not None
|
| 99 |
+
assert isinstance(result.tool_config, ToolConfig)
|
| 100 |
+
assert result.tool_config.diameter == 6.0
|
| 101 |
|
| 102 |
def test_uses_default_post_processor(self):
|
| 103 |
mock_shape = MagicMock()
|
| 104 |
result = generate_gcode(shape=mock_shape, operations=["pocket"])
|
| 105 |
assert result.post_processor == "grbl"
|
| 106 |
|
| 107 |
+
def test_tool_config_attribute_access(self):
|
| 108 |
+
mock_shape = MagicMock()
|
| 109 |
+
tc = ToolConfig(diameter=3.0, h_feed=400, v_feed=100, speed=24000)
|
| 110 |
+
result = generate_gcode(
|
| 111 |
+
shape=mock_shape,
|
| 112 |
+
operations=["pocket"],
|
| 113 |
+
tool_config=tc,
|
| 114 |
+
)
|
| 115 |
+
assert result.tool_config.diameter == 3.0
|
| 116 |
+
assert result.tool_config.h_feed == 400
|
| 117 |
+
|
| 118 |
|
| 119 |
from core.cam import CAMPlan
|
| 120 |
|
|
|
|
| 144 |
def test_to_tool_config(self):
|
| 145 |
plan = CAMPlan(operations=["pocket"])
|
| 146 |
config = plan.to_tool_config()
|
| 147 |
+
assert isinstance(config, ToolConfig)
|
| 148 |
+
assert config.diameter == 6.0
|
| 149 |
+
assert config.h_feed == 800
|
| 150 |
+
assert config.v_feed == 200
|
| 151 |
+
assert config.speed == 18000
|
|
|