From 145496b4ae7cbf53e78c71546744cf88e5fef720 Mon Sep 17 00:00:00 2001 From: Juan Pablo Caram Date: Wed, 8 Jan 2014 01:46:50 -0500 Subject: [PATCH] Fixed g-code arc parse/plot --- camlib.py | 402 +++++++++++++++++------------------------ camlib.pyc | Bin 18832 -> 18281 bytes cirkuix.py | 340 +++++++++++++++++----------------- cirkuix.ui | 119 ++++++++++-- descartes/__init__.py | 4 + descartes/__init__.pyc | Bin 0 -> 247 bytes descartes/patch.py | 66 +++++++ descartes/patch.pyc | Bin 0 -> 2859 bytes descartes/tests.py | 38 ++++ 9 files changed, 546 insertions(+), 423 deletions(-) create mode 100644 descartes/__init__.py create mode 100644 descartes/__init__.pyc create mode 100644 descartes/patch.py create mode 100644 descartes/patch.pyc create mode 100644 descartes/tests.py diff --git a/camlib.py b/camlib.py index 83e3e70..442c9d6 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 04f9034ea030fa485a2b7e1a5bbbd42af0d3ab8f..c02cf4a10594e9822e9172ab4b6ba72d46621534 100644 GIT binary patch delta 6077 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 diff --git a/cirkuix.py b/cirkuix.py index 1ddd289..b2f4d85 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 2bc8283..7554388 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 0000000..8fd72b2 --- /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 0000000000000000000000000000000000000000..2433c763424fd472e6a7491171db1c461b549e15 GIT binary patch literal 247 zcmY+9!3x4K42H8#R7COQ#Z!+v?FmGD0vXfO5Xx-JtaR&Gv%_A%7uJ&(O~r!-@+BmH z5)$7Rv*+WMOZXb2xFh##f&wUr5hw;Wf|^0WAW|_Ll2Op4gk#?|ywa}L*cr*)`Kl#) zZW?sFF0r)^tvU0yM0wHPyvUM>=5S8PoE<9HWTClF;!vItgiuZjflJDj_Ok3y`}DIS irxGOp@T*_lNeI&z6e4X0oKj~~JJnmg4Oe~mZ_M8Ab3D@k literal 0 HcmV?d00001 diff --git a/descartes/patch.py b/descartes/patch.py new file mode 100644 index 0000000..34686f7 --- /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 0000000000000000000000000000000000000000..c8b496332c96e1e6ecb9400aa9292ea86f7d5868 GIT binary patch literal 2859 zcmb7GTW{P{5FXo`&7~YLlXIQ(&CEA*9Q=89>F>XOjZ&Ka0{s6F%~lW@ zQ6JKy%%{?$kxzY}JRS#B1~lqWztfI8RL;?;OZ_g5=BYnVqXp_OU~P`{A`Nk(OTQ2u z5IyWKQ8v$m{u#;^q_a%fB5j3BeDA9e_Y;>}l~kFk6PM=N7XRQEdRTIc({=1g^LDB; zoeCMen}l69U}Y7X;sf|nf-vQmX|jUpWedLK8Q2kD|9>E#!~WOMPC`DxqxZ4= zJmjaKl2CScftp(<;a|ogG8*esJ0cEU#2hL(YA*!McZ#3g9D0wFa;#tOan^eg%`#co zGt%@pp7u2O^(azqKUY^~ql0MEn_^lnj6BlM6Ug;JckU&i`%vi7R)y z^y&BK>1n$h@Y)YP{rDMOWUU=Ow=cN#FMI`>n1ZjMXXKJLS1{xtXPLq={Y>zZ-u zP#RTgMztBELXHwwm&TRFfQx0WhK(6veJf9DT~3r~)aTlKxpi;z-Llx#OA0?uHSL-r zRc5fGQzu?anFawGyKoE5egy$1IjK-w^xFh}gPFw!dSamX3GNXaJ??br0y_;o*x_?! zyp4Z{`NJ_t4N}Gmt1`~?Hg7m?a2Zjlt2XU9bs|o?h`~wyOHHQRGQo8!;h>5 zQ43SuZ5q==j69N*IU=pV4jG-4-nxBo1d+=N8(roVM%hSiJZ5z%UMCI#8~PtGa~YgI zWOxyC)@Uz4P_XQ+dW-V`s`E9kl@ek`$WS@Dmg~$>h!In8{5LKUjLH51d4h8%sg@pb z?O8qv$wWNJwR$u)`w^%*Q3I_`CFhim>PZ0}gShA?Z!>TUmdEzbW){Snc)i+ z!O!q@n)jAO&E36ZRF_(XV+-mWW!`;*cUeASl4Dv_vZw9Z2kl9vHa0fYK;2QYIb6S? z*4KK2@o=b{@V(ypn&@wLi5ap}zFGu8IJlvP>7DBXROah9l%92yvd)t`>%CjYRU~_b z?Il?ji>^@QT8l(J2O>nBqH7TC@Ctg86s^Nwo48n5XYHG z@dEHi#*9*jaS{-YGymjT-qrw4jcPoqCvB=p(s70h&ST|F%!nBB47T)6Oa29Hz1==X V8$y3Jrt6>c&Uu%D*Do%v{tFkZM`8c~ literal 0 HcmV?d00001 diff --git a/descartes/tests.py b/descartes/tests.py new file mode 100644 index 0000000..8cb48b4 --- /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)