Bläddra i källkod

Bug fix in Excellon parser. Was not supporting a '+' in from of numbers.

Juan Pablo Caram 11 år sedan
förälder
incheckning
23ba2105c1
5 ändrade filer med 694 tillägg och 635 borttagningar
  1. 68 629
      FlatCAM.py
  2. 616 0
      FlatCAMObj.py
  3. 4 4
      camlib.py
  4. 5 1
      doc/source/devman.rst
  5. 1 1
      recent.json

+ 68 - 629
FlatCAM.py

@@ -29,609 +29,9 @@ import urllib
 import copy
 import random
 
+from FlatCAMObj import *
 
-########################################
-##            FlatCAMObj              ##
-########################################
-class FlatCAMObj:
-    """
-    Base type of objects handled in FlatCAM. These become interactive
-    in the GUI, can be plotted, and their options can be modified
-    by the user in their respective forms.
-    """
-
-    # Instance of the application to which these are related.
-    # The app should set this value.
-    app = None
-
-    def __init__(self, name):
-        self.options = {"name": name}
-        self.form_kinds = {"name": "entry_text"}  # Kind of form element for each option
-        self.radios = {}  # Name value pairs for radio sets
-        self.radios_inv = {}  # Inverse of self.radios
-        self.axes = None  # Matplotlib axes
-        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
-        :rtype: 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()
-
-        # Remove all decoration. The app's axes will have
-        # the ticks and grid.
-        self.axes.set_frame_on(False)  # No frame
-        self.axes.set_xticks([])  # No tick
-        self.axes.set_yticks([])  # No ticks
-        self.axes.patch.set_visible(False)  # No background
-        self.axes.set_aspect(1)
-
-    def to_form(self):
-        """
-        Copies options to the UI form.
-
-        :return: None
-        """
-        for option in self.options:
-            self.set_form_item(option)
-
-    def read_form(self):
-        """
-        Reads form into ``self.options``.
-
-        :return: None
-        :rtype: None
-        """
-        for option in self.options:
-            self.read_form_item(option)
-
-    def build_ui(self):
-        """
-        Sets up the UI/form for this object.
-
-        :return: None
-        :rtype: None
-        """
-
-        # Where the UI for this object is drawn
-        box_selected = self.app.builder.get_object("box_selected")
-
-        # Remove anything else in the box
-        box_children = box_selected.get_children()
-        for child in box_children:
-            box_selected.remove(child)
-
-        osw = self.app.builder.get_object("offscrwindow_" + self.kind)  # offscreenwindow
-        sw = self.app.builder.get_object("sw_" + self.kind)  # scrollwindows
-        osw.remove(sw)  # TODO: Is this needed ?
-        vp = self.app.builder.get_object("vp_" + self.kind)  # Viewport
-        vp.override_background_color(Gtk.StateType.NORMAL, Gdk.RGBA(1, 1, 1, 1))
-
-        # Put in the UI
-        box_selected.pack_start(sw, True, True, 0)
-
-        entry_name = self.app.builder.get_object("entry_text_" + self.kind + "_name")
-        entry_name.connect("activate", self.app.on_activate_name)
-        self.to_form()
-        sw.show()
-
-    def set_form_item(self, option):
-        """
-        Copies the specified option to the UI form.
-
-        :param option: Name of the option (Key in ``self.options``).
-        :type option: str
-        :return: None
-        """
-        fkind = self.form_kinds[option]
-        fname = fkind + "_" + self.kind + "_" + option
-
-        if fkind == 'entry_eval' or fkind == 'entry_text':
-            self.app.builder.get_object(fname).set_text(str(self.options[option]))
-            return
-        if fkind == 'cb':
-            self.app.builder.get_object(fname).set_active(self.options[option])
-            return
-        if fkind == 'radio':
-            self.app.builder.get_object(self.radios_inv[option][self.options[option]]).set_active(True)
-            return
-        print "Unknown kind of form item:", fkind
-
-    def read_form_item(self, option):
-        """
-        Reads the specified option from the UI form into ``self.options``.
-
-        :param option: Name of the option.
-        :type option: str
-        :return: None
-        """
-        fkind = self.form_kinds[option]
-        fname = fkind + "_" + self.kind + "_" + option
-
-        if fkind == 'entry_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)
-            return
-        if fkind == 'cb':
-            self.options[option] = self.app.builder.get_object(fname).get_active()
-            return
-        if fkind == 'radio':
-            self.options[option] = self.app.get_radio_value(self.radios[option])
-            return
-        print "Unknown kind of form item:", fkind
-
-    def plot(self):
-        """
-        Plot this object (Extend this method to implement the actual plotting).
-        Axes get created, appended to canvas and cleared before plotting.
-        Call this in descendants before doing the plotting.
-
-        :return: Whether to continue plotting or not depending on the "plot" option.
-        :rtype: bool
-        """
-
-        # Axes must exist and be attached to canvas.
-        if self.axes is None or self.axes not in self.app.plotcanvas.figure.axes:
-            self.axes = self.app.plotcanvas.new_axes(self.options['name'])
-
-        if not self.options["plot"]:
-            self.axes.cla()
-            self.app.plotcanvas.auto_adjust_axes()
-            return False
-
-        # Clear axes or we will plot on top of them.
-        self.axes.cla()
-        # GLib.idle_add(self.axes.cla)
-        return True
-
-    def serialize(self):
-        """
-        Returns a representation of the object as a dictionary so
-        it can be later exported as JSON. Override this method.
-
-        :return: Dictionary representing the object
-        :rtype: dict
-        """
-        return
-
-    def deserialize(self, obj_dict):
-        """
-        Re-builds an object from its serialized version.
-
-        :param obj_dict: Dictionary representing a FlatCAMObj
-        :type obj_dict: dict
-        :return: None
-        """
-        return
-
-
-class FlatCAMGerber(FlatCAMObj, Gerber):
-    """
-    Represents Gerber code.
-    """
-
-    def __init__(self, name):
-        Gerber.__init__(self)
-        FlatCAMObj.__init__(self, name)
-
-        self.kind = "gerber"
-
-        # The 'name' is already in self.options from FlatCAMObj
-        self.options.update({
-            "plot": True,
-            "mergepolys": True,
-            "multicolored": False,
-            "solid": False,
-            "isotooldia": 0.016,
-            "isopasses": 1,
-            "isooverlap": 0.15,
-            "cutouttooldia": 0.07,
-            "cutoutmargin": 0.2,
-            "cutoutgapsize": 0.15,
-            "gaps": "tb",
-            "noncoppermargin": 0.0,
-            "noncopperrounded": False,
-            "bboxmargin": 0.0,
-            "bboxrounded": False
-        })
-
-        # The 'name' is already in self.form_kinds from FlatCAMObj
-        self.form_kinds.update({
-            "plot": "cb",
-            "mergepolys": "cb",
-            "multicolored": "cb",
-            "solid": "cb",
-            "isotooldia": "entry_eval",
-            "isopasses": "entry_eval",
-            "isooverlap": "entry_eval",
-            "cutouttooldia": "entry_eval",
-            "cutoutmargin": "entry_eval",
-            "cutoutgapsize": "entry_eval",
-            "gaps": "radio",
-            "noncoppermargin": "entry_eval",
-            "noncopperrounded": "cb",
-            "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"}}
-
-        # Attributes to be included in serialization
-        # Always append to it because it carries contents
-        # from predecessors.
-        self.ser_attrs += ['options', 'kind']
-
-    def convert_units(self, units):
-        """
-        Converts the units of the object by scaling dimensions in all geometry
-        and options.
-
-        :param units: Units to which to convert the object: "IN" or "MM".
-        :type units: str
-        :return: None
-        :rtype: None
-        """
-
-        factor = Gerber.convert_units(self, units)
-
-        self.options['isotooldia'] *= factor
-        self.options['cutoutmargin'] *= factor
-        self.options['cutoutgapsize'] *= factor
-        self.options['noncoppermargin'] *= factor
-        self.options['bboxmargin'] *= factor
-
-    def plot(self):
-
-        # Does all the required setup and returns False
-        # if the 'ptint' option is set to False.
-        if not FlatCAMObj.plot(self):
-            return
-
-        if self.options["mergepolys"]:
-            geometry = self.solid_geometry
-        else:
-            geometry = self.buffered_paths + \
-                        [poly['polygon'] for poly in self.regions] + \
-                        self.flash_geometry
-
-        if self.options["multicolored"]:
-            linespec = '-'
-        else:
-            linespec = 'k-'
-
-        if self.options["solid"]:
-            for poly in geometry:
-                # TODO: Too many things hardcoded.
-                patch = PolygonPatch(poly,
-                                     facecolor="#BBF268",
-                                     edgecolor="#006E20",
-                                     alpha=0.75,
-                                     zorder=2)
-                self.axes.add_patch(patch)
-        else:
-            for poly in geometry:
-                x, y = poly.exterior.xy
-                self.axes.plot(x, y, linespec)
-                for ints in poly.interiors:
-                    x, y = ints.coords.xy
-                    self.axes.plot(x, y, linespec)
-
-        # self.app.plotcanvas.auto_adjust_axes()
-        GLib.idle_add(self.app.plotcanvas.auto_adjust_axes)
-
-    def serialize(self):
-        return {
-            "options": self.options,
-            "kind": self.kind
-        }
-
-
-class FlatCAMExcellon(FlatCAMObj, Excellon):
-    """
-    Represents Excellon/Drill code.
-    """
-
-    def __init__(self, name):
-        Excellon.__init__(self)
-        FlatCAMObj.__init__(self, name)
-
-        self.kind = "excellon"
-
-        self.options.update({
-            "plot": True,
-            "solid": False,
-            "drillz": -0.1,
-            "travelz": 0.1,
-            "feedrate": 5.0,
-            "toolselection": ""
-        })
-
-        self.form_kinds.update({
-            "plot": "cb",
-            "solid": "cb",
-            "drillz": "entry_eval",
-            "travelz": "entry_eval",
-            "feedrate": "entry_eval",
-            "toolselection": "entry_text"
-        })
-
-        # TODO: Document this.
-        self.tool_cbs = {}
-
-        # Attributes to be included in serialization
-        # Always append to it because it carries contents
-        # from predecessors.
-        self.ser_attrs += ['options', 'kind']
-
-    def convert_units(self, units):
-        factor = Excellon.convert_units(self, units)
-
-        self.options['drillz'] *= factor
-        self.options['travelz'] *= factor
-        self.options['feedrate'] *= factor
-
-    def plot(self):
-
-        # Does all the required setup and returns False
-        # if the 'ptint' option is set to False.
-        if not FlatCAMObj.plot(self):
-            return
-
-        try:
-            _ = iter(self.solid_geometry)
-        except TypeError:
-            self.solid_geometry = [self.solid_geometry]
-
-        # Plot excellon (All polygons?)
-        if self.options["solid"]:
-            for geo in self.solid_geometry:
-                patch = PolygonPatch(geo,
-                                     facecolor="#C40000",
-                                     edgecolor="#750000",
-                                     alpha=0.75,
-                                     zorder=3)
-                self.axes.add_patch(patch)
-        else:
-            for geo in self.solid_geometry:
-                x, y = geo.exterior.coords.xy
-                self.axes.plot(x, y, 'r-')
-                for ints in geo.interiors:
-                    x, y = ints.coords.xy
-                    self.axes.plot(x, y, 'g-')
-
-        #self.app.plotcanvas.auto_adjust_axes()
-        GLib.idle_add(self.app.plotcanvas.auto_adjust_axes)
-
-    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 + ": " + str(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 toolx in self.tool_cbs:
-                if self.tool_cbs[toolx].get_active():
-                    tool_list.append(toolx)
-            self.options["toolselection"] = ", ".join(tool_list)
-            self.to_form()
-
-        button.connect("activate", on_accept)
-        button.connect("clicked", on_accept)
-
-
-class FlatCAMCNCjob(FlatCAMObj, CNCjob):
-    """
-    Represents G-Code.
-    """
-
-    def __init__(self, name, units="in", kind="generic", z_move=0.1,
-                 feedrate=3.0, z_cut=-0.002, tooldia=0.0):
-        CNCjob.__init__(self, units=units, kind=kind, z_move=z_move,
-                        feedrate=feedrate, z_cut=z_cut, tooldia=tooldia)
-        FlatCAMObj.__init__(self, name)
-
-        self.kind = "cncjob"
-
-        self.options.update({
-            "plot": True,
-            "tooldia": 0.4 / 25.4  # 0.4mm in inches
-        })
-
-        self.form_kinds.update({
-            "plot": "cb",
-            "tooldia": "entry_eval"
-        })
-
-        # Attributes to be included in serialization
-        # Always append to it because it carries contents
-        # from predecessors.
-        self.ser_attrs += ['options', 'kind']
-
-    def plot(self):
-
-        # Does all the required setup and returns False
-        # if the 'ptint' option is set to False.
-        if not FlatCAMObj.plot(self):
-            return
-
-        self.plot2(self.axes, tooldia=self.options["tooldia"])
-
-        #self.app.plotcanvas.auto_adjust_axes()
-        GLib.idle_add(self.app.plotcanvas.auto_adjust_axes)
-
-    def convert_units(self, units):
-        factor = CNCjob.convert_units(self, units)
-        print "FlatCAMCNCjob.convert_units()"
-        self.options["tooldia"] *= factor
-
-
-class FlatCAMGeometry(FlatCAMObj, Geometry):
-    """
-    Geometric object not associated with a specific
-    format.
-    """
-
-    def __init__(self, name):
-        FlatCAMObj.__init__(self, name)
-        Geometry.__init__(self)
-
-        self.kind = "geometry"
-
-        self.options.update({
-            "plot": True,
-            "solid": False,
-            "multicolored": False,
-            "cutz": -0.002,
-            "travelz": 0.1,
-            "feedrate": 5.0,
-            "cnctooldia": 0.4 / 25.4,
-            "painttooldia": 0.0625,
-            "paintoverlap": 0.15,
-            "paintmargin": 0.01
-        })
-
-        self.form_kinds.update({
-            "plot": "cb",
-            "solid": "cb",
-            "multicolored": "cb",
-            "cutz": "entry_eval",
-            "travelz": "entry_eval",
-            "feedrate": "entry_eval",
-            "cnctooldia": "entry_eval",
-            "painttooldia": "entry_eval",
-            "paintoverlap": "entry_eval",
-            "paintmargin": "entry_eval"
-        })
-
-        # Attributes to be included in serialization
-        # Always append to it because it carries contents
-        # from predecessors.
-        self.ser_attrs += ['options', 'kind']
-
-    def scale(self, factor):
-        """
-        Scales all geometry by a given factor.
-
-        :param factor: Factor by which to scale the object's geometry/
-        :type factor: float
-        :return: None
-        :rtype: None
-        """
-
-        if type(self.solid_geometry) == list:
-            self.solid_geometry = [affinity.scale(g, factor, factor, origin=(0, 0))
-                                   for g in self.solid_geometry]
-        else:
-            self.solid_geometry = affinity.scale(self.solid_geometry, factor, factor,
-                                                 origin=(0, 0))
-
-    def offset(self, vect):
-        """
-        Offsets all geometry by a given vector/
-
-        :param vect: (x, y) vector by which to offset the object's geometry.
-        :type vect: tuple
-        :return: None
-        :rtype: None
-        """
-
-        dx, dy = vect
-
-        if type(self.solid_geometry) == list:
-            self.solid_geometry = [affinity.translate(g, xoff=dx, yoff=dy)
-                                   for g in self.solid_geometry]
-        else:
-            self.solid_geometry = affinity.translate(self.solid_geometry, xoff=dx, yoff=dy)
-
-    def convert_units(self, units):
-        factor = Geometry.convert_units(self, units)
-
-        self.options['cutz'] *= factor
-        self.options['travelz'] *= factor
-        self.options['feedrate'] *= factor
-        self.options['cnctooldia'] *= factor
-        self.options['painttooldia'] *= factor
-        self.options['paintmargin'] *= factor
-
-        return factor
-
-    def plot(self):
-        """
-        Plots the object into its axes. If None, of if the axes
-        are not part of the app's figure, it fetches new ones.
-
-        :return: None
-        """
-
-        # Does all the required setup and returns False
-        # if the 'ptint' option is set to False.
-        if not FlatCAMObj.plot(self):
-            return
-
-        # Make sure solid_geometry is iterable.
-        try:
-            _ = iter(self.solid_geometry)
-        except TypeError:
-            self.solid_geometry = [self.solid_geometry]
-
-        for geo in self.solid_geometry:
-
-            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
-
-            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))
-
-        #self.app.plotcanvas.auto_adjust_axes()
-        GLib.idle_add(self.app.plotcanvas.auto_adjust_axes)
-
+from FlatCAMWorker import Worker
 
 ########################################
 ##                App                 ##
