Forráskód Böngészése

Cutout generator implemented

Juan Pablo Caram 12 éve
szülő
commit
e6b5fd6632
3 módosított fájl, 387 hozzáadás és 147 törlés
  1. 126 125
      camlib.py
  2. 75 16
      cirkuix.py
  3. 186 6
      cirkuix.ui

+ 126 - 125
camlib.py

@@ -7,8 +7,10 @@ from shapely.geometry import MultiPoint, MultiPolygon
 from shapely.geometry import box as shply_box
 from shapely.ops import cascaded_union
 
+# Used for solid polygons in Matplotlib
 from descartes.patch import PolygonPatch
 
+
 class Geometry:
     def __init__(self):
         # Units (in or mm)
@@ -18,20 +20,20 @@ class Geometry:
         self.solid_geometry = None
         
     def isolation_geometry(self, offset):
-        '''
+        """
         Creates contours around geometry at a given
         offset distance.
-        '''
+        """
         return self.solid_geometry.buffer(offset)
         
     def bounds(self):
-        '''
+        """
         Returns coordinates of rectangular bounds
         of geometry: (xmin, ymin, xmax, ymax).
-        '''
-        if self.solid_geometry == None:
+        """
+        if self.solid_geometry is None:
             print "Warning: solid_geometry not computed yet."
-            return (0,0,0,0)
+            return (0, 0, 0, 0)
             
         if type(self.solid_geometry) == list:
             return cascaded_union(self.solid_geometry).bounds
@@ -39,33 +41,33 @@ class Geometry:
             return self.solid_geometry.bounds
         
     def size(self):
-        '''
+        """
         Returns (width, height) of rectangular
         bounds of geometry.
-        '''
-        if self.solid_geometry == None:
+        """
+        if self.solid_geometry is None:
             print "Warning: solid_geometry not computed yet."
             return 0
         bounds = self.bounds()
         return (bounds[2]-bounds[0], bounds[3]-bounds[1])
         
     def get_empty_area(self, boundary=None):
-        '''
+        """
         Returns the complement of self.solid_geometry within
         the given boundary polygon. If not specified, it defaults to
         the rectangular bounding box of self.solid_geometry.
-        '''
-        if boundary == None:
+        """
+        if boundary is None:
             boundary = self.solid_geometry.envelope
         return boundary.difference(self.solid_geometry)
         
-    def clear_polygon(self, polygon, tooldia, overlap = 0.15):
-        '''
+    def clear_polygon(self, polygon, tooldia, overlap=0.15):
+        """
         Creates geometry inside a polygon for a tool to cover
         the whole area.
-        '''
+        """
         poly_cuts = [polygon.buffer(-tooldia/2.0)]
-        while(1):
+        while True:
             polygon = poly_cuts[-1].buffer(-tooldia*(1-overlap))
             if polygon.area > 0:
                 poly_cuts.append(polygon)
@@ -107,12 +109,12 @@ class Gerber (Geometry):
         self.flash_geometry = []
         
     def fix_regions(self):
-        '''
+        """
         Overwrites the region polygons with fixed
         versions if found to be invalid (according to Shapely).
-        '''
+        """
         for region in self.regions:
-            if region['polygon'].is_valid == False:
+            if not region['polygon'].is_valid:
                 region['polygon'] = region['polygon'].buffer(0)
     
     def buffer_paths(self):
@@ -122,98 +124,98 @@ class Gerber (Geometry):
             self.buffered_paths.append(path["linestring"].buffer(width/2))
     
     def aperture_parse(self, gline):
