Spaces:
Sleeping
Sleeping
| """CAM engine — generates CNC toolpaths and G-code from CadQuery shapes. | |
| Wraps ocp-freecad-cam to convert cq.Workplane objects into G-code strings. | |
| Falls back gracefully when ocp-freecad-cam or FreeCAD is not installed. | |
| """ | |
| from __future__ import annotations | |
| from pydantic import BaseModel, Field | |
| class ToolConfig(BaseModel): | |
| """CNC tool configuration for G-code generation.""" | |
| diameter: float = 6.0 | |
| h_feed: float = 800 | |
| v_feed: float = 200 | |
| speed: float = 18000 | |
| class CAMResult(BaseModel): | |
| """Result of G-code generation from a CadQuery shape.""" | |
| success: bool | |
| gcode: str | None = None | |
| operations: list[str] = Field(default_factory=list) | |
| tool_config: ToolConfig = Field(default_factory=ToolConfig) | |
| post_processor: str = "grbl" | |
| error: str | None = None | |
| class CAMPlan(BaseModel): | |
| """Structured machining plan output from CAM agent.""" | |
| operations: list[str] = Field(description="Ordered list: adaptive, pocket, profile, face, drill, surface, waterline") | |
| tool_diameter: float = Field(default=6.0, description="Endmill diameter in mm") | |
| tool_h_feed: float = Field(default=800, description="Horizontal feed rate mm/min") | |
| tool_v_feed: float = Field(default=200, description="Vertical feed rate mm/min") | |
| tool_speed: float = Field(default=18000, description="Spindle speed RPM") | |
| post_processor: str = Field(default="grbl", description="G-code format") | |
| def to_tool_config(self) -> ToolConfig: | |
| """Convert to ToolConfig for generate_gcode().""" | |
| return ToolConfig( | |
| diameter=self.tool_diameter, | |
| h_feed=self.tool_h_feed, | |
| v_feed=self.tool_v_feed, | |
| speed=self.tool_speed, | |
| ) | |
| from config.settings import settings | |
| def _get_default_tool_config() -> ToolConfig: | |
| """Load default roughing tool config from config.yaml cam section.""" | |
| roughing = settings.cam.tools.get("roughing") | |
| if roughing: | |
| return ToolConfig(**roughing.model_dump()) | |
| return ToolConfig() | |
| def _get_default_post_processor() -> str: | |
| """Load default post-processor from config.yaml cam section.""" | |
| return settings.cam.default_post_processor | |
| def _get_stock_offset() -> float: | |
| """Load stock offset from config.yaml cam section.""" | |
| return settings.cam.stock_offset_mm | |
| def generate_gcode( | |
| shape, | |
| operations: list[str], | |
| tool_config: ToolConfig | None = None, | |
| post_processor: str | None = None, | |
| stock_offset_mm: float | None = None, | |
| ) -> CAMResult: | |
| """Generate G-code from a CadQuery Workplane shape. | |
| Args: | |
| shape: A cq.Workplane object from the executor. | |
| operations: List of operation names: "adaptive", "pocket", "profile", | |
| "face", "drill", "surface", "waterline". | |
| tool_config: ToolConfig with diameter, h_feed, v_feed, speed. Uses config defaults if None. | |
| post_processor: Post-processor name (e.g. "grbl", "linuxcnc"). Uses config default if None. | |
| stock_offset_mm: Stock offset from bounding box in mm. Uses config default if None. | |
| Returns: | |
| CAMResult with success status and G-code string. | |
| """ | |
| if tool_config is None: | |
| tool_config = _get_default_tool_config() | |
| if post_processor is None: | |
| post_processor = _get_default_post_processor() | |
| if stock_offset_mm is None: | |
| stock_offset_mm = _get_stock_offset() | |
| if not operations: | |
| return CAMResult( | |
| success=False, gcode=None, operations=[], tool_config=tool_config, | |
| post_processor=post_processor, error="No operations specified", | |
| ) | |
| try: | |
| from ocp_freecad_cam import Job, Endmill, Drill, Ballnose | |
| from ocp_freecad_cam.fc_impl import Stock | |
| except ImportError: | |
| return CAMResult( | |
| success=False, gcode=None, operations=operations, | |
| tool_config=tool_config, post_processor=post_processor, | |
| error="ocp-freecad-cam is not installed. Install it with: pip install ocp-freecad-cam (requires FreeCAD >= 1.0.1)", | |
| ) | |
| try: | |
| tool = Endmill( | |
| diameter=tool_config.diameter, | |
| h_feed=tool_config.h_feed, | |
| v_feed=tool_config.v_feed, | |
| speed=tool_config.speed, | |
| ) | |
| so = stock_offset_mm | |
| stock = Stock(xn=so, xp=so, yn=so, yp=so, zn=0, zp=so) | |
| top = shape.faces(">Z").workplane() | |
| job = Job(top, shape, post_processor, stock=stock) | |
| for op in operations: | |
| job = _apply_operation(job, shape, tool, op) | |
| gcode = job.to_gcode() | |
| return CAMResult( | |
| success=True, gcode=gcode, operations=operations, | |
| tool_config=tool_config, post_processor=post_processor, | |
| ) | |
| except Exception as exc: | |
| return CAMResult( | |
| success=False, gcode=None, operations=operations, | |
| tool_config=tool_config, post_processor=post_processor, | |
| error=f"G-code generation failed: {exc}", | |
| ) | |
| def _apply_operation(job, shape, tool, operation: str): | |
| """Apply a single CAM operation to the job. Returns the updated job.""" | |
| op = operation.lower().strip() | |
| if op == "adaptive": | |
| try: | |
| faces = shape.faces(">Z[1]") | |
| return job.adaptive(faces, tool, step_over=40, stock_to_leave="0.2 mm") | |
| except Exception: | |
| return job.adaptive(shape.faces(">Z"), tool, step_over=40) | |
| elif op == "pocket": | |
| try: | |
| faces = shape.faces(">Z[1]") | |
| return job.pocket(faces, tool, pattern="offset") | |
| except Exception: | |
| return job.pocket(shape.faces(">Z"), tool, pattern="zigzag") | |
| elif op == "profile": | |
| return job.profile(shape.faces("<Z"), tool, side="out") | |
| elif op == "face": | |
| return job.face(shape.faces(">Z"), tool) | |
| elif op == "drill": | |
| try: | |
| from ocp_freecad_cam import Drill as DrillTool | |
| drill = DrillTool( | |
| diameter=tool._diameter if hasattr(tool, '_diameter') else 5, | |
| h_feed=100, v_feed=50, speed=8000, | |
| ) | |
| holes = shape.faces("<Z") | |
| return job.drill(holes, drill, peck_depth=2.0) | |
| except Exception: | |
| return job | |
| elif op == "surface": | |
| return job.surface(None, tool, cut_pattern="zigzag") | |
| elif op == "waterline": | |
| return job.waterline(None, tool) | |
| else: | |
| return job | |