@@ -655,9 +55,12 @@ class App:
         # GLib.log_set_handler()
 
         #### GUI ####
+        # Glade init
         self.gladefile = "FlatCAM.ui"
         self.builder = Gtk.Builder()
         self.builder.add_from_file(self.gladefile)
+
+        # References to UI widgets
         self.window = self.builder.get_object("window1")
         self.position_label = self.builder.get_object("label3")
         self.grid = self.builder.get_object("grid1")
@@ -678,6 +81,7 @@ class App:
         self.setup_project_list()  # The "Project" tab
         self.setup_component_editor()  # The "Selected" tab
 
+        ## Setup the toolbar. Adds buttons.
         self.setup_toolbar()
 
         #### Event handling ####
@@ -759,11 +163,16 @@ class App:
         self.units_label.set_text("[" + self.options["units"] + "]")
         self.setup_recent_items()
 
+        self.worker = Worker()
+        self.worker.daemon = True
+        self.worker.start()
+
         #### Check for updates ####
-        self.version = 3
+        self.version = 4
         t1 = threading.Thread(target=self.versionCheck)
         t1.daemon = True
         t1.start()
+        # self.worker.add_task(self.versionCheck)
 
         #### For debugging only ###
         def somethreadfunc(app_obj):
@@ -780,7 +189,7 @@ class App:
         self.icon48 = GdkPixbuf.Pixbuf.new_from_file('share/flatcam_icon48.png')
         self.icon16 = GdkPixbuf.Pixbuf.new_from_file('share/flatcam_icon16.png')
         Gtk.Window.set_default_icon_list([self.icon16, self.icon48, self.icon256])
