فهرست منبع

Excellon to g-code and bed flattening features.

Juan Pablo Caram 12 سال پیش
والد
کامیت
3459c55da4
3فایلهای تغییر یافته به همراه836 افزوده شده و 168 حذف شده
  1. 165 89
      camlib.py
  2. 261 50
      cirkuix.py
  3. 410 29
      cirkuix.ui

+ 165 - 89
camlib.py

@@ -129,27 +129,27 @@ class Gerber (Geometry):
         into dictionary of apertures.
         """
         indexstar = gline.find("*")
-        indexC = gline.find("C,")
-        if indexC != -1: # Circle, example: %ADD11C,0.1*%
-            apid = gline[4:indexC]
+        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])}
+                                    "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")
+        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])}
+                                    "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")
+        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])}
+                                    "width": float(gline[indexo+2:indexx]),
+                                    "height": float(gline[indexx+1:indexstar])}
             return apid
         print "WARNING: Aperture not implemented:", gline
         return None
@@ -187,12 +187,12 @@ class Gerber (Geometry):
                 path = [coord(gline, self.digits, self.fraction)]
                 continue
             
-            indexD3 = gline.find("D03*")
-            if indexD3 > 0:  # Flash
+            indexd3 = gline.find("D03*")
+            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
             
@@ -216,16 +216,16 @@ class Gerber (Geometry):
                 continue
             
             if gline.find("%FS") != -1:  # Format statement
-                indexX = gline.find("X")
-                self.digits = int(gline[indexX + 1])
-                self.fraction = int(gline[indexX + 2])
+                indexx = gline.find("X")
+                self.digits = int(gline[indexx + 1])
+                self.fraction = int(gline[indexx + 2])
                 continue
             print "WARNING: Line ignored:", gline
         
         if len(path) > 1:
             # EOF, create shapely LineString if something in path
-            self.paths.append({"linestring":LineString(path), 
-                               "aperture":last_path_aperture})
+            self.paths.append({"linestring": LineString(path),
+                               "aperture": last_path_aperture})
     
     def do_flashes(self):
         """
@@ -318,11 +318,11 @@ class Excellon(Geometry):
                 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)
+            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
             
@@ -342,29 +342,28 @@ class Excellon(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):
-        
-        # Options
+        """
+
+            @param units:
+            @param kind:
+            @param z_move:
+            @param feedrate:
+            @param z_cut:
+            @param tooldia:
+            """
+        Geometry.__init__(self)
         self.kind = kind
         self.units = units
         self.z_cut = z_cut
         self.z_move = z_move
         self.feedrate = feedrate
         self.tooldia = tooldia
-        
-        # Constants
         self.unitcode = {"in": "G20", "mm": "G21"}
         self.pausecode = "G04 P1"
         self.feedminutecode = "G94"
         self.absolutecode = "G90"
-        
-        # Input/Output G-Code
         self.gcode = ""
-        
-        # 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):
@@ -377,13 +376,12 @@ class CNCjob(Geometry):
         self.gcode = []
         
         t = "G00 X%.4fY%.4f\n"
-        down = "G01 Z%.4f\n"%self.z_cut
-        up = "G01 Z%.4f\n"%self.z_move
-        
+        down = "G01 Z%.4f\n" % self.z_cut
+        up = "G01 Z%.4f\n" % self.z_move
+
         for tool in exobj.tools:
             
             points = []
-            gcode = ""
             
             for drill in exobj.drill:
                 if drill['tool'] == tool:
@@ -392,79 +390,111 @@ class CNCjob(Geometry):
             gcode = self.unitcode[self.units] + "\n"
             gcode += self.absolutecode + "\n"
             gcode += self.feedminutecode + "\n"
-            gcode += "F%.2f\n"%self.feedrate
-            gcode += "G00 Z%.4f\n"%self.z_move  # Move to travel height
-            gcode += "M03\n" # Spindle start
+            gcode += "F%.2f\n" % self.feedrate
+            gcode += "G00 Z%.4f\n" % self.z_move  # Move to travel height
+            gcode += "M03\n"  # Spindle start
             gcode += self.pausecode + "\n"
             
             for point in points:
-                gcode += t%point
+                gcode += t % point
                 gcode += down + up
             
-            gcode += t%(0, 0)
+            gcode += t % (0, 0)
             gcode += "M05\n"  # Spindle stop
             
             self.gcode.append(gcode)
-            
+
+    def generate_from_excellon_by_tool(self, exobj, tools="all"):
+        """
+        Creates gcode for this object from an Excellon object
+        for the specified tools.
+        @param exobj: Excellon object to process
+        @type exobj: Excellon
+        @param tools: Comma separated tool names
+        @type: tools: str
+        @return: None
+        """
+        print "Creating CNC Job from Excellon..."
+        if tools == "all":
+            tools = [tool for tool in exobj.tools]
+        else:
+            tools = [x.strip() for x in tools.split(",")]
+            tools = filter(lambda y: y in exobj.tools, tools)
+        print "Tools are:", tools
+
+        points = []
+        for drill in exobj.drills:
+            if drill['tool'] in tools:
+                points.append(drill['point'])
+
+        print "Found %d drills." % len(points)
+        #self.kind = "drill"
+        self.gcode = []
+
+        t = "G00 X%.4fY%.4f\n"
+        down = "G01 Z%.4f\n" % self.z_cut
+        up = "G01 Z%.4f\n" % self.z_move
+
+        gcode = self.unitcode[self.units] + "\n"
+        gcode += self.absolutecode + "\n"
+        gcode += self.feedminutecode + "\n"
+        gcode += "F%.2f\n" % self.feedrate
+        gcode += "G00 Z%.4f\n" % self.z_move  # Move to travel height
+        gcode += "M03\n"  # Spindle start
+        gcode += self.pausecode + "\n"
+
+        for point in points:
+            x, y = point.coords.xy
+            gcode += t % (x[0], y[0])
+            gcode += down + up
+
+        gcode += t % (0, 0)
+        gcode += "M05\n"  # Spindle stop
+
+        self.gcode = gcode
+
     def generate_from_geometry(self, geometry, append=True, tooldia=None):
         """
-        Generates G-Code for geometry (Shapely collection).
+        Generates G-Code from a Geometry object.
         """
