Bläddra i källkod

Fixed g-code arc parse/plot

Juan Pablo Caram 12 år sedan
förälder
incheckning
145496b4ae
9 ändrade filer med 497 tillägg och 374 borttagningar
  1. 166 234
      camlib.py
  2. BIN
      camlib.pyc
  3. 116 128
      cirkuix.py
  4. 107 12
      cirkuix.ui
  5. 4 0
      descartes/__init__.py
  6. BIN
      descartes/__init__.pyc
  7. 66 0
      descartes/patch.py
  8. BIN
      descartes/patch.pyc
  9. 38 0
      descartes/tests.py

+ 166 - 234
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:
-            
-            ## 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
+        if tooldia == None:
+            tooldia = self.tooldia
             
-            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 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)
-        
-        
-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() )
+        #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)
         
-        # Separate codes from values
-        cmds = {}
-        for part in parts:
-            cmds[part[0]] = float(part[1:])
-        gcmds.append(cmds)
+        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)
         
-    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 get_bounds(geometry_sets):
+    xmin = Inf
+    ymin = Inf
+    xmax = -Inf
+    ymax = -Inf
     
+    #geometry_sets = [self.gerbers, self.excellons]
     
-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)
+    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]
 
-    print "x:", min(x), max(x)
-    print "y:", min(y), max(y)
-    print "z:", min(z), max(z)
-
-    print xypoints[-1]
+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)
     