-        self.window.set_title("FlatCAM - Alpha 3 UNSTABLE - Check for updates!")
+        self.window.set_title("FlatCAM - Alpha 4 UNSTABLE")
         self.window.set_default_size(900, 600)
         self.window.show_all()
 
@@ -900,11 +309,16 @@ class App:
 
         # Closure needed to create callbacks in a loop.
         # Otherwise late binding occurs.
+        # def make_callback(func, fname):
+        #     def opener(*args):
+        #         t = threading.Thread(target=lambda: func(fname))
+        #         t.daemon = True
+        #         t.start()
+        #     return opener
+
         def make_callback(func, fname):
             def opener(*args):
-                t = threading.Thread(target=lambda: func(fname))
-                t.daemon = True
-                t.start()
+                self.worker.add_task(func, [fname])
             return opener
 
         try:
@@ -1003,9 +417,10 @@ class App:
             GLib.idle_add(lambda: self.on_zoom_fit(None))
             GLib.timeout_add(300, lambda: app_obj.set_progress_bar(0.0, ""))
 
-        t = threading.Thread(target=thread_func, args=(self,))
-        t.daemon = True
-        t.start()
+        # t = threading.Thread(target=thread_func, args=(self,))
+        # t.daemon = True
+        # t.start()
+        self.worker.add_task(thread_func, [self])
 
     def get_eval(self, widget_name):
         """
@@ -1470,9 +885,10 @@ class App:
             GLib.idle_add(app_obj.plotcanvas.auto_adjust_axes)
             GLib.timeout_add(300, lambda: app_obj.set_progress_bar(0.0, ""))
 
-        t = threading.Thread(target=thread_func, args=(self,))
-        t.daemon = True
-        t.start()
+        # t = threading.Thread(target=thread_func, args=(self,))
+        # t.daemon = True
+        # t.start()
+        self.worker.add_task(thread_func, [self])
 
     def enable_all_plots(self, *args):
         self.plotcanvas.clear()
@@ -1494,9 +910,10 @@ class App:
             GLib.idle_add(app_obj.plotcanvas.auto_adjust_axes)
             GLib.timeout_add(300, lambda: app_obj.set_progress_bar(0.0, ""))
 
-        t = threading.Thread(target=thread_func, args=(self,))
-        t.daemon = True
-        t.start()
+        # t = threading.Thread(target=thread_func, args=(self,))
+        # t.daemon = True
+        # t.start()
+        self.worker.add_task(thread_func, [self])
 
     def register_recent(self, kind, filename):
         record = {'kind': kind, 'filename': filename}
@@ -2158,9 +1575,10 @@ class App:
             obj.plot()
             GLib.timeout_add(300, lambda: app_obj.set_progress_bar(0.0, "Idle"))
 
-        t = threading.Thread(target=thread_func, args=(self,))
-        t.daemon = True
-        t.start()
+        # t = threading.Thread(target=thread_func, args=(self,))
+        # t.daemon = True
+        # t.start()
+        self.worker.add_task(thread_func, [self])
 
     def on_generate_excellon_cncjob(self, widget):
         """
