From 3459c55da4e1bc345482e412ec3e31a40637cb5a Mon Sep 17 00:00:00 2001 From: Juan Pablo Caram Date: Mon, 20 Jan 2014 13:08:36 -0500 Subject: [PATCH] Excellon to g-code and bed flattening features. --- camlib.py | 254 ++++++++++++++++++++----------- cirkuix.py | 311 +++++++++++++++++++++++++++++++------ cirkuix.ui | 439 +++++++++++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 836 insertions(+), 168 deletions(-) diff --git a/camlib.py b/camlib.py index 9a4d57b6..a7ed07bc 100644 --- a/camlib.py +++ b/camlib.py @@ -129,27 +129,27 @@ class Gerber (Geometry): into dictionary of apertures. """ indexstar = gline.find("*") - indexC = gline.find("C,") - if indexC != -1: # Circle, example: %ADD11C,0.1*% - apid = gline[4:indexC] + indexc = gline.find("C,") + if indexc != -1: # Circle, example: %ADD11C,0.1*% + apid = gline[4:indexc] self.apertures[apid] = {"type": "C", - "size": float(gline[indexC+2:indexstar])} + "size": float(gline[indexc+2:indexstar])} return apid - indexR = gline.find("R,") - if indexR != -1: # Rectangle, example: %ADD15R,0.05X0.12*% - apid = gline[4:indexR] - indexX = gline.find("X") + indexr = gline.find("R,") + if indexr != -1: # Rectangle, example: %ADD15R,0.05X0.12*% + apid = gline[4:indexr] + indexx = gline.find("X") self.apertures[apid] = {"type": "R", - "width": float(gline[indexR+2:indexX]), - "height": float(gline[indexX+1:indexstar])} + "width": float(gline[indexr+2:indexx]), + "height": float(gline[indexx+1:indexstar])} return apid - indexO = gline.find("O,") - if indexO != -1: # Obround - apid = gline[4:indexO] - indexX = gline.find("X") + indexo = gline.find("O,") + if indexo != -1: # Obround + apid = gline[4:indexo] + indexx = gline.find("X") self.apertures[apid] = {"type": "O", - "width": float(gline[indexO+2:indexX]), - "height": float(gline[indexX+1:indexstar])} + "width": float(gline[indexo+2:indexx]), + "height": float(gline[indexx+1:indexstar])} return apid print "WARNING: Aperture not implemented:", gline return None @@ -187,12 +187,12 @@ class Gerber (Geometry): path = [coord(gline, self.digits, self.fraction)] continue - indexD3 = gline.find("D03*") - if indexD3 > 0: # Flash + indexd3 = gline.find("D03*") + if indexd3 > 0: # Flash self.flashes.append({"loc": coord(gline, self.digits, self.fraction), "aperture": current_aperture}) continue - if indexD3 == 0: # Flash? + if indexd3 == 0: # Flash? print "WARNING: Uninplemented flash style:", gline continue @@ -216,16 +216,16 @@ class Gerber (Geometry): continue if gline.find("%FS") != -1: # Format statement - indexX = gline.find("X") - self.digits = int(gline[indexX + 1]) - self.fraction = int(gline[indexX + 2]) + indexx = gline.find("X") + self.digits = int(gline[indexx + 1]) + self.fraction = int(gline[indexx + 2]) continue print "WARNING: Line ignored:", gline if len(path) > 1: # EOF, create shapely LineString if something in path - self.paths.append({"linestring":LineString(path), - "aperture":last_path_aperture}) + self.paths.append({"linestring": LineString(path), + "aperture": last_path_aperture}) def do_flashes(self): """ @@ -318,11 +318,11 @@ class Excellon(Geometry): continue ## Drill - indexX = eline.find("X") - indexY = eline.find("Y") - if indexX != -1 and indexY != -1: - x = float(int(eline[indexX+1:indexY])/10000.0) - y = float(int(eline[indexY+1:-1])/10000.0) + indexx = eline.find("X") + indexy = eline.find("Y") + if indexx != -1 and indexy != -1: + x = float(int(eline[indexx+1:indexy])/10000.0) + y = float(int(eline[indexy+1:-1])/10000.0) self.drills.append({'point': Point((x, y)), 'tool': current_tool}) continue @@ -342,29 +342,28 @@ class Excellon(Geometry): class CNCjob(Geometry): def __init__(self, units="in", kind="generic", z_move=0.1, feedrate=3.0, z_cut=-0.002, tooldia=0.0): - - # Options + """ + + @param units: + @param kind: + @param z_move: + @param feedrate: + @param z_cut: + @param tooldia: + """ + Geometry.__init__(self) self.kind = kind self.units = units self.z_cut = z_cut self.z_move = z_move self.feedrate = feedrate self.tooldia = tooldia - - # Constants self.unitcode = {"in": "G20", "mm": "G21"} self.pausecode = "G04 P1" self.feedminutecode = "G94" self.absolutecode = "G90" - - # Input/Output G-Code self.gcode = "" - - # Bounds of geometry given to CNCjob.generate_from_geometry() self.input_geometry_bounds = None - - # Output generated by CNCjob.create_gcode_geometry() - #self.G_geometry = None self.gcode_parsed = None def generate_from_excellon(self, exobj): @@ -377,13 +376,12 @@ class CNCjob(Geometry): self.gcode = [] t = "G00 X%.4fY%.4f\n" - down = "G01 Z%.4f\n"%self.z_cut - up = "G01 Z%.4f\n"%self.z_move - + down = "G01 Z%.4f\n" % self.z_cut + up = "G01 Z%.4f\n" % self.z_move + for tool in exobj.tools: points = [] - gcode = "" for drill in exobj.drill: if drill['tool'] == tool: @@ -392,79 +390,111 @@ class CNCjob(Geometry): gcode = self.unitcode[self.units] + "\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 += "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 += t % point gcode += down + up - gcode += t%(0, 0) + gcode += t % (0, 0) gcode += "M05\n" # Spindle stop self.gcode.append(gcode) - + + def generate_from_excellon_by_tool(self, exobj, tools="all"): + """ + Creates gcode for this object from an Excellon object + for the specified tools. + @param exobj: Excellon object to process + @type exobj: Excellon + @param tools: Comma separated tool names + @type: tools: str + @return: None + """ + print "Creating CNC Job from Excellon..." + if tools == "all": + tools = [tool for tool in exobj.tools] + else: + tools = [x.strip() for x in tools.split(",")] + tools = filter(lambda y: y in exobj.tools, tools) + print "Tools are:", tools + + points = [] + for drill in exobj.drills: + if drill['tool'] in tools: + points.append(drill['point']) + + print "Found %d drills." % len(points) + #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 + + gcode = self.unitcode[self.units] + "\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: + x, y = point.coords.xy + gcode += t % (x[0], y[0]) + gcode += down + up + + gcode += t % (0, 0) + gcode += "M05\n" # Spindle stop + + self.gcode = gcode + def generate_from_geometry(self, geometry, append=True, tooldia=None): """ - Generates G-Code for geometry (Shapely collection). + Generates G-Code from a Geometry object. """ - if tooldia is None: - tooldia = self.tooldia - else: + if tooldia is not None: self.tooldia = tooldia - self.input_geometry_bounds = geometry.bounds + self.input_geometry_bounds = geometry.bounds() if not append: self.gcode = "" - t = "G0%d X%.4fY%.4f\n" + self.gcode = self.unitcode[self.units] + "\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 to travel height + self.gcode += "F%.2f\n" % self.feedrate + self.gcode += "G00 Z%.4f\n" % self.z_move # Move to travel height self.gcode += "M03\n" # Spindle start self.gcode += self.pausecode + "\n" - for geo in geometry: + for geo in geometry.solid_geometry: if type(geo) == Polygon: - path = list(geo.exterior.coords) # Polygon exterior - self.gcode += t%(0, path[0][0], path[0][1]) # Move to first point - self.gcode += "G01 Z%.4f\n"%self.z_cut # Start cutting - for pt in path[1:]: - self.gcode += t%(1, pt[0], pt[1]) # Linear motion to point - self.gcode += "G00 Z%.4f\n"%self.z_move # Stop cutting - for ints in geo.interiors: # Polygon interiors - path = list(ints.coords) - self.gcode += t%(0, path[0][0], path[0][1]) # Move to first point - self.gcode += "G01 Z%.4f\n"%self.z_cut # Start cutting - for pt in path[1:]: - self.gcode += t%(1, pt[0], pt[1]) # Linear motion to point - self.gcode += "G00 Z%.4f\n"%self.z_move # Stop cutting + self.gcode += self.polygon2gcode(geo) continue if type(geo) == LineString or type(geo) == LinearRing: - path = list(geo.coords) - self.gcode += t%(0, path[0][0], path[0][1]) # Move to first point - self.gcode += "G01 Z%.4f\n"%self.z_cut # Start cutting - for pt in path[1:]: - self.gcode += t%(1, pt[0], pt[1]) # Linear motion to point - self.gcode += "G00 Z%.4f\n"%self.z_move # Stop cutting + self.gcode += self.linear2gcode(geo) continue if type(geo) == Point: - path = list(geo.coords) - self.gcode += t%(0, path[0][0], path[0][1]) # Move to first point - self.gcode += "G01 Z%.4f\n"%self.z_cut # Start cutting - self.gcode += "G00 Z%.4f\n"%self.z_move # Stop cutting + self.gcode += self.point2gcode(geo) continue - - print "WARNING: G-code generation not implemented for %s"%(str(type(geo))) + + if type(geo) == MultiPolygon: + for poly in geo: + self.gcode += self.polygon2gcode(poly) + continue + + print "WARNING: G-code generation not implemented for %s" % (str(type(geo))) - self.gcode += "G00 Z%.4f\n"%self.z_move # Stop cutting + self.gcode += "G00 Z%.4f\n" % self.z_move # Stop cutting self.gcode += "G00 X0Y0\n" self.gcode += "M05\n" # Spindle stop @@ -475,8 +505,10 @@ class CNCjob(Geometry): fast or feedrate speed. """ + # TODO: Make this a parameter steps_per_circ = 20 + # Results go here geometry = [] # TODO: ???? bring this into the class?? @@ -523,8 +555,8 @@ class CNCjob(Geometry): if current['G'] in [2, 3]: # arc center = [gobj['I'] + current['X'], gobj['J'] + current['Y']] radius = sqrt(gobj['I']**2 + gobj['J']**2) - start = arctan2( -gobj['J'], -gobj['I']) - stop = arctan2(-center[1]+y, -center[0]+x) + start = arctan2(-gobj['J'], -gobj['I']) + stop = arctan2(-center[1]+y, -center[0]+x) geometry.append({'geom': arc(center, radius, start, stop, arcdir[current['G']], steps_per_circ), @@ -601,6 +633,49 @@ class CNCjob(Geometry): def create_geometry(self): self.solid_geometry = cascaded_union([geo['geom'] for geo in self.gcode_parsed]) + def polygon2gcode(self, polygon): + """ + Creates G-Code for the exterior and all interior paths + of a polygon. + @param polygon: A Shapely.Polygon + @type polygon: Shapely.Polygon + """ + gcode = "" + t = "G0%d X%.4fY%.4f\n" + path = list(polygon.exterior.coords) # Polygon exterior + gcode += t % (0, path[0][0], path[0][1]) # Move to first point + 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 polygon.interiors: # Polygon interiors + path = list(ints.coords) + gcode += t % (0, path[0][0], path[0][1]) # Move to first point + 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): + gcode = "" + t = "G0%d X%.4fY%.4f\n" + path = list(linear.coords) + gcode += t%(0, path[0][0], path[0][1]) # Move to first point + 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 point2gcode(self, point): + gcode = "" + t = "G0%d X%.4fY%.4f\n" + path = list(point.coords) + gcode += t % (0, path[0][0], path[0][1]) # Move to first point + gcode += "G01 Z%.4f\n" % self.z_cut # Start cutting + gcode += "G00 Z%.4f\n" % self.z_move # Stop cutting + def gparse1b(gtext): """ @@ -653,7 +728,8 @@ def get_bounds(geometry_set): ymin = Inf xmax = -Inf ymax = -Inf - + + print "Getting bounds of:", str(geometry_set) for gs in geometry_set: gxmin, gymin, gxmax, gymax = geometry_set[gs].bounds() xmin = min([xmin, gxmin]) @@ -699,7 +775,7 @@ def arc(center, radius, start, stop, direction, steps_per_circ): return LineString(points) -def clear_poly(poly, tooldia, overlap = 0.1): +def clear_poly(poly, tooldia, overlap=0.1): """ Creates a list of Shapely geometry objects covering the inside of a Shapely.Polygon. Use for removing all the copper in a region diff --git a/cirkuix.py b/cirkuix.py index 2fa6574d..dbccd001 100644 --- a/cirkuix.py +++ b/cirkuix.py @@ -21,15 +21,8 @@ class CirkuixObj: by the user in their respective forms. """ - form_getters = {} - - form_setters = {} - # Instance of the application to which these are related. # The app should set this value. - # TODO: Move the definitions of form_getters and form_setters - # TODO: to their respective classes and use app to reference - # TODO: the builder. app = None def __init__(self, name): @@ -41,11 +34,25 @@ class CirkuixObj: self.kind = None # Override with proper name def setup_axes(self, figure): + """ + 1) Creates axes if they don't exist. 2) Clears axes. 3) Attaches + them to figure if not part of the figure. 4) Sets transparent + background. 5) Sets 1:1 scale aspect ratio. + @param figure: A Matplotlib.Figure on which to add/configure axes. + @type figure: matplotlib.figure.Figure + @return: None + """ if self.axes is None: + print "New axes" self.axes = figure.add_axes([0.05, 0.05, 0.9, 0.9], label=self.options["name"]) elif self.axes not in figure.axes: + print "Clearing and attaching axes" + self.axes.cla() figure.add_axes(self.axes) + else: + print "Clearing Axes" + self.axes.cla() self.axes.patch.set_visible(False) # No background self.axes.set_aspect(1) @@ -62,7 +69,11 @@ class CirkuixObj: self.set_form_item(option) def read_form(self): - for option in self.form_getters: + """ + Reads form into self.options + @rtype : None + """ + for option in self.options: self.read_form_item(option) def build_ui(self): @@ -114,7 +125,7 @@ class CirkuixObj: fname = fkind + "_" + self.kind + "_" + option if fkind == 'entry_text': - self.options[option] = self.app.builder(fname).get_text() + self.options[option] = self.app.builder.get_object(fname).get_text() return if fkind == 'entry_eval': self.options[option] = self.app.get_eval(fname) @@ -127,6 +138,18 @@ class CirkuixObj: return print "Unknown kind of form item:", fkind + def plot(self, figure): + """ + Extend this method! Sets up axes if needed and + clears them. Descendants must do the actual plotting. + """ + # Creates the axes if necessary and sets them up. + self.setup_axes(figure) + + # Clear axes. + # self.axes.cla() + # return + class CirkuixGerber(CirkuixObj, Gerber): """ @@ -149,9 +172,12 @@ class CirkuixGerber(CirkuixObj, Gerber): "cutoutmargin": 0.2, "cutoutgapsize": 0.15, "gaps": "tb", - "noncoppermargin": 0.0 + "noncoppermargin": 0.0, + "bboxmargin": 0.0, + "bboxrounded": False }) + # The 'name' is already in self.form_kinds self.form_kinds.update({ "plot": "cb", "mergepolys": "cb", @@ -161,14 +187,16 @@ class CirkuixGerber(CirkuixObj, Gerber): "cutoutmargin": "entry_eval", "cutoutgapsize": "entry_eval", "gaps": "radio", - "noncoppermargin": "entry_eval" + "noncoppermargin": "entry_eval", + "bboxmargin": "entry_eval", + "bboxrounded": "cb" }) self.radios = {"gaps": {"rb_2tb": "tb", "rb_2lr": "lr", "rb_4": "4"}} self.radios_inv = {"gaps": {"tb": "rb_2tb", "lr": "rb_2lr", "4": "rb_4"}} def plot(self, figure): - self.setup_axes(figure) + CirkuixObj.plot(self, figure) self.create_geometry() @@ -208,15 +236,25 @@ class CirkuixExcellon(CirkuixObj, Excellon): self.options.update({ "plot": True, "solid": False, - "multicolored": False + "multicolored": False, + "drillz": -0.1, + "travelz": 0.1, + "feedrate": 5.0, + "toolselection": "" }) self.form_kinds.update({ "plot": "cb", "solid": "cb", - "multicolored": "cb" + "multicolored": "cb", + "drillz": "entry_eval", + "travelz": "entry_eval", + "feedrate": "entry_eval", + "toolselection": "entry_text" }) + self.tool_cbs = {} + def plot(self, figure): self.setup_axes(figure) self.create_geometry() @@ -229,6 +267,30 @@ class CirkuixExcellon(CirkuixObj, Excellon): x, y = ints.coords.xy self.axes.plot(x, y, 'g-') + def show_tool_chooser(self): + win = Gtk.Window() + box = Gtk.Box(spacing=2) + box.set_orientation(Gtk.Orientation(1)) + win.add(box) + for tool in self.tools: + self.tool_cbs[tool] = Gtk.CheckButton(label=tool+": "+self.tools[tool]) + box.pack_start(self.tool_cbs[tool], False, False, 1) + button = Gtk.Button(label="Accept") + box.pack_start(button, False, False, 1) + win.show_all() + + def on_accept(widget): + win.destroy() + tool_list = [] + for tool in self.tool_cbs: + if self.tool_cbs[tool].get_active(): + tool_list.append(tool) + self.options["toolselection"] = ", ".join(tool_list) + self.to_form() + + button.connect("activate", on_accept) + button.connect("clicked", on_accept) + class CirkuixCNCjob(CirkuixObj, CNCjob): """ @@ -245,7 +307,7 @@ class CirkuixCNCjob(CirkuixObj, CNCjob): self.options.update({ "plot": True, "solid": False, - "multicolored": "cb", + "multicolored": False, "tooldia": 0.4/25.4 # 0.4mm in inches }) @@ -259,6 +321,7 @@ class CirkuixCNCjob(CirkuixObj, CNCjob): def plot(self, figure): self.setup_axes(figure) self.plot2(self.axes, tooldia=self.options["tooldia"]) + app.canvas.queue_draw() class CirkuixGeometry(CirkuixObj, Geometry): @@ -279,8 +342,10 @@ class CirkuixGeometry(CirkuixObj, Geometry): "cutz": -0.002, "travelz": 0.1, "feedrate": 5.0, + "cnctooldia": 0.4/25.4, "painttooldia": 0.0625, - "paintoverlap": 0.15 + "paintoverlap": 0.15, + "paintmargin": 0.01 }) self.form_kinds.update({ @@ -290,13 +355,20 @@ class CirkuixGeometry(CirkuixObj, Geometry): "cutz": "entry_eval", "travelz": "entry_eval", "feedrate": "entry_eval", + "cnctooldia": "entry_eval", "painttooldia": "entry_eval", - "paintoverlap": "entry_eval" + "paintoverlap": "entry_eval", + "paintmargin": "entry_eval" }) def plot(self, figure): self.setup_axes(figure) + try: + _ = iter(self.solid_geometry) + except TypeError: + self.solid_geometry = [self.solid_geometry] + for geo in self.solid_geometry: if type(geo) == Polygon: @@ -312,6 +384,17 @@ class CirkuixGeometry(CirkuixObj, Geometry): self.axes.plot(x, y, 'r-') continue + if type(geo) == MultiPolygon: + for poly in geo: + x, y = poly.exterior.coords.xy + self.axes.plot(x, y, 'r-') + for ints in poly.interiors: + x, y = ints.coords.xy + self.axes.plot(x, y, 'r-') + continue + + print "WARNING: Did not plot:", str(type(geo)) + ######################################## ## App ## @@ -332,9 +415,7 @@ class App: GObject.threads_init() #Gdk.threads_init() - ######################################## - ## GUI ## - ######################################## + ## GUI ## self.gladefile = "cirkuix.ui" self.builder = Gtk.Builder() self.builder.add_from_file(self.gladefile) @@ -359,9 +440,7 @@ class App: self.setup_component_viewer() self.setup_component_editor() - ######################################## - ## DATA ## - ######################################## + ## DATA ## self.setup_obj_classes() self.stuff = {} # CirkuixObj's by name self.mouse = None # Mouse coordinates over plot @@ -634,12 +713,107 @@ class App: ######################################## ## EVENT HANDLERS ## ######################################## + def on_generate_gerber_bounding_box(self, widget): + gerber = self.stuff[self.selected_item_name] + gerber.read_form() + name = self.selected_item_name + "_bbox" + + def geo_init(geo_obj, app_obj): + assert isinstance(geo_obj, CirkuixGeometry) + bounding_box = gerber.solid_geometry.envelope.buffer(gerber.options["bboxmargin"]) + if not gerber.options["bboxrounded"]: + bounding_box = bounding_box.envelope + geo_obj.solid_geometry = bounding_box + + self.new_object("geometry", name, geo_init) + + def on_update_plot(self, widget): + """ + Callback for button on form for all kinds of objects. + Re-plot the current object only. + @param widget: The widget from which this was called. + @return: None + """ + self.stuff[self.selected_item_name].read_form() + self.stuff[self.selected_item_name].plot(self.figure) + + def on_generate_excellon_cncjob(self, widget): + """ + Callback for button active/click on Excellon form to + create a CNC Job for the Excellon file. + @param widget: The widget from which this was called. + @return: None + """ + + job_name = self.selected_item_name + "_cnc" + excellon = self.stuff[self.selected_item_name] + assert isinstance(excellon, CirkuixExcellon) + excellon.read_form() + + # Object initialization function for app.new_object() + def job_init(job_obj, app_obj): + excellon_ = self.stuff[self.selected_item_name] + assert isinstance(excellon_, CirkuixExcellon) + assert isinstance(job_obj, CirkuixCNCjob) + + GLib.idle_add(lambda: app_obj.set_progress_bar(0.2, "Creating CNC Job...")) + job_obj.z_cut = excellon_.options["drillz"] + job_obj.z_move = excellon_.options["travelz"] + job_obj.feedrate = excellon_.options["feedrate"] + # There could be more than one drill size... + # job_obj.tooldia = # TODO: duplicate variable! + # job_obj.options["tooldia"] = + job_obj.generate_from_excellon_by_tool(excellon_, excellon_.options["toolselection"]) + + GLib.idle_add(lambda: app_obj.set_progress_bar(0.5, "Parsing G-Code...")) + job_obj.gcode_parse() + + GLib.idle_add(lambda: app_obj.set_progress_bar(0.6, "Creating New Geometry...")) + job_obj.create_geometry() + + GLib.idle_add(lambda: app_obj.set_progress_bar(0.8, "Plotting...")) + + # To be run in separate thread + def job_thread(app_obj): + app_obj.new_object("cncjob", job_name, job_init) + GLib.idle_add(lambda: app_obj.set_progress_bar(1.0, "Done!")) + GLib.timeout_add_seconds(1, lambda: app_obj.set_progress_bar(0.0, "")) + + # Start the thread + t = threading.Thread(target=job_thread, args=(self,)) + t.daemon = True + t.start() + + def on_excellon_tool_choose(self, widget): + """ + Callback for button on Excellon form to open up a window for + selecting tools. + @param widget: The widget from which this was called. + @return: None + """ + excellon = self.stuff[self.selected_item_name] + assert isinstance(excellon, CirkuixExcellon) + excellon.show_tool_chooser() + + def on_entry_eval_activate(self, widget): + self.on_eval_update(widget) + obj = self.stuff[self.selected_item_name] + assert isinstance(obj, CirkuixObj) + obj.read_form() + def on_gerber_generate_noncopper(self, widget): + """ + Callback for button on Gerber form to create a geometry object + with polygons covering the area without copper or negative of the + Gerber. + @param widget: The widget from which this was called. + @return: None + """ name = self.selected_item_name + "_noncopper" def geo_init(geo_obj, app_obj): assert isinstance(geo_obj, CirkuixGeometry) - gerber = app_obj.stuff[self.selected_item_name] + gerber = app_obj.stuff[app_obj.selected_item_name] assert isinstance(gerber, CirkuixGerber) gerber.read_form() bounding_box = gerber.solid_geometry.envelope.buffer(gerber.options["noncoppermargin"]) @@ -650,6 +824,12 @@ class App: self.new_object("geometry", name, geo_init) def on_gerber_generate_cutout(self, widget): + """ + Callback for button on Gerber form to create geometry with lines + for cutting off the board. + @param widget: The widget from which this was called. + @return: None + """ name = self.selected_item_name + "_cutout" def geo_init(geo_obj, app_obj): @@ -696,11 +876,18 @@ class App: Modifies the content of a Gtk.Entry by running eval() on its contents and puting it back as a string. + @param widget: The widget from which this was called. + @return: None """ # TODO: error handling here widget.set_text(str(eval(widget.get_text()))) def on_generate_isolation(self, widget): + """ + Callback for button on Gerber form to create isolation routing geometry. + @param widget: The widget from which this was called. + @return: None + """ print "Generating Isolation Geometry:" iso_name = self.selected_item_name + "_iso" @@ -714,45 +901,75 @@ class App: self.new_object("geometry", iso_name, iso_init) def on_generate_cncjob(self, widget): + """ + Callback for button on geometry form to generate CNC job. + @param widget: The widget from which this was called. + @return: None + """ print "Generating CNC job" - job_name = self.selected_item_name + "_cnc" + # Object initialization function for app.new_object() def job_init(job_obj, app_obj): - # TODO: Object must be updated on form change and the options - # TODO: read from the object. - z_cut = app_obj.get_eval("entry_eval_geometry_cutz") - z_move = app_obj.get_eval("entry_eval_geometry_travelz") - feedrate = app_obj.get_eval("entry_eval_geometry_feedrate") - - geometry = app_obj.stuff[app_obj.selected_item_name] assert isinstance(job_obj, CirkuixCNCjob) - job_obj.z_cut = z_cut - job_obj.z_move = z_move - job_obj.feedrate = feedrate - job_obj.generate_from_geometry(geometry.solid_geometry) + geometry = app_obj.stuff[app_obj.selected_item_name] + assert isinstance(geometry, CirkuixGeometry) + geometry.read_form() + + GLib.idle_add(lambda: app_obj.set_progress_bar(0.2, "Creating CNC Job...")) + job_obj.z_cut = geometry.options["cutz"] + job_obj.z_move = geometry.options["travelz"] + job_obj.feedrate = geometry.options["feedrate"] + job_obj.options["tooldia"] = geometry.options["cnctooldia"] + + GLib.idle_add(lambda: app_obj.set_progress_bar(0.4, "Analyzing Geometry...")) + job_obj.generate_from_geometry(geometry) + + GLib.idle_add(lambda: app_obj.set_progress_bar(0.5, "Parsing G-Code...")) job_obj.gcode_parse() + + GLib.idle_add(lambda: app_obj.set_progress_bar(0.6, "Creating New Geometry...")) job_obj.create_geometry() - self.new_object("cncjob", job_name, job_init) + GLib.idle_add(lambda: app_obj.set_progress_bar(0.8, "Plotting...")) + + # To be run in separate thread + def job_thread(app_obj): + app_obj.new_object("cncjob", job_name, job_init) + GLib.idle_add(lambda: app_obj.set_progress_bar(1.0, "Done!")) + GLib.timeout_add_seconds(1, lambda: app_obj.set_progress_bar(0.0, "")) + + # Start the thread + t = threading.Thread(target=job_thread, args=(self,)) + t.daemon = True + t.start() def on_generate_paintarea(self, widget): + """ + Callback for button on geometry form. + Subscribes to the "Click on plot" event and continues + after the click. Finds the polygon containing + the clicked point and runs clear_poly() on it, resulting + in a new CirkuixGeometry object. + """ self.info("Click inside the desired polygon.") geo = self.stuff[self.selected_item_name] geo.read_form() tooldia = geo.options["painttooldia"] overlap = geo.options["paintoverlap"] + # To be called after clicking on the plot. def doit(event): self.plot_click_subscribers.pop("generate_paintarea") self.info("") point = [event.xdata, event.ydata] poly = find_polygon(geo.solid_geometry, point) + # Initializes the new geometry object def gen_paintarea(geo_obj, app_obj): assert isinstance(geo_obj, CirkuixGeometry) assert isinstance(app_obj, App) - cp = clear_poly(poly, tooldia, overlap) + cp = clear_poly(poly.buffer(-geo.options["paintmargin"]), tooldia, overlap) geo_obj.solid_geometry = cp name = self.selected_item_name + "_paint" @@ -760,12 +977,6 @@ class App: self.plot_click_subscribers["generate_paintarea"] = doit - def on_cncjob_tooldia_activate(self, widget): - job = self.stuff[self.selected_item_name] - tooldia = self.get_eval("entry_eval_cncjob_tooldia") - job.tooldia = tooldia - print "Changing tool diameter to:", tooldia - def on_cncjob_exportgcode(self, widget): def on_success(self, filename): cncjob = self.stuff[self.selected_item_name] @@ -840,10 +1051,10 @@ class App: Gtk.main_quit() def file_chooser_action(self, on_success): - ''' + """ Opens the file chooser and runs on_success on a separate thread upon completion of valid file choice. - ''' + """ dialog = Gtk.FileChooserDialog("Please choose a file", self.window, Gtk.FileChooserAction.OPEN, (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, @@ -979,7 +1190,7 @@ class App: for subscriber in self.plot_click_subscribers: self.plot_click_subscribers[subscriber](event) - except: + except Exception, e: print "Outside plot!" def on_zoom_in(self, event): @@ -1045,15 +1256,15 @@ class App: def on_key_over_plot(self, event): print 'you pressed', event.key, event.xdata, event.ydata - if event.key == '1': # 1 + if event.key == '1': # 1 self.on_zoom_fit(None) return - if event.key == '2': # 2 + if event.key == '2': # 2 self.zoom(1/1.5, self.mouse) return - if event.key == '3': # 3 + if event.key == '3': # 3 self.zoom(1.5, self.mouse) return diff --git a/cirkuix.ui b/cirkuix.ui index c900cf21..08b39ab6 100644 --- a/cirkuix.ui +++ b/cirkuix.ui @@ -94,6 +94,7 @@ True False + 5 0 3 Plot Options: @@ -179,7 +180,7 @@ True True - + False @@ -194,6 +195,21 @@ 6 + + + Update Plot + True + True + True + + + + + False + True + 7 + + True @@ -209,7 +225,7 @@ False True - 7 + 8 @@ -224,12 +240,9 @@ False True - 8 + 9 - - - @@ -375,16 +388,206 @@ - + + Update Plot + True + True + True + + + + + False + True + 6 + - + + True + False + 5 + 0 + 3 + Create CNC Job: + + + + + + False + True + 7 + - + + True + False + 2 + 4 + + + True + True + + True + + + + 1 + 0 + 1 + 1 + + + + + True + True + + True + + + + 1 + 1 + 1 + 1 + + + + + True + True + + True + + + + 1 + 2 + 1 + 1 + + + + + True + False + 1 + Drill Z: + + + 0 + 0 + 1 + 1 + + + + + True + False + 1 + Travel Z: + + + 0 + 1 + 1 + 1 + + + + + True + False + 1 + Feed rate: + + + 0 + 2 + 1 + 1 + + + + + True + False + 1 + Tools: + + + 0 + 3 + 1 + 1 + + + + + True + False + + + True + True + + True + + + False + True + 0 + + + + + Choose + True + True + True + + + + + False + True + 1 + + + + + 1 + 3 + 1 + 1 + + + + + False + True + 8 + - + + Generate + True + True + True + + + + + False + True + 9 + @@ -531,6 +734,21 @@ 5 + + + Update Plot + True + True + True + + + + + False + True + 6 + + True @@ -546,7 +764,7 @@ False True - 6 + 7 @@ -561,6 +779,7 @@ True True + 1 @@ -575,6 +794,7 @@ True True + 1 @@ -589,6 +809,7 @@ True True + 1 @@ -639,11 +860,40 @@ 1 + + + True + False + 1 + Tool diam: + + + 0 + 3 + 1 + 1 + + + + + True + True + + True + + + + 1 + 3 + 1 + 1 + + False True - 7 + 8 @@ -658,7 +908,7 @@ False True - 8 + 9 @@ -676,7 +926,7 @@ False True - 9 + 10 @@ -705,6 +955,7 @@ True True + 1 @@ -733,6 +984,7 @@ True True + 1 @@ -741,11 +993,40 @@ 1 + + + True + False + 1 + Margin: + + + 0 + 2 + 1 + 1 + + + + + True + True + + True + + + + 1 + 2 + 1 + 1 + + False True - 10 + 11 @@ -760,15 +1041,12 @@ False True - 11 + 12 - - - @@ -854,6 +1132,7 @@ True False + 5 0 3 Plot Options: @@ -929,6 +1208,21 @@ 6 + + + Update Plot + True + True + True + + + + + False + True + 7 + + True @@ -944,7 +1238,7 @@ False True - 7 + 8 @@ -990,7 +1284,7 @@ False True - 8 + 9 @@ -1005,7 +1299,7 @@ False True - 9 + 10 @@ -1023,7 +1317,7 @@ False True - 10 + 11 @@ -1171,7 +1465,7 @@ False True - 11 + 12 @@ -1186,7 +1480,7 @@ False True - 12 + 13 @@ -1204,7 +1498,7 @@ False True - 13 + 14 @@ -1246,7 +1540,7 @@ False True - 14 + 15 @@ -1261,11 +1555,98 @@ False True - 15 + 16 - + + True + False + 5 + 0 + 3 + Bounding box: + + + + + + False + True + 17 + + + + + True + False + 4 + 4 + 1 + + + True + False + 1 + Boundary margin: + + + False + True + 0 + + + + + True + True + + 14 + True + + + + False + True + 1 + + + + + False + True + 18 + + + + + Rounded corners + True + True + False + 0 + True + + + False + True + 19 + + + + + Generate Bounding Box + True + True + True + + + + + False + True + 20 +