-        if tooldia is None:
-            tooldia = self.tooldia
-        else:
+        if tooldia is not None:
             self.tooldia = tooldia
             
-        self.input_geometry_bounds = geometry.bounds
+        self.input_geometry_bounds = geometry.bounds()
         
         if not append:
             self.gcode = ""
-        t = "G0%d X%.4fY%.4f\n"
+
         self.gcode = self.unitcode[self.units] + "\n"
         self.gcode += self.absolutecode + "\n"
         self.gcode += self.feedminutecode + "\n"
-        self.gcode += "F%.2f\n"%self.feedrate
-        self.gcode += "G00 Z%.4f\n"%self.z_move  # Move to travel height
+        self.gcode += "F%.2f\n" % self.feedrate
+        self.gcode += "G00 Z%.4f\n" % self.z_move  # Move to travel height
         self.gcode += "M03\n"  # Spindle start
         self.gcode += self.pausecode + "\n"
         
-        for geo in geometry:
+        for geo in geometry.solid_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
-                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
-                    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
-                    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 += self.polygon2gcode(geo)
                 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
-                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 += self.linear2gcode(geo)
                 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 += self.point2gcode(geo)
                 continue
-            
-            print "WARNING: G-code generation not implemented for %s"%(str(type(geo)))
+
+            if type(geo) == MultiPolygon:
+                for poly in geo:
+                    self.gcode += self.polygon2gcode(poly)
+                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 Z%.4f\n" % self.z_move  # Stop cutting
         self.gcode += "G00 X0Y0\n"
         self.gcode += "M05\n"  # Spindle stop
     
@@ -475,8 +505,10 @@ class CNCjob(Geometry):
         fast or feedrate speed.
         """
 
+        # TODO: Make this a parameter
         steps_per_circ = 20
 
+        # Results go here
         geometry = []        
         
         # TODO: ???? bring this into the class??
@@ -523,8 +555,8 @@ class CNCjob(Geometry):
                 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)
+                    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),
@@ -601,6 +633,49 @@ class CNCjob(Geometry):
     def create_geometry(self):
         self.solid_geometry = cascaded_union([geo['geom'] for geo in self.gcode_parsed])
 
+    def polygon2gcode(self, polygon):
+        """
+        Creates G-Code for the exterior and all interior paths
+        of a polygon.
+        @param polygon: A Shapely.Polygon
+        @type polygon: Shapely.Polygon
+        """
+        gcode = ""
+        t = "G0%d X%.4fY%.4f\n"
+        path = list(polygon.exterior.coords)             # Polygon exterior
+        gcode += t % (0, path[0][0], path[0][1])  # Move to first point
+        gcode += "G01 Z%.4f\n" % self.z_cut       # Start cutting
+        for pt in path[1:]:
+            gcode += t % (1, pt[0], pt[1])    # Linear motion to point
+        gcode += "G00 Z%.4f\n" % self.z_move  # Stop cutting
+        for ints in polygon.interiors:               # Polygon interiors
+            path = list(ints.coords)
+            gcode += t % (0, path[0][0], path[0][1])  # Move to first point
+            gcode += "G01 Z%.4f\n" % self.z_cut       # Start cutting
+            for pt in path[1:]:
+                gcode += t % (1, pt[0], pt[1])    # Linear motion to point
+            gcode += "G00 Z%.4f\n" % self.z_move  # Stop cutting
+        return gcode
+
+    def linear2gcode(self, linear):
+        gcode = ""
+        t = "G0%d X%.4fY%.4f\n"
+        path = list(linear.coords)
+        gcode += t%(0, path[0][0], path[0][1])  # Move to first point
+        gcode += "G01 Z%.4f\n" % self.z_cut       # Start cutting
+        for pt in path[1:]:
+            gcode += t % (1, pt[0], pt[1])    # Linear motion to point
+        gcode += "G00 Z%.4f\n" % self.z_move  # Stop cutting
+        return gcode
+
+    def point2gcode(self, point):
+        gcode = ""
+        t = "G0%d X%.4fY%.4f\n"
+        path = list(point.coords)
+        gcode += t % (0, path[0][0], path[0][1])  # Move to first point
+        gcode += "G01 Z%.4f\n" % self.z_cut       # Start cutting
+        gcode += "G00 Z%.4f\n" % self.z_move      # Stop cutting
+
 
 def gparse1b(gtext):
     """
@@ -653,7 +728,8 @@ def get_bounds(geometry_set):
     ymin = Inf
     xmax = -Inf
     ymax = -Inf
-    
+
+    print "Getting bounds of:", str(geometry_set)
     for gs in geometry_set:
         gxmin, gymin, gxmax, gymax = geometry_set[gs].bounds()
         xmin = min([xmin, gxmin])
@@ -699,7 +775,7 @@ def arc(center, radius, start, stop, direction, steps_per_circ):
     return LineString(points)
 
 
-def clear_poly(poly, tooldia, overlap = 0.1):
+def clear_poly(poly, tooldia, overlap=0.1):
     """
     Creates a list of Shapely geometry objects covering the inside
     of a Shapely.Polygon. Use for removing all the copper in a region

+ 261 - 50
cirkuix.py

@@ -21,15 +21,8 @@ class CirkuixObj:
     by the user in their respective forms.
     """
 
-    form_getters = {}
-
-    form_setters = {}
-
     # Instance of the application to which these are related.
     # The app should set this value.
-    # TODO: Move the definitions of form_getters and form_setters
-    # TODO: to their respective classes and use app to reference
-    # TODO: the builder.
     app = None
 
     def __init__(self, name):
@@ -41,11 +34,25 @@ class CirkuixObj:
         self.kind = None  # Override with proper name
 
     def setup_axes(self, figure):