@@ -2205,9 +1623,10 @@ class App:
             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()
+        # t = threading.Thread(target=job_thread, args=(self,))
+        # t.daemon = True
+        # t.start()
+        self.worker.add_task(job_thread, [self])
 
     def on_excellon_tool_choose(self, widget):
         """
@@ -2397,9 +1816,10 @@ class App:
             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()
+        # t = threading.Thread(target=job_thread, args=(self,))
+        # t.daemon = True
+        # t.start()
+        self.worker.add_task(job_thread, [self])
 
     def on_generate_paintarea(self, widget):
         """
@@ -2623,9 +2043,10 @@ class App:
         if response == Gtk.ResponseType.OK:
             filename = dialog.get_filename()
             dialog.destroy()
-            t = threading.Thread(target=on_success, args=(self, filename))
-            t.daemon = True
-            t.start()
+            # t = threading.Thread(target=on_success, args=(self, filename))
+            # t.daemon = True
+            # t.start()
+            self.worker.add_task(on_success, [self, filename])
             #on_success(self, filename)
         elif response == Gtk.ResponseType.CANCEL:
             self.info("Open cancelled.")  # print("Cancel clicked")
@@ -3372,7 +2793,6 @@ class PlotCanvas:
                 self.pan(0, -0.3)
             return
 
-
     def on_mouse_move(self, event):
         """
         Mouse movement event hadler.
@@ -3382,5 +2802,24 @@ class PlotCanvas:
         """
         self.mouse = [event.xdata, event.ydata]
 
+
+class ObjectCollection:
+    def __init__(self):
+        self.collection = []
+        self.active = None
+
+    def set_active(self, name):
+        for obj in self.collection:
+            if obj.options['name'] == name:
+                self.active = obj
+                return self.active
+        return None
+
+    def get_active(self):
+        return self.active
+
+    def append(self, obj):
+        self.collection.append(obj)
+
 app = App()
 Gtk.main()