-        '''
+        """
         Parse gerber aperture definition
         into dictionary of apertures.
-        '''
+        """
         indexstar = gline.find("*")
         indexC = gline.find("C,")
         if indexC != -1: # Circle, example: %ADD11C,0.1*%
             apid = gline[4:indexC]
-            self.apertures[apid] = {"type":"C", 
-                                    "size":float(gline[indexC+2:indexstar])}
+            self.apertures[apid] = {"type": "C",
+                                    "size": float(gline[indexC+2:indexstar])}
             return apid
         indexR = gline.find("R,")
         if indexR != -1: # Rectangle, example: %ADD15R,0.05X0.12*%
             apid = gline[4:indexR]
             indexX = gline.find("X")
-            self.apertures[apid] = {"type":"R", 
-                                    "width":float(gline[indexR+2:indexX]), 
-                                    "height":float(gline[indexX+1:indexstar])}
+            self.apertures[apid] = {"type": "R",
+                                    "width": float(gline[indexR+2:indexX]),
+                                    "height": float(gline[indexX+1:indexstar])}
             return apid
         indexO = gline.find("O,")
         if indexO != -1: # Obround
             apid = gline[4:indexO]
             indexX = gline.find("X")
-            self.apertures[apid] = {"type":"O", 
-                                    "width":float(gline[indexO+2:indexX]), 
-                                    "height":float(gline[indexX+1:indexstar])}
+            self.apertures[apid] = {"type": "O",
+                                    "width": float(gline[indexO+2:indexX]),
+                                    "height": float(gline[indexX+1:indexstar])}
             return apid
         print "WARNING: Aperture not implemented:", gline
         return None
         
     def parse_file(self, filename):
-        '''
+        """
         Calls Gerber.parse_lines() with array of lines
         read from the given file.
-        '''
+        """
         gfile = open(filename, 'r')
         gstr = gfile.readlines()
         gfile.close()
         self.parse_lines(gstr)
         
     def parse_lines(self, glines):
-        '''
+        """
         Main Gerber parser.
-        '''
-        path = [] # Coordinates of the current path
+        """
+        path = []  # Coordinates of the current path
         last_path_aperture = None
         current_aperture = None
         
         for gline in glines:
             
-            if gline.find("D01*") != -1: # pen down
+            if gline.find("D01*") != -1:  # pen down
                 path.append(coord(gline, self.digits, self.fraction))
                 last_path_aperture = current_aperture
                 continue
         
-            if gline.find("D02*") != -1: # pen up
+            if gline.find("D02*") != -1:  # pen up
                 if len(path) > 1:
                     # Path completed, create shapely LineString
-                    self.paths.append({"linestring":LineString(path), 
-                                       "aperture":last_path_aperture})
+                    self.paths.append({"linestring": LineString(path),
+                                       "aperture": last_path_aperture})
                 path = [coord(gline, self.digits, self.fraction)]
                 continue
             
             indexD3 = gline.find("D03*")
-            if indexD3 > 0: # Flash
-                self.flashes.append({"loc":coord(gline, self.digits, self.fraction),
-                                     "aperture":current_aperture})
+            if indexD3 > 0:  # Flash
+                self.flashes.append({"loc": coord(gline, self.digits, self.fraction),
+                                     "aperture": current_aperture})
                 continue
-            if indexD3 == 0: # Flash?
+            if indexD3 == 0:  # Flash?
                 print "WARNING: Uninplemented flash style:", gline
                 continue
             
-            if gline.find("G37*") != -1: # end region
+            if gline.find("G37*") != -1:  # end region
                 # Only one path defines region?
-                self.regions.append({"polygon":Polygon(path), 
-                                     "aperture":last_path_aperture})
+                self.regions.append({"polygon": Polygon(path),
+                                     "aperture": last_path_aperture})
                 path = []
                 continue
             
-            if gline.find("%ADD") != -1: # aperture definition
-                self.aperture_parse(gline) # adds element to apertures
+            if gline.find("%ADD") != -1:  # aperture definition
+                self.aperture_parse(gline)  # adds element to apertures
                 continue
             
             indexstar = gline.find("*")
-            if gline.find("D") == 0: # Aperture change
+            if gline.find("D") == 0:  # Aperture change
                 current_aperture = gline[1:indexstar]
                 continue
-            if gline.find("G54D") == 0: # Aperture change (deprecated)
+            if gline.find("G54D") == 0:  # Aperture change (deprecated)
                 current_aperture = gline[4:indexstar]
                 continue
             
-            if gline.find("%FS") != -1: # Format statement
+            if gline.find("%FS") != -1:  # Format statement
                 indexX = gline.find("X")
                 self.digits = int(gline[indexX + 1])
                 self.fraction = int(gline[indexX + 2])
@@ -226,17 +228,17 @@ 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']]
-            if aperture['type'] == 'C': # Circles
+            if aperture['type'] == 'C':  # Circles
                 circle = Point(flash['loc']).buffer(aperture['size']/2)
                 self.flash_geometry.append(circle)
                 continue
-            if aperture['type'] == 'R': # Rectangles
+            if aperture['type'] == 'R':  # Rectangles
                 loc = flash['loc']
                 width = aperture['width']
                 height = aperture['height']
