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