+ 616 - 0
FlatCAMObj.py

@@ -0,0 +1,616 @@
+############################################################
+# FlatCAM: 2D Post-processing for Manufacturing            #
+# http://caram.cl/software/flatcam                         #
+# Author: Juan Pablo Caram (c)                             #
+# Date: 2/5/2014                                           #
+# MIT Licence                                              #
+############################################################
+
+from gi.repository import Gtk
+from gi.repository import Gdk
+from gi.repository import GLib
+
+from camlib import *
+
+
+########################################
+##            FlatCAMObj              ##
+########################################
+class FlatCAMObj:
+    """
+    Base type of objects handled in FlatCAM. These become interactive
+    in the GUI, can be plotted, and their options can be modified
+    by the user in their respective forms.
+    """
+
+    # Instance of the application to which these are related.
+    # The app should set this value.
+    app = None
+
+    def __init__(self, name):
+        self.options = {"name": name}
+        self.form_kinds = {"name": "entry_text"}  # Kind of form element for each option
+        self.radios = {}  # Name value pairs for radio sets
+        self.radios_inv = {}  # Inverse of self.radios
+        self.axes = None  # Matplotlib axes
+        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
+        :rtype: 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()
+
+        # Remove all decoration. The app's axes will have
+        # the ticks and grid.
+        self.axes.set_frame_on(False)  # No frame
+        self.axes.set_xticks([])  # No tick
+        self.axes.set_yticks([])  # No ticks
+        self.axes.patch.set_visible(False)  # No background
+        self.axes.set_aspect(1)
+
+    def to_form(self):
+        """
+        Copies options to the UI form.
+
+        :return: None
+        """
+        for option in self.options:
+            self.set_form_item(option)
+
+    def read_form(self):
+        """
+        Reads form into ``self.options``.
+
+        :return: None
+        :rtype: None
+        """
+        for option in self.options:
+            self.read_form_item(option)
+
+    def build_ui(self):
+        """
+        Sets up the UI/form for this object.
+
+        :return: None
+        :rtype: None
+        """
+
+        # Where the UI for this object is drawn
+        box_selected = self.app.builder.get_object("box_selected")
+
+        # Remove anything else in the box
+        box_children = box_selected.get_children()
+        for child in box_children:
+            box_selected.remove(child)
+
+        osw = self.app.builder.get_object("offscrwindow_" + self.kind)  # offscreenwindow
+        sw = self.app.builder.get_object("sw_" + self.kind)  # scrollwindows
+        osw.remove(sw)  # TODO: Is this needed ?
+        vp = self.app.builder.get_object("vp_" + self.kind)  # Viewport
+        vp.override_background_color(Gtk.StateType.NORMAL, Gdk.RGBA(1, 1, 1, 1))
+
+        # Put in the UI
+        box_selected.pack_start(sw, True, True, 0)
+
+        entry_name = self.app.builder.get_object("entry_text_" + self.kind + "_name")
+        entry_name.connect("activate", self.app.on_activate_name)
+        self.to_form()
+        sw.show()
+
+    def set_form_item(self, option):
+        """
+        Copies the specified option to the UI form.
+
+        :param option: Name of the option (Key in ``self.options``).
+        :type option: str
+        :return: None
+        """
+        fkind = self.form_kinds[option]
+        fname = fkind + "_" + self.kind + "_" + option
+
+        if fkind == 'entry_eval' or fkind == 'entry_text':
+            self.app.builder.get_object(fname).set_text(str(self.options[option]))
+            return
+        if fkind == 'cb':
+            self.app.builder.get_object(fname).set_active(self.options[option])
+            return
+        if fkind == 'radio':
+            self.app.builder.get_object(self.radios_inv[option][self.options[option]]).set_active(True)
+            return
+        print "Unknown kind of form item:", fkind
+
+    def read_form_item(self, option):
+        """
+        Reads the specified option from the UI form into ``self.options``.
+
+        :param option: Name of the option.
+        :type option: str
+        :return: None
+        """
+        fkind = self.form_kinds[option]
+        fname = fkind + "_" + self.kind + "_" + option
+
+        if fkind == 'entry_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)
+            return
+        if fkind == 'cb':
+            self.options[option] = self.app.builder.get_object(fname).get_active()
+            return
+        if fkind == 'radio':
+            self.options[option] = self.app.get_radio_value(self.radios[option])
+            return
+        print "Unknown kind of form item:", fkind
+
+    def plot(self):
+        """
+        Plot this object (Extend this method to implement the actual plotting).
+        Axes get created, appended to canvas and cleared before plotting.
+        Call this in descendants before doing the plotting.
+
+        :return: Whether to continue plotting or not depending on the "plot" option.
+        :rtype: bool
+        """
+
+        # Axes must exist and be attached to canvas.
+        if self.axes is None or self.axes not in self.app.plotcanvas.figure.axes:
+            self.axes = self.app.plotcanvas.new_axes(self.options['name'])
+
+        if not self.options["plot"]:
+            self.axes.cla()
+            self.app.plotcanvas.auto_adjust_axes()
+            return False
+
+        # Clear axes or we will plot on top of them.
+        self.axes.cla()
+        # GLib.idle_add(self.axes.cla)
+        return True
+
+    def serialize(self):
+        """
+        Returns a representation of the object as a dictionary so
+        it can be later exported as JSON. Override this method.
+
+        :return: Dictionary representing the object
+        :rtype: dict
+        """
+        return
+
+    def deserialize(self, obj_dict):
+        """
+        Re-builds an object from its serialized version.
+
+        :param obj_dict: Dictionary representing a FlatCAMObj
+        :type obj_dict: dict
+        :return: None
+        """
+        return
+
+
+class FlatCAMGerber(FlatCAMObj, Gerber):
+    """
+    Represents Gerber code.
+    """
+
+    def __init__(self, name):
+        Gerber.__init__(self)
+        FlatCAMObj.__init__(self, name)
+
+        self.kind = "gerber"
+
+        # The 'name' is already in self.options from FlatCAMObj
+        self.options.update({
+            "plot": True,
+            "mergepolys": True,
+            "multicolored": False,
+            "solid": False,
+            "isotooldia": 0.016,
+            "isopasses": 1,
+            "isooverlap": 0.15,
+            "cutouttooldia": 0.07,
+            "cutoutmargin": 0.2,
+            "cutoutgapsize": 0.15,
+            "gaps": "tb",
+            "noncoppermargin": 0.0,
+            "noncopperrounded": False,
+            "bboxmargin": 0.0,
+            "bboxrounded": False
+        })
+
+        # The 'name' is already in self.form_kinds from FlatCAMObj
+        self.form_kinds.update({
+            "plot": "cb",
+            "mergepolys": "cb",
+            "multicolored": "cb",
+            "solid": "cb",
+            "isotooldia": "entry_eval",
+            "isopasses": "entry_eval",
+            "isooverlap": "entry_eval",
+            "cutouttooldia": "entry_eval",
+            "cutoutmargin": "entry_eval",
+            "cutoutgapsize": "entry_eval",
+            "gaps": "radio",
+            "noncoppermargin": "entry_eval",
+            "noncopperrounded": "cb",
+            "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"}}
+
+        # Attributes to be included in serialization
+        # Always append to it because it carries contents
+        # from predecessors.
+        self.ser_attrs += ['options', 'kind']
+
+    def convert_units(self, units):
+        """
+        Converts the units of the object by scaling dimensions in all geometry
+        and options.
+
+        :param units: Units to which to convert the object: "IN" or "MM".
+        :type units: str
+        :return: None
+        :rtype: None
+        """
+
+        factor = Gerber.convert_units(self, units)
+
+        self.options['isotooldia'] *= factor
+        self.options['cutoutmargin'] *= factor
+        self.options['cutoutgapsize'] *= factor
+        self.options['noncoppermargin'] *= factor
+        self.options['bboxmargin'] *= factor
+
+    def plot(self):
+
+        # Does all the required setup and returns False
+        # if the 'ptint' option is set to False.
+        if not FlatCAMObj.plot(self):
+            return
+
+        if self.options["mergepolys"]:
+            geometry = self.solid_geometry
+        else:
+            geometry = self.buffered_paths + \
+                        [poly['polygon'] for poly in self.regions] + \
+                        self.flash_geometry
+
+        if self.options["multicolored"]:
+            linespec = '-'
+        else:
+            linespec = 'k-'
+
+        if self.options["solid"]:
+            for poly in geometry:
+                # TODO: Too many things hardcoded.
+                patch = PolygonPatch(poly,
+                                     facecolor="#BBF268",
+                                     edgecolor="#006E20",
+                                     alpha=0.75,
+                                     zorder=2)
+                self.axes.add_patch(patch)
+        else:
+            for poly in geometry:
+                x, y = poly.exterior.xy
+                self.axes.plot(x, y, linespec)
+                for ints in poly.interiors:
+                    x, y = ints.coords.xy
+                    self.axes.plot(x, y, linespec)
+
+        # self.app.plotcanvas.auto_adjust_axes()
+        GLib.idle_add(self.app.plotcanvas.auto_adjust_axes)
+
+    def serialize(self):
+        return {
+            "options": self.options,
+            "kind": self.kind
+        }
+
+
+class FlatCAMExcellon(FlatCAMObj, Excellon):
+    """
+    Represents Excellon/Drill code.
+    """
+
+    def __init__(self, name):
+        Excellon.__init__(self)
+        FlatCAMObj.__init__(self, name)
+
+        self.kind = "excellon"
+
+        self.options.update({
+            "plot": True,
+            "solid": False,
+            "drillz": -0.1,
+            "travelz": 0.1,
+            "feedrate": 5.0,
+            "toolselection": ""
+        })
+
+        self.form_kinds.update({
+            "plot": "cb",
+            "solid": "cb",
+            "drillz": "entry_eval",
+            "travelz": "entry_eval",
+            "feedrate": "entry_eval",
+            "toolselection": "entry_text"
+        })
+
+        # TODO: Document this.
+        self.tool_cbs = {}
+
+        # Attributes to be included in serialization
+        # Always append to it because it carries contents
+        # from predecessors.
+        self.ser_attrs += ['options', 'kind']
+
+    def convert_units(self, units):
+        factor = Excellon.convert_units(self, units)
+
+        self.options['drillz'] *= factor
+        self.options['travelz'] *= factor
+        self.options['feedrate'] *= factor
+
+    def plot(self):
+
+        # Does all the required setup and returns False
+        # if the 'ptint' option is set to False.
+        if not FlatCAMObj.plot(self):
+            return
+
+        try:
+            _ = iter(self.solid_geometry)
+        except TypeError:
+            self.solid_geometry = [self.solid_geometry]
+
+        # Plot excellon (All polygons?)
+        if self.options["solid"]:
+            for geo in self.solid_geometry:
+                patch = PolygonPatch(geo,
+                                     facecolor="#C40000",
+                                     edgecolor="#750000",
+                                     alpha=0.75,
+                                     zorder=3)
+                self.axes.add_patch(patch)
+        else:
+            for geo in self.solid_geometry:
+                x, y = geo.exterior.coords.xy
+                self.axes.plot(x, y, 'r-')
+                for ints in geo.interiors:
+                    x, y = ints.coords.xy
+                    self.axes.plot(x, y, 'g-')
+
+        #self.app.plotcanvas.auto_adjust_axes()
+        GLib.idle_add(self.app.plotcanvas.auto_adjust_axes)
+
+    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 + ": " + str(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 toolx in self.tool_cbs:
+                if self.tool_cbs[toolx].get_active():
+                    tool_list.append(toolx)
+            self.options["toolselection"] = ", ".join(tool_list)
+            self.to_form()
+
+        button.connect("activate", on_accept)
+        button.connect("clicked", on_accept)
+
+
+class FlatCAMCNCjob(FlatCAMObj, CNCjob):
+    """
+    Represents G-Code.
+    """
+
+    def __init__(self, name, units="in", kind="generic", z_move=0.1,
+                 feedrate=3.0, z_cut=-0.002, tooldia=0.0):
+        CNCjob.__init__(self, units=units, kind=kind, z_move=z_move,
+                        feedrate=feedrate, z_cut=z_cut, tooldia=tooldia)
+        FlatCAMObj.__init__(self, name)
+
+        self.kind = "cncjob"
+
+        self.options.update({
+            "plot": True,
+            "tooldia": 0.4 / 25.4  # 0.4mm in inches
+        })
+
+        self.form_kinds.update({
+            "plot": "cb",
+            "tooldia": "entry_eval"
+        })
+
+        # Attributes to be included in serialization
+        # Always append to it because it carries contents
+        # from predecessors.
+        self.ser_attrs += ['options', 'kind']
+
+    def plot(self):
+
+        # Does all the required setup and returns False
+        # if the 'ptint' option is set to False.
+        if not FlatCAMObj.plot(self):
+            return
+
+        self.plot2(self.axes, tooldia=self.options["tooldia"])
+
+        #self.app.plotcanvas.auto_adjust_axes()
+        GLib.idle_add(self.app.plotcanvas.auto_adjust_axes)
+
+    def convert_units(self, units):
+        factor = CNCjob.convert_units(self, units)
+        print "FlatCAMCNCjob.convert_units()"
+        self.options["tooldia"] *= factor
+
+
+class FlatCAMGeometry(FlatCAMObj, Geometry):
+    """
+    Geometric object not associated with a specific
+    format.
+    """
+
+    def __init__(self, name):
+        FlatCAMObj.__init__(self, name)
+        Geometry.__init__(self)
+
+        self.kind = "geometry"
+
+        self.options.update({
+            "plot": True,
+            "solid": False,
+            "multicolored": False,
+            "cutz": -0.002,
+            "travelz": 0.1,
+            "feedrate": 5.0,
+            "cnctooldia": 0.4 / 25.4,
+            "painttooldia": 0.0625,
+            "paintoverlap": 0.15,
+            "paintmargin": 0.01
+        })
+
+        self.form_kinds.update({
+            "plot": "cb",
+            "solid": "cb",
+            "multicolored": "cb",
+            "cutz": "entry_eval",
+            "travelz": "entry_eval",
+            "feedrate": "entry_eval",
+            "cnctooldia": "entry_eval",
+            "painttooldia": "entry_eval",
+            "paintoverlap": "entry_eval",
+            "paintmargin": "entry_eval"
+        })
+
+        # Attributes to be included in serialization
+        # Always append to it because it carries contents
+        # from predecessors.
+        self.ser_attrs += ['options', 'kind']
+
+    def scale(self, factor):
+        """
+        Scales all geometry by a given factor.
+
+        :param factor: Factor by which to scale the object's geometry/
+        :type factor: float
+        :return: None
+        :rtype: None
+        """
+
+        if type(self.solid_geometry) == list:
+            self.solid_geometry = [affinity.scale(g, factor, factor, origin=(0, 0))
+                                   for g in self.solid_geometry]
+        else:
+            self.solid_geometry = affinity.scale(self.solid_geometry, factor, factor,
+                                                 origin=(0, 0))
+
+    def offset(self, vect):
+        """
+        Offsets all geometry by a given vector/
+
+        :param vect: (x, y) vector by which to offset the object's geometry.
+        :type vect: tuple
+        :return: None
+        :rtype: None
+        """
+
+        dx, dy = vect
+
+        if type(self.solid_geometry) == list:
+            self.solid_geometry = [affinity.translate(g, xoff=dx, yoff=dy)
+                                   for g in self.solid_geometry]
+        else:
+            self.solid_geometry = affinity.translate(self.solid_geometry, xoff=dx, yoff=dy)
+
+    def convert_units(self, units):
+        factor = Geometry.convert_units(self, units)
+
+        self.options['cutz'] *= factor
+        self.options['travelz'] *= factor
+        self.options['feedrate'] *= factor
+        self.options['cnctooldia'] *= factor
+        self.options['painttooldia'] *= factor
+        self.options['paintmargin'] *= factor
+
+        return factor
+
+    def plot(self):
+        """
+        Plots the object into its axes. If None, of if the axes
+        are not part of the app's figure, it fetches new ones.
+
+        :return: None
+        """
+
+        # Does all the required setup and returns False
+        # if the 'ptint' option is set to False.
+        if not FlatCAMObj.plot(self):
+            return
+
+        # Make sure solid_geometry is iterable.
+        try:
+            _ = iter(self.solid_geometry)
+        except TypeError:
+            self.solid_geometry = [self.solid_geometry]
+
+        for geo in self.solid_geometry:
+
+            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
+
+            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))
+
+        #self.app.plotcanvas.auto_adjust_axes()
+        GLib.idle_add(self.app.plotcanvas.auto_adjust_axes)

