diff --git a/camlib.py b/camlib.py index 442c9d6..f6f8e3c 100644 --- a/camlib.py +++ b/camlib.py @@ -1,4 +1,4 @@ -import cairo +#import cairo #from string import * #from math import * @@ -11,7 +11,7 @@ from numpy import arctan2, Inf, array, sqrt, pi, ceil, sin, cos from matplotlib.figure import Figure # See: http://toblerity.org/shapely/manual.html -from shapely.geometry import Polygon, LineString, Point +from shapely.geometry import Polygon, LineString, Point, LinearRing from shapely.geometry import MultiPoint, MultiPolygon from shapely.geometry import box as shply_box from shapely.ops import cascaded_union @@ -41,7 +41,11 @@ class Geometry: if self.solid_geometry == None: print "Warning: solid_geometry not computed yet." return (0,0,0,0) - return self.solid_geometry.bounds + + if type(self.solid_geometry) == list: + return cascaded_union(self.solid_geometry).bounds + else: + return self.solid_geometry.bounds def size(self): ''' @@ -62,7 +66,7 @@ class Geometry: ''' if boundary == None: boundary = self.solid_geometry.envelope - return boundary.difference(g.solid_geometry) + return boundary.difference(self.solid_geometry) def clear_polygon(self, polygon, tooldia, overlap = 0.15): ''' @@ -340,9 +344,10 @@ class Excellon(Geometry): self.solid_geometry.append(poly) self.solid_geometry = cascaded_union(self.solid_geometry) -class CNCjob: +class CNCjob(Geometry): def __init__(self, units="in", kind="generic", z_move = 0.1, feedrate = 3.0, z_cut = -0.002): + # Options self.kind = kind self.units = units @@ -366,7 +371,8 @@ class CNCjob: self.tooldia = 0 # Output generated by CNCjob.create_gcode_geometry() - self.G_geometry = None + #self.G_geometry = None + self.gcode_parsed = None def generate_from_excellon(self, exobj): ''' @@ -447,7 +453,7 @@ class CNCjob: self.gcode += "G00 Z%.4f\n"%self.z_move # Stop cutting continue - if type(geo) == LineString or type(geo) == LineRing: + 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 @@ -469,7 +475,7 @@ class CNCjob: self.gcode += "G00 X0Y0\n" self.gcode += "M05\n" # Spindle stop - def create_gcode_geometry(self): + def gcode_parse(self): steps_per_circ = 20 ''' G-Code parser (from self.gcode). Generates dictionary with @@ -534,7 +540,8 @@ class CNCjob: for code in gobj: current[code] = gobj[code] - self.G_geometry = geometry + #self.G_geometry = geometry + self.gcode_parsed = geometry return geometry def plot(self, tooldia=None, dpi=75, margin=0.1, @@ -555,7 +562,7 @@ class CNCjob: ax.set_ylim(ymin-margin, ymax+margin) if tooldia == 0: - for geo in self.G_geometry: + for geo in self.gcode_parsed: linespec = '--' linecolor = color[geo['kind'][0]][1] if geo['kind'][0] == 'C': @@ -563,7 +570,7 @@ class CNCjob: x, y = geo['geom'].coords.xy ax.plot(x, y, linespec, color=linecolor) else: - for geo in self.G_geometry: + for geo in self.gcode_parsed: poly = geo['geom'].buffer(tooldia/2.0) patch = PolygonPatch(poly, facecolor=color[geo['kind'][0]][0], edgecolor=color[geo['kind'][0]][1], @@ -576,20 +583,13 @@ class CNCjob: color={"T":["#F0E24D", "#B5AB3A"], "C":["#5E6CFF", "#4650BD"]}, alpha={"T":0.3, "C":1.0}): ''' - Plots the G-code job onto the given axes + Plots the G-code job onto the given axes. ''' if tooldia == None: tooldia = self.tooldia - - #fig = Figure(dpi=dpi) - #ax = fig.add_subplot(111) - #ax.set_aspect(1) - #xmin, ymin, xmax, ymax = self.input_geometry_bounds - #ax.set_xlim(xmin-margin, xmax+margin) - #ax.set_ylim(ymin-margin, ymax+margin) if tooldia == 0: - for geo in self.G_geometry: + for geo in self.gcode_parsed: linespec = '--' linecolor = color[geo['kind'][0]][1] if geo['kind'][0] == 'C': @@ -597,13 +597,17 @@ class CNCjob: x, y = geo['geom'].coords.xy axes.plot(x, y, linespec, color=linecolor) else: - for geo in self.G_geometry: + for geo in self.gcode_parsed: poly = geo['geom'].buffer(tooldia/2.0) patch = PolygonPatch(poly, facecolor=color[geo['kind'][0]][0], edgecolor=color[geo['kind'][0]][1], alpha=alpha[geo['kind'][0]], zorder=2) axes.add_patch(patch) + def create_geometry(self): + self.solid_geometry = cascaded_union([geo['geom'] for geo in self.gcode_parsed]) + + def gparse1b(gtext): ''' @@ -650,21 +654,18 @@ def gparse1b(gtext): gcmds.append(cmds) return gcmds -def get_bounds(geometry_sets): +def get_bounds(geometry_set): xmin = Inf ymin = Inf xmax = -Inf ymax = -Inf - #geometry_sets = [self.gerbers, self.excellons] - - for gs in geometry_sets: - for g in gs: - gxmin, gymin, gxmax, gymax = g.solid_geometry.bounds - xmin = min([xmin, gxmin]) - ymin = min([ymin, gymin]) - xmax = max([xmax, gxmax]) - ymax = max([ymax, gymax]) + for gs in geometry_set: + gxmin, gymin, gxmax, gymax = geometry_set[gs].bounds() + xmin = min([xmin, gxmin]) + ymin = min([ymin, gymin]) + xmax = max([xmax, gxmax]) + ymax = max([ymax, gymax]) return [xmin, ymin, xmax, ymax] diff --git a/camlib.pyc b/camlib.pyc index c02cf4a..c70f7ce 100644 Binary files a/camlib.pyc and b/camlib.pyc differ diff --git a/cirkuix.py b/cirkuix.py index b2f4d85..236eeeb 100644 --- a/cirkuix.py +++ b/cirkuix.py @@ -13,18 +13,92 @@ from matplotlib.backends.backend_gtk3agg import FigureCanvasGTK3Agg as FigureCan from camlib import * +class CirkuixObj: + def __init__(self, name, kind): + self.name = name + self.kind = kind + +class CirkuixGerber(CirkuixObj, Gerber): + def __init__(self, name): + Gerber.__init__(self) + CirkuixObj.__init__(self, name, "gerber") + self.fields = [{"name":"plot", + "type":bool, + "value":True, + "get":None, + "set":None, + "onchange":None}, + {}] + +class CirkuixExcellon(CirkuixObj, Excellon): + def __init__(self, name): + Excellon.__init__(self) + CirkuixObj.__init__(self, name, "excellon") + self.options = {"plot": True, + "solid": False, + "multicolored": False} + +class CirkuixCNCjob(CirkuixObj, CNCjob): + def __init__(self, name): + CNCjob.__init__(self) + CirkuixObj.__init__(self, name, "cncjob") + self.options = {"plot": True} + +class CirkuixGeometry(CirkuixObj, Geometry): + def __init__(self, name): + CirkuixObj.__init__(self, name, "geometry") + self.options = {"plot": True, + "solid": False, + "multicolored": False} + +class CirkuixObjForm: + def __init__(self, container, Cobj): + self.Cobj = Cobj + self.container = container + self.fields = {} + + def populate(self): + return + + def save(self): + return + +#class CirkuixGerberForm(CirkuixObjForm) + + +def get_entry_text(entry): + return entry.get_text() + +def get_entry_int(entry): + return int(entry.get_text()) + +def get_entry_float(entry): + return float(entry.get_text()) + +def get_entry_eval(entry): + return eval(entry.get_text) + +getters = {"entry_text":get_entry_text, + "entry_int":get_entry_int, + "entry_float":get_entry_float, + "entry_eval":get_entry_eval} + +setters = {"entry"} + class App: def __init__(self): ######################################## ## GUI ## - ######################################## + ######################################## self.gladefile = "cirkuix.ui" self.builder = Gtk.Builder() self.builder.add_from_file(self.gladefile) self.window = self.builder.get_object("window1") + self.window.set_title("Cirkuix") self.positionLabel = self.builder.get_object("label3") self.grid = self.builder.get_object("grid1") + self.notebook = self.builder.get_object("notebook1") ## Event handling ## self.builder.connect_signals(self) @@ -33,34 +107,36 @@ class App: self.figure = None self.axes = None self.canvas = None - self.mplpaint() + self.plot_setup() + self.setup_component_viewer() + self.setup_component_editor() ######################################## ## DATA ## ######################################## - self.gerbers = [] - self.excellons = [] - self.cncjobs = [] + self.stuff = {} # CirkuixObj's by name self.mouse = None + # What is selected by the user. It is + # a key if self.stuff + self.selected_item_name = None + ######################################## ## START ## ######################################## self.window.show_all() Gtk.main() - def mplpaint(self): + def plot_setup(self): self.figure = Figure(dpi=50) - #self.axes = self.figure.add_subplot(111) self.axes = self.figure.add_axes([0.05,0.05,0.9,0.9]) self.axes.set_aspect(1) #t = arange(0.0,5.0,0.01) #s = sin(2*pi*t) #self.axes.plot(t,s) self.axes.grid() - #a.patch.set_visible(False) Background of the axes self.figure.patch.set_visible(False) self.canvas = FigureCanvas(self.figure) # a Gtk.DrawingArea @@ -76,11 +152,13 @@ class App: self.canvas.mpl_connect('key_press_event', self.on_key_over_plot) self.canvas.mpl_connect('scroll_event', self.on_scroll_over_plot) - #self.builder.get_object("viewport2").add(self.canvas) self.grid.attach(self.canvas,0,0,600,400) - #self.builder.get_object("scrolledwindow1").add(self.canvas) def zoom(self, factor, center=None): + ''' + Zooms the plot by factor around a given + center point. Takes care of re-drawing. + ''' xmin, xmax = self.axes.get_xlim() ymin, ymax = self.axes.get_ylim() width = xmax-xmin @@ -129,6 +207,8 @@ class App: for ints in poly.interiors: x, y = ints.coords.xy self.axes.plot(x, y, linespec) + + self.canvas.queue_draw() def plot_excellon(self, excellon): excellon.create_geometry() @@ -140,31 +220,152 @@ class App: for ints in geo.interiors: x, y = ints.coords.xy self.axes.plot(x, y, 'g-') + + self.canvas.queue_draw() def plot_cncjob(self, job): - job.create_gcode_geometry() + #job.gcode_parse() tooldia_text = self.builder.get_object("entry_tooldia").get_text() tooldia_val = eval(tooldia_text) job.plot2(self.axes, tooldia=tooldia_val) - return - - def file_chooser_action(self, on_success): - dialog = Gtk.FileChooserDialog("Please choose a file", self.window, - Gtk.FileChooserAction.OPEN, - (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, - Gtk.STOCK_OPEN, Gtk.ResponseType.OK)) - response = dialog.run() - if response == Gtk.ResponseType.OK: - on_success(self, dialog.get_filename()) - elif response == Gtk.ResponseType.CANCEL: - print("Cancel clicked") - dialog.destroy() + self.canvas.queue_draw() + def setup_component_viewer(self): + ''' + List or Tree where whatever has been loaded or created is + displayed. + ''' + self.store = Gtk.ListStore(str) + self.tree = Gtk.TreeView(self.store) + select = self.tree.get_selection() + self.signal_id = select.connect("changed", self.on_tree_selection_changed) + renderer = Gtk.CellRendererText() + column = Gtk.TreeViewColumn("Title", renderer, text=0) + self.tree.append_column(column) + self.builder.get_object("notebook1").append_page(self.tree, Gtk.Label("Project")) + + def setup_component_editor(self): + box1 = Gtk.Box(Gtk.Orientation.VERTICAL) + label1 = Gtk.Label("Choose an item from Project") + box1.pack_start(label1, False, False, 1) + self.builder.get_object("notebook1").append_page(box1, Gtk.Label("Selection")) + + def build_list(self): + self.store.clear() + for key in self.stuff: + self.store.append([key]) + + def build_gerber_ui(self): + print "build_gerber_ui()" + osw = self.builder.get_object("offscrwindow_gerber") + box1 = self.builder.get_object("box_gerber") + osw.remove(box1) + self.notebook.append_page(box1, Gtk.Label("Selection")) + gerber = self.stuff[self.selected_item_name] + entry_name = self.builder.get_object("entry_gerbername") + entry_name.set_text(self.selected_item_name) + entry_name.connect("activate", self.on_activate_name) + box1.show() + + def build_excellon_ui(self): + print "build_excellon_ui()" + osw = self.builder.get_object("offscrwindow_excellon") + box1 = self.builder.get_object("box_excellon") + osw.remove(box1) + self.notebook.append_page(box1, Gtk.Label("Selection")) + entry_name = self.builder.get_object("entry_excellonname") + entry_name.set_text(self.selected_item_name) + entry_name.connect("activate", self.on_activate_name) + box1.show() + + def build_cncjob_ui(self): + print "build_cncjob_ui()" + osw = self.builder.get_object("offscrwindow_cncjob") + box1 = self.builder.get_object("box_cncjob") + osw.remove(box1) + self.notebook.append_page(box1, Gtk.Label("Selection")) + entry_name = self.builder.get_object("entry_cncjobname") + entry_name.set_text(self.selected_item_name) + entry_name.connect("activate", self.on_activate_name) + box1.show() + + def plot_all(self): + self.clear_plots() + plotter = {"gerber":self.plot_gerber, + "excellon":self.plot_excellon, + "cncjob":self.plot_cncjob} + + for i in self.stuff: + kind = self.stuff[i].kind + plotter[kind](self.stuff[i]) + + self.on_zoom_fit(None) + self.axes.grid() + self.canvas.queue_draw() + + def clear_plots(self): + self.axes.cla() + self.canvas.queue_draw() + ######################################## ## EVENT HANDLERS ## ######################################## - + def on_delete(self, widget): + self.stuff.pop(self.selected_item_name) + + #self.tree.get_selection().disconnect(self.signal_id) + self.build_list() # Update the items list + #self.signal_id = self.tree.get_selection().connect( + # "changed", self.on_tree_selection_changed) + + self.plot_all() + #self.notebook.set_current_page(1) + + def on_replot(self, widget): + self.plot_all() + + def on_clear_plots(self, widget): + self.clear_plots() + + def on_activate_name(self, entry): + ''' + Hitting 'Enter' after changing the name of an item + updates the item dictionary and re-builds the item list. + ''' + print "Changing name" + self.tree.get_selection().disconnect(self.signal_id) + new_name = entry.get_text() # Get from form + self.stuff[new_name] = self.stuff.pop(self.selected_item_name) # Update dictionary + self.selected_item_name = new_name # Update selection name + self.build_list() # Update the items list + self.signal_id = self.tree.get_selection().connect( + "changed", self.on_tree_selection_changed) + + def on_tree_selection_changed(self, selection): + model, treeiter = selection.get_selected() + + + if treeiter != None: + print "You selected", model[treeiter][0] + else: + return # TODO: Clear "Selected" page + + self.selected_item_name = model[treeiter][0] + # Remove the current selection page + # from the notebook + # TODO: Assuming it was last page or #2. Find the right page + self.builder.get_object("notebook1").remove_page(2) + + # Determine the kind of item selected + kind = self.stuff[model[treeiter][0]].kind + + # Build the UI + builder = {"gerber": self.build_gerber_ui, + "excellon": self.build_excellon_ui, + "cncjob": self.build_cncjob_ui} + builder[kind]() + def on_filequit(self, param): print "quit from menu" self.window.destroy() @@ -175,31 +376,61 @@ class App: self.window.destroy() Gtk.main_quit() + def file_chooser_action(self, on_success): + ''' + Opens the file chooser and runs on_success + upon completion of valid file choice. + ''' + dialog = Gtk.FileChooserDialog("Please choose a file", self.window, + Gtk.FileChooserAction.OPEN, + (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, + Gtk.STOCK_OPEN, Gtk.ResponseType.OK)) + response = dialog.run() + if response == Gtk.ResponseType.OK: + on_success(self, dialog.get_filename()) + elif response == Gtk.ResponseType.CANCEL: + print("Cancel clicked") + dialog.destroy() + def on_fileopengerber(self, param): def on_success(self, filename): - gerber = Gerber() + name = filename.split('/')[-1].split('\\')[-1] + gerber = CirkuixGerber(name) gerber.parse_file(filename) - self.gerbers.append(gerber) + self.store.append([name]) + #self.gerbers.append(gerber) + self.stuff[name] = gerber self.plot_gerber(gerber) + self.on_zoom_fit(None) self.file_chooser_action(on_success) def on_fileopenexcellon(self, param): def on_success(self, filename): - excellon = Excellon() + name = filename.split('/')[-1].split('\\')[-1] + excellon = CirkuixExcellon(name) excellon.parse_file(filename) - self.excellons.append(excellon) + self.store.append([name]) + #self.excellons.append(excellon) + self.stuff[name] = excellon self.plot_excellon(excellon) + self.on_zoom_fit(None) self.file_chooser_action(on_success) def on_fileopengcode(self, param): def on_success(self, filename): + name = filename.split('/')[-1].split('\\')[-1] f = open(filename) gcode = f.read() f.close() - job = CNCjob() + job = CirkuixCNCjob(name) job.gcode = gcode - self.cncjobs.append(job) + job.gcode_parse() + job.create_geometry() + self.store.append([name]) + #self.cncjobs.append(job) + self.stuff[name] = job self.plot_cncjob(job) + self.on_zoom_fit(None) self.file_chooser_action(on_success) def on_mouse_move_over_plot(self, event): @@ -226,7 +457,7 @@ class App: self.zoom(1/1.5) def on_zoom_fit(self, event): - xmin, ymin, xmax, ymax = get_bounds([self.gerbers, self.excellons]) + xmin, ymin, xmax, ymax = get_bounds(self.stuff) width = xmax-xmin height = ymax-ymin self.axes.set_xlim((xmin-0.05*width, xmax+0.05*width)) diff --git a/cirkuix.pyc b/cirkuix.pyc new file mode 100644 index 0000000..3732da8 Binary files /dev/null and b/cirkuix.pyc differ diff --git a/cirkuix.ui b/cirkuix.ui index 7554388..6ed2297 100644 --- a/cirkuix.ui +++ b/cirkuix.ui @@ -16,6 +16,607 @@ False gtk-open + + False + + + True + False + 5 + 5 + 5 + 5 + vertical + + + True + False + 3 + CNC Job Object + + + + + + False + True + 0 + + + + + True + False + + + True + False + 3 + Name: + + + False + True + 0 + + + + + True + True + + True + + + False + True + 1 + + + + + False + True + 1 + + + + + True + False + 0 + 3 + Plot Options: + + + + + + False + True + 2 + + + + + Plot + True + True + False + 0 + True + True + + + False + True + 3 + + + + + Solid + True + True + False + 0 + True + + + False + True + 4 + + + + + Multi-colored + True + True + False + 0 + True + + + False + True + 5 + + + + + Refresh Plot + True + True + True + + + False + True + 6 + + + + + + + + + + + + + + + + False + + + True + False + 5 + 5 + 5 + 5 + vertical + + + True + False + 3 + Excellon Object + + + + + + False + True + 0 + + + + + True + False + + + True + False + 3 + Name: + + + False + True + 0 + + + + + True + True + + True + + + False + True + 1 + + + + + False + True + 1 + + + + + True + False + 0 + 3 + Plot Options: + + + + + + False + True + 2 + + + + + Plot + True + True + False + 0 + True + True + + + False + True + 3 + + + + + Solid + True + True + False + 0 + True + + + False + True + 4 + + + + + Multi-colored + True + True + False + 0 + True + + + False + True + 5 + + + + + Refresh Plot + True + True + True + + + False + True + 6 + + + + + + + + + + + + + + + + False + + + True + False + 5 + 5 + 5 + 5 + vertical + + + True + False + 3 + Gerber Object + + + + + + False + True + 0 + + + + + True + False + + + True + False + 3 + Name: + + + False + True + 0 + + + + + True + True + + + + False + True + 1 + + + + + False + True + 1 + + + + + True + False + 0 + 3 + Plot Options: + + + + + + False + True + 2 + + + + + Plot + True + True + False + 0 + True + True + + + False + True + 3 + + + + + Merge Polygons + True + True + False + 0 + True + True + + + False + True + 4 + + + + + Solid + True + True + False + 0 + True + + + False + True + 5 + + + + + Multi-colored + True + True + False + 0 + True + + + False + True + 6 + + + + + Refresh Plot + True + True + True + + + False + True + 7 + + + + + True + False + 0 + 3 + Isolation Routing: + + + + + + False + True + 8 + + + + + True + False + 2 + 2 + + + True + False + 1 + 3 + Tool diam: + + + 0 + 0 + 1 + 1 + + + + + True + True + + True + + + 1 + 0 + 1 + 1 + + + + + True + False + 1 + 3 + Cut X: + + + 0 + 1 + 1 + 1 + + + + + True + True + + True + + + 1 + 1 + 1 + 1 + + + + + True + False + 1 + 3 + Travel X: + + + 0 + 2 + 1 + 1 + + + + + True + True + + True + + + 1 + 2 + 1 + 1 + + + + + False + True + 9 + + + + + Generate Geometry + True + True + True + + + False + True + 10 + + + + + + + + + + + + + + 600 400 @@ -237,6 +838,48 @@ True + + + True + False + Clear Plots + True + gtk-stop + + + + False + True + + + + + True + False + Re-plot + True + gtk-redo + + + + False + True + + + + + True + False + Delete Object + True + gtk-delete + + + + False + True + + False