diff --git a/scripts/pygcode-crop b/scripts/pygcode-crop index a5f79fc..f693872 100755 --- a/scripts/pygcode-crop +++ b/scripts/pygcode-crop @@ -14,7 +14,9 @@ for pygcode_lib_type in ('installed_lib', 'relative_lib'): try: # pygcode from pygcode import Machine, Mode - from pygcode import Line + from pygcode import Line, Comment + from pygcode import GCodePlaneSelect, GCodeSelectXYPlane + from pygcode import GCodeRapidMove except ImportError: import sys, os, inspect @@ -131,9 +133,11 @@ def range_type(value): # --- Create Parser -parser = argparse.ArgumentParser(description='Crop a gcode file down to the') +parser = argparse.ArgumentParser( + description="Remove gcode before and after given 'from' and 'to' events" +) parser.add_argument( - 'infile', type=argparse.FileType('r'), nargs=1, + 'infile', type=argparse.FileType('r'), help="gcode file to normalize", ) parser.add_argument( @@ -159,35 +163,52 @@ post_crop = False (is_first, is_last) = args.range - for (i, line_str) in enumerate(args.infile.readlines()): line = Line(line_str) - # TODO: remember machine's maximum values for each axis - # TODO: based on machine.abs_pos - - # remember machine's (relevant) state before processing - # TODO: create machine.__copy__ to copy position & state + # remember machine's state before processing the current line old_machine = copy(machine) machine.process_block(line.block) if pre_crop: - if is_first(i + 1, old_machine.pos): + if is_first(i + 1, machine.pos): # First line inside cropping range pre_crop = False - # TODO: print mode - print(old_machine.mode) - # TODO: position machine before first cropped line - # TODO: rapid move to Z (maximum Z) - # TODO: rapid move to X,Y - # TODO: rapid move to Z (machine.pos.Z) + # Set machine's accumulated mode (from everything that's been cut) + mode_str = str(old_machine.mode) + if mode_str: + print(Comment("machine mode before cropping")) + print(mode_str) + # Getting machine's current (modal) selected plane + plane = old_machine.mode.plane_selection + if not isinstance(plane, GCodePlaneSelect): + plane = GCodeSelectXYPlane() # default to XY plane + + # --- position machine before first cropped line + print(Comment("traverse into position, up, over, and down")) + # rapid move to Z (maximum Z the machine has experienced thus far) + print(GCodeRapidMove(**{ + plane.normal_axis: getattr(old_machine.abs_range_max, plane.normal_axis), + })) + # rapid move to X,Y + print(GCodeRapidMove(**dict( + (k, v) for (k, v) in old_machine.pos.values.items() + if k in plane.plane_axes + ))) + # rapid move to Z (machine.pos.Z) + print(GCodeRapidMove(**{ + plane.normal_axis: getattr(old_machine.pos, plane.normal_axis), + })) + print('') if (pre_crop, post_crop) == (False, False): - - print(line) - - if is_last(i + 1, old_machine.pos): - # Last line in cropping range - post_crop = True + if is_last(i + 1, machine.pos): + # First line **outside** the area being cropped + # (ie: this line won't be output) + post_crop = True # although, irrelevant because... + break + else: + # inside cropping area + print(line) diff --git a/src/pygcode/gcodes.py b/src/pygcode/gcodes.py index 83f3735..5013fb1 100644 --- a/src/pygcode/gcodes.py +++ b/src/pygcode/gcodes.py @@ -824,9 +824,11 @@ class GCodePlaneSelect(GCode): # vectorYZ = GCodeSelectYZPlane.quat * (GCodeSelectZXPlane.quat.conjugate() * vectorZX) quat = None # Quaternion - # -- Plane Normal + # -- Plane Axis Information # Vector normal to plane (such that XYZ axes follow the right-hand rule) normal_axis = None # Letter of normal axis (upper case) + # Axes of plane + plane_axes = set() normal = None # Vector3 @@ -835,6 +837,7 @@ class GCodeSelectXYPlane(GCodePlaneSelect): word_key = Word('G', 17) quat = Quaternion() # no effect normal_axis = 'Z' + plane_axes = set('XY') normal = Vector3(0., 0., 1.) @@ -846,6 +849,7 @@ class GCodeSelectZXPlane(GCodePlaneSelect): Vector3(0., 0., 1.), Vector3(1., 0., 0.) ) normal_axis = 'Y' + plane_axes = set('ZX') normal = Vector3(0., 1., 0.) @@ -857,6 +861,7 @@ class GCodeSelectYZPlane(GCodePlaneSelect): Vector3(0., 1., 0.), Vector3(0., 0., 1.) ) normal_axis = 'X' + plane_axes = set('YZ') normal = Vector3(1., 0., 0.) diff --git a/src/pygcode/machine.py b/src/pygcode/machine.py index 42c2f15..72017d3 100644 --- a/src/pygcode/machine.py +++ b/src/pygcode/machine.py @@ -52,6 +52,9 @@ class Position(object): self._value = defaultdict(lambda: 0.0, dict((k, 0.0) for k in self.axes)) self._value.update(kwargs) + def __copy__(self): + return self.__class__(axes=copy(self.axes), unit=self._unit, **self.values) + def update(self, **coords): for (k, v) in coords.items(): setattr(self, k, v) @@ -74,10 +77,6 @@ class Position(object): else: self.__dict__[key] = value - # Copy - def __copy__(self): - return self.__class__(axes=copy(self.axes), unit=self._unit, **self.values) - # Equality def __eq__(self, other): if self.axes ^ other.axes: @@ -137,6 +136,36 @@ class Position(object): self._value[k] *= factor self._unit = value + # Min/Max + @classmethod + def _cmp(cls, p1, p2, key): + """ + Returns a position of the combined min/max values for each axis + (eg: key=min for) + note: the result is not necessarily equal to either p1 or p2. + :param p1: Position instance + :param p2: Position instance + :return: Position instance with the highest/lowest value per axis + """ + if p2.unit != p1.unit: + p2 = copy(p2) + p2.unit = p1.unit + return cls( + unit=p1.unit, + **dict( + (k, key(getattr(p1, k), getattr(p2, k))) for k in p1.values + ) + ) + + @classmethod + def min(cls, a, b): + return cls._cmp(a, b, key=min) + + @classmethod + def max(cls, a, b): + return cls._cmp(a, b, key=max) + + # Words & Values @property def words(self): return sorted(Word(k, self._value[k]) for k in self.axes) @@ -149,6 +178,7 @@ class Position(object): def vector(self): return Vector3(self._value['X'], self._value['Y'], self._value['Z']) + # String representation(s) def __repr__(self): return "<{class_name}: {coordinates}>".format( class_name=self.__class__.__name__, @@ -157,12 +187,13 @@ class Position(object): class CoordinateSystem(object): - def __init__(self, axes): - self.offset = Position(axes) + def __init__(self, axes=None): + self.offset = Position(axes=axes) - def __add__(self, other): - if isinstance(other, CoordinateSystem): - pass + def __copy__(self): + obj = self.__class__() + obj.offset = copy(self.offset) + return obj def __repr__(self): return "<{class_name}: offset={offset}>".format( @@ -178,6 +209,7 @@ class State(object): # AFAIK: this is everything needed to remember a machine's state that isn't # handled by modal gcodes. def __init__(self, axes=None): + self._axes = axes # Coordinate Systems self.coord_systems = {} for i in range(1, 10): # G54-G59.3 @@ -206,6 +238,12 @@ class State(object): # Coordinate System selection: # - G54-G59: select coordinate system (offsets from machine coordinates set by G10 L2) + def __copy__(self): + obj = self.__class__(axes=self._axes) + obj.coord_systems = [copy(cs) for cs in self.coord_systems] + obj.offset = copy(self.offset) + return obj + @property def coord_sys(self): """Current equivalent coordinate system, including all """ @@ -249,11 +287,17 @@ class Mode(object): # Mode is defined by gcodes set by processed blocks: # see modal_group in gcode.py module for details - def __init__(self): + def __init__(self, set_default=True): self.modal_groups = defaultdict(lambda: None) # Initialize - self.set_mode(*Line(self.default_mode).block.gcodes) + if set_default: + self.set_mode(*Line(self.default_mode).block.gcodes) + + def __copy__(self): + obj = self.__class__(set_default=False) + obj.modal_groups = deepcopy(self.modal_groups) + return obj def set_mode(self, *gcode_list): """ @@ -335,6 +379,18 @@ class Machine(object): # Absolute machine position self.abs_pos = self.Position() + # Machine's motion range (min/max corners of a bounding box) + self.abs_range_min = copy(self.abs_pos) + self.abs_range_max = copy(self.abs_pos) + + def __copy__(self): + obj = self.__class__() + obj.mode = copy(self.mode) + obj.state = copy(self.state) + obj.abs_pos = copy(self.abs_pos) + obj.abs_range_min = copy(self.abs_range_min) + obj.abs_range_max = copy(self.abs_range_max) + return obj def set_mode(self, *gcode_list): self.mode.set_mode(*gcode_list) # passthrough @@ -425,6 +481,11 @@ class Machine(object): coord_sys_offset = getattr(self.state.coord_sys, 'offset', Position(axes=self.axes)) temp_offset = self.state.offset self.abs_pos = (value + temp_offset) + coord_sys_offset + self._update_abs_range(self.abs_pos) + + def _update_abs_range(self, pos): + self.abs_range_min = Position.min(pos, self.abs_range_min) + self.abs_range_max = Position.max(pos, self.abs_range_max) # =================== Machine Actions =================== def move_to(self, rapid=False, **coords):