+ 4 - 4
camlib.py

@@ -1488,7 +1488,7 @@ class Excellon(Geometry):
         self.toolset_re = re.compile(r'^T(0?\d|\d\d)(?=.*C(\d*\.?\d*))?' +
                                      r'(?=.*F(\d*\.?\d*))?(?=.*S(\d*\.?\d*))?' +
                                      r'(?=.*B(\d*\.?\d*))?(?=.*H(\d*\.?\d*))?' +
-                                     r'(?=.*Z(-?\d*\.?\d*))?[CFSBHT]')
+                                     r'(?=.*Z([-\+]?\d*\.?\d*))?[CFSBHT]')
 
         # Tool select
         # Can have additional data after tool number but
@@ -1513,11 +1513,11 @@ class Excellon(Geometry):
         # Coordinates
         #self.xcoord_re = re.compile(r'^X(\d*\.?\d*)(?:Y\d*\.?\d*)?$')
         #self.ycoord_re = re.compile(r'^(?:X\d*\.?\d*)?Y(\d*\.?\d*)$')
-        self.coordsperiod_re = re.compile(r'(?=.*X(-?\d*\.\d*))?(?=.*Y(-?\d*\.\d*))?[XY]')
-        self.coordsnoperiod_re = re.compile(r'(?!.*\.)(?=.*X(-?\d*))?(?=.*Y(-?\d*))?[XY]')
+        self.coordsperiod_re = re.compile(r'(?=.*X([-\+]?\d*\.\d*))?(?=.*Y([-\+]?\d*\.\d*))?[XY]')
+        self.coordsnoperiod_re = re.compile(r'(?!.*\.)(?=.*X([-\+]?\d*))?(?=.*Y([-\+]?\d*))?[XY]')
 
         # R - Repeat hole (# times, X offset, Y offset)
