Source code for pyrobopath.toolpath.toolpath_core

from __future__ import annotations
from typing import List, Union, Optional
from enum import Enum
from gcodeparser import GcodeParser, GcodeLine
import numpy as np

from pyrobopath.tools.utils import pairwise
from pyrobopath.tools.types import NDArray


[docs] class Contour(object): """ A class representing a contiguous set of 3D waypoints followed by a given `tool`. Parameters ---------- path : list of ndarray, optional A list of 3D points defining the contour. Defaults to an empty list. tool : int or Enum, optional An identifier for the tool used in this contour. Defaults to 0. """ counter: int = 0 def __init__( self, path: Optional[List[NDArray]] = None, tool: Union[int, Enum] = 0 ): if path is None: path = [] self.path: List[NDArray] = path self.tool: Union[int, Enum] = tool Contour.counter += 1 self.id = Contour.counter def __repr__(self): return str(f"c{self.id}")
[docs] def path_length(self) -> float: """ Compute the total length of the contour path. Returns ------- float The sum of distances between consecutive waypoints in the path. Returns 0.0 if the path contains fewer than 2 points. """ length = 0.0 for s, e in pairwise(self.path): length += np.linalg.norm(e - s) return float(length)
[docs] def n_segments(self) -> int: """ Return the number of linear segments in the contour path. Returns ------- int The number of segments, defined as one less than the number of waypoints in the path. Returns 0 if the path is empty or has only one point. """ return max(len(self.path) - 1, 0)
[docs] class Toolpath(object): """ A container for a list of `Contour` objects, representing a complete toolpath. Parameters ---------- contours : list of Contour, optional A list of `Contour` instances. Defaults to an empty list. """ def __init__(self, contours: Optional[List[Contour]] = None): if contours is None: contours = [] self.contours: List[Contour] = contours
[docs] def tools(self) -> List[Union[int, Enum]]: """ Return a unique list of tools in the toolpath Returns ------- list of int or Enum A list of unique tool identifiers used across all contours """ tools = set(c.tool for c in self.contours) return list(tools)
[docs] def scale(self, value: float): """ Uniformly scale all waypoints in each contour by a scalar value. Parameters ---------- value : float The scale factor to apply to each point in all contours. """ for c in self.contours: for p in c.path: p *= value
[docs] @classmethod def combine(cls, toolpaths: List[Toolpath]) -> Toolpath: """ Combine multiple `Toolpath` objects into a single toolpath. Parameters ---------- toolpaths : list of Toolpath A list of `Toolpath` instances to be merged. Returns ------- Toolpath A new `Toolpath` containing all contours from the input toolpaths. """ contours = [p for t in toolpaths for p in t.contours] return cls(contours)
[docs] @classmethod def from_gcode(cls, gcode: List[GcodeLine]) -> Toolpath: """ Construct a Toolpath from a sequence of G-code lines. Parses linear motion (`G1`) and tool change (`T`) commands to generate a set of extruding paths grouped as contours. Parameters ---------- gcode : list of GcodeLine A list of parsed G-code lines from which to generate the toolpath. Returns ------- Toolpath A new `Toolpath` object constructed from the G-code input. Notes ----- - Only extrusion movements (with positive delta E) are considered part of a contour. - Tool changes are captured using `T` commands. """ toolpath = cls() contour = Contour() xyze = np.array([0.0, 0.0, 0.0, 0.0]) tool = 0 prev_ext = False for line in gcode: if line.command[0] == "G": if line.command[1] == 1: F = line.get_param("F", default=None) X = line.get_param("X", default=None) Y = line.get_param("Y", default=None) Z = line.get_param("Z", default=None) if not X and not Y and not Z and F: pass new_xyze = np.array( [ line.get_param("X", default=xyze[0]), line.get_param("Y", default=xyze[1]), line.get_param("Z", default=xyze[2]), line.get_param("E", default=xyze[3]), ] ) delta_xyz = new_xyze[0:3] - xyze[0:3] delta_e = new_xyze[3] - xyze[3] if np.any(np.abs(delta_xyz) > 0): if delta_e > 0: if not prev_ext: contour.path.append(xyze[0:3]) contour.path.append(new_xyze[0:3]) prev_ext = True elif prev_ext: prev_ext = False contour.tool = tool toolpath.contours.append(contour) contour = Contour() xyze = new_xyze elif line.command[1] == 92: xyze[3] = line.get_param("E", default=xyze[3]) elif line.command[0] == "T": tool = line.command[1] return toolpath
[docs] def split_by_layers(toolpath: Toolpath) -> List[Toolpath]: """ Split a `Toolpath` into separate layers based on the Z-height of its contours. Parameters ---------- toolpath : Toolpath The input `Toolpath` containing a flat list of contours. Returns ------- list of Toolpath A list of `Toolpath` instances, where each toolpath contains contours that share the same base Z-height. Notes ----- - The lowest Z value in each contour is used to determine its layer. - Layer ordering is from lowest to highest Z. """ layers = [] # find unique set of z values contour_z = [] for contour in toolpath.contours: z_values = np.sort(np.array(contour.path)[:, 2]) contour_z.append(z_values[0]) unique_z = sorted(set(contour_z)) for z in unique_z: contour_ind = np.where(contour_z == z)[0] layers.append(Toolpath([toolpath.contours[i] for i in contour_ind])) return layers
if __name__ == "__main__": with open("my_gcode.gcode", "r") as f: gcode = f.read() parsed_gcode = GcodeParser(gcode) toolpath = Toolpath.from_gcode(parsed_gcode.lines) print(f"Number of Contours: {len(toolpath.contours)}")