+        """
+        1) Creates axes if they don't exist. 2) Clears axes. 3) Attaches
+        them to figure if not part of the figure. 4) Sets transparent
+        background. 5) Sets 1:1 scale aspect ratio.
+        @param figure: A Matplotlib.Figure on which to add/configure axes.
+        @type figure: matplotlib.figure.Figure
+        @return: None
+        """
         if self.axes is None:
+            print "New axes"
             self.axes = figure.add_axes([0.05, 0.05, 0.9, 0.9],
                                         label=self.options["name"])
         elif self.axes not in figure.axes:
+            print "Clearing and attaching axes"
+            self.axes.cla()
             figure.add_axes(self.axes)
+        else:
+            print "Clearing Axes"
+            self.axes.cla()
 
         self.axes.patch.set_visible(False)  # No background
         self.axes.set_aspect(1)
@@ -62,7 +69,11 @@ class CirkuixObj:
             self.set_form_item(option)
 
     def read_form(self):
-        for option in self.form_getters:
+        """
+        Reads form into self.options
+        @rtype : None
+        """
+        for option in self.options:
             self.read_form_item(option)
 
     def build_ui(self):
@@ -114,7 +125,7 @@ class CirkuixObj:
         fname = fkind + "_" + self.kind + "_" + option
 
         if fkind == 'entry_text':
-            self.options[option] = self.app.builder(fname).get_text()
+            self.options[option] = self.app.builder.get_object(fname).get_text()
             return
         if fkind == 'entry_eval':
             self.options[option] = self.app.get_eval(fname)
@@ -127,6 +138,18 @@ class CirkuixObj:
             return
         print "Unknown kind of form item:", fkind
 
+    def plot(self, figure):
+        """
+        Extend this method! Sets up axes if needed and
+        clears them. Descendants must do the actual plotting.
+        """
+        # Creates the axes if necessary and sets them up.
+        self.setup_axes(figure)
+
+        # Clear axes.
+        # self.axes.cla()
+        # return
+
 
 class CirkuixGerber(CirkuixObj, Gerber):
     """
@@ -149,9 +172,12 @@ class CirkuixGerber(CirkuixObj, Gerber):
             "cutoutmargin": 0.2,
             "cutoutgapsize": 0.15,
             "gaps": "tb",
-            "noncoppermargin": 0.0
+            "noncoppermargin": 0.0,
+            "bboxmargin": 0.0,
+            "bboxrounded": False
         })
 
+        # The 'name' is already in self.form_kinds
         self.form_kinds.update({
             "plot": "cb",
             "mergepolys": "cb",
@@ -161,14 +187,16 @@ class CirkuixGerber(CirkuixObj, Gerber):
             "cutoutmargin": "entry_eval",
             "cutoutgapsize": "entry_eval",
             "gaps": "radio",
-            "noncoppermargin": "entry_eval"
+            "noncoppermargin": "entry_eval",
+            "bboxmargin": "entry_eval",
+            "bboxrounded": "cb"
         })
 
         self.radios = {"gaps": {"rb_2tb": "tb", "rb_2lr": "lr", "rb_4": "4"}}
         self.radios_inv = {"gaps": {"tb": "rb_2tb", "lr": "rb_2lr", "4": "rb_4"}}
 
     def plot(self, figure):
-        self.setup_axes(figure)
+        CirkuixObj.plot(self, figure)
 
         self.create_geometry()
 
@@ -208,15 +236,25 @@ class CirkuixExcellon(CirkuixObj, Excellon):
         self.options.update({
             "plot": True,
             "solid": False,
-            "multicolored": False
+            "multicolored": False,
+            "drillz": -0.1,
+            "travelz": 0.1,
+            "feedrate": 5.0,
+            "toolselection": ""
         })
 
         self.form_kinds.update({
             "plot": "cb",
             "solid": "cb",
-            "multicolored": "cb"
+            "multicolored": "cb",
+            "drillz": "entry_eval",
+            "travelz": "entry_eval",
+            "feedrate": "entry_eval",
+            "toolselection": "entry_text"
         })
 
+        self.tool_cbs = {}
+
     def plot(self, figure):
         self.setup_axes(figure)
         self.create_geometry()
@@ -229,6 +267,30 @@ class CirkuixExcellon(CirkuixObj, Excellon):
                 x, y = ints.coords.xy
                 self.axes.plot(x, y, 'g-')
 
+    def show_tool_chooser(self):
+        win = Gtk.Window()
+        box = Gtk.Box(spacing=2)
+        box.set_orientation(Gtk.Orientation(1))
+        win.add(box)
+        for tool in self.tools:
+            self.tool_cbs[tool] = Gtk.CheckButton(label=tool+": "+self.tools[tool])
+            box.pack_start(self.tool_cbs[tool], False, False, 1)
+        button = Gtk.Button(label="Accept")
+        box.pack_start(button, False, False, 1)
+        win.show_all()
+
+        def on_accept(widget):
+            win.destroy()
+            tool_list = []
+            for tool in self.tool_cbs:
+                if self.tool_cbs[tool].get_active():
+                    tool_list.append(tool)
+            self.options["toolselection"] = ", ".join(tool_list)
+            self.to_form()
+
+        button.connect("activate", on_accept)
+        button.connect("clicked", on_accept)
+
 
 class CirkuixCNCjob(CirkuixObj, CNCjob):
     """
@@ -245,7 +307,7 @@ class CirkuixCNCjob(CirkuixObj, CNCjob):
         self.options.update({
             "plot": True,
             "solid": False,
-            "multicolored": "cb",
+            "multicolored": False,
             "tooldia": 0.4/25.4  # 0.4mm in inches
         })
 
@@ -259,6 +321,7 @@ class CirkuixCNCjob(CirkuixObj, CNCjob):
     def plot(self, figure):
         self.setup_axes(figure)
         self.plot2(self.axes, tooldia=self.options["tooldia"])