@@ -260,6 +262,7 @@ class Gerber (Geometry):
                                 [poly['polygon'] for poly in self.regions] +
                                 self.flash_geometry)
 
+
 class Excellon(Geometry):
     def __init__(self):
         Geometry.__init__(self)
@@ -275,9 +278,9 @@ class Excellon(Geometry):
         self.parse_lines(estr)
         
     def parse_lines(self, elines):
-        '''
+        """
         Main Excellon parser.
-        '''
+        """
         current_tool = ""
         
         for eline in elines:
@@ -320,7 +323,7 @@ class Excellon(Geometry):
             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})
+                self.drills.append({'point': Point((x, y)), 'tool': current_tool})
                 continue
             
             print "WARNING: Line ignored:", eline
@@ -335,9 +338,10 @@ class Excellon(Geometry):
             self.solid_geometry.append(poly)
         self.solid_geometry = cascaded_union(self.solid_geometry)
 
+
 class CNCjob(Geometry):
-    def __init__(self, units="in", kind="generic", z_move = 0.1,
-                 feedrate = 3.0, z_cut = -0.002, tooldia = 0.0):
+    def __init__(self, units="in", kind="generic", z_move=0.1,
+                 feedrate=3.0, z_cut=-0.002, tooldia=0.0):
         
         # Options
         self.kind = kind
@@ -358,19 +362,17 @@ class CNCjob(Geometry):
         
         # Bounds of geometry given to CNCjob.generate_from_geometry()
         self.input_geometry_bounds = None
-        
-        
-        
+
         # Output generated by CNCjob.create_gcode_geometry()
         #self.G_geometry = None
         self.gcode_parsed = None
         
     def generate_from_excellon(self, exobj):
-        '''
+        """
         Generates G-code for drilling from excellon text.
         self.gcode becomes a list, each element is a
         different job for each tool in the excellon code.
-        '''
+        """
         self.kind = "drill"
         self.gcode = []
         
@@ -399,23 +401,23 @@ class CNCjob(Geometry):
                 gcode += t%point
                 gcode += down + up
             
-            gcode += t%(0,0)
-            gcode += "M05\n" # Spindle stop
+            gcode += t%(0, 0)
+            gcode += "M05\n"  # Spindle stop
             
             self.gcode.append(gcode)
             
     def generate_from_geometry(self, geometry, append=True, tooldia=None):
-        '''
+        """
         Generates G-Code for geometry (Shapely collection).
-        '''
-        if tooldia == None:
+        """
+        if tooldia is None:
             tooldia = self.tooldia
         else:
             self.tooldia = tooldia
             
         self.input_geometry_bounds = geometry.bounds
         
-        if append == False:
+        if not append:
             self.gcode = ""
         t = "G0%d X%.4fY%.4f\n"
         self.gcode = self.unitcode[self.units] + "\n"
@@ -429,50 +431,50 @@ class CNCjob(Geometry):
         for geo in geometry:
             
             if type(geo) == Polygon:
-                path = list(geo.exterior.coords)            # Polygon exterior
-                self.gcode += t%(0, path[0][0], path[0][1]) # Move to first point
-                self.gcode += "G01 Z%.4f\n"%self.z_cut      # Start cutting
+                path = list(geo.exterior.coords)             # Polygon exterior
+                self.gcode += t%(0, path[0][0], path[0][1])  # Move to first point
+                self.gcode += "G01 Z%.4f\n"%self.z_cut       # Start cutting
                 for pt in path[1:]:
-                    self.gcode += t%(1, pt[0], pt[1])   # Linear motion to point
-                self.gcode += "G00 Z%.4f\n"%self.z_move # Stop cutting
-                for ints in geo.interiors:             # Polygon interiors
+                    self.gcode += t%(1, pt[0], pt[1])    # Linear motion to point
+                self.gcode += "G00 Z%.4f\n"%self.z_move  # Stop cutting
+                for ints in geo.interiors:               # Polygon interiors
                     path = list(ints.coords)
-                    self.gcode += t%(0, path[0][0], path[0][1]) # Move to first point
-                    self.gcode += "G01 Z%.4f\n"%self.z_cut # Start cutting
+                    self.gcode += t%(0, path[0][0], path[0][1])  # Move to first point
+                    self.gcode += "G01 Z%.4f\n"%self.z_cut       # Start cutting
                     for pt in path[1:]:
