Ver Fonte

- added the Exclusion zones processing to Geometry GCode generation

Marius Stanciu há 5 anos atrás
pai
commit
2e8d5b3b96
5 ficheiros alterados com 346 adições e 63 exclusões
  1. 11 8
      AppObjects/FlatCAMCNCJob.py
  2. 1 1
      App_Main.py
  3. 1 0
      CHANGELOG.md
  4. 331 52
      camlib.py
  5. 2 2
      preprocessors/Toolchange_Probe_MACH3.py

+ 11 - 8
AppObjects/FlatCAMCNCJob.py

@@ -989,7 +989,6 @@ class CNCJobObject(FlatCAMObj, CNCjob):
                 for key in self.cnc_tools:
                     ppg = self.cnc_tools[key]['data']['ppname_g']
                     if 'toolchange_custom' not in str(ppg).lower():
-                        print(ppg)
                         if self.ui.toolchange_cb.get_value():
                             self.ui.toolchange_cb.set_value(False)
                             self.app.inform.emit('[WARNING_NOTCL] %s' %
@@ -1107,7 +1106,7 @@ class CNCJobObject(FlatCAMObj, CNCjob):
                 except ValueError:
                     # we may have a tuple with only one element and a comma
                     dia_plot = [float(el) for el in self.options["tooldia"].split(',') if el != ''][0]
-                self.plot2(dia_plot, obj=self, visible=visible, kind=kind)
+                self.plot2(tooldia=dia_plot, obj=self, visible=visible, kind=kind)
             else:
                 # multiple tools usage
                 if self.cnc_tools:
@@ -1117,12 +1116,16 @@ class CNCJobObject(FlatCAMObj, CNCjob):
                         self.plot2(tooldia=tooldia, obj=self, visible=visible, gcode_parsed=gcode_parsed, kind=kind)
 
                 # TODO: until the gcode parsed will be stored on each Excellon tool this will not get executed
-                if self.exc_cnc_tools:
-                    for tooldia_key in self.exc_cnc_tools:
-                        tooldia = float('%.*f' % (self.decimals, float(tooldia_key)))
-                        # gcode_parsed = self.cnc_tools[tooldia_key]['gcode_parsed']
-                        gcode_parsed = self.gcode_parsed
-                        self.plot2(tooldia=tooldia, obj=self, visible=visible, gcode_parsed=gcode_parsed, kind=kind)
+                # I do this so the travel lines thickness will reflect the tool diameter
+                # may work only for objects created within the app and not Gcode imported from elsewhere for which we
+                # don't know the origin
+                if self.origin_kind == "excellon":
+                    if self.exc_cnc_tools:
+                        for tooldia_key in self.exc_cnc_tools:
+                            tooldia = float('%.*f' % (self.decimals, float(tooldia_key)))
+                            # gcode_parsed = self.exc_cnc_tools[tooldia_key]['gcode_parsed']
+                            gcode_parsed = self.gcode_parsed
+                            self.plot2(tooldia=tooldia, obj=self, visible=visible, gcode_parsed=gcode_parsed, kind=kind)
 
             self.shapes.redraw()
         except (ObjectDeleted, AttributeError):

+ 1 - 1
App_Main.py

@@ -14,8 +14,8 @@ import urllib.error
 import getopt
 import random
 import simplejson as json
-import lzma
 import shutil
+import lzma
 from datetime import datetime
 import time
 import ctypes

+ 1 - 0
CHANGELOG.md

@@ -10,6 +10,7 @@ CHANGELOG for FlatCAM beta
 22.05.2020
 
 - fixed the algorithm for calculating closest points in the Exclusion areas
+- added the Exclusion zones processing to Geometry GCode generation
 
 21.05.2020
 

+ 331 - 52
camlib.py

@@ -1103,15 +1103,17 @@ class Geometry(object):
         """
         Imports shapes from an IMAGE file into the object's geometry.
 
-        :param filename: Path to the IMAGE file.
-        :type filename: str
-        :param flip: Flip the object vertically.
-        :type flip: bool
-        :param units: FlatCAM units
-        :param dpi: dots per inch on the imported image
-        :param mode: how to import the image: as 'black' or 'color'
-        :param mask: level of detail for the import
-        :return: None
+        :param filename:    Path to the IMAGE file.
+        :type filename:     str
+        :param flip:        Flip the object vertically.
+        :type flip:         bool
+        :param units:       FlatCAM units
+        :type units:        str
+        :param dpi:         dots per inch on the imported image
+        :param mode:        how to import the image: as 'black' or 'color'
+        :type mode:         str
+        :param mask:        level of detail for the import
+        :return:            None
         """
         if mask is None:
             mask = [128, 128, 128, 128]
@@ -1985,7 +1987,7 @@ class Geometry(object):
         it again in descendents.
 
         :param obj_units:   "IN" or "MM"
-        :type units:        str
+        :type obj_units:    str
         :return:            Scaling factor resulting from unit change.
         :rtype:             float
         """
@@ -2550,9 +2552,23 @@ class CNCjob(Geometry):
 
     @property
     def postdata(self):
+        """
+        This will return all the attributes of the class in the form of a dictionary
+
+        :return:    Class attributes
+        :rtype:     dict
+        """
         return self.__dict__
 
     def convert_units(self, units):
+        """
+        Will convert the parameters in the class that are relevant, from metric to imperial and reverse
+
+        :param units:   FlatCAM units
+        :type units:    str
+        :return:        conversion factor
+        :rtype:         float
+        """
         log.debug("camlib.CNCJob.convert_units()")
 
         factor = Geometry.convert_units(self, units)
@@ -2573,6 +2589,17 @@ class CNCjob(Geometry):
         return self.doformat2(fun, **kwargs) + "\n"
 
     def doformat2(self, fun, **kwargs):
+        """
+        This method will call one of the current preprocessor methods having as parameters all the attributes of
+        current class to which will add the kwargs parameters
+
+        :param fun:     One of the methods inside the preprocessor classes which get loaded here in the 'p' object
+        :type fun:      class 'function'
+        :param kwargs:  keyword args which will update attributes of the current class
+        :type kwargs:   dict
+        :return:        Gcode line
+        :rtype:         str
+        """
         attributes = AttrDict()
         attributes.update(self.postdata)
         attributes.update(kwargs)
@@ -2584,6 +2611,16 @@ class CNCjob(Geometry):
             return ''
 
     def parse_custom_toolchange_code(self, data):
+        """
+        Will parse a text and get a toolchange sequence in text format suitable to be included in a Gcode file.
+        The '%' symbol is used to surround class variables name and must be removed in the returned string.
+        After that, the class variables (attributes) are replaced with the current values. The result is returned.
+
+        :param data:    Toolchange sequence
+        :type data:     str
+        :return:        Processed toolchange sequence
+        :rtype:         str
+        """
         text = data
         match_list = self.re_toolchange_custom.findall(text)
 
@@ -2615,6 +2652,13 @@ class CNCjob(Geometry):
         [2, 3], [2, 4], [3, 4], [3, 3], [3, 2], [3, 1], [3, 0], [4, 0], [4, 1], [4, 2], [4, 3], [4, 4]]
         >>> optimized_travelling_salesman([[0,0],[10,0],[6,0]])
         [[0, 0], [6, 0], [10, 0]]
+
+        :param points:  List of tuples with x, y coordinates
+        :type points:   list
+        :param start:   a tuple with a x,y coordinates of the start point
+        :type start:    tuple
+        :return:        List of points ordered in a optimized way
+        :rtype:         list
         """
 
         if start is None:
@@ -3899,7 +3943,9 @@ class CNCjob(Geometry):
                     # calculate the cut distance
                     total_cut = total_cut + geo.length
 
-                    self.gcode += self.create_gcode_single_pass(geo, extracut, extracut_length, tolerance,
+                    self.gcode += self.create_gcode_single_pass(geo, current_tooldia, extracut, extracut_length,
+                                                                tolerance,
+                                                                z_move=z_move, postproc=p,
                                                                 old_point=current_pt)
 
                 # --------- Multi-pass ---------
@@ -3914,8 +3960,10 @@ class CNCjob(Geometry):
 
                     total_cut += (geo.length * nr_cuts)
 
-                    self.gcode += self.create_gcode_multi_pass(geo, extracut, extracut_length, tolerance,
-                                                               postproc=p, old_point=current_pt)
+                    self.gcode += self.create_gcode_multi_pass(geo, current_tooldia, extracut, extracut_length,
+                                                               tolerance,
+                                                               z_move=z_move, postproc=p,
+                                                               old_point=current_pt)
 
                 # calculate the total distance
                 total_travel = total_travel + abs(distance(pt1=current_pt, pt2=pt))
@@ -4216,20 +4264,24 @@ class CNCjob(Geometry):
 
         # this is the tool diameter, it is used as such to accommodate the preprocessor who need the tool diameter
         # given under the name 'toolC'
+        # this is a fancy way of adding a class attribute (which should be added in the __init__ method) without doing
+        # it there :)
         self.postdata['toolC'] = self.tooldia
 
         # Initial G-Code
         self.pp_geometry = self.app.preprocessors[self.pp_geometry_name]
+
+        # the 'p' local attribute is a reference to the current preprocessor class
         p = self.pp_geometry
 
         self.oldx = 0.0
         self.oldy = 0.0
 
         self.gcode = self.doformat(p.start_code)
-
         self.gcode += self.doformat(p.feedrate_code)  # sets the feed rate
 
         if toolchange is False:
+            # all the x and y parameters in self.doformat() are used only by some preprocessors not by all
             self.gcode += self.doformat(p.lift_code, x=self.oldx, y=self.oldy)  # Move (up) to travel height
             self.gcode += self.doformat(p.startz_code, x=self.oldx, y=self.oldy)
 
@@ -4277,6 +4329,9 @@ class CNCjob(Geometry):
         path_count = 0
         current_pt = (0, 0)
         pt, geo = storage.nearest(current_pt)
+
+        # when nothing is left in the storage a StopIteration exception will be raised therefore stopping
+        # the whole process including the infinite loop while True below.
         try:
             while True:
                 if self.app.abort_flag:
@@ -4297,7 +4352,9 @@ class CNCjob(Geometry):
                 if not multidepth:
                     # calculate the cut distance
                     total_cut += geo.length
-                    self.gcode += self.create_gcode_single_pass(geo, extracut, self.extracut_length, tolerance,
+                    self.gcode += self.create_gcode_single_pass(geo, current_tooldia, extracut, self.extracut_length,
+                                                                tolerance,
+                                                                z_move=z_move, postproc=p,
                                                                 old_point=current_pt)
 
                 # --------- Multi-pass ---------
@@ -4312,8 +4369,10 @@ class CNCjob(Geometry):
 
                     total_cut += (geo.length * nr_cuts)
 
-                    self.gcode += self.create_gcode_multi_pass(geo, extracut, self.extracut_length, tolerance,
-                                                               postproc=p, old_point=current_pt)
+                    self.gcode += self.create_gcode_multi_pass(geo, current_tooldia, extracut, self.extracut_length,
+                                                               tolerance,
+                                                               z_move=z_move,_postproc=p,
+                                                               old_point=current_pt)
 
                 # calculate the travel distance
                 total_travel += abs(distance(pt1=current_pt, pt2=pt))
@@ -4321,6 +4380,7 @@ class CNCjob(Geometry):
 
                 pt, geo = storage.nearest(current_pt)  # Next
 
+                # update the activity counter (lower left side of the app, status bar)
                 disp_number = int(np.interp(path_count, [0, geo_len], [0, 100]))
                 if old_disp_number < disp_number <= 100:
                     self.app.proc_container.update_view_text(' %d%%' % disp_number)
@@ -4535,27 +4595,76 @@ class CNCjob(Geometry):
             gcode += self.doformat(p.lift_code)
         return gcode
 
-    def create_gcode_single_pass(self, geometry, extracut, extracut_length, tolerance, old_point=(0, 0)):
+    def create_gcode_single_pass(self, geometry, cdia, extracut, extracut_length, tolerance, z_move, postproc,
+                                 old_point=(0, 0)):
+        """
         # G-code. Note: self.linear2gcode() and self.point2gcode() will lower and raise the tool every time.
 
+        :param geometry:            A Shapely Geometry (LineString or LinearRing) which is the path to be cut
+        :type geometry:             LineString, LinearRing
+        :param cdia:                Tool diameter
+        :type cdia:                 float
+        :param extracut:            Will add an extra cut over the point where start of the cut is met with the end cut
+        :type extracut:             bool
+        :param extracut_length:     The length of the extra cut: half before the meeting point, half after
+        :type extracut_length:      float
+        :param tolerance:           Tolerance used to simplify the paths (making them mre rough)
+        :type tolerance:            float
+        :param z_move:              Travel Z
+        :type z_move:               float
+        :param postproc:            Preprocessor class
+        :type postproc:             class
+        :param old_point:           Previous point
+        :type old_point:            tuple
+        :return:                    Gcode
+        :rtype:                     str
+        """
+        # p = postproc
+
         if type(geometry) == LineString or type(geometry) == LinearRing:
             if extracut is False:
-                gcode_single_pass = self.linear2gcode(geometry, tolerance=tolerance, old_point=old_point)
+                gcode_single_pass = self.linear2gcode(geometry, z_move=z_move, dia=cdia, tolerance=tolerance,
+                                                      old_point=old_point)
             else:
                 if geometry.is_ring:
                     gcode_single_pass = self.linear2gcode_extra(geometry, extracut_length, tolerance=tolerance,
+                                                                z_move=z_move, dia=cdia,
                                                                 old_point=old_point)
                 else:
-                    gcode_single_pass = self.linear2gcode(geometry, tolerance=tolerance, old_point=old_point)
+                    gcode_single_pass = self.linear2gcode(geometry, tolerance=tolerance, z_move=z_move, dia=cdia,
+                                                          old_point=old_point)
         elif type(geometry) == Point:
-            gcode_single_pass = self.point2gcode(geometry)
+            gcode_single_pass = self.point2gcode(geometry, dia=cdia, z_move=z_move, old_point=old_point)
         else:
             log.warning("G-code generation not implemented for %s" % (str(type(geometry))))
             return
 
         return gcode_single_pass
 
-    def create_gcode_multi_pass(self, geometry, extracut, extracut_length, tolerance, postproc, old_point=(0, 0)):
+    def create_gcode_multi_pass(self, geometry, cdia, extracut, extracut_length, tolerance, postproc, z_move,
+                                old_point=(0, 0)):
+        """
+
+        :param geometry:            A Shapely Geometry (LineString or LinearRing) which is the path to be cut
+        :type geometry:             LineString, LinearRing
+        :param cdia:                Tool diameter
+        :type cdia:                 float
+        :param extracut:            Will add an extra cut over the point where start of the cut is met with the end cut
+        :type extracut:             bool
+        :param extracut_length:     The length of the extra cut: half before the meeting point, half after
+        :type extracut_length:      float
+        :param tolerance:           Tolerance used to simplify the paths (making them mre rough)
+        :type tolerance:            float
+        :param postproc:            Preprocessor class
+        :type postproc:             class
+        :param z_move:              Travel Z
+        :type z_move:               float
+        :param old_point:           Previous point
+        :type old_point:            tuple
+        :return:                    Gcode
+        :rtype:                     str
+        """
+        p = postproc
 
         gcode_multi_pass = ''
 
@@ -4585,18 +4694,20 @@ class CNCjob(Geometry):
             if type(geometry) == LineString or type(geometry) == LinearRing:
                 if extracut is False:
                     gcode_multi_pass += self.linear2gcode(geometry, tolerance=tolerance, z_cut=depth, up=False,
-                                                          old_point=old_point)
+                                                          z_move=z_move, dia=cdia, old_point=old_point)
                 else:
                     if geometry.is_ring:
                         gcode_multi_pass += self.linear2gcode_extra(geometry, extracut_length, tolerance=tolerance,
-                                                                    z_cut=depth, up=False, old_point=old_point)
+                                                                    dia=cdia, z_move=z_move, z_cut=depth, up=False,
+                                                                    old_point=old_point)
                     else:
                         gcode_multi_pass += self.linear2gcode(geometry, tolerance=tolerance, z_cut=depth, up=False,
+                                                              dia=cdia, z_move=z_move,
                                                               old_point=old_point)
 
             # Ignore multi-pass for points.
             elif type(geometry) == Point:
-                gcode_multi_pass += self.point2gcode(geometry, old_point=old_point)
+                gcode_multi_pass += self.point2gcode(geometry, dia=cdia, z_move=z_move, old_point=old_point)
                 break  # Ignoring ...
             else:
                 log.warning("G-code generation not implemented for %s" % (str(type(geometry))))
@@ -4612,7 +4723,7 @@ class CNCjob(Geometry):
                 geometry.coords = list(geometry.coords)[::-1]
 
         # Lift the tool
-        gcode_multi_pass += self.doformat(postproc.lift_code, x=old_point[0], y=old_point[1])
+        gcode_multi_pass += self.doformat(p.lift_code, x=old_point[0], y=old_point[1])
         return gcode_multi_pass
 
     def codes_split(self, gline):
@@ -4620,8 +4731,10 @@ class CNCjob(Geometry):
         Parses a line of G-Code such as "G01 X1234 Y987" into
         a dictionary: {'G': 1.0, 'X': 1234.0, 'Y': 987.0}
 
-        :param gline: G-Code line string
-        :return: Dictionary with parsed line.
+        :param gline:       G-Code line string
+        :type gline:        str
+        :return:            Dictionary with parsed line.
+        :rtype:             dict
         """
 
         command = {}
@@ -4690,6 +4803,18 @@ class CNCjob(Geometry):
         G-Code parser (from self.gcode). Generates dictionary with
         single-segment LineString's and "kind" indicating cut or travel,
         fast or feedrate speed.
+
+        Will return a dict in the format:
+        {
+            "geom": LineString(path),
+            "kind": kind
+        }
+        where kind can be either ["C", "F"]  # T=travel, C=cut, F=fast, S=slow
+
+        :param force_parsing:
+        :type force_parsing:
+        :return:
+        :rtype:                 dict
         """
 
         kind = ["C", "F"]  # T=travel, C=cut, F=fast, S=slow
@@ -4879,16 +5004,28 @@ class CNCjob(Geometry):
         """
         Plots the G-code job onto the given axes.
 
-        :param tooldia: Tool diameter.
-        :param dpi: Not used!
-        :param margin: Not used!
-        :param color: Color specification.
-        :param alpha: Transparency specification.
-        :param tool_tolerance: Tolerance when drawing the toolshape.
-        :param obj
-        :param visible
-        :param kind
-        :return: None
+        :param tooldia:             Tool diameter.
+        :type tooldia:              float
+        :param dpi:                 Not used!
+        :type dpi:                  float
+        :param margin:              Not used!
+        :type margin:               float
+        :param gcode_parsed:        Parsed Gcode
+        :type gcode_parsed:         str
+        :param color:               Color specification.
+        :type color:                str
+        :param alpha:               Transparency specification.
+        :type alpha:                dict
+        :param tool_tolerance:      Tolerance when drawing the toolshape.
+        :type tool_tolerance:       float
+        :param obj:                 The object for whih to plot
+        :type obj:                  class
+        :param visible:             Visibility status
+        :type visible:              bool
+        :param kind:                Can be: "travel", "cut", "all"
+        :type kind:                 str
+        :return:                    None
+        :rtype:
         """
         # units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
 
@@ -5045,9 +5182,17 @@ class CNCjob(Geometry):
                 log.debug("CNCJob.plot2() --> annotations --> %s" % str(e))
 
     def create_geometry(self):
-        self.app.inform.emit('%s: %s' % (_("Unifying Geometry from parsed Geometry segments"),
-                                         str(len(self.gcode_parsed))))
+        """
+        It is used by the Excellon objects. Will create the solid_geometry which will be an attribute of the
+        Excellon object class.
+
+        :return:    List of Shapely geometry elements
+        :rtype:     list
+        """
+
         # TODO: This takes forever. Too much data?
+        # self.app.inform.emit('%s: %s' % (_("Unifying Geometry from parsed Geometry segments"),
+        #                                  str(len(self.gcode_parsed))))
         # self.solid_geometry = cascaded_union([geo['geom'] for geo in self.gcode_parsed])
 
         # This is much faster but not so nice to look at as you can see different segments of the geometry
@@ -5055,10 +5200,15 @@ class CNCjob(Geometry):
 
         return self.solid_geometry
 
-    # code snippet added by Lei Zheng in a rejected pull request on FlatCAM https://bitbucket.org/realthunder/
     def segment(self, coords):
         """
-        break long linear lines to make it more auto level friendly
+        Break long linear lines to make it more auto level friendly.
+        Code snippet added by Lei Zheng in a rejected pull request on FlatCAM https://bitbucket.org/realthunder/
+
+        :param coords:  List of coordinates tuples
+        :type coords:   list
+        :return:        A path; list with the multiple coordinates breaking a line.
+        :rtype:         list
         """
 
         if len(coords) < 2 or self.segx <= 0 and self.segy <= 0:
@@ -5110,7 +5260,7 @@ class CNCjob(Geometry):
 
         return path
 
-    def linear2gcode(self, linear, tolerance=0, down=True, up=True, z_cut=None, z_move=None, zdownrate=None,
+    def linear2gcode(self, linear, dia, tolerance=0, down=True, up=True, z_cut=None, z_move=None, zdownrate=None,
                      feedrate=None, feedrate_z=None, feedrate_rapid=None, cont=False, old_point=(0, 0)):
         """
 
@@ -5118,6 +5268,8 @@ class CNCjob(Geometry):
 
         :param linear:          The path to cut along.
         :type:                  Shapely.LinearRing or Shapely.Linear String
+        :param dia:             The tool diameter that is going on the path
+        :type dia:              float
         :param tolerance:       All points in the simplified object will be within the
                                 tolerance distance of the original geometry.
         :type tolerance:        float
@@ -5177,7 +5329,42 @@ class CNCjob(Geometry):
 
         # Move fast to 1st point
         if not cont:
-            gcode += self.doformat(p.rapid_code, x=first_x, y=first_y)  # Move to first point
+            current_tooldia = dia
+            travels = self.app.exc_areas.travel_coordinates(start_point=(old_point[0], old_point[1]),
+                                                            end_point=(first_x, first_y),
+                                                            tooldia=current_tooldia)
+            prev_z = None
+            for travel in travels:
+                locx = travel[1][0]
+                locy = travel[1][1]
+
+                if travel[0] is not None:
+                    # move to next point
+                    gcode += self.doformat(p.rapid_code, x=locx, y=locy)
+
+                    # raise to safe Z (travel[0]) each time because safe Z may be different
+                    self.z_move = travel[0]
+                    gcode += self.doformat(p.lift_code, x=locx, y=locy)
+
+                    # restore z_move
+                    self.z_move = z_move
+                else:
+                    if prev_z is not None:
+                        # move to next point
+                        gcode += self.doformat(p.rapid_code, x=locx, y=locy)
+
+                        # we assume that previously the z_move was altered therefore raise to
+                        # the travel_z (z_move)
+                        self.z_move = z_move
+                        gcode += self.doformat(p.lift_code, x=locx, y=locy)
+                    else:
+                        # move to next point
+                        gcode += self.doformat(p.rapid_code, x=locx, y=locy)
+
+                # store prev_z
+                prev_z = travel[0]
+
+            # gcode += self.doformat(p.rapid_code, x=first_x, y=first_y)  # Move to first point
 
         # Move down to cutting depth
         if down:
@@ -5216,7 +5403,7 @@ class CNCjob(Geometry):
             gcode += self.doformat(p.lift_code, x=prev_x, y=prev_y, z_move=z_move)  # Stop cutting
         return gcode
 
-    def linear2gcode_extra(self, linear, extracut_length, tolerance=0, down=True, up=True,
+    def linear2gcode_extra(self, linear, dia, extracut_length, tolerance=0, down=True, up=True,
                            z_cut=None, z_move=None, zdownrate=None,
                            feedrate=None, feedrate_z=None, feedrate_rapid=None, cont=False, old_point=(0, 0)):
         """
@@ -5225,6 +5412,8 @@ class CNCjob(Geometry):
 
         :param linear:              The path to cut along.
         :type:                      Shapely.LinearRing or Shapely.Linear String
+        :param dia:                 The tool diameter that is going on the path
+        :type dia:                  float
         :param extracut_length:     how much to cut extra over the first point at the end of the path
         :param tolerance:           All points in the simplified object will be within the
                                     tolerance distance of the original geometry.
@@ -5284,7 +5473,42 @@ class CNCjob(Geometry):
 
         # Move fast to 1st point
         if not cont:
-            gcode += self.doformat(p.rapid_code, x=first_x, y=first_y)  # Move to first point
+            current_tooldia = dia
+            travels = self.app.exc_areas.travel_coordinates(start_point=(old_point[0], old_point[1]),
+                                                            end_point=(first_x, first_y),
+                                                            tooldia=current_tooldia)
+            prev_z = None
+            for travel in travels:
+                locx = travel[1][0]
+                locy = travel[1][1]
+
+                if travel[0] is not None:
+                    # move to next point
+                    gcode += self.doformat(p.rapid_code, x=locx, y=locy)
+
+                    # raise to safe Z (travel[0]) each time because safe Z may be different
+                    self.z_move = travel[0]
+                    gcode += self.doformat(p.lift_code, x=locx, y=locy)
+
+                    # restore z_move
+                    self.z_move = z_move
+                else:
+                    if prev_z is not None:
+                        # move to next point
+                        gcode += self.doformat(p.rapid_code, x=locx, y=locy)
+
+                        # we assume that previously the z_move was altered therefore raise to
+                        # the travel_z (z_move)
+                        self.z_move = z_move
+                        gcode += self.doformat(p.lift_code, x=locx, y=locy)
+                    else:
+                        # move to next point
+                        gcode += self.doformat(p.rapid_code, x=locx, y=locy)
+
+                # store prev_z
+                prev_z = travel[0]
+
+            # gcode += self.doformat(p.rapid_code, x=first_x, y=first_y)  # Move to first point
 
         # Move down to cutting depth
         if down:
@@ -5450,7 +5674,20 @@ class CNCjob(Geometry):
 
         return gcode
 
-    def point2gcode(self, point, old_point=(0, 0)):
+    def point2gcode(self, point, dia, z_move=None, old_point=(0, 0)):
+        """
+
+        :param point:               A Shapely Point
+        :type point:                Point
+        :param dia:                 The tool diameter that is going on the path
+        :type dia:                  float
+        :param z_move:              Travel Z
+        :type z_move:               float
+        :param old_point:           Old point coordinates from which we moved to the 'point'
+        :type old_point:            tuple
+        :return:                    G-code to cut on the Point feature.
+        :rtype:                     str
+        """
         gcode = ""
 
         if self.app.abort_flag:
@@ -5474,7 +5711,42 @@ class CNCjob(Geometry):
             first_x = path[0][0]
             first_y = path[0][1]
 
-        gcode += self.doformat(p.linear_code, x=first_x, y=first_y)  # Move to first point
+        current_tooldia = dia
+        travels = self.app.exc_areas.travel_coordinates(start_point=(old_point[0], old_point[1]),
+                                                        end_point=(first_x, first_y),
+                                                        tooldia=current_tooldia)
+        prev_z = None
+        for travel in travels:
+            locx = travel[1][0]
+            locy = travel[1][1]
+
+            if travel[0] is not None:
+                # move to next point
+                gcode += self.doformat(p.rapid_code, x=locx, y=locy)
+
+                # raise to safe Z (travel[0]) each time because safe Z may be different
+                self.z_move = travel[0]
+                gcode += self.doformat(p.lift_code, x=locx, y=locy)
+
+                # restore z_move
+                self.z_move = z_move
+            else:
+                if prev_z is not None:
+                    # move to next point
+                    gcode += self.doformat(p.rapid_code, x=locx, y=locy)
+
+                    # we assume that previously the z_move was altered therefore raise to
+                    # the travel_z (z_move)
+                    self.z_move = z_move
+                    gcode += self.doformat(p.lift_code, x=locx, y=locy)
+                else:
+                    # move to next point
+                    gcode += self.doformat(p.rapid_code, x=locx, y=locy)
+
+            # store prev_z
+            prev_z = travel[0]
+
+        # gcode += self.doformat(p.linear_code, x=first_x, y=first_y)  # Move to first point
 
         if self.z_feedrate is not None:
             gcode += self.doformat(p.z_feedrate_code)
@@ -5490,8 +5762,10 @@ class CNCjob(Geometry):
         """
         Exports the CNC Job as a SVG Element
 
-        :scale_factor: float
-        :return: SVG Element string
+        :param scale_stroke_factor:     A factor to scale the SVG geometry
+        :type scale_stroke_factor:      float
+        :return:                        SVG Element string
+        :rtype:                         str
         """
         # scale_factor is a multiplication factor for the SVG stroke-width used within shapely's svg export
         # If not specified then try and use the tool diameter
@@ -5511,6 +5785,9 @@ class CNCjob(Geometry):
         # This way we can add different formatting / colors to both
         cuts = []
         travels = []
+        cutsgeom = ''
+        travelsgeom = ''
+
         for g in self.gcode_parsed:
             if self.app.abort_flag:
                 # graceful abort requested by the user
@@ -5548,10 +5825,12 @@ class CNCjob(Geometry):
 
     def bounds(self, flatten=None):
         """
-        Returns coordinates of rectangular bounds
-        of geometry: (xmin, ymin, xmax, ymax).
+        Returns coordinates of rectangular bounds of geometry: (xmin, ymin, xmax, ymax).
 
         :param flatten:     Not used, it is here for compatibility with base class method
+        :type flatten:      bool
+        :return:            Bounding values in format (xmin, ymin, xmax, ymax)
+        :rtype:             tuple
         """
 
         log.debug("camlib.CNCJob.bounds()")

+ 2 - 2
preprocessors/Toolchange_Probe_MACH3.py

@@ -106,10 +106,10 @@ class Toolchange_Probe_MACH3(PreProc):
         return g
 
     def lift_code(self, p):
-        return 'G00 Z' + self.coordinate_format%(p.coords_decimals, p.z_move)
+        return 'G00 Z' + self.coordinate_format % (p.coords_decimals, p.z_move)
 
     def down_code(self, p):
-        return 'G01 Z' + self.coordinate_format%(p.coords_decimals, p.z_cut)
+        return 'G01 Z' + self.coordinate_format % (p.coords_decimals, p.z_cut)
 
     def toolchange_code(self, p):
         z_toolchange = p.z_toolchange