-    return xmin, xmax, ymin, ymax, motions
-
 ############### cam.py ####################
 def coord(gstr,digits,fraction):
     '''

BIN
camlib.pyc


+ 116 - 128
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,74 +62,45 @@ 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 zoom(self, factor, center=None):
+        xmin, xmax = self.axes.get_xlim()
+        ymin, ymax = self.axes.get_ylim()
+        width = xmax-xmin
+        height = ymax-ymin
         
-    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()
+        if center == None:
+            center = [(xmin+xmax)/2.0, (ymin+ymax)/2.0]
         
-    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()
+        # For keeping the point at the pointer location
+        relx = (xmax-center[0])/width
+        rely = (ymax-center[1])/height         
+        
+        new_width = width/factor
+        new_height = height/factor
+        
+        self.axes.set_xlim((center[0]-new_width*(1-relx), center[0]+new_width*relx))
+        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()
         
@@ -146,9 +121,7 @@ class App:
             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)
@@ -157,12 +130,6 @@ class App:
                 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()
         
@@ -173,6 +140,67 @@ class App:
             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
@@ -180,31 +208,16 @@ class App:
                                          event.xdata, event.ydata))
             self.mouse = [event.xdata, event.ydata]
         except:
-            self.positionLabel.set_label("X: ---   Y: ---")
+            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 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
@@ -213,7 +226,7 @@ class App:
         self.zoom(1/1.5)
          
     def on_zoom_fit(self, event):
-        xmin, ymin, xmax, ymax = self.get_bounds()
+        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))
@@ -221,54 +234,29 @@ class App:
         self.canvas.queue_draw()
         return
         
-    def zoom(self, factor, center=None):
-        xmin, xmax = self.axes.get_xlim()
-        ymin, ymax = self.axes.get_ylim()
-        width = xmax-xmin
-        height = ymax-ymin
-        
-        if center == None:
-            center = [(xmin+xmax)/2.0, (ymin+ymax)/2.0]
-        
-        # For keeping the point at the pointer location
-        relx = (xmax-center[0])/width
-        rely = (ymax-center[1])/height         
-        
-        new_width = width/factor
-        new_height = height/factor
-        
-        self.axes.set_xlim((center[0]-new_width*(1-relx), center[0]+new_width*relx))
-        self.axes.set_ylim((center[1]-new_height*(1-rely), center[1]+new_height*rely))
-        
-        self.canvas.queue_draw()
-        
-#    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
+    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_window_key_press(self, widget, event):
-        print event.get_keycode(), event.get_keyval()
-        val = int(event.get_keyval()[1])
+    def on_key_over_plot(self, event):
+        print 'you pressed', event.key, event.xdata, event.ydata
         
-        if val == 49: # 1
+        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
 

+ 107 - 12
cirkuix.ui

@@ -11,12 +11,16 @@
     <property name="can_focus">False</property>
     <property name="stock">gtk-open</property>
   </object>
+  <object class="GtkImage" id="image3">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <property name="stock">gtk-open</property>
+  </object>
   <object class="GtkWindow" id="window1">
     <property name="width_request">600</property>
     <property name="height_request">400</property>
     <property name="can_focus">False</property>
     <signal name="destroy" handler="on_closewindow" swapped="no"/>
-    <signal name="key-press-event" handler="on_window_key_press" swapped="no"/>
     <child>
       <object class="GtkBox" id="box1">
         <property name="visible">True</property>
@@ -67,11 +71,12 @@
                     </child>
                     <child>
                       <object class="GtkImageMenuItem" id="imagemenuitem4">
-                        <property name="label">gtk-save-as</property>
+                        <property name="label">Open G-Code</property>
                         <property name="visible">True</property>
                         <property name="can_focus">False</property>
-                        <property name="use_underline">True</property>
-                        <property name="use_stock">True</property>
+                        <property name="image">image3</property>
+                        <property name="use_stock">False</property>
+                        <signal name="activate" handler="on_fileopengcode" swapped="no"/>
                       </object>
                     </child>
                     <child>
@@ -259,6 +264,23 @@
                     <property name="margin_right">3</property>
                     <property name="margin_top">3</property>
                     <property name="orientation">vertical</property>
+                    <child>
+                      <object class="GtkLabel" id="label2">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="ypad">4</property>
+                        <property name="label" translatable="yes">GERBER</property>
+                        <property name="use_underline">True</property>
+                        <attributes>
+                          <attribute name="weight" value="semibold"/>
+                        </attributes>
+                      </object>
+                      <packing>
+                        <property name="expand">False</property>
+                        <property name="fill">False</property>
+                        <property name="position">0</property>
+                      </packing>
+                    </child>
                     <child>
                       <object class="GtkCheckButton" id="cb_mergepolys">
                         <property name="label" translatable="yes">Merge Polygons</property>
@@ -272,7 +294,22 @@
                       <packing>
                         <property name="expand">False</property>
                         <property name="fill">True</property>
-                        <property name="position">0</property>
+                        <property name="position">1</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkCheckButton" id="checkbutton1">
+                        <property name="label" translatable="yes">Solid</property>
+                        <property name="visible">True</property>
+                        <property name="can_focus">True</property>
+                        <property name="receives_default">False</property>
+                        <property name="xalign">0</property>
+                        <property name="draw_indicator">True</property>
+                      </object>
+                      <packing>
+                        <property name="expand">False</property>
+                        <property name="fill">True</property>
+                        <property name="position">2</property>
                       </packing>
                     </child>
                     <child>
@@ -287,9 +324,67 @@
                       <packing>
                         <property name="expand">False</property>
                         <property name="fill">True</property>
-                        <property name="position">1</property>
+                        <property name="position">3</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkLabel" id="label5">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="ypad">4</property>
+                        <property name="label" translatable="yes">G-CODE</property>
+                        <attributes>
+                          <attribute name="weight" value="semibold"/>
+                        </attributes>
+                      </object>
+                      <packing>
+                        <property name="expand">False</property>
+                        <property name="fill">True</property>
+                        <property name="position">4</property>
                       </packing>
                     </child>
+                    <child>
+                      <object class="GtkBox" id="box4">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <child>
+                          <object class="GtkLabel" id="label6">
+                            <property name="visible">True</property>
+                            <property name="can_focus">False</property>
+                            <property name="label" translatable="yes">Tool dia: </property>
+                          </object>
+                          <packing>
+                            <property name="expand">False</property>
+                            <property name="fill">True</property>
+                            <property name="position">0</property>
+                          </packing>
+                        </child>
+                        <child>
+                          <object class="GtkEntry" id="entry_tooldia">
+                            <property name="visible">True</property>
+                            <property name="can_focus">True</property>
+                            <property name="invisible_char">●</property>
+                            <property name="text" translatable="yes">0.0</property>
+                          </object>
+                          <packing>
+                            <property name="expand">False</property>
+                            <property name="fill">True</property>
+                            <property name="position">1</property>
+                          </packing>
+                        </child>
+                      </object>
+                      <packing>
+                        <property name="expand">False</property>
+                        <property name="fill">True</property>
+                        <property name="position">5</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <placeholder/>
+                    </child>
+                    <child>
+                      <placeholder/>
+                    </child>
                     <child>
                       <placeholder/>
                     </child>
@@ -327,12 +422,6 @@
               <object class="GtkGrid" id="grid1">
                 <property name="visible">True</property>
                 <property name="can_focus">False</property>
-                <child>
-                  <placeholder/>
-                </child>
-                <child>
-                  <placeholder/>
-                </child>
                 <child>
                   <object class="GtkScrollbar" id="scrollbar1">
                     <property name="height_request">25</property>
@@ -364,6 +453,12 @@
                     <property name="height">1</property>
                   </packing>
                 </child>
+                <child>
+                  <placeholder/>
+                </child>
+                <child>
+                  <placeholder/>
+                </child>
               </object>
               <packing>
                 <property name="resize">True</property>

+ 4 - 0
descartes/__init__.py

@@ -0,0 +1,4 @@
+"""Turn geometric objects into matplotlib patches"""
+
+from descartes.patch import PolygonPatch
+

BIN
descartes/__init__.pyc


+ 66 - 0
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)

BIN
descartes/patch.pyc


+ 38 - 0
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)), 
+            "<class 'matplotlib.patches.PathPatch'>")
+        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)), 
+            "<class 'matplotlib.patches.PathPatch'>")
+        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)), 
+            "<class 'matplotlib.patches.PathPatch'>")
+        path = patch.get_path()
+        self.failUnless(len(path.vertices) == len(path.codes) == 198)