-        self.rep_re = re.compile(r'^R(\d+)(?=.*[XY])+(?:X(-?\d*\.?\d*))?(?:Y(-?\d*\.?\d*))?$')
+        self.rep_re = re.compile(r'^R(\d+)(?=.*[XY])+(?:X([-\+]?\d*\.?\d*))?(?:Y([-\+]?\d*\.?\d*))?$')
 
         # Various stop/pause commands
         self.stop_re = re.compile(r'^((G04)|(M09)|(M06)|(M00)|(M30))')

+ 5 - 1
doc/source/devman.rst

@@ -55,4 +55,8 @@ This creates a dictionary with attributes specified in the object's ``ser_attrs`
             }
         return geo
 
-This is used in ``json.dump(d, f, default=to_dict)`` and is applied to objects that json encounters to be in a non-serialized form.
+This is used in ``json.dump(d, f, default=to_dict)`` and is applied to objects that json encounters to be in a non-serialized form.
+
+Geometry Processing
+~~~~~~~~~~~~~~~~~~~
+

+ 1 - 1
recent.json

@@ -1 +1 @@
-[{"kind": "gerber", "filename": "C:\\Users\\jpcaram\\Dropbox\\CNC\\pcbcam\\test_files\\BLDC_1303_Bottom.gbr"}, {"kind": "excellon", "filename": "C:\\Users\\jpcaram\\Dropbox\\CNC\\pcbcam\\test_files\\PlacaReles.drl"}, {"kind": "gerber", "filename": "C:\\Users\\jpcaram\\Dropbox\\CNC\\pcbcam\\test_files\\PlacaReles-F_Cu.gtl"}, {"kind": "gerber", "filename": "C:\\Users\\jpcaram\\Dropbox\\CNC\\pcbcam\\test_files\\Example1_copper_bottom.gbr"}, {"kind": "cncjob", "filename": "C:\\Users\\jpcaram\\Dropbox\\PhD\\PLLs\\RTWO\\RTWO1_CNC\\cutout1.gcode"}, {"kind": "project", "filename": "C:\\Users\\jpcaram\\Dropbox\\PhD\\PLLs\\RTWO\\RTWO1.fcproj"}, {"kind": "excellon", "filename": "C:\\Users\\jpcaram\\Dropbox\\PhD\\PLLs\\RTWO\\Project Outputs for RTWO1\\PCB1.TXT"}, {"kind": "excellon", "filename": "C:\\Users\\jpcaram\\Dropbox\\PhD\\PLLs\\RTWO\\Project Outputs for RTWO1\\PCB1.DRL"}, {"kind": "gerber", "filename": "C:\\Users\\jpcaram\\Dropbox\\PhD\\PLLs\\RTWO\\Project Outputs for RTWO1\\PCB1.GBL"}, {"kind": "project", "filename": "C:\\Users\\jpcaram\\Dropbox\\CNC\\pcbcam\\test_files\\full.fcproj"}]
+[{"kind": "excellon", "filename": "C:\\Users\\jpcaram\\Dropbox\\CNC\\pcbcam\\test_files\\BLDC2003Through.drl"}, {"kind": "gerber", "filename": "C:\\Users\\jpcaram\\Dropbox\\PhD\\PLLs\\RTWO\\Project Outputs for RTWO1\\PCB1.GTL"}, {"kind": "gerber", "filename": "C:\\Users\\jpcaram\\Dropbox\\CNC\\pcbcam\\test_files\\BLDC_1303_Bottom.gbr"}, {"kind": "excellon", "filename": "C:\\Users\\jpcaram\\Dropbox\\CNC\\pcbcam\\test_files\\PlacaReles.drl"}, {"kind": "gerber", "filename": "C:\\Users\\jpcaram\\Dropbox\\CNC\\pcbcam\\test_files\\PlacaReles-F_Cu.gtl"}, {"kind": "gerber", "filename": "C:\\Users\\jpcaram\\Dropbox\\CNC\\pcbcam\\test_files\\Example1_copper_bottom.gbr"}, {"kind": "cncjob", "filename": "C:\\Users\\jpcaram\\Dropbox\\PhD\\PLLs\\RTWO\\RTWO1_CNC\\cutout1.gcode"}, {"kind": "project", "filename": "C:\\Users\\jpcaram\\Dropbox\\PhD\\PLLs\\RTWO\\RTWO1.fcproj"}, {"kind": "excellon", "filename": "C:\\Users\\jpcaram\\Dropbox\\PhD\\PLLs\\RTWO\\Project Outputs for RTWO1\\PCB1.TXT"}, {"kind": "excellon", "filename": "C:\\Users\\jpcaram\\Dropbox\\PhD\\PLLs\\RTWO\\Project Outputs for RTWO1\\PCB1.DRL"}]