Skip to content

post.processor

toolpath_engine.post.processor.PostConfig dataclass

Configuration for G-code output format.

Source code in utde_v0.1.0/toolpath_engine/post/processor.py
@dataclass
class PostConfig:
    """Configuration for G-code output format."""
    # General
    program_number: int = 1000
    use_line_numbers: bool = False
    line_number_start: int = 10
    line_number_increment: int = 10

    # Formatting
    decimal_places_linear: int = 3
    decimal_places_rotary: int = 3
    decimal_places_feed: int = 0
    axis_names: Dict[str, str] = field(default_factory=lambda: {
        "X": "X", "Y": "Y", "Z": "Z", "A": "A", "B": "B", "C": "C"
    })

    # Motion
    rapid_code: str = "G0"
    linear_code: str = "G1"
    use_incremental: bool = False
    output_ijk: bool = False  # Output IJK tool vector instead of rotary axes

    # Units and coordinates
    units: str = "metric"  # "metric" or "imperial"
    units_code: str = "G21"  # G21=mm, G20=inch

    # TCP (Tool Center Point) control
    tcp_on_code: str = "G43.4 H1"
    tcp_off_code: str = "G49"
    use_tcp: bool = True

    # Safe start block
    safe_start: List[str] = field(default_factory=lambda: [
        "G90",      # Absolute mode
        "G21",      # Metric
        "G17",      # XY plane
        "G40",      # Cancel cutter compensation
    ])

    # Program end
    program_end: List[str] = field(default_factory=lambda: [
        "M5",       # Spindle off
        "G49",      # Cancel TCP
        "G28 G91 Z0",  # Return to Z home
        "M30",      # Program end
    ])

    # Process parameter mapping (process param key -> G-code letter)
    param_codes: Dict[str, str] = field(default_factory=lambda: {
        "spindle_speed": "S",
        "spindle_on_cw": "M3",
        "spindle_on_ccw": "M4",
        "coolant_on": "M8",
        "coolant_off": "M9",
        "laser_power": "S",
        "extrusion_rate": "E",
    })

toolpath_engine.post.processor.PostProcessor

Generates G-code from toolpath data using machine kinematics.

Usage

machine = Machine.gantry_5axis_ac() post = PostProcessor(machine) gcode = post.process(toolpath_collection)

