#!/usr/bin/env python import argparse import os import re import sys from decimal import Decimal as d AXIS = "XYZ" AXIS_REGEX = r"\b([%s][\+\-0-9\.]+)\b" % AXIS class GCodeFilter(object): def __init__(self, lower, upper, func=None): self.pos = tuple([d(0)] * len(AXIS)) self.lower = lower self.upper = upper self.recalc_func = func self.destination_position = PositionHandler() def is_inside(self, position): for i in range(len(position)): if not ((self.lower[i] is None or (self.lower[i] <= position[i])) and \ (self.upper[i] is None or (self.upper[i] >= position[i]))): return False return True def update_position(self, line): for comment_sep in ";(": if comment_sep in line: line = line.split(comment_sep)[0] new_pos = list(self.pos) for token in re.findall(AXIS_REGEX, line, flags=re.I): for i, axis in enumerate(AXIS): if token.upper().startswith(axis): new_pos[i] = d(token[1:]) new_pos = tuple(new_pos) if self.pos != new_pos: self.pos = new_pos return True else: return False def get_processed_lines(self, line): """ parse the line, update current position and replace line string """ if self.update_position(line): result = [] handler = LineHandler(line) positions = self.recalc_func(self.pos, is_inside=self.is_inside) for pos in positions: changed_axes = self.destination_position.get_changed_axes(pos) yield handler.get_line(pos, changed_axes) self.destination_position.update(pos) else: # no coordinate given yield line class PositionHandler(object): def __init__(self): self.pos = tuple([0] * len(AXIS)) def update(self, position): new_pos = tuple(position) if self.pos != new_pos: self.pos = new_pos return True else: return False def get_changed_axes(self, position): for i, axis in enumerate(AXIS): if position[i] != self.pos[i]: yield (i, axis) class LineHandler(object): def __init__(self, line): self._digits = 0 self._processed = [] # remove trailing whitespace (and linebreak) self.line = line.rstrip() # all missing whitespace self._suffix = line[len(self.line):] def _get_digits(self, text): if "." in text: return len(text.split(".", 1)) else: return 0 def _get_number_string(self, value): return str(value) return ("%%.%df" % self._digits) % value def replace_value(self, match): token = match.group(0).upper() for i, axis in enumerate(AXIS): if token.startswith(axis): self._processed.append(i) self._digits = max(self._digits, self._get_digits(token[1:])) if d(token[1:]) != self._position[i]: return token[0] + self._get_number_string(self._position[i]) else: return token def get_line(self, position, axes): result = [] self._processed = [] self._position = position line = re.sub(AXIS_REGEX, self.replace_value, self.line, re.I) if not self._processed: # no axis definition line = self.line else: # at least one axis was defined for i, axis in axes: if not i in self._processed: line += " %s%s" % (axis, self._get_number_string(position[i])) return line + self._suffix def shift_position(pos, shift, inside_func): if inside_func(pos): new_pos = [(axis + shift_axis) for axis, shift_axis in zip(pos, shift)] else: new_pos = pos return (tuple(new_pos), ) class CropFilter(object): def __init__(self): self._was_inside = True self._previous_position = None self._recurse_counter = 0 self._max_recurse = 20 def crop_bounds(self, pos, inside_func): is_inside = inside_func(pos) result = None if is_inside and self._was_inside: # inside -> inside result = (pos, ) elif not is_inside and not self._was_inside: # outside -> outside result = [] else: # outside -> inside OR inside -> outside border_pos = self._recurse_border_position(self._previous_position, pos, inside_func) self._was_inside = is_inside if is_inside: result = (border_pos, pos) else: result = (pos, ) self._previous_position = pos return result def _recurse_border_position(self, p1, p2, inside_func): """ simple and stupid: bisections between p1 and p2 """ p_middle = tuple([0.5 * (axis1 + axis2) for axis1, axis2 in zip(p1, p2)]) if self._recurse_counter >= self._max_recurse: self._recurse_counter = 0 return p_middle else: if inside_func(p_middle) == inside_func(p1): p1_new, p2_new = p_middle, p2 else: p1_new, p2_new = p1, p_middle return self._recurse_border_position(p1_new, p2_new, inside_func) if __name__ == "__main__": parser = argparse.ArgumentParser(description="Shift parts of gcode") parser.add_argument('--minx', dest="minx", type=d) parser.add_argument('--maxx', dest="maxx", type=d) parser.add_argument('--miny', dest="miny", type=d) parser.add_argument('--maxy', dest="maxy", type=d) parser.add_argument('--minz', dest="minz", type=d) parser.add_argument('--maxz', dest="maxz", type=d) parser.add_argument('--shiftx', dest="shiftx", type=d, default=d(0)) parser.add_argument('--shifty', dest="shifty", type=d, default=d(0)) parser.add_argument('--shiftz', dest="shiftz", type=d, default=d(0)) parser.add_argument('--rotatex', dest="rotatex", type=d, default=d(0)) parser.add_argument('--rotatey', dest="rotatey", type=d, default=d(0)) parser.add_argument('--rotatez', dest="rotatez", type=d, default=d(0)) parser.add_argument('action', choices=("shift", "crop", "rotate")) infile = sys.stdin outfile = sys.stdout options = parser.parse_args() if options.action == "shift": shift = (options.shiftx, options.shifty, options.shiftz) func = lambda pos, is_inside: shift_position(pos, shift, is_inside) elif options.action == "crop": crop_filter = CropFilter() func = crop_filter.crop_bounds elif options.action == "rotate": rotate = (options.rotatex, options.rotatey, options.rotatez) func = lambda pos, is_inside: rotate_position(pos, shift, is_inside) else: print >>sys.stderr, "No valid action choosen" sys.exit(1) gcode_filter = GCodeFilter((options.minx, options.miny, options.minz), (options.maxx, options.maxy, options.maxz), func=func) for line in infile.readlines(): for out_line in gcode_filter.get_processed_lines(line): outfile.write(out_line)