diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 5a176529..652fb740 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -133,6 +133,10 @@ class App(QtCore.QObject): json.dump([], f) f.close() + self.app_home = os.path.dirname(os.path.realpath(__file__)) + App.log.debug("Application path is " + self.app_home) + App.log.debug("Started in " + os.getcwd()) + QtCore.QObject.__init__(self) self.ui = FlatCAMGUI(self.version) @@ -243,7 +247,8 @@ class App(QtCore.QObject): "zoom_ratio": 1.5, "point_clipboard_format": "(%.4f, %.4f)", "zdownrate": None, - "excellon_zeros": "L" + "excellon_zeros": "L", + "cncjob_coordinate_format": "X%.4fY%.4f" }) ############################### @@ -1771,7 +1776,8 @@ class App(QtCore.QObject): # Which objects to update the given parameters. routes = { "zdownrate": CNCjob, - "excellon_zeros": Excellon + "excellon_zeros": Excellon, + "cncjob_coordinate_format": CNCjob } for param in routes: @@ -2187,7 +2193,7 @@ class App(QtCore.QObject): od = collections.OrderedDict(sorted(commands.items())) for cmd, val in od.iteritems(): #print cmd, '\n', ''.join(['~']*len(cmd)) - output += cmd + ' \n' + ''.join(['~']*len(cmd)) + '\n' + output += cmd + ' \n' + ''.join(['~'] * len(cmd)) + '\n' t = val['help'] usage_i = t.find('>') @@ -2197,7 +2203,7 @@ class App(QtCore.QObject): output += expl + '\n\n' continue - expl = t[:usage_i-1] + expl = t[:usage_i - 1] #print expl + '\n' output += expl + '\n\n' @@ -2250,8 +2256,25 @@ class App(QtCore.QObject): return "ERROR: No such system parameter." def set_sys(param, value): + # tcl string to python keywords: + tcl2py = { + "None": None, + "none": None, + "false": False, + "False": False, + "true": True, + "True": True + } + if param in self.defaults: + + try: + value = tcl2py[value] + except KeyError: + pass + self.defaults[param] = value + self.propagate_defaults() return "Ok" diff --git a/camlib.py b/camlib.py index aa938586..2c55d6e5 100644 --- a/camlib.py +++ b/camlib.py @@ -145,6 +145,17 @@ class Geometry(object): return self.solid_geometry.bounds def flatten(self, geometry=None, reset=True, pathonly=False): + """ + Creates a list of non-iterable linear geometry objects. + Polygons are expanded into its exterior and interiors if specified. + + Results are placed in self.flat_geoemtry + + :param geometry: Shapely type or list or list of list of such. + :param reset: Clears the contents of self.flat_geometry. + :param pathonly: Expands polygons into linear elements. + """ + if geometry is None: geometry = self.solid_geometry @@ -2166,7 +2177,8 @@ class CNCjob(Geometry): """ defaults = { - "zdownrate": None + "zdownrate": None, + "coordinate_format": "X%.4fY%.4f" } def __init__(self, units="in", kind="generic", z_move=0.1, @@ -2240,43 +2252,44 @@ class CNCjob(Geometry): return factor - def generate_from_excellon(self, exobj): - """ - Generates G-code for drilling from Excellon object. - self.gcode becomes a list, each element is a - different job for each tool in the excellon code. - """ - self.kind = "drill" - self.gcode = [] - - t = "G00 X%.4fY%.4f\n" - down = "G01 Z%.4f\n" % self.z_cut - up = "G01 Z%.4f\n" % self.z_move - - for tool in exobj.tools: - - points = [] - - for drill in exobj.drill: - if drill['tool'] == tool: - points.append(drill['point']) - - gcode = self.unitcode[self.units.upper()] + "\n" - gcode += self.absolutecode + "\n" - gcode += self.feedminutecode + "\n" - gcode += "F%.2f\n" % self.feedrate - gcode += "G00 Z%.4f\n" % self.z_move # Move to travel height - gcode += "M03\n" # Spindle start - gcode += self.pausecode + "\n" - - for point in points: - gcode += t % point - gcode += down + up - - gcode += t % (0, 0) - gcode += "M05\n" # Spindle stop - - self.gcode.append(gcode) + # def generate_from_excellon(self, exobj): + # """ + # Generates G-code for drilling from Excellon object. + # self.gcode becomes a list, each element is a + # different job for each tool in the excellon code. + # """ + # self.kind = "drill" + # self.gcode = [] + # + # #t = "G00 X%.4fY%.4f\n" + # t = "G00 " + CNCjob.defaults["coordinate_format"] + "\n" + # down = "G01 Z%.4f\n" % self.z_cut + # up = "G01 Z%.4f\n" % self.z_move + # + # for tool in exobj.tools: + # + # points = [] + # + # for drill in exobj.drill: + # if drill['tool'] == tool: + # points.append(drill['point']) + # + # gcode = self.unitcode[self.units.upper()] + "\n" + # gcode += self.absolutecode + "\n" + # gcode += self.feedminutecode + "\n" + # gcode += "F%.2f\n" % self.feedrate + # gcode += "G00 Z%.4f\n" % self.z_move # Move to travel height + # gcode += "M03\n" # Spindle start + # gcode += self.pausecode + "\n" + # + # for point in points: + # gcode += t % point + # gcode += down + up + # + # gcode += t % (0, 0) + # gcode += "M05\n" # Spindle stop + # + # self.gcode.append(gcode) def generate_from_excellon_by_tool(self, exobj, tools="all"): """ @@ -2290,7 +2303,9 @@ class CNCjob(Geometry): :return: None :rtype: None """ + log.debug("Creating CNC Job from Excellon...") + if tools == "all": tools = [tool for tool in exobj.tools] else: @@ -2304,10 +2319,9 @@ class CNCjob(Geometry): points.append(drill['point']) log.debug("Found %d drills." % len(points)) - #self.kind = "drill" self.gcode = [] - t = "G00 X%.4fY%.4f\n" + t = "G00 " + CNCjob.defaults["coordinate_format"] + "\n" down = "G01 Z%.4f\n" % self.z_cut up = "G01 Z%.4f\n" % self.z_move @@ -2329,71 +2343,71 @@ class CNCjob(Geometry): self.gcode = gcode - def generate_from_geometry(self, geometry, append=True, tooldia=None, tolerance=0): - """ - Generates G-Code from a Geometry object. Stores in ``self.gcode``. - - Algorithm description: - ---------------------- - Follow geometry paths in the order they are being read. No attempt - to optimize. - - :param geometry: Geometry defining the toolpath - :type geometry: Geometry - :param append: Wether to append to self.gcode or re-write it. - :type append: bool - :param tooldia: If given, sets the tooldia property but does - not affect the process in any other way. - :type tooldia: bool - :param tolerance: All points in the simplified object will be within the - tolerance distance of the original geometry. - :return: None - :rtype: None - """ - if tooldia is not None: - self.tooldia = tooldia - - self.input_geometry_bounds = geometry.bounds() - - if not append: - self.gcode = "" - - # Initial G-Code - self.gcode = self.unitcode[self.units.upper()] + "\n" - self.gcode += self.absolutecode + "\n" - self.gcode += self.feedminutecode + "\n" - self.gcode += "F%.2f\n" % self.feedrate - self.gcode += "G00 Z%.4f\n" % self.z_move # Move (up) to travel height - self.gcode += "M03\n" # Spindle start - self.gcode += self.pausecode + "\n" - - # Iterate over geometry and run individual methods - # depending on type - for geo in geometry.solid_geometry: - - if type(geo) == Polygon: - self.gcode += self.polygon2gcode(geo, tolerance=tolerance) - continue - - if type(geo) == LineString or type(geo) == LinearRing: - self.gcode += self.linear2gcode(geo, tolerance=tolerance) - continue - - if type(geo) == Point: - self.gcode += self.point2gcode(geo) - continue - - if type(geo) == MultiPolygon: - for poly in geo: - self.gcode += self.polygon2gcode(poly, tolerance=tolerance) - continue - - log.warning("G-code generation not implemented for %s" % (str(type(geo)))) - - # Finish - self.gcode += "G00 Z%.4f\n" % self.z_move # Stop cutting - self.gcode += "G00 X0Y0\n" - self.gcode += "M05\n" # Spindle stop + # def generate_from_geometry(self, geometry, append=True, tooldia=None, tolerance=0): + # """ + # Generates G-Code from a Geometry object. Stores in ``self.gcode``. + # + # Algorithm description: + # ---------------------- + # Follow geometry paths in the order they are being read. No attempt + # to optimize. + # + # :param geometry: Geometry defining the toolpath + # :type geometry: Geometry + # :param append: Wether to append to self.gcode or re-write it. + # :type append: bool + # :param tooldia: If given, sets the tooldia property but does + # not affect the process in any other way. + # :type tooldia: bool + # :param tolerance: All points in the simplified object will be within the + # tolerance distance of the original geometry. + # :return: None + # :rtype: None + # """ + # if tooldia is not None: + # self.tooldia = tooldia + # + # self.input_geometry_bounds = geometry.bounds() + # + # if not append: + # self.gcode = "" + # + # # Initial G-Code + # self.gcode = self.unitcode[self.units.upper()] + "\n" + # self.gcode += self.absolutecode + "\n" + # self.gcode += self.feedminutecode + "\n" + # self.gcode += "F%.2f\n" % self.feedrate + # self.gcode += "G00 Z%.4f\n" % self.z_move # Move (up) to travel height + # self.gcode += "M03\n" # Spindle start + # self.gcode += self.pausecode + "\n" + # + # # Iterate over geometry and run individual methods + # # depending on type + # for geo in geometry.solid_geometry: + # + # if type(geo) == Polygon: + # self.gcode += self.polygon2gcode(geo, tolerance=tolerance) + # continue + # + # if type(geo) == LineString or type(geo) == LinearRing: + # self.gcode += self.linear2gcode(geo, tolerance=tolerance) + # continue + # + # if type(geo) == Point: + # self.gcode += self.point2gcode(geo) + # continue + # + # if type(geo) == MultiPolygon: + # for poly in geo: + # self.gcode += self.polygon2gcode(poly, tolerance=tolerance) + # continue + # + # log.warning("G-code generation not implemented for %s" % (str(type(geo)))) + # + # # Finish + # self.gcode += "G00 Z%.4f\n" % self.z_move # Stop cutting + # self.gcode += "G00 X0Y0\n" + # self.gcode += "M05\n" # Spindle stop def generate_from_geometry_2(self, geometry, append=True, tooldia=None, tolerance=0): """ @@ -2412,6 +2426,7 @@ class CNCjob(Geometry): assert isinstance(geometry, Geometry) ## Flatten the geometry + # Only linear elements (no polygons) remain. flat_geometry = geometry.flatten(pathonly=True) log.debug("%d paths" % len(flat_geometry)) @@ -2456,11 +2471,14 @@ class CNCjob(Geometry): # deletion will fail. storage.remove(geo) + # If last point in geometry is the nearest + # then reverse coordinates. if list(pt) == list(geo.coords[-1]): - #print "Reversing" geo.coords = list(geo.coords)[::-1] # G-code + # Note: self.linear2gcode() and self.point2gcode() will + # lower and raise the tool every time. if type(geo) == LineString or type(geo) == LinearRing: self.gcode += self.linear2gcode(geo, tolerance=tolerance) elif type(geo) == Point: @@ -2536,7 +2554,7 @@ class CNCjob(Geometry): # Separate codes in line parts = [] - for p in range(n_codes-1): + for p in range(n_codes - 1): parts.append(line[codes_idx[p]:codes_idx[p+1]].strip()) parts.append(line[codes_idx[-1]:].strip()) @@ -2712,55 +2730,55 @@ class CNCjob(Geometry): # TODO: This takes forever. Too much data? self.solid_geometry = cascaded_union([geo['geom'] for geo in self.gcode_parsed]) - def polygon2gcode(self, polygon, tolerance=0): - """ - Creates G-Code for the exterior and all interior paths - of a polygon. - - :param polygon: A Shapely.Polygon - :type polygon: Shapely.Polygon - :param tolerance: All points in the simplified object will be within the - tolerance distance of the original geometry. - :type tolerance: float - :return: G-code to cut along polygon. - :rtype: str - """ - - if tolerance > 0: - target_polygon = polygon.simplify(tolerance) - else: - target_polygon = polygon - - gcode = "" - t = "G0%d X%.4fY%.4f\n" - path = list(target_polygon.exterior.coords) # Polygon exterior - gcode += t % (0, path[0][0], path[0][1]) # Move to first point - - if self.zdownrate is not None: - gcode += "F%.2f\n" % self.zdownrate - gcode += "G01 Z%.4f\n" % self.z_cut # Start cutting - gcode += "F%.2f\n" % self.feedrate - else: - gcode += "G01 Z%.4f\n" % self.z_cut # Start cutting - - for pt in path[1:]: - gcode += t % (1, pt[0], pt[1]) # Linear motion to point - gcode += "G00 Z%.4f\n" % self.z_move # Stop cutting - for ints in target_polygon.interiors: # Polygon interiors - path = list(ints.coords) - gcode += t % (0, path[0][0], path[0][1]) # Move to first point - - if self.zdownrate is not None: - gcode += "F%.2f\n" % self.zdownrate - gcode += "G01 Z%.4f\n" % self.z_cut # Start cutting - gcode += "F%.2f\n" % self.feedrate - else: - gcode += "G01 Z%.4f\n" % self.z_cut # Start cutting - - for pt in path[1:]: - gcode += t % (1, pt[0], pt[1]) # Linear motion to point - gcode += "G00 Z%.4f\n" % self.z_move # Stop cutting - return gcode + # def polygon2gcode(self, polygon, tolerance=0): + # """ + # Creates G-Code for the exterior and all interior paths + # of a polygon. + # + # :param polygon: A Shapely.Polygon + # :type polygon: Shapely.Polygon + # :param tolerance: All points in the simplified object will be within the + # tolerance distance of the original geometry. + # :type tolerance: float + # :return: G-code to cut along polygon. + # :rtype: str + # """ + # + # if tolerance > 0: + # target_polygon = polygon.simplify(tolerance) + # else: + # target_polygon = polygon + # + # gcode = "" + # t = "G0%d X%.4fY%.4f\n" + # path = list(target_polygon.exterior.coords) # Polygon exterior + # gcode += t % (0, path[0][0], path[0][1]) # Move to first point + # + # if self.zdownrate is not None: + # gcode += "F%.2f\n" % self.zdownrate + # gcode += "G01 Z%.4f\n" % self.z_cut # Start cutting + # gcode += "F%.2f\n" % self.feedrate + # else: + # gcode += "G01 Z%.4f\n" % self.z_cut # Start cutting + # + # for pt in path[1:]: + # gcode += t % (1, pt[0], pt[1]) # Linear motion to point + # gcode += "G00 Z%.4f\n" % self.z_move # Stop cutting + # for ints in target_polygon.interiors: # Polygon interiors + # path = list(ints.coords) + # gcode += t % (0, path[0][0], path[0][1]) # Move to first point + # + # if self.zdownrate is not None: + # gcode += "F%.2f\n" % self.zdownrate + # gcode += "G01 Z%.4f\n" % self.z_cut # Start cutting + # gcode += "F%.2f\n" % self.feedrate + # else: + # gcode += "G01 Z%.4f\n" % self.z_cut # Start cutting + # + # for pt in path[1:]: + # gcode += t % (1, pt[0], pt[1]) # Linear motion to point + # gcode += "G00 Z%.4f\n" % self.z_move # Stop cutting + # return gcode def linear2gcode(self, linear, tolerance=0): """ @@ -2781,7 +2799,8 @@ class CNCjob(Geometry): target_linear = linear gcode = "" - t = "G0%d X%.4fY%.4f\n" + #t = "G0%d X%.4fY%.4f\n" + t = "G0%d " + CNCjob.defaults["coordinate_format"] + "\n" path = list(target_linear.coords) gcode += t % (0, path[0][0], path[0][1]) # Move to first point @@ -2799,7 +2818,8 @@ class CNCjob(Geometry): def point2gcode(self, point): gcode = "" - t = "G0%d X%.4fY%.4f\n" + #t = "G0%d X%.4fY%.4f\n" + t = "G0%d " + CNCjob.defaults["coordinate_format"] + "\n" path = list(point.coords) gcode += t % (0, path[0][0], path[0][1]) # Move to first point @@ -2906,7 +2926,7 @@ def arc(center, radius, start, stop, direction, steps_per_circ): :return: The desired arc, as list of tuples :rtype: list """ - # TODO: Resolution should be established by fraction of total length, not angle. + # TODO: Resolution should be established by maximum error from the exact arc. da_sign = {"cw": -1.0, "ccw": 1.0} points = []