-                        self.gcode += t%(1, pt[0], pt[1]) # Linear motion to point
-                    self.gcode += "G00 Z%.4f\n"%self.z_move # Stop cutting
+                        self.gcode += t%(1, pt[0], pt[1])    # Linear motion to point
+                    self.gcode += "G00 Z%.4f\n"%self.z_move  # Stop cutting
                 continue
             
             if type(geo) == LineString or type(geo) == LinearRing:
                 path = list(geo.coords)
-                self.gcode += t%(0, path[0][0], path[0][1]) # Move to first point
-                self.gcode += "G01 Z%.4f\n"%self.z_cut      # Start cutting
+                self.gcode += t%(0, path[0][0], path[0][1])  # Move to first point
+                self.gcode += "G01 Z%.4f\n"%self.z_cut       # Start cutting
                 for pt in path[1:]:
-                    self.gcode += t%(1, pt[0], pt[1])   # Linear motion to point
-                self.gcode += "G00 Z%.4f\n"%self.z_move # Stop cutting
+                    self.gcode += t%(1, pt[0], pt[1])    # Linear motion to point
+                self.gcode += "G00 Z%.4f\n"%self.z_move  # Stop cutting
                 continue
             
             if type(geo) == Point:
                 path = list(geo.coords)
-                self.gcode += t%(0, path[0][0], path[0][1]) # Move to first point
-                self.gcode += "G01 Z%.4f\n"%self.z_cut      # Start cutting
-                self.gcode += "G00 Z%.4f\n"%self.z_move     # Stop cutting
+                self.gcode += t%(0, path[0][0], path[0][1])  # Move to first point
+                self.gcode += "G01 Z%.4f\n"%self.z_cut       # Start cutting
+                self.gcode += "G00 Z%.4f\n"%self.z_move      # Stop cutting
                 continue
             
             print "WARNING: G-code generation not implemented for %s"%(str(type(geo)))
         
         self.gcode += "G00 Z%.4f\n"%self.z_move     # Stop cutting
         self.gcode += "G00 X0Y0\n"
-        self.gcode += "M05\n" # Spindle stop
+        self.gcode += "M05\n"  # Spindle stop
     
     def gcode_parse(self):
         steps_per_circ = 20
-        '''
+        """
         G-Code parser (from self.gcode). Generates dictionary with 
         single-segment LineString's and "kind" indicating cut or travel, 
         fast or feedrate speed.
-        '''
+        """
         geometry = []        
         
         # TODO: ???? bring this into the class??
@@ -513,20 +515,19 @@ class CNCjob(Geometry):
                     kind[1] = 'S'
                    
                 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
+                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, 
+                    geometry.append({'geom': arc(center, radius, start, stop,
                                                 arcdir[current['G']],
                                                 steps_per_circ),
-                                     'kind':kind})
-                
-            
+                                     'kind': kind})
+
             # Update current instruction
             for code in gobj:
                 current[code] = gobj[code]
@@ -536,13 +537,13 @@ class CNCjob(Geometry):
         return geometry
         
     def plot(self, tooldia=None, dpi=75, margin=0.1,
-             color={"T":["#F0E24D", "#B5AB3A"], "C":["#5E6CFF", "#4650BD"]},
-             alpha={"T":0.3, "C":1.0}):
-        '''
+             color={"T": ["#F0E24D", "#B5AB3A"], "C": ["#5E6CFF", "#4650BD"]},
+             alpha={"T": 0.3, "C": 1.0}):
+        """
         Creates a Matplotlib figure with a plot of the
         G-code job.
-        '''
-        if tooldia == None:
+        """
+        if tooldia is None:
             tooldia = self.tooldia
             
         fig = Figure(dpi=dpi)
@@ -571,12 +572,12 @@ class CNCjob(Geometry):
         return fig
         
     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}):
-        '''
+             color={"T": ["#F0E24D", "#B5AB3A"], "C": ["#5E6CFF", "#4650BD"]},
+             alpha={"T": 0.3, "C":1.0}):
+        """
         Plots the G-code job onto the given axes.
-        '''
-        if tooldia == None:
+        """
+        if tooldia is None:
             tooldia = self.tooldia
         
         if tooldia == 0:
@@ -598,14 +599,13 @@ class CNCjob(Geometry):
     def create_geometry(self):
         self.solid_geometry = cascaded_union([geo['geom'] for geo in self.gcode_parsed])
 
-                
 
 def gparse1b(gtext):
-    '''
+    """
     gtext is a single string with g-code
-    '''
+    """
     gcmds = []
-    lines = gtext.split("\n") # TODO: This is probably a lot of work!
+    lines = gtext.split("\n")  # TODO: This is probably a lot of work!
     for line in lines:
         line = line.strip()
         
@@ -635,8 +635,8 @@ def gparse1b(gtext):
         # 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() )
+            parts.append(line[codes_idx[p]:codes_idx[p+1]].strip())
+        parts.append(line[codes_idx[-1]:].strip())
         
         # Separate codes from values
         cmds = {}
@@ -645,6 +645,7 @@ def gparse1b(gtext):
         gcmds.append(cmds)
     return gcmds
 
+
 def get_bounds(geometry_set):
     xmin = Inf
     ymin = Inf
@@ -660,12 +661,13 @@ def get_bounds(geometry_set):
             
     return [xmin, ymin, xmax, ymax]
 
+
 def arc(center, radius, start, stop, direction, steps_per_circ):
-    da_sign = {"cw":-1.0, "ccw":1.0}    
+    da_sign = {"cw": -1.0, "ccw": 1.0}
     points = []
-    if direction=="ccw" and stop <= start:
+    if direction == "ccw" and stop <= start:
         stop += 2*pi
-    if direction=="cw" and stop >= start:
+    if direction == "cw" and stop >= start:
         stop -= 2*pi
     
     angle = abs(stop - start)
@@ -677,20 +679,21 @@ def arc(center, radius, start, stop, direction, steps_per_circ):
         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):
-    '''
+    """
     Parse Gerber coordinates
-    '''
+    """
     global gerbx, gerby
     xindex = gstr.find("X")
     yindex = gstr.find("Y")
     index = gstr.find("D")
-    if (xindex == -1):
+    if xindex == -1:
         x = gerbx
         y = int(gstr[(yindex+1):index])*(10**(-fraction))
-    elif (yindex == -1):
+    elif yindex == -1:
         y = gerby
         x = int(gstr[(xindex+1):index])*(10**(-fraction))
     else:
@@ -698,7 +701,5 @@ def coord(gstr,digits,fraction):
         y = int(gstr[(yindex+1):index])*(10**(-fraction))
     gerbx = x
     gerby = y
-    return [x,y]
-
-
+    return [x, y]
 ################ end of cam.py #############

+ 75 - 16
cirkuix.py

@@ -86,10 +86,10 @@ def get_entry_float(entry):
 def get_entry_eval(entry):
     return eval(entry.get_text)
 
-getters = {"entry_text":get_entry_text,
-           "entry_int":get_entry_int,
-           "entry_float":get_entry_float,
-           "entry_eval":get_entry_eval}
+getters = {"entry_text": get_entry_text,
+           "entry_int": get_entry_int,
+           "entry_float": get_entry_float,
+           "entry_eval": get_entry_eval}
 
 setters = {"entry"}
 
@@ -105,7 +105,7 @@ class App:
         self.builder.add_from_file(self.gladefile)
         self.window = self.builder.get_object("window1")
         self.window.set_title("Cirkuix")
-        self.positionLabel = self.builder.get_object("label3")      
+        self.position_label = self.builder.get_object("label3")
         self.grid = self.builder.get_object("grid1")
         self.notebook = self.builder.get_object("notebook1")
         self.info_label = self.builder.get_object("label_status")
@@ -248,20 +248,27 @@ class App:
         
     def plot_geometry(self, geometry):
         for geo in geometry.solid_geometry:
-            x, y = geo.exterior.coords.xy
-            self.axes.plot(x, y, 'r-')
-            for ints in geo.interiors:
-                x, y = ints.coords.xy
+
+            if type(geo) == Polygon:
+                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, 'r-')
+                continue
+
+            if type(geo) == LineString or type(geo) == LinearRing:
+                x, y = geo.coords.xy
+                self.axes.plot(x, y, 'r-')
+                continue
                 
         self.canvas.queue_draw()
-            
-        
+
     def setup_component_viewer(self):
-        '''
+        """
         List or Tree where whatever has been loaded or created is
         displayed.
-        '''
+        """
         self.store = Gtk.ListStore(str)
         self.tree = Gtk.TreeView(self.store)
         select = self.tree.get_selection()
