diff --git a/camlib.py b/camlib.py index 83e3e70b..442c9d60 100644 --- a/camlib.py +++ b/camlib.py @@ -7,7 +7,7 @@ import cairo #import os #import sys -from numpy import arctan2, Inf, array +from numpy import arctan2, Inf, array, sqrt, pi, ceil, sin, cos from matplotlib.figure import Figure # See: http://toblerity.org/shapely/manual.html @@ -16,6 +16,7 @@ from shapely.geometry import MultiPoint, MultiPolygon from shapely.geometry import box as shply_box from shapely.ops import cascaded_union +from descartes.patch import PolygonPatch class Geometry: def __init__(self): @@ -117,8 +118,6 @@ class Gerber (Geometry): ''' for region in self.regions: if region['polygon'].is_valid == False: - #polylist = fix_poly(region['polygon']) - #region['polygon'] = fix_poly3(polylist) region['polygon'] = region['polygon'].buffer(0) def buffer_paths(self): @@ -232,6 +231,9 @@ class Gerber (Geometry): "aperture":last_path_aperture}) def do_flashes(self): + ''' + Creates geometry for Gerber flashes (aperture on a single point). + ''' self.flash_geometry = [] for flash in self.flashes: aperture = self.apertures[flash['aperture']] @@ -263,6 +265,81 @@ class Gerber (Geometry): [poly['polygon'] for poly in self.regions] + self.flash_geometry) +class Excellon(Geometry): + def __init__(self): + Geometry.__init__(self) + + self.tools = {} + + self.drills = [] + + def parse_file(self, filename): + efile = open(filename, 'r') + estr = efile.readlines() + efile.close() + self.parse_lines(estr) + + def parse_lines(self, elines): + ''' + Main Excellon parser. + ''' + current_tool = "" + + for eline in elines: + + ## Tool definitions ## + # TODO: Verify all this + indexT = eline.find("T") + indexC = eline.find("C") + indexF = eline.find("F") + # Type 1 + if indexT != -1 and indexC > indexT and indexF > indexF: + tool = eline[1:indexC] + spec = eline[indexC+1:indexF] + self.tools[tool] = spec + continue + # Type 2 + # TODO: Is this inches? + #indexsp = eline.find(" ") + #indexin = eline.find("in") + #if indexT != -1 and indexsp > indexT and indexin > indexsp: + # tool = eline[1:indexsp] + # spec = eline[indexsp+1:indexin] + # self.tools[tool] = spec + # continue + # Type 3 + if indexT != -1 and indexC > indexT: + tool = eline[1:indexC] + spec = eline[indexC+1:-1] + self.tools[tool] = spec + continue + + ## Tool change + if indexT == 0: + current_tool = eline[1:-1] + 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) + self.drills.append({'point':Point((x,y)), 'tool':current_tool}) + continue + + print "WARNING: Line ignored:", eline + + def create_geometry(self): + self.solid_geometry = [] + sizes = {} + for tool in self.tools: + sizes[tool] = float(self.tools[tool]) + for drill in self.drills: + poly = Point(drill['point']).buffer(sizes[drill['tool']]/2.0) + self.solid_geometry.append(poly) + self.solid_geometry = cascaded_union(self.solid_geometry) + class CNCjob: def __init__(self, units="in", kind="generic", z_move = 0.1, feedrate = 3.0, z_cut = -0.002): @@ -279,7 +356,7 @@ class CNCjob: self.feedminutecode = "G94" self.absolutecode = "G90" - # Output G-Code + # Input/Output G-Code self.gcode = "" # Bounds of geometry given to CNCjob.generate_from_geometry() @@ -393,6 +470,7 @@ class CNCjob: self.gcode += "M05\n" # Spindle stop def create_gcode_geometry(self): + steps_per_circ = 20 ''' G-Code parser (from self.gcode). Generates dictionary with single-segment LineString's and "kind" indicating cut or travel, @@ -415,26 +493,42 @@ class CNCjob: current['Z'] = gobj['Z'] if 'G' in gobj: - current['G'] = gobj['G'] + current['G'] = int(gobj['G']) if 'X' in gobj or 'Y' in gobj: x = 0 y = 0 kind = ["C","F"] # T=travel, C=cut, F=fast, S=slow + if 'X' in gobj: x = gobj['X'] else: x = current['X'] + if 'Y' in gobj: y = gobj['Y'] else: y = current['Y'] + if current['Z'] > 0: kind[0] = 'T' - if current['G'] == 1: + if current['G'] > 0: kind[1] = 'S' - geometry.append({'geom':LineString([(current['X'],current['Y']), - (x,y)]), 'kind':kind}) + + arcdir = [None, None, "cw", "ccw"] + if current['G'] in [0,1]: # line + geometry.append({'geom':LineString([(current['X'],current['Y']), + (x,y)]), 'kind':kind}) + 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) + geometry.append({'geom':arc(center, radius, start, stop, + arcdir[current['G']], + steps_per_circ), + 'kind':kind}) + # Update current instruction for code in gobj: @@ -477,153 +571,46 @@ class CNCjob: ax.add_patch(patch) return fig - - -class Excellon(Geometry): - def __init__(self): - Geometry.__init__(self) - self.tools = {} - - self.drills = [] - - def parse_file(self, filename): - efile = open(filename, 'r') - estr = efile.readlines() - efile.close() - self.parse_lines(estr) - - def parse_lines(self, elines): + def plot2(self, axes, tooldia=None, dpi=75, margin=0.1, + color={"T":["#F0E24D", "#B5AB3A"], "C":["#5E6CFF", "#4650BD"]}, + alpha={"T":0.3, "C":1.0}): ''' - Main Excellon parser. + Plots the G-code job onto the given axes ''' - current_tool = "" - - for eline in elines: + if tooldia == None: + tooldia = self.tooldia - ## Tool definitions ## - # TODO: Verify all this - indexT = eline.find("T") - indexC = eline.find("C") - indexF = eline.find("F") - # Type 1 - if indexT != -1 and indexC > indexT and indexF > indexF: - tool = eline[1:indexC] - spec = eline[indexC+1:indexF] - self.tools[tool] = spec - continue - # Type 2 - # TODO: Is this inches? - #indexsp = eline.find(" ") - #indexin = eline.find("in") - #if indexT != -1 and indexsp > indexT and indexin > indexsp: - # tool = eline[1:indexsp] - # spec = eline[indexsp+1:indexin] - # self.tools[tool] = spec - # continue - # Type 3 - if indexT != -1 and indexC > indexT: - tool = eline[1:indexC] - spec = eline[indexC+1:-1] - self.tools[tool] = spec - continue - - ## Tool change - if indexT == 0: - current_tool = eline[1:-1] - 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) - self.drills.append({'point':Point((x,y)), 'tool':current_tool}) - continue - - print "WARNING: Line ignored:", eline + #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) - def create_geometry(self): - self.solid_geometry = [] - sizes = {} - for tool in self.tools: - sizes[tool] = float(self.tools[tool]) - for drill in self.drills: - poly = Point(drill['point']).buffer(sizes[drill['tool']]/2.0) - self.solid_geometry.append(poly) - self.solid_geometry = cascaded_union(self.solid_geometry) - - - -class motion: - ''' - Represents a machine motion, which can be cutting or just travelling. - ''' - def __init__(self, start, end, depth, typ='line', offset=None, center=None, - radius=None, tooldia=0.5): - self.typ = typ - self.start = start - self.end = end - self.depth = depth - self.center = center - self.radius = radius - self.tooldia = tooldia - self.offset = offset # (I, J) + if tooldia == 0: + for geo in self.G_geometry: + linespec = '--' + linecolor = color[geo['kind'][0]][1] + if geo['kind'][0] == 'C': + linespec = 'k-' + x, y = geo['geom'].coords.xy + axes.plot(x, y, linespec, color=linecolor) + else: + for geo in self.G_geometry: + 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 gparse1(filename): - ''' - Parses G-code file into list of dictionaries like - Examples: {'G': 1.0, 'X': 0.085, 'Y': -0.125}, - {'G': 3.0, 'I': -0.01, 'J': 0.0, 'X': 0.0821, 'Y': -0.1179} - ''' - f = open(filename) - gcmds = [] - for line in f: - line = line.strip() - - # Remove comments - # NOTE: Limited to 1 bracket pair - op = line.find("(") - cl = line.find(")") - if op > -1 and cl > op: - #comment = line[op+1:cl] - line = line[:op] + line[(cl+1):] - - # Parse GCode - # 0 4 12 - # G01 X-0.007 Y-0.057 - # --> codes_idx = [0, 4, 12] - codes = "NMGXYZIJFP" - codes_idx = [] - i = 0 - for ch in line: - if ch in codes: - codes_idx.append(i) - i += 1 - n_codes = len(codes_idx) - if n_codes == 0: - continue - - # Separate codes in line - parts = [] - 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() ) - - # Separate codes from values - cmds = {} - for part in parts: - cmds[part[0]] = float(part[1:]) - gcmds.append(cmds) - - f.close() - return gcmds def gparse1b(gtext): + ''' + gtext is a single string with g-code + ''' gcmds = [] - lines = gtext.split("\n") + lines = gtext.split("\n") # TODO: This is probably a lot of work! for line in lines: line = line.strip() @@ -662,98 +649,43 @@ def gparse1b(gtext): cmds[part[0]] = float(part[1:]) gcmds.append(cmds) return gcmds - - -def gparse2(gcmds): - - x = [] - y = [] - z = [] - xypoints = [] - motions = [] - current_g = None - - for cmds in gcmds: - - # Destination point - x_ = None - y_ = None - z_ = None - - if 'X' in cmds: - x_ = cmds['X'] - x.append(x_) - if 'Y' in cmds: - y_ = cmds['Y'] - y.append(y_) - if 'Z' in cmds: - z_ = cmds['Z'] - z.append(z_) - - # Ingnore anything but XY movements from here on - if x_ is None and y_ is None: - #print "-> no x,y" - continue - - if x_ is None: - x_ = xypoints[-1][0] - - if y_ is None: - y_ = xypoints[-1][1] - - if z_ is None: - z_ = z[-1] - - - mot = None - - if 'G' in cmds: - current_g = cmds['G'] - - if current_g == 0: # Fast linear - if len(xypoints) > 0: - #print "motion(", xypoints[-1], ", (", x_, ",", y_, "),", z_, ")" - mot = motion(xypoints[-1], (x_, y_), z_) - - if current_g == 1: # Feed-rate linear - if len(xypoints) > 0: - #print "motion(", xypoints[-1], ", (", x_, ",", y_, "),", z_, ")" - mot = motion(xypoints[-1], (x_, y_), z_) - - if current_g == 2: # Clockwise arc - if len(xypoints) > 0: - if 'I' in cmds and 'J' in cmds: - mot = motion(xypoints[-1], (x_, y_), z_, offset=(cmds['I'], - cmds['J']), typ='arccw') - - if current_g == 3: # Counter-clockwise arc - if len(xypoints) > 0: - if 'I' in cmds and 'J' in cmds: - mot = motion(xypoints[-1], (x_, y_), z_, offset=(cmds['I'], - cmds['J']), typ='arcacw') - - if mot is not None: - motions.append(mot) - - xypoints.append((x_, y_)) - - x = array(x) - y = array(y) - z = array(z) - xmin = min(x) - xmax = max(x) - ymin = min(y) - ymax = max(y) - - print "x:", min(x), max(x) - print "y:", min(y), max(y) - print "z:", min(z), max(z) - - print xypoints[-1] +def get_bounds(geometry_sets): + xmin = Inf + ymin = Inf + xmax = -Inf + ymax = -Inf - return xmin, xmax, ymin, ymax, motions + #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]) + + return [xmin, ymin, xmax, ymax] +def arc(center, radius, start, stop, direction, steps_per_circ): + da_sign = {"cw":-1.0, "ccw":1.0} + points = [] + if direction=="ccw" and stop <= start: + stop += 2*pi + if direction=="cw" and stop >= start: + stop -= 2*pi + + angle = abs(stop - start) + + #angle = stop-start + steps = max([int(ceil(angle/(2*pi)*steps_per_circ)), 2]) + delta_angle = da_sign[direction]*angle*1.0/steps + for i in range(steps+1): + theta = start + delta_angle*i + points.append([center[0]+radius*cos(theta), center[1]+radius*sin(theta)]) + return LineString(points) + ############### cam.py #################### def coord(gstr,digits,fraction): ''' diff --git a/camlib.pyc b/camlib.pyc index 04f9034e..c02cf4a1 100644 Binary files a/camlib.pyc and b/camlib.pyc differ diff --git a/cirkuix.py b/cirkuix.py index 1ddd289e..b2f4d855 100644 --- a/cirkuix.py +++ b/cirkuix.py @@ -29,22 +29,26 @@ class App: ## Event handling ## self.builder.connect_signals(self) + ## Make plot area ## self.figure = None self.axes = None self.canvas = None - - ## Make plot area ## self.mplpaint() - self.window.show_all() + ######################################## ## DATA ## ######################################## self.gerbers = [] self.excellons = [] + self.cncjobs = [] self.mouse = None + ######################################## + ## START ## + ######################################## + self.window.show_all() Gtk.main() def mplpaint(self): @@ -58,169 +62,24 @@ class App: self.axes.grid() #a.patch.set_visible(False) Background of the axes self.figure.patch.set_visible(False) - #self.figure.tight_layout() self.canvas = FigureCanvas(self.figure) # a Gtk.DrawingArea - #self.canvas.set_size_request(600,400) + self.canvas.set_hexpand(1) + self.canvas.set_vexpand(1) + + ######################################## + ## EVENTS ## + ######################################## self.canvas.mpl_connect('button_press_event', self.on_click_over_plot) self.canvas.mpl_connect('motion_notify_event', self.on_mouse_move_over_plot) - ##self.canvas.mpl_connect('scroll_event', self.on_scroll_over_plot) - ##self.canvas.mpl_connect('key_press_event', self.on_key_over_plot) - - - self.canvas.set_hexpand(1) - self.canvas.set_vexpand(1) + self.canvas.set_can_focus(True) # For key press + 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 on_filequit(self, param): - print "quit from menu" - self.window.destroy() - Gtk.main_quit() - - def on_closewindow(self, param): - print "quit from X" - self.window.destroy() - Gtk.main_quit() - - def on_fileopengerber(self, param): - print "File->Open Gerber" - 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: - ## Load the file ## - print("Open clicked") - print("File selected: " + dialog.get_filename()) - gerber = Gerber() - gerber.parse_file(dialog.get_filename()) - self.gerbers.append(gerber) - self.plot_gerber(gerber) - ## End ## - elif response == Gtk.ResponseType.CANCEL: - print("Cancel clicked") - dialog.destroy() - - def on_fileopenexcellon(self, param): - print "File->Open Excellon" - 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: - ## Load the file ## - print("Open clicked") - print("File selected: " + dialog.get_filename()) - excellon = Excellon() - excellon.parse_file(dialog.get_filename()) - self.excellons.append(excellon) - self.plot_excellon(excellon) - ## End ## - elif response == Gtk.ResponseType.CANCEL: - print("Cancel clicked") - dialog.destroy() - - def plot_gerber(self, gerber): - gerber.create_geometry() - - # Options - mergepolys = self.builder.get_object("cb_mergepolys").get_active() - multicolored = self.builder.get_object("cb_multicolored").get_active() - - geometry = None - if mergepolys: - geometry = gerber.solid_geometry - else: - geometry = gerber.buffered_paths + \ - [poly['polygon'] for poly in gerber.regions] + \ - gerber.flash_geometry - - linespec = None - if multicolored: - linespec = '-' - else: - linespec = 'k-' - #f = Figure(dpi=75) - #a = f.add_subplot(111) - #a.set_aspect(1) - for poly in geometry: - x, y = poly.exterior.xy - #a.plot(x, y) - self.axes.plot(x, y, linespec) - for ints in poly.interiors: - x, y = ints.coords.xy - self.axes.plot(x, y, linespec) - - #f.tight_layout() - #canvas = FigureCanvas(f) # a Gtk.DrawingArea - #canvas.set_size_request(600,400) - #self.grid.attach(canvas,1,1,600,400) - #self.window.show_all() - - def plot_excellon(self, excellon): - excellon.create_geometry() - - # Plot excellon - for geo in excellon.solid_geometry: - x, y = geo.exterior.coords.xy - self.axes.plot(x, y, 'r-') - for ints in geo.interiors: - x, y = ints.coords.xy - self.axes.plot(x, y, 'g-') - - def on_mouse_move_over_plot(self, event): - try: # May fail in case mouse not within axes - self.positionLabel.set_label("X: %.4f Y: %.4f"%( - event.xdata, event.ydata)) - self.mouse = [event.xdata, event.ydata] - except: - self.positionLabel.set_label("X: --- Y: ---") - self.mouse = None - - def on_click_over_plot(self, event): - print 'button=%d, x=%d, y=%d, xdata=%f, ydata=%f'%( - event.button, event.x, event.y, event.xdata, event.ydata) - - def get_bounds(self): - 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]) - - return [xmin, ymin, xmax, ymax] - - def on_zoom_in(self, event): - self.zoom(1.5) - return - - def on_zoom_out(self, event): - self.zoom(1/1.5) - - def on_zoom_fit(self, event): - xmin, ymin, xmax, ymax = self.get_bounds() - width = xmax-xmin - height = ymax-ymin - self.axes.set_xlim((xmin-0.05*width, xmax+0.05*width)) - self.axes.set_ylim((ymin-0.05*height, ymax+0.05*height)) - self.canvas.queue_draw() - return - def zoom(self, factor, center=None): xmin, xmax = self.axes.get_xlim() ymin, ymax = self.axes.get_ylim() @@ -241,34 +100,163 @@ class App: self.axes.set_ylim((center[1]-new_height*(1-rely), center[1]+new_height*rely)) self.canvas.queue_draw() + + def plot_gerber(self, gerber): + gerber.create_geometry() -# def on_scroll_over_plot(self, event): -# print "Scroll" -# center = [event.xdata, event.ydata] -# if sign(event.step): -# self.zoom(1.5, center=center) -# else: -# self.zoom(1/1.5, center=center) -# -# def on_window_scroll(self, event): -# print "Scroll" -# -# def on_key_over_plot(self, event): -# print 'you pressed', event.key, event.xdata, event.ydata + # Options + mergepolys = self.builder.get_object("cb_mergepolys").get_active() + multicolored = self.builder.get_object("cb_multicolored").get_active() - def on_window_key_press(self, widget, event): - print event.get_keycode(), event.get_keyval() - val = int(event.get_keyval()[1]) + geometry = None + if mergepolys: + geometry = gerber.solid_geometry + else: + geometry = gerber.buffered_paths + \ + [poly['polygon'] for poly in gerber.regions] + \ + gerber.flash_geometry - if val == 49: # 1 + linespec = None + if multicolored: + linespec = '-' + else: + linespec = 'k-' + + for poly in geometry: + x, y = poly.exterior.xy + #a.plot(x, y) + self.axes.plot(x, y, linespec) + for ints in poly.interiors: + x, y = ints.coords.xy + self.axes.plot(x, y, linespec) + + def plot_excellon(self, excellon): + excellon.create_geometry() + + # Plot excellon + for geo in excellon.solid_geometry: + x, y = geo.exterior.coords.xy + self.axes.plot(x, y, 'r-') + for ints in geo.interiors: + x, y = ints.coords.xy + self.axes.plot(x, y, 'g-') + + def plot_cncjob(self, job): + job.create_gcode_geometry() + 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() + + + ######################################## + ## EVENT HANDLERS ## + ######################################## + + def on_filequit(self, param): + print "quit from menu" + self.window.destroy() + Gtk.main_quit() + + def on_closewindow(self, param): + print "quit from X" + self.window.destroy() + Gtk.main_quit() + + def on_fileopengerber(self, param): + def on_success(self, filename): + gerber = Gerber() + gerber.parse_file(filename) + self.gerbers.append(gerber) + self.plot_gerber(gerber) + self.file_chooser_action(on_success) + + def on_fileopenexcellon(self, param): + def on_success(self, filename): + excellon = Excellon() + excellon.parse_file(filename) + self.excellons.append(excellon) + self.plot_excellon(excellon) + self.file_chooser_action(on_success) + + def on_fileopengcode(self, param): + def on_success(self, filename): + f = open(filename) + gcode = f.read() + f.close() + job = CNCjob() + job.gcode = gcode + self.cncjobs.append(job) + self.plot_cncjob(job) + self.file_chooser_action(on_success) + + def on_mouse_move_over_plot(self, event): + try: # May fail in case mouse not within axes + self.positionLabel.set_label("X: %.4f Y: %.4f"%( + event.xdata, event.ydata)) + self.mouse = [event.xdata, event.ydata] + except: + self.positionLabel.set_label("") + self.mouse = None + + def on_click_over_plot(self, event): + # For key presses + self.canvas.grab_focus() + + print 'button=%d, x=%d, y=%d, xdata=%f, ydata=%f'%( + event.button, event.x, event.y, event.xdata, event.ydata) + + def on_zoom_in(self, event): + self.zoom(1.5) + return + + def on_zoom_out(self, event): + self.zoom(1/1.5) + + def on_zoom_fit(self, event): + xmin, ymin, xmax, ymax = get_bounds([self.gerbers, self.excellons]) + width = xmax-xmin + height = ymax-ymin + self.axes.set_xlim((xmin-0.05*width, xmax+0.05*width)) + self.axes.set_ylim((ymin-0.05*height, ymax+0.05*height)) + self.canvas.queue_draw() + return + + def on_scroll_over_plot(self, event): + print "Scroll" + center = [event.xdata, event.ydata] + if sign(event.step): + self.zoom(1.5, center=center) + else: + self.zoom(1/1.5, center=center) + + def on_window_scroll(self, event): + print "Scroll" + + def on_key_over_plot(self, event): + print 'you pressed', event.key, event.xdata, event.ydata + + if event.key == '1': # 1 self.on_zoom_fit(None) return - if val == 50: # 2 + if event.key == '2': # 2 self.zoom(1/1.5, self.mouse) return - if val == 51: # 3 + if event.key == '3': # 3 self.zoom(1.5, self.mouse) return diff --git a/cirkuix.ui b/cirkuix.ui index 2bc82838..7554388e 100644 --- a/cirkuix.ui +++ b/cirkuix.ui @@ -11,12 +11,16 @@ False gtk-open + + True + False + gtk-open + 600 400 False - True @@ -67,11 +71,12 @@ - gtk-save-as + Open G-Code True False - True - True + image3 + False + @@ -259,6 +264,23 @@ 3 3 vertical + + + True + False + 4 + GERBER + True + + + + + + False + False + 0 + + Merge Polygons @@ -272,7 +294,22 @@ False True - 0 + 1 + + + + + Solid + True + True + False + 0 + True + + + False + True + 2 @@ -287,9 +324,67 @@ False True - 1 + 3 + + + True + False + 4 + G-CODE + + + + + + False + True + 4 + + + + + True + False + + + True + False + Tool dia: + + + False + True + 0 + + + + + True + True + + 0.0 + + + False + True + 1 + + + + + False + True + 5 + + + + + + + + @@ -327,12 +422,6 @@ True False - - - - - - 25 @@ -364,6 +453,12 @@ 1 + + + + + + True diff --git a/descartes/__init__.py b/descartes/__init__.py new file mode 100644 index 00000000..8fd72b2b --- /dev/null +++ b/descartes/__init__.py @@ -0,0 +1,4 @@ +"""Turn geometric objects into matplotlib patches""" + +from descartes.patch import PolygonPatch + diff --git a/descartes/__init__.pyc b/descartes/__init__.pyc new file mode 100644 index 00000000..2433c763 Binary files /dev/null and b/descartes/__init__.pyc differ diff --git a/descartes/patch.py b/descartes/patch.py new file mode 100644 index 00000000..34686f78 --- /dev/null +++ b/descartes/patch.py @@ -0,0 +1,66 @@ +"""Paths and patches""" + +from matplotlib.patches import PathPatch +from matplotlib.path import Path +from numpy import asarray, concatenate, ones + + +class Polygon(object): + # Adapt Shapely or GeoJSON/geo_interface polygons to a common interface + def __init__(self, context): + if hasattr(context, 'interiors'): + self.context = context + else: + self.context = getattr(context, '__geo_interface__', context) + @property + def geom_type(self): + return (getattr(self.context, 'geom_type', None) + or self.context['type']) + @property + def exterior(self): + return (getattr(self.context, 'exterior', None) + or self.context['coordinates'][0]) + @property + def interiors(self): + value = getattr(self.context, 'interiors', None) + if value is None: + value = self.context['coordinates'][1:] + return value + + +def PolygonPath(polygon): + """Constructs a compound matplotlib path from a Shapely or GeoJSON-like + geometric object""" + this = Polygon(polygon) + assert this.geom_type == 'Polygon' + def coding(ob): + # The codes will be all "LINETO" commands, except for "MOVETO"s at the + # beginning of each subpath + n = len(getattr(ob, 'coords', None) or ob) + vals = ones(n, dtype=Path.code_type) * Path.LINETO + vals[0] = Path.MOVETO + return vals + vertices = concatenate( + [asarray(this.exterior)] + + [asarray(r) for r in this.interiors]) + codes = concatenate( + [coding(this.exterior)] + + [coding(r) for r in this.interiors]) + return Path(vertices, codes) + + +def PolygonPatch(polygon, **kwargs): + """Constructs a matplotlib patch from a geometric object + + The `polygon` may be a Shapely or GeoJSON-like object with or without holes. + The `kwargs` are those supported by the matplotlib.patches.Polygon class + constructor. Returns an instance of matplotlib.patches.PathPatch. + + Example (using Shapely Point and a matplotlib axes): + + >>> b = Point(0, 0).buffer(1.0) + >>> patch = PolygonPatch(b, fc='blue', ec='blue', alpha=0.5) + >>> axis.add_patch(patch) + + """ + return PathPatch(PolygonPath(polygon), **kwargs) diff --git a/descartes/patch.pyc b/descartes/patch.pyc new file mode 100644 index 00000000..c8b49633 Binary files /dev/null and b/descartes/patch.pyc differ diff --git a/descartes/tests.py b/descartes/tests.py new file mode 100644 index 00000000..8cb48b4f --- /dev/null +++ b/descartes/tests.py @@ -0,0 +1,38 @@ +from shapely.geometry import * +import unittest + +from descartes.patch import PolygonPatch + +class PolygonTestCase(unittest.TestCase): + polygon = Point(0, 0).buffer(10.0).difference( + MultiPoint([(-5, 0), (5, 0)]).buffer(3.0)) + def test_patch(self): + patch = PolygonPatch(self.polygon) + self.failUnlessEqual(str(type(patch)), + "") + path = patch.get_path() + self.failUnless(len(path.vertices) == len(path.codes) == 198) + +class JSONPolygonTestCase(unittest.TestCase): + polygon = Point(0, 0).buffer(10.0).difference( + MultiPoint([(-5, 0), (5, 0)]).buffer(3.0)) + def test_patch(self): + geo = self.polygon.__geo_interface__ + patch = PolygonPatch(geo) + self.failUnlessEqual(str(type(patch)), + "") + path = patch.get_path() + self.failUnless(len(path.vertices) == len(path.codes) == 198) + +class GeoInterfacePolygonTestCase(unittest.TestCase): + class GeoThing: + __geo_interface__ = None + thing = GeoThing() + thing.__geo_interface__ = Point(0, 0).buffer(10.0).difference( + MultiPoint([(-5, 0), (5, 0)]).buffer(3.0)).__geo_interface__ + def test_patch(self): + patch = PolygonPatch(self.thing) + self.failUnlessEqual(str(type(patch)), + "") + path = patch.get_path() + self.failUnless(len(path.vertices) == len(path.codes) == 198)