+        app.canvas.queue_draw()
 
 
 class CirkuixGeometry(CirkuixObj, Geometry):
@@ -279,8 +342,10 @@ class CirkuixGeometry(CirkuixObj, Geometry):
             "cutz": -0.002,
             "travelz": 0.1,
             "feedrate": 5.0,
+            "cnctooldia": 0.4/25.4,
             "painttooldia": 0.0625,
-            "paintoverlap": 0.15
+            "paintoverlap": 0.15,
+            "paintmargin": 0.01
         })
 
         self.form_kinds.update({
@@ -290,13 +355,20 @@ class CirkuixGeometry(CirkuixObj, Geometry):
             "cutz": "entry_eval",
             "travelz": "entry_eval",
             "feedrate": "entry_eval",
+            "cnctooldia": "entry_eval",
             "painttooldia": "entry_eval",
-            "paintoverlap": "entry_eval"
+            "paintoverlap": "entry_eval",
+            "paintmargin": "entry_eval"
         })
 
     def plot(self, figure):
         self.setup_axes(figure)
 
+        try:
+            _ = iter(self.solid_geometry)
+        except TypeError:
+            self.solid_geometry = [self.solid_geometry]
+
         for geo in self.solid_geometry:
 
             if type(geo) == Polygon:
@@ -312,6 +384,17 @@ class CirkuixGeometry(CirkuixObj, Geometry):
                 self.axes.plot(x, y, 'r-')
                 continue
 
+            if type(geo) == MultiPolygon:
+                for poly in geo:
+                    x, y = poly.exterior.coords.xy
+                    self.axes.plot(x, y, 'r-')
+                    for ints in poly.interiors:
+                        x, y = ints.coords.xy
+                        self.axes.plot(x, y, 'r-')
+                continue
+
+            print "WARNING: Did not plot:", str(type(geo))
+
 
 ########################################
 ##                App                 ##
@@ -332,9 +415,7 @@ class App:
         GObject.threads_init()
         #Gdk.threads_init()
 
-        ########################################
-        ##                GUI                 ##
-        ########################################       
+        ## GUI ##
         self.gladefile = "cirkuix.ui"
         self.builder = Gtk.Builder()
         self.builder.add_from_file(self.gladefile)
@@ -359,9 +440,7 @@ class App:
         self.setup_component_viewer()
         self.setup_component_editor()
         
-        ########################################
-        ##               DATA                 ##
-        ########################################
+        ## DATA ##
         self.setup_obj_classes()
         self.stuff = {}    # CirkuixObj's by name
         self.mouse = None  # Mouse coordinates over plot
@@ -634,12 +713,107 @@ class App:
     ########################################
     ##         EVENT HANDLERS             ##
     ########################################
+    def on_generate_gerber_bounding_box(self, widget):
+        gerber = self.stuff[self.selected_item_name]
+        gerber.read_form()
+        name = self.selected_item_name + "_bbox"
+
+        def geo_init(geo_obj, app_obj):
+            assert isinstance(geo_obj, CirkuixGeometry)
+            bounding_box = gerber.solid_geometry.envelope.buffer(gerber.options["bboxmargin"])
+            if not gerber.options["bboxrounded"]:
+                bounding_box = bounding_box.envelope
+            geo_obj.solid_geometry = bounding_box
+
+        self.new_object("geometry", name, geo_init)
+
+    def on_update_plot(self, widget):
+        """
+        Callback for button on form for all kinds of objects.
+        Re-plot the current object only.
+        @param widget: The widget from which this was called.
+        @return: None
+        """
+        self.stuff[self.selected_item_name].read_form()
+        self.stuff[self.selected_item_name].plot(self.figure)
+
+    def on_generate_excellon_cncjob(self, widget):
+        """
+        Callback for button active/click on Excellon form to
+        create a CNC Job for the Excellon file.
+        @param widget: The widget from which this was called.
+        @return: None
+        """
+
+        job_name = self.selected_item_name + "_cnc"
+        excellon = self.stuff[self.selected_item_name]
+        assert isinstance(excellon, CirkuixExcellon)
+        excellon.read_form()
+
+        # Object initialization function for app.new_object()
+        def job_init(job_obj, app_obj):
+            excellon_ = self.stuff[self.selected_item_name]
+            assert isinstance(excellon_, CirkuixExcellon)
+            assert isinstance(job_obj, CirkuixCNCjob)
+
+            GLib.idle_add(lambda: app_obj.set_progress_bar(0.2, "Creating CNC Job..."))
+            job_obj.z_cut = excellon_.options["drillz"]
+            job_obj.z_move = excellon_.options["travelz"]
+            job_obj.feedrate = excellon_.options["feedrate"]
+            # There could be more than one drill size...
+            # job_obj.tooldia =   # TODO: duplicate variable!
+            # job_obj.options["tooldia"] =
+            job_obj.generate_from_excellon_by_tool(excellon_, excellon_.options["toolselection"])
+
+            GLib.idle_add(lambda: app_obj.set_progress_bar(0.5, "Parsing G-Code..."))
+            job_obj.gcode_parse()
+
+            GLib.idle_add(lambda: app_obj.set_progress_bar(0.6, "Creating New Geometry..."))
+            job_obj.create_geometry()
+
+            GLib.idle_add(lambda: app_obj.set_progress_bar(0.8, "Plotting..."))
+
+        # To be run in separate thread
+        def job_thread(app_obj):
+            app_obj.new_object("cncjob", job_name, job_init)
+            GLib.idle_add(lambda: app_obj.set_progress_bar(1.0, "Done!"))
+            GLib.timeout_add_seconds(1, lambda: app_obj.set_progress_bar(0.0, ""))
+
+        # Start the thread
+        t = threading.Thread(target=job_thread, args=(self,))
+        t.daemon = True
+        t.start()
+
+    def on_excellon_tool_choose(self, widget):
+        """
+        Callback for button on Excellon form to open up a window for
+        selecting tools.
+        @param widget: The widget from which this was called.
+        @return: None
+        """
+        excellon = self.stuff[self.selected_item_name]
+        assert isinstance(excellon, CirkuixExcellon)
+        excellon.show_tool_chooser()
+
+    def on_entry_eval_activate(self, widget):
+        self.on_eval_update(widget)
+        obj = self.stuff[self.selected_item_name]
+        assert isinstance(obj, CirkuixObj)
+        obj.read_form()
+
     def on_gerber_generate_noncopper(self, widget):