@@ -353,7 +360,59 @@ class App:
     ########################################
     ##         EVENT HANDLERS             ##
     ########################################
+    def on_gerber_generate_boundary(self, widget):
+        margin = self.get_eval("entry_gerber_cutout_margin")
+        gap_size = self.get_eval("entry_gerber_cutout_gapsize")
+        gerber = self.stuff[self.selected_item_name]
+        minx, miny, maxx, maxy = gerber.bounds()
+        minx -= margin
+        maxx += margin
+        miny -= margin
+        maxy += margin
+        midx = 0.5 * (minx + maxx)
+        midy = 0.5 * (miny + maxy)
+        hgap = 0.5 * gap_size
+        pts = [[midx-hgap, maxy],
+               [minx, maxy],
+               [minx, midy+hgap],
+               [minx, midy-hgap],
+               [minx, miny],
+               [midx-hgap, miny],
+               [midx+hgap, miny],
+               [maxx, miny],
+               [maxx, midy-hgap],
+               [maxx, midy+hgap],
+               [maxx, maxy],
+               [midx+hgap, maxy]]
+        cases = {"tb": [[pts[0], pts[1], pts[4], pts[5]],
+                        [pts[6], pts[7], pts[10], pts[11]]],
+                 "lr": [[pts[9], pts[10], pts[1], pts[2]],
+                        [pts[3], pts[4], pts[7], pts[8]]],
+                 "4": [[pts[0], pts[1], pts[2]],
+                       [pts[3], pts[4], pts[5]],
+                       [pts[6], pts[7], pts[8]],
+                       [pts[9], pts[10], pts[11]]]}
+        name = self.selected_item_name + "_cutout"
+        geometry = CirkuixGeometry(name)
+        cuts = None
+        if self.builder.get_object("rb_2tb").get_active():
+            cuts = cases["tb"]
+        elif self.builder.get_object("rb_2lr").get_active():
+            cuts = cases["lr"]
+        else:
+            cuts = cases["4"]
+        geometry.solid_geometry = cascaded_union([LineString(segment) for segment in cuts])
+
+        # Add to App and update.
+        self.stuff[name] = geometry
+        self.build_list()
+
     def on_eval_update(self, widget):
+        """
+        Modifies the content of a Gtk.Entry by running
+        eval() on its contents and puting it back as a
+        string.
+        """
         # TODO: error handling here
         widget.set_text(str(eval(widget.get_text())))
 
@@ -384,7 +443,7 @@ class App:
         
         geometry = self.stuff[self.selected_item_name]
         job_name = self.selected_item_name + "_cnc"
-        job = CirkuixCNCjob(job_name, z_move = travelz, z_cut = cutz, feedrate = feedrate)
+        job = CirkuixCNCjob(job_name, z_move=travelz, z_cut=cutz, feedrate=feedrate)
         job.generate_from_geometry(geometry.solid_geometry)
         job.gcode_parse()
         job.create_geometry()
@@ -613,11 +672,11 @@ class App:
         
     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"%(
+            self.position_label.set_label("X: %.4f   Y: %.4f"%(
                                          event.xdata, event.ydata))
             self.mouse = [event.xdata, event.ydata]
         except:
-            self.positionLabel.set_label("")
+            self.position_label.set_label("")
             self.mouse = None
         
     def on_click_over_plot(self, event):

+ 186 - 6
cirkuix.ui

@@ -767,9 +767,6 @@
             <property name="position">6</property>
           </packing>
         </child>
-        <child>
-          <placeholder/>
-        </child>
         <child>
           <object class="GtkLabel" id="label13">
             <property name="visible">True</property>
@@ -785,7 +782,7 @@
           <packing>
             <property name="expand">False</property>
             <property name="fill">True</property>
-            <property name="position">8</property>
+            <property name="position">7</property>
           </packing>
         </child>
         <child>
@@ -830,7 +827,7 @@
           <packing>
             <property name="expand">False</property>
             <property name="fill">True</property>
-            <property name="position">9</property>
+            <property name="position">8</property>
           </packing>
         </child>
         <child>
@@ -842,12 +839,195 @@
             <signal name="activate" handler="on_generate_isolation" swapped="no"/>
             <signal name="clicked" handler="on_generate_isolation" swapped="no"/>
           </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">9</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkLabel" id="label4">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="margin_top">5</property>
+            <property name="xalign">0</property>
+            <property name="ypad">3</property>
+            <property name="label" translatable="yes">Board cutout:</property>
+            <attributes>
+              <attribute name="weight" value="semibold"/>
+            </attributes>
+          </object>
           <packing>
             <property name="expand">False</property>
             <property name="fill">True</property>
             <property name="position">10</property>
           </packing>
         </child>
+        <child>
+          <object class="GtkGrid" id="grid4">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="row_spacing">3</property>
+            <child>
+              <object class="GtkLabel" id="label27">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="xalign">1</property>
+                <property name="label" translatable="yes">Margin: </property>
+              </object>
+              <packing>
+                <property name="left_attach">0</property>
+                <property name="top_attach">0</property>
+                <property name="width">1</property>
+                <property name="height">1</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkEntry" id="entry_gerber_cutout_margin">
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="invisible_char">●</property>
+              </object>
+              <packing>
+                <property name="left_attach">1</property>
+                <property name="top_attach">0</property>
+                <property name="width">1</property>
+                <property name="height">1</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkEntry" id="entry_gerber_cutout_gapsize">
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="invisible_char">●</property>
+              </object>
+              <packing>
+                <property name="left_attach">1</property>
+                <property name="top_attach">1</property>
+                <property name="width">1</property>
+                <property name="height">1</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkLabel" id="label28">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="xalign">1</property>
+                <property name="label" translatable="yes">Gap size: </property>
+              </object>
+              <packing>
+                <property name="left_attach">0</property>
+                <property name="top_attach">1</property>
+                <property name="width">1</property>
+                <property name="height">1</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkLabel" id="label29">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="xalign">1</property>
+                <property name="label" translatable="yes">Gaps: </property>
+              </object>
+              <packing>
+                <property name="left_attach">0</property>
+                <property name="top_attach">2</property>
+                <property name="width">1</property>
+                <property name="height">1</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkBox" id="box10">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <child>
+                  <object class="GtkRadioButton" id="rb_2tb">
+                    <property name="label" translatable="yes">2 (T/B)</property>
+                    <property name="visible">True</property>
+                    <property name="can_focus">True</property>
+                    <property name="receives_default">False</property>
+                    <property name="margin_right">8</property>
+                    <property name="xalign">0</property>
+                    <property name="active">True</property>
+                    <property name="draw_indicator">True</property>
+                    <property name="group">rb_2lr</property>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="position">0</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkRadioButton" id="rb_2lr">
+                    <property name="label" translatable="yes">2 (L/R)</property>
+                    <property name="visible">True</property>
+                    <property name="can_focus">True</property>
+                    <property name="receives_default">False</property>
+                    <property name="margin_right">8</property>
+                    <property name="xalign">0</property>
+                    <property name="active">True</property>
+                    <property name="draw_indicator">True</property>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="position">1</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkRadioButton" id="rb_4">
+                    <property name="label" translatable="yes">4</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="active">True</property>
+                    <property name="draw_indicator">True</property>
+                    <property name="group">rb_2lr</property>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="position">2</property>
+                  </packing>
+                </child>
+              </object>
+              <packing>
+                <property name="left_attach">1</property>
+                <property name="top_attach">2</property>
+                <property name="width">1</property>
+                <property name="height">1</property>
+              </packing>
+            </child>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">11</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkButton" id="button4">
+            <property name="label" translatable="yes">Generate Geometry</property>
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="receives_default">True</property>
+            <signal name="activate" handler="on_gerber_generate_boundary" swapped="no"/>
+            <signal name="clicked" handler="on_gerber_generate_boundary" swapped="no"/>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">12</property>
+          </packing>
+        </child>
+        <child>
+          <placeholder/>
+        </child>
+        <child>
+          <placeholder/>
+        </child>
         <child>
           <placeholder/>
         </child>
@@ -997,7 +1177,7 @@
               <object class="GtkMenuItem" id="menuitem3">
                 <property name="visible">True</property>
                 <property name="can_focus">False</property>
-                <property name="label" translatable="yes">_View</property>
+                <property name="label" translatable="yes">_Tools</property>
                 <property name="use_underline">True</property>
               </object>
             </child>