Source code in utde_v0.1.0/toolpath_engine/post/processor.py
class PostProcessor:
    """
    Generates G-code from toolpath data using machine kinematics.

    Usage:
        machine = Machine.gantry_5axis_ac()
        post = PostProcessor(machine)
        gcode = post.process(toolpath_collection)
    """

    def __init__(self, machine: Machine, config: Optional[PostConfig] = None):
        self.machine = machine
        self.config = config or PostConfig()
        self._prev_values: Dict[str, float] = {}
        self._line_number = self.config.line_number_start

    def process(self, collection: ToolpathCollection, resolve_ik: bool = True) -> str:
        """
        Generate G-code string from a ToolpathCollection.

        If resolve_ik is True, runs inverse kinematics at each point.
        If False, expects joint values in point.process_params.
        """
        out = StringIO()

        # Header
        self._write_header(out, collection)

        # Process each toolpath
        for tp in collection.toolpaths:
            out.write(f"\n( Toolpath: {tp.name} )\n")
            self._prev_values = {}

            for i, point in enumerate(tp.points):
                line = self._process_point(point, resolve_ik)
                if line:
                    if self.config.use_line_numbers:
                        out.write(f"N{self._line_number} {line}\n")
                        self._line_number += self.config.line_number_increment
                    else:
                        out.write(f"{line}\n")

        # Footer
        self._write_footer(out)

        return out.getvalue()

    def _write_header(self, out: StringIO, collection: ToolpathCollection):
        out.write(f"( Generated by Universal Toolpath Design Environment )\n")
        out.write(f"( Machine: {self.machine.name} )\n")
        out.write(f"( Toolpath: {collection.name} )\n")
        out.write(f"( Points: {collection.total_points()}, Length: {collection.total_length():.1f}mm )\n")
        out.write(f"%\n")
        out.write(f"O{self.config.program_number}\n")

        # Safe start block
        for code in self.config.safe_start:
            out.write(f"{code}\n")

        # TCP mode
        if self.config.use_tcp and self._has_rotary_axes():
            out.write(f"{self.config.tcp_on_code}\n")

        out.write("\n")

    def _write_footer(self, out: StringIO):
        out.write("\n")
        for code in self.config.program_end:
            out.write(f"{code}\n")
        out.write("%\n")

    def _process_point(self, point: ToolpathPoint, resolve_ik: bool) -> str:
        """Generate a single G-code line for a toolpath point."""

        if resolve_ik and self._has_rotary_axes():
            # Solve IK to get joint values
            joint_values = self.machine.inverse_kinematics(
                point.position, point.orientation
            )
        else:
            # For 3-axis or pre-solved: use position directly
            joint_values = {
                "X": point.position.x,
                "Y": point.position.y,
                "Z": point.position.z,
            }
            # Add any rotary values from process params
            for key in ["A", "B", "C"]:
                if key in point.process_params:
                    joint_values[key] = point.process_params[key]

        # Build G-code line
        motion = self.config.rapid_code if point.rapid else self.config.linear_code
        parts = [motion]

        # Axis values
        dp_lin = self.config.decimal_places_linear
        dp_rot = self.config.decimal_places_rotary

        for joint_name, value in joint_values.items():
            axis_letter = self.config.axis_names.get(joint_name, joint_name)
            is_rotary = joint_name in ("A", "B", "C")
            dp = dp_rot if is_rotary else dp_lin

            # Only output changed values (modal suppression)
            prev = self._prev_values.get(joint_name)
            if prev is not None and abs(value - prev) < 10 ** (-dp - 1):
                continue

            parts.append(f"{axis_letter}{value:.{dp}f}")
            self._prev_values[joint_name] = value

        # Feed rate (not on rapids)
        if not point.rapid and point.feed_rate > 0:
            dp_f = self.config.decimal_places_feed
            prev_f = self._prev_values.get("F")
            if prev_f is None or abs(point.feed_rate - prev_f) > 0.1:
                parts.append(f"F{point.feed_rate:.{dp_f}f}")
                self._prev_values["F"] = point.feed_rate

        # Process-specific parameters
        for param_key, gcode_letter in self.config.param_codes.items():
            if param_key in point.process_params:
                val = point.process_params[param_key]
                if isinstance(val, (int, float)):
                    parts.append(f"{gcode_letter}{val:.0f}")
                elif isinstance(val, str):
                    parts.append(val)

        # If only the motion code, nothing changed — skip
        if len(parts) <= 1:
            return ""

        return " ".join(parts)

    def _has_rotary_axes(self) -> bool:
        """Check if machine has any rotary joints."""
        from ..kinematics.machine import Rotary
        all_joints = self.machine.tool_chain.joints + self.machine.workpiece_chain.joints
        return any(isinstance(j, Rotary) for j in all_joints)

    # --- convenience ---------------------------------------------------------
    def save(self, collection: ToolpathCollection, filepath: str, **kwargs):
        """Generate and save G-code to a file."""
        gcode = self.process(collection, **kwargs)
        with open(filepath, "w") as f:
            f.write(gcode)
        return filepath

process(collection, resolve_ik=True)

Generate G-code string from a ToolpathCollection.

If resolve_ik is True, runs inverse kinematics at each point. If False, expects joint values in point.process_params.

Source code in utde_v0.1.0/toolpath_engine/post/processor.py
def process(self, collection: ToolpathCollection, resolve_ik: bool = True) -> str:
    """
    Generate G-code string from a ToolpathCollection.

    If resolve_ik is True, runs inverse kinematics at each point.
    If False, expects joint values in point.process_params.
    """
    out = StringIO()

    # Header
    self._write_header(out, collection)

    # Process each toolpath
    for tp in collection.toolpaths:
        out.write(f"\n( Toolpath: {tp.name} )\n")
        self._prev_values = {}

        for i, point in enumerate(tp.points):
            line = self._process_point(point, resolve_ik)
            if line:
                if self.config.use_line_numbers:
                    out.write(f"N{self._line_number} {line}\n")
                    self._line_number += self.config.line_number_increment
                else:
                    out.write(f"{line}\n")

    # Footer
    self._write_footer(out)

    return out.getvalue()

save(collection, filepath, **kwargs)

Generate and save G-code to a file.

Source code in utde_v0.1.0/toolpath_engine/post/processor.py
def save(self, collection: ToolpathCollection, filepath: str, **kwargs):
    """Generate and save G-code to a file."""
    gcode = self.process(collection, **kwargs)
    with open(filepath, "w") as f:
        f.write(gcode)
    return filepath