+        """
+        Callback for button on Gerber form to create a geometry object
+        with polygons covering the area without copper or negative of the
+        Gerber.
+        @param widget: The widget from which this was called.
+        @return: None
+        """
         name = self.selected_item_name + "_noncopper"
 
         def geo_init(geo_obj, app_obj):
             assert isinstance(geo_obj, CirkuixGeometry)
-            gerber = app_obj.stuff[self.selected_item_name]
+            gerber = app_obj.stuff[app_obj.selected_item_name]
             assert isinstance(gerber, CirkuixGerber)
             gerber.read_form()
             bounding_box = gerber.solid_geometry.envelope.buffer(gerber.options["noncoppermargin"])
@@ -650,6 +824,12 @@ class App:
         self.new_object("geometry", name, geo_init)
 
     def on_gerber_generate_cutout(self, widget):
+        """
+        Callback for button on Gerber form to create geometry with lines
+        for cutting off the board.
+        @param widget: The widget from which this was called.
+        @return: None
+        """
         name = self.selected_item_name + "_cutout"
 
         def geo_init(geo_obj, app_obj):
@@ -696,11 +876,18 @@ class App:
         Modifies the content of a Gtk.Entry by running
         eval() on its contents and puting it back as a
         string.
+        @param widget: The widget from which this was called.
+        @return: None
         """
         # TODO: error handling here
         widget.set_text(str(eval(widget.get_text())))
 
     def on_generate_isolation(self, widget):
+        """
+        Callback for button on Gerber form to create isolation routing geometry.
+        @param widget: The widget from which this was called.
+        @return: None
+        """
         print "Generating Isolation Geometry:"
         iso_name = self.selected_item_name + "_iso"
 
@@ -714,45 +901,75 @@ class App:
         self.new_object("geometry", iso_name, iso_init)
 
     def on_generate_cncjob(self, widget):
+        """
+        Callback for button on geometry form to generate CNC job.
+        @param widget: The widget from which this was called.
+        @return: None
+        """
         print "Generating CNC job"
-
         job_name = self.selected_item_name + "_cnc"
 
+        # Object initialization function for app.new_object()
         def job_init(job_obj, app_obj):
-            # TODO: Object must be updated on form change and the options
-            # TODO: read from the object.
-            z_cut = app_obj.get_eval("entry_eval_geometry_cutz")
-            z_move = app_obj.get_eval("entry_eval_geometry_travelz")
-            feedrate = app_obj.get_eval("entry_eval_geometry_feedrate")
-
-            geometry = app_obj.stuff[app_obj.selected_item_name]
             assert isinstance(job_obj, CirkuixCNCjob)
-            job_obj.z_cut = z_cut
-            job_obj.z_move = z_move
-            job_obj.feedrate = feedrate
-            job_obj.generate_from_geometry(geometry.solid_geometry)
+            geometry = app_obj.stuff[app_obj.selected_item_name]
+            assert isinstance(geometry, CirkuixGeometry)
+            geometry.read_form()
+
+            GLib.idle_add(lambda: app_obj.set_progress_bar(0.2, "Creating CNC Job..."))
+            job_obj.z_cut = geometry.options["cutz"]
+            job_obj.z_move = geometry.options["travelz"]
+            job_obj.feedrate = geometry.options["feedrate"]
+            job_obj.options["tooldia"] = geometry.options["cnctooldia"]
+
+            GLib.idle_add(lambda: app_obj.set_progress_bar(0.4, "Analyzing Geometry..."))
+            job_obj.generate_from_geometry(geometry)
+
+            GLib.idle_add(lambda: app_obj.set_progress_bar(0.5, "Parsing G-Code..."))
             job_obj.gcode_parse()
+
+            GLib.idle_add(lambda: app_obj.set_progress_bar(0.6, "Creating New Geometry..."))
             job_obj.create_geometry()
 
-        self.new_object("cncjob", job_name, job_init)
+            GLib.idle_add(lambda: app_obj.set_progress_bar(0.8, "Plotting..."))
+
+        # To be run in separate thread
+        def job_thread(app_obj):
+            app_obj.new_object("cncjob", job_name, job_init)
+            GLib.idle_add(lambda: app_obj.set_progress_bar(1.0, "Done!"))
+            GLib.timeout_add_seconds(1, lambda: app_obj.set_progress_bar(0.0, ""))
+
+        # Start the thread
+        t = threading.Thread(target=job_thread, args=(self,))
+        t.daemon = True
+        t.start()
 
     def on_generate_paintarea(self, widget):
+        """
+        Callback for button on geometry form.
+        Subscribes to the "Click on plot" event and continues
+        after the click. Finds the polygon containing
+        the clicked point and runs clear_poly() on it, resulting
+        in a new CirkuixGeometry object.
+        """
         self.info("Click inside the desired polygon.")
         geo = self.stuff[self.selected_item_name]
         geo.read_form()
         tooldia = geo.options["painttooldia"]
         overlap = geo.options["paintoverlap"]
 
+        # To be called after clicking on the plot.
         def doit(event):
             self.plot_click_subscribers.pop("generate_paintarea")
             self.info("")
             point = [event.xdata, event.ydata]
             poly = find_polygon(geo.solid_geometry, point)
 
+            # Initializes the new geometry object
             def gen_paintarea(geo_obj, app_obj):
                 assert isinstance(geo_obj, CirkuixGeometry)
                 assert isinstance(app_obj, App)
-                cp = clear_poly(poly, tooldia, overlap)
+                cp = clear_poly(poly.buffer(-geo.options["paintmargin"]), tooldia, overlap)
                 geo_obj.solid_geometry = cp
 
             name = self.selected_item_name + "_paint"
@@ -760,12 +977,6 @@ class App:
 
         self.plot_click_subscribers["generate_paintarea"] = doit
 
-    def on_cncjob_tooldia_activate(self, widget):
-        job = self.stuff[self.selected_item_name]
-        tooldia = self.get_eval("entry_eval_cncjob_tooldia")
-        job.tooldia = tooldia
-        print "Changing tool diameter to:", tooldia
-
     def on_cncjob_exportgcode(self, widget):
         def on_success(self, filename):
             cncjob = self.stuff[self.selected_item_name]
@@ -840,10 +1051,10 @@ class App:
         Gtk.main_quit()
     
     def file_chooser_action(self, on_success):
-        '''
+        """
         Opens the file chooser and runs on_success on a separate thread
         upon completion of valid file choice.
-        '''
+        """
         dialog = Gtk.FileChooserDialog("Please choose a file", self.window,
             Gtk.FileChooserAction.OPEN,
             (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
@@ -979,7 +1190,7 @@ class App:
 
             for subscriber in self.plot_click_subscribers:
                 self.plot_click_subscribers[subscriber](event)
-        except:
+        except Exception, e:
             print "Outside plot!"
         
     def on_zoom_in(self, event):
@@ -1045,15 +1256,15 @@ class App:
     def on_key_over_plot(self, event):
         print 'you pressed', event.key, event.xdata, event.ydata
         
-        if event.key == '1': # 1
+        if event.key == '1':  # 1
             self.on_zoom_fit(None)
             return
             
-        if event.key == '2': # 2
+        if event.key == '2':  # 2
             self.zoom(1/1.5, self.mouse)
             return
             
-        if event.key == '3': # 3
+        if event.key == '3':  # 3
             self.zoom(1.5, self.mouse)
             return
 

+ 410 - 29
cirkuix.ui

@@ -94,6 +94,7 @@
                   <object class="GtkLabel" id="label19">
                     <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">Plot Options:</property>
@@ -179,7 +180,7 @@
                         <property name="can_focus">True</property>
                         <property name="invisible_char">●</property>
                         <property name="invisible_char_set">True</property>
-                        <signal name="activate" handler="on_cncjob_tooldia_activate" swapped="no"/>
+                        <signal name="activate" handler="on_entry_eval_activate" swapped="no"/>
                       </object>
                       <packing>
                         <property name="expand">False</property>
@@ -194,6 +195,21 @@
                     <property name="position">6</property>
                   </packing>
                 </child>
+                <child>
+                  <object class="GtkButton" id="button11">
+                    <property name="label" translatable="yes">Update Plot</property>
+                    <property name="visible">True</property>
+                    <property name="can_focus">True</property>
+                    <property name="receives_default">True</property>
+                    <signal name="activate" handler="on_update_plot" swapped="no"/>
+                    <signal name="clicked" handler="on_update_plot" swapped="no"/>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="position">7</property>
+                  </packing>
+                </child>
                 <child>
                   <object class="GtkLabel" id="label26">
                     <property name="visible">True</property>
@@ -209,7 +225,7 @@
                   <packing>
                     <property name="expand">False</property>
                     <property name="fill">True</property>
-                    <property name="position">7</property>
+                    <property name="position">8</property>
                   </packing>
                 </child>
                 <child>
@@ -224,12 +240,9 @@
                   <packing>
                     <property name="expand">False</property>
                     <property name="fill">True</property>
-                    <property name="position">8</property>
+                    <property name="position">9</property>
                   </packing>
                 </child>
-                <child>
-                  <placeholder/>
-                </child>
               </object>
             </child>
           </object>
@@ -375,16 +388,206 @@
                   </packing>
                 </child>
                 <child>
-                  <placeholder/>
+                  <object class="GtkButton" id="button12">
+                    <property name="label" translatable="yes">Update Plot</property>
+                    <property name="visible">True</property>
+                    <property name="can_focus">True</property>
+                    <property name="receives_default">True</property>
+                    <signal name="activate" handler="on_update_plot" swapped="no"/>
+                    <signal name="clicked" handler="on_update_plot" swapped="no"/>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="position">6</property>
+                  </packing>
                 </child>
                 <child>
-                  <placeholder/>
+                  <object class="GtkLabel" id="label36">
+                    <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">Create CNC Job:</property>
+                    <attributes>
+                      <attribute name="weight" value="semibold"/>
+                    </attributes>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="position">7</property>
+                  </packing>
                 </child>
                 <child>
-                  <placeholder/>
+                  <object class="GtkGrid" id="grid6">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="row_spacing">2</property>
+                    <property name="column_spacing">4</property>
+                    <child>
+                      <object class="GtkEntry" id="entry_eval_excellon_drillz">
+                        <property name="visible">True</property>
+                        <property name="can_focus">True</property>
+                        <property name="invisible_char">●</property>
+                        <property name="invisible_char_set">True</property>
+                        <signal name="activate" handler="on_entry_eval_activate" swapped="no"/>
+                      </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_eval_excellon_travelz">
+                        <property name="visible">True</property>
+                        <property name="can_focus">True</property>
+                        <property name="invisible_char">●</property>
+                        <property name="invisible_char_set">True</property>
+                        <signal name="activate" handler="on_entry_eval_activate" swapped="no"/>
+                      </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="GtkEntry" id="entry_eval_excellon_feedrate">
+                        <property name="visible">True</property>
+                        <property name="can_focus">True</property>
+                        <property name="invisible_char">●</property>
+                        <property name="invisible_char_set">True</property>
+                        <signal name="activate" handler="on_entry_eval_activate" swapped="no"/>
+                      </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>
+                    <child>
+                      <object class="GtkLabel" id="label37">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="xalign">1</property>
+                        <property name="label" translatable="yes">Drill Z:</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="GtkLabel" id="label38">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="xalign">1</property>
+                        <property name="label" translatable="yes">Travel Z:</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="label39">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="xalign">1</property>
+                        <property name="label" translatable="yes">Feed rate:</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="GtkLabel" id="label40">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="xalign">1</property>
+                        <property name="label" translatable="yes">Tools:</property>
+                      </object>
+                      <packing>
+                        <property name="left_attach">0</property>
+                        <property name="top_attach">3</property>
+                        <property name="width">1</property>
+                        <property name="height">1</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkBox" id="box3">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <child>
+                          <object class="GtkEntry" id="entry_text_excellon_toolselection">
+                            <property name="visible">True</property>
+                            <property name="can_focus">True</property>
+                            <property name="invisible_char">●</property>
+                            <property name="invisible_char_set">True</property>
+                          </object>
+                          <packing>
+                            <property name="expand">False</property>
+                            <property name="fill">True</property>
+                            <property name="position">0</property>
+                          </packing>
+                        </child>
+                        <child>
+                          <object class="GtkButton" id="button8">
+                            <property name="label" translatable="yes">Choose</property>
+                            <property name="visible">True</property>
+                            <property name="can_focus">True</property>
+                            <property name="receives_default">True</property>
+                            <signal name="activate" handler="on_excellon_tool_choose" swapped="no"/>
+                            <signal name="clicked" handler="on_excellon_tool_choose" swapped="no"/>
+                          </object>
+                          <packing>
+                            <property name="expand">False</property>
+                            <property name="fill">True</property>
+                            <property name="position">1</property>
+                          </packing>
+                        </child>
+                      </object>
+                      <packing>
+                        <property name="left_attach">1</property>
+                        <property name="top_attach">3</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">8</property>
+                  </packing>
                 </child>
                 <child>
-                  <placeholder/>
+                  <object class="GtkButton" id="button7">
+                    <property name="label" translatable="yes">Generate</property>
+                    <property name="visible">True</property>
+                    <property name="can_focus">True</property>
+                    <property name="receives_default">True</property>
+                    <signal name="activate" handler="on_generate_excellon_cncjob" swapped="no"/>
+                    <signal name="clicked" handler="on_generate_excellon_cncjob" swapped="no"/>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="position">9</property>
+                  </packing>
                 </child>
               </object>
             </child>
@@ -531,6 +734,21 @@
                     <property name="position">5</property>
                   </packing>
                 </child>
+                <child>
+                  <object class="GtkButton" id="button13">
+                    <property name="label" translatable="yes">Update Plot</property>
+                    <property name="visible">True</property>
+                    <property name="can_focus">True</property>
+                    <property name="receives_default">True</property>
+                    <signal name="activate" handler="on_update_plot" swapped="no"/>
+                    <signal name="clicked" handler="on_update_plot" swapped="no"/>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="position">6</property>
+                  </packing>
+                </child>
                 <child>
                   <object class="GtkLabel" id="label21">
                     <property name="visible">True</property>
@@ -546,7 +764,7 @@
                   <packing>
                     <property name="expand">False</property>
                     <property name="fill">True</property>
-                    <property name="position">6</property>
+                    <property name="position">7</property>
                   </packing>
                 </child>
                 <child>
@@ -561,6 +779,7 @@
                         <property name="can_focus">True</property>
                         <property name="invisible_char">●</property>
                         <property name="invisible_char_set">True</property>
+                        <signal name="activate" handler="on_entry_eval_activate" swapped="no"/>
                       </object>
                       <packing>
                         <property name="left_attach">1</property>
@@ -575,6 +794,7 @@
                         <property name="can_focus">True</property>
                         <property name="invisible_char">●</property>
                         <property name="invisible_char_set">True</property>
+                        <signal name="activate" handler="on_entry_eval_activate" swapped="no"/>
                       </object>
                       <packing>
                         <property name="left_attach">1</property>
@@ -589,6 +809,7 @@
                         <property name="can_focus">True</property>
                         <property name="invisible_char">●</property>
                         <property name="invisible_char_set">True</property>
+                        <signal name="activate" handler="on_entry_eval_activate" swapped="no"/>
                       </object>
                       <packing>
                         <property name="left_attach">1</property>
@@ -639,11 +860,40 @@
                         <property name="height">1</property>
                       </packing>
                     </child>
+                    <child>
+                      <object class="GtkLabel" id="label34">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="xalign">1</property>
+                        <property name="label" translatable="yes">Tool diam:</property>
+                      </object>
+                      <packing>
+                        <property name="left_attach">0</property>
+                        <property name="top_attach">3</property>
+                        <property name="width">1</property>
+                        <property name="height">1</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkEntry" id="entry_eval_geometry_cnctooldia">
+                        <property name="visible">True</property>
+                        <property name="can_focus">True</property>
+                        <property name="invisible_char">●</property>
+                        <property name="invisible_char_set">True</property>
+                        <signal name="activate" handler="on_entry_eval_activate" swapped="no"/>
+                      </object>
+                      <packing>
+                        <property name="left_attach">1</property>
+                        <property name="top_attach">3</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">7</property>
+                    <property name="position">8</property>
                   </packing>
                 </child>
                 <child>
@@ -658,7 +908,7 @@
                   <packing>
                     <property name="expand">False</property>
                     <property name="fill">True</property>
-                    <property name="position">8</property>
+                    <property name="position">9</property>
                   </packing>
                 </child>
                 <child>
@@ -676,7 +926,7 @@
                   <packing>
                     <property name="expand">False</property>
                     <property name="fill">True</property>
-                    <property name="position">9</property>
+                    <property name="position">10</property>
                   </packing>
                 </child>
                 <child>
@@ -705,6 +955,7 @@
                         <property name="can_focus">True</property>
                         <property name="invisible_char">●</property>
                         <property name="invisible_char_set">True</property>
+                        <signal name="activate" handler="on_entry_eval_activate" swapped="no"/>
                       </object>
                       <packing>
                         <property name="left_attach">1</property>
@@ -733,6 +984,7 @@
                         <property name="can_focus">True</property>
                         <property name="invisible_char">●</property>
                         <property name="invisible_char_set">True</property>
+                        <signal name="activate" handler="on_entry_eval_activate" swapped="no"/>
                       </object>
                       <packing>
                         <property name="left_attach">1</property>
@@ -741,11 +993,40 @@
                         <property name="height">1</property>
                       </packing>
                     </child>
+                    <child>
+                      <object class="GtkLabel" id="label35">
+                        <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">2</property>
+                        <property name="width">1</property>
+                        <property name="height">1</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkEntry" id="entry_eval_geometry_paintmargin">
+                        <property name="visible">True</property>
+                        <property name="can_focus">True</property>
+                        <property name="invisible_char">●</property>
+                        <property name="invisible_char_set">True</property>
+                        <signal name="activate" handler="on_entry_eval_activate" swapped="no"/>
+                      </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">10</property>
+                    <property name="position">11</property>
                   </packing>
                 </child>
                 <child>
@@ -760,15 +1041,12 @@
                   <packing>
                     <property name="expand">False</property>
                     <property name="fill">True</property>
-                    <property name="position">11</property>
+                    <property name="position">12</property>
                   </packing>
                 </child>
                 <child>
                   <placeholder/>
                 </child>
-                <child>
-                  <placeholder/>
-                </child>
               </object>
             </child>
           </object>
@@ -854,6 +1132,7 @@
                   <object class="GtkLabel" id="label9">
                     <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">Plot Options:</property>
@@ -929,6 +1208,21 @@
                     <property name="position">6</property>
                   </packing>
                 </child>
+                <child>
+                  <object class="GtkButton" id="button9">
+                    <property name="label" translatable="yes">Update Plot</property>
+                    <property name="visible">True</property>
+                    <property name="can_focus">True</property>
+                    <property name="receives_default">True</property>
+                    <signal name="activate" handler="on_update_plot" swapped="no"/>
+                    <signal name="clicked" handler="on_update_plot" swapped="no"/>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="position">7</property>
+                  </packing>
+                </child>
                 <child>
                   <object class="GtkLabel" id="label13">
                     <property name="visible">True</property>
@@ -944,7 +1238,7 @@
                   <packing>
                     <property name="expand">False</property>
                     <property name="fill">True</property>
-                    <property name="position">7</property>
+                    <property name="position">8</property>
                   </packing>
                 </child>
                 <child>
@@ -990,7 +1284,7 @@
                   <packing>
                     <property name="expand">False</property>
                     <property name="fill">True</property>
-                    <property name="position">8</property>
+                    <property name="position">9</property>
                   </packing>
                 </child>
                 <child>
@@ -1005,7 +1299,7 @@
                   <packing>
                     <property name="expand">False</property>
                     <property name="fill">True</property>
-                    <property name="position">9</property>
+                    <property name="position">10</property>
                   </packing>
                 </child>
                 <child>
@@ -1023,7 +1317,7 @@
                   <packing>
                     <property name="expand">False</property>
                     <property name="fill">True</property>
-                    <property name="position">10</property>
+                    <property name="position">11</property>
                   </packing>
                 </child>
                 <child>
@@ -1171,7 +1465,7 @@
                   <packing>
                     <property name="expand">False</property>
                     <property name="fill">True</property>
-                    <property name="position">11</property>
+                    <property name="position">12</property>
                   </packing>
                 </child>
                 <child>
@@ -1186,7 +1480,7 @@
                   <packing>
                     <property name="expand">False</property>
                     <property name="fill">True</property>
-                    <property name="position">12</property>
+                    <property name="position">13</property>
                   </packing>
                 </child>
                 <child>
@@ -1204,7 +1498,7 @@
                   <packing>
                     <property name="expand">False</property>
                     <property name="fill">True</property>
-                    <property name="position">13</property>
+                    <property name="position">14</property>
                   </packing>
                 </child>
                 <child>
@@ -1246,7 +1540,7 @@
                   <packing>
                     <property name="expand">False</property>
                     <property name="fill">True</property>
-                    <property name="position">14</property>
+                    <property name="position">15</property>
                   </packing>
                 </child>
                 <child>
@@ -1261,11 +1555,98 @@
                   <packing>
                     <property name="expand">False</property>
                     <property name="fill">True</property>
-                    <property name="position">15</property>
+                    <property name="position">16</property>
                   </packing>
                 </child>
                 <child>
-                  <placeholder/>
+                  <object class="GtkLabel" id="label41">
+                    <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">Bounding box:</property>
+                    <attributes>
+                      <attribute name="weight" value="semibold"/>
+                    </attributes>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="position">17</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkBox" id="box4">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="margin_top">4</property>
+                    <property name="margin_bottom">4</property>
+                    <property name="spacing">1</property>
+                    <child>
+                      <object class="GtkLabel" id="label42">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="xalign">1</property>
+                        <property name="label" translatable="yes">Boundary margin: </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_eval_gerber_bboxmargin">
+                        <property name="visible">True</property>
+                        <property name="can_focus">True</property>
+                        <property name="invisible_char">●</property>
+                        <property name="width_chars">14</property>
+                        <property name="invisible_char_set">True</property>
+                        <signal name="activate" handler="on_eval_update" swapped="no"/>
+                      </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">18</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkCheckButton" id="cb_gerber_bboxrounded">
+                    <property name="label" translatable="yes">Rounded corners</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">19</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkButton" id="button10">
+                    <property name="label" translatable="yes">Generate Bounding Box</property>
+                    <property name="visible">True</property>
+                    <property name="can_focus">True</property>
+                    <property name="receives_default">True</property>
+                    <signal name="activate" handler="on_generate_gerber_bounding_box" swapped="no"/>
+                    <signal name="clicked" handler="on_generate_gerber_bounding_box" swapped="no"/>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="position">20</property>
+                  </packing>
                 </child>
                 <child>
                   <placeholder/>