From 3dfd860a2dbb6e7f63e3757395f9c2a72090b053 Mon Sep 17 00:00:00 2001 From: lars Date: Thu, 15 Nov 2012 23:28:55 +0000 Subject: [PATCH] reimplemented shifting added crop action (not tested) added rotate action (not implemented) --- gcode-tools/gcode-shifter/gcode_shift.py | 212 +++++++++++++++++++---- 1 file changed, 179 insertions(+), 33 deletions(-) diff --git a/gcode-tools/gcode-shifter/gcode_shift.py b/gcode-tools/gcode-shifter/gcode_shift.py index d0e6d5d..c5c0982 100755 --- a/gcode-tools/gcode-shifter/gcode_shift.py +++ b/gcode-tools/gcode-shifter/gcode_shift.py @@ -6,45 +6,175 @@ import re import sys from decimal import Decimal as d +AXIS = "XYZ" +AXIS_REGEX = r"\b([%s][\+\-0-9\.]+)\b" % AXIS -class GCodePosition(object): - def __init__(self, lower, upper, shift): - self.pos = [d(0), d(0), d(0)] +class GCodeFilter(object): + + def __init__(self, lower, upper, func=None): + self.pos = tuple([d(0)] * len(AXIS)) self.lower = lower self.upper = upper - self.shift = shift + self.recalc_func = func + self.destination_position = PositionHandler() - def is_inside(self): - for i in range(len(self.pos)): - if not ((self.lower[i] is None or (self.lower[i] <= self.pos[i])) and \ - (self.upper[i] is None or (self.upper[i] >= self.pos[i]))): + 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 get_shifted_line(self, line): - """ parse the line, update current position replace shifted values - """ - axis_pattern = r"\b([XYZ][\+\-0-9\.]+)\b" - result = [] - # first: update the x/y/z position (make sure that we are in the box) - for token in re.findall(axis_pattern, line, flags=re.I): - for i, axis in enumerate("XYZ"): + 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): - self.pos[i] = d(token[1:]) - # second: replace values - if self.is_inside(): - def replace_value(match): - token = match.group(0).upper() - for i, axis in enumerate("XYZ"): - if token.startswith(axis): - value = d(token[1:]) - return token[0] + str(value + self.shift[i]) - return token - return re.sub(axis_pattern, replace_value, line, re.I) + new_pos[i] = d(token[1:]) + new_pos = tuple(new_pos) + if self.pos != new_pos: + self.pos = new_pos + return True else: - return line + 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__": @@ -58,12 +188,28 @@ if __name__ == "__main__": 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() - shifter = GCodePosition((options.minx, options.miny, options.minz), - (options.maxx, options.maxy, options.maxz), - (options.shiftx, options.shifty, options.shiftz)) + 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(): - outfile.write(shifter.get_shifted_line(line)) + for out_line in gcode_filter.get_processed_lines(line): + outfile.write(out_line)