Explorar el Código

Added full support for Aperture Macros in Gerber parser.

Juan Pablo Caram hace 12 años
padre
commit
04b9a0ecd7
Se han modificado 4 ficheros con 1002 adiciones y 369 borrados
  1. 46 24
      FlatCAM.py
  2. 471 294
      FlatCAM.ui
  3. 484 50
      camlib.py
  4. 1 1
      defaults.json

+ 46 - 24
FlatCAM.py

@@ -61,6 +61,7 @@ class FlatCAMObj:
         :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],
@@ -177,15 +178,17 @@ class FlatCAMObj:
             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)
-
     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'])
 
@@ -194,6 +197,9 @@ class FlatCAMObj:
             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):
@@ -237,10 +243,12 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
             "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
         })
@@ -254,10 +262,12 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
             "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"
         })
@@ -325,7 +335,8 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
                     x, y = ints.coords.xy
                     self.axes.plot(x, y, linespec)
 
-        self.app.plotcanvas.auto_adjust_axes()
+        # self.app.plotcanvas.auto_adjust_axes()
+        GLib.idle_add(self.app.plotcanvas.auto_adjust_axes)
 
     def serialize(self):
         return {
@@ -348,7 +359,6 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
         self.options.update({
             "plot": True,
             "solid": False,
-            "multicolored": False,
             "drillz": -0.1,
             "travelz": 0.1,
             "feedrate": 5.0,
@@ -358,7 +368,6 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
         self.form_kinds.update({
             "plot": "cb",
             "solid": "cb",
-            "multicolored": "cb",
             "drillz": "entry_eval",
             "travelz": "entry_eval",
             "feedrate": "entry_eval",
@@ -393,12 +402,21 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
             self.solid_geometry = [self.solid_geometry]
 
         # Plot excellon (All polygons?)
-        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-')
+        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()
 
@@ -914,8 +932,10 @@ class App:
                 percentage += delta
                 GLib.idle_add(lambda: app_obj.set_progress_bar(percentage, "Re-plotting..."))
 
-            app_obj.plotcanvas.auto_adjust_axes()
-            self.on_zoom_fit(None)
+            #app_obj.plotcanvas.auto_adjust_axes()
+            #self.on_zoom_fit(None)
+            GLib.idle_add(app_obj.plotcanvas.auto_adjust_axes)
+            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,))
@@ -1340,7 +1360,7 @@ class App:
             "cb_gerber_plot": "Plot this object on the main window.",
             "cb_gerber_mergepolys": "Show overlapping polygons as single.",
             "cb_gerber_solid": "Paint inside polygons.",
-            "cb_gerber_multicolored": "Draw polygons with different polygons."
+            "cb_gerber_multicolored": "Draw polygons with different colors."
         }
 
         for widget in tooltips:
@@ -1961,7 +1981,7 @@ class App:
             #GLib.idle_add(lambda: app_obj.set_progress_bar(0.5, "Plotting..."))
             #GLib.idle_add(lambda: app_obj.get_current().plot(app_obj.figure))
             obj.plot()
-            GLib.idle_add(lambda: app_obj.on_zoom_fit(None))
+            #GLib.idle_add(lambda: app_obj.on_zoom_fit(None))
             GLib.timeout_add(300, lambda: app_obj.set_progress_bar(0.0, "Idle"))
 
         t = threading.Thread(target=thread_func, args=(self,))
@@ -2058,6 +2078,8 @@ class App:
         def geo_init(geo_obj, app_obj):
             assert isinstance(geo_obj, FlatCAMGeometry)
             bounding_box = gerb.solid_geometry.envelope.buffer(gerb.options["noncoppermargin"])
+            if not gerb.options["noncopperrounded"]:
+                bounding_box = bounding_box.envelope
             non_copper = bounding_box.difference(gerb.solid_geometry)
             geo_obj.solid_geometry = non_copper
 
@@ -2078,8 +2100,8 @@ class App:
         name = gerb.options["name"] + "_cutout"
 
         def geo_init(geo_obj, app_obj):
-            margin = gerb.options["cutoutmargin"]
-            gap_size = gerb.options["cutoutgapsize"]
+            margin = gerb.options["cutoutmargin"] + gerb.options["cutouttooldia"]/2
+            gap_size = gerb.options["cutoutgapsize"] + gerb.options["cutouttooldia"]
             minx, miny, maxx, maxy = gerb.bounds()
             minx -= margin
             maxx += margin
@@ -2990,7 +3012,7 @@ class PlotCanvas:
         width = xmax - xmin
         height = ymax - ymin
 
-        if center is None:
+        if center is None or center == [None, None]:
             center = [(xmin + xmax) / 2.0, (ymin + ymax) / 2.0]
 
         # For keeping the point at the pointer location

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 471 - 294
FlatCAM.ui


+ 484 - 50
camlib.py

@@ -180,6 +180,336 @@ class Geometry:
             setattr(self, attr, d[attr])
 
 
+class ApertureMacro:
+
+    ## Regular expressions
+    am1_re = re.compile(r'^%AM([^\*]+)\*(.+)?(%)?$')
+    am2_re = re.compile(r'(.*)%$')
+    amcomm_re = re.compile(r'^0(.*)')
+    amprim_re = re.compile(r'^[1-9].*')
+    amvar_re = re.compile(r'^\$([0-9a-zA-z]+)=(.*)')
+
+    def __init__(self, name=None):
+        self.name = name
+        self.raw = ""
+        self.primitives = []
+        self.locvars = {}
+        self.geometry = None
+
+    def parse_content(self):
+        """
+        Creates numerical lists for all primitives in the aperture
+        macro (in ``self.raw``) by replacing all variables by their
+        values iteratively and evaluating expressions. Results
+        are stored in ``self.primitives``.
+
+        :return: None
+        """
+        # Cleanup
+        self.raw = self.raw.replace('\n', '').replace('\r', '').strip(" *")
+        self.primitives = []
+
+        # Separate parts
+        parts = self.raw.split('*')
+
+        #### Every part in the macro ####
+        for part in parts:
+            ### Comments. Ignored.
+            match = ApertureMacro.amcomm_re.search(part)
+            if match:
+                continue
+
+            ### Variables
+            # These are variables defined locally inside the macro. They can be
+            # numerical constant or defind in terms of previously define
+            # variables, which can be defined locally or in an aperture
+            # definition. All replacements ocurr here.
+            match = ApertureMacro.amvar_re.search(part)
+            if match:
+                var = match.group(1)
+                val = match.group(2)
+
+                # Replace variables in value
+                for v in self.locvars:
+                    val = re.sub(r'\$'+str(v)+r'(?![0-9a-zA-Z])', str(self.locvars[v]), val)
+
+                # Make all others 0
+                val = re.sub(r'\$[0-9a-zA-Z](?![0-9a-zA-Z])', "0", val)
+
+                # Change x with *
+                val = re.sub(r'x', "\*", val)
+
+                # Eval() and store.
+                self.locvars[var] = eval(val)
+                continue
+
+            ### Primitives
+            # Each is an array. The first identifies the primitive, while the
+            # rest depend on the primitive. All are strings representing a
+            # number and may contain variable definition. The values of these
+            # variables are defined in an aperture definition.
+            match = ApertureMacro.amprim_re.search(part)
+            if match:
+                ## Replace all variables
+                for v in self.locvars:
+                    part = re.sub(r'\$'+str(v)+r'(?![0-9a-zA-Z])', str(self.locvars[v]), part)
+
+                # Make all others 0
+                part = re.sub(r'\$[0-9a-zA-Z](?![0-9a-zA-Z])', "0", part)
+
+                # Change x with *
+                part = re.sub(r'x', "\*", part)
+
+                ## Store
+                elements = part.split(",")
+                self.primitives.append([eval(x) for x in elements])
+                continue
+
+            print "WARNING: Unknown syntax of aperture macro part:", part
+
+    def append(self, data):
+        """
+        Appends a string to the raw macro.
+
+        :param data: Part of the macro.
+        :type data: str
+        :return: None
+        """
+        self.raw += data
+
+    @staticmethod
+    def default2zero(n, mods):
+        """
+        Pads the ``mods`` list with zeros resulting in an
+        list of length n.
+
+        :param n: Length of the resulting list.
+        :type n: int
+        :param mods: List to be padded.
+        :type mods: list
+        :return: Zero-padded list.
+        :rtype: list
+        """
+        x = [0.0]*n
+        na = len(mods)
+        x[0:na] = mods
+        return x
+
+    @staticmethod
+    def make_circle(mods):
+        """
+
+        :param mods: (Exposure 0/1, Diameter >=0, X-coord, Y-coord)
+        :return:
+        """
+
+        pol, dia, x, y = ApertureMacro.default2zero(4, mods)
+
+        return {"pol": int(pol), "geometry": Point(x, y).buffer(dia/2)}
+
+    @staticmethod
+    def make_vectorline(mods):
+        """
+
+        :param mods: (Exposure 0/1, Line width >= 0, X-start, Y-start, X-end, Y-end,
+            rotation angle around origin in degrees)
+        :return:
+        """
+        pol, width, xs, ys, xe, ye, angle = ApertureMacro.default2zero(7, mods)
+
+        line = LineString([(xs, ys), (xe, ye)])
+        box = line.buffer(width/2, cap_style=2)
+        box_rotated = affinity.rotate(box, angle, origin=(0, 0))
+
+        return {"pol": int(pol), "geometry": box_rotated}
+
+    @staticmethod
+    def make_centerline(mods):
+        """
+
+        :param mods: (Exposure 0/1, width >=0, height >=0, x-center, y-center,
+            rotation angle around origin in degrees)
+        :return:
+        """
+
+        pol, width, height, x, y, angle = ApertureMacro.default2zero(6, mods)
+
+        box = shply_box(x-width/2, y-height/2, x+width/2, y+height/2)
+        box_rotated = affinity.rotate(box, angle, origin=(0, 0))
+
+        return {"pol": int(pol), "geometry": box_rotated}
+
+    @staticmethod
+    def make_lowerleftline(mods):
+        """
+
+        :param mods: (exposure 0/1, width >=0, height >=0, x-lowerleft, y-lowerleft,
+            rotation angle around origin in degrees)
+        :return:
+        """
+
+        pol, width, height, x, y, angle = ApertureMacro.default2zero(6, mods)
+
+        box = shply_box(x, y, x+width, y+height)
+        box_rotated = affinity.rotate(box, angle, origin=(0, 0))
+
+        return {"pol": int(pol), "geometry": box_rotated}
+
+    @staticmethod
+    def make_outline(mods):
+        """
+
+        :param mods:
+        :return:
+        """
+
+        pol = mods[0]
+        n = mods[1]
+        points = [(0, 0)]*(n+1)
+
+        for i in range(n+1):
+            points[i] = mods[2*i + 2:2*i + 4]
+
+        angle = mods[2*n + 4]
+
+        poly = Polygon(points)
+        poly_rotated = affinity.rotate(poly, angle, origin=(0, 0))
+
+        return {"pol": int(pol), "geometry": poly_rotated}
+
+    @staticmethod
+    def make_polygon(mods):
+        """
+        Note: Specs indicate that rotation is only allowed if the center
+        (x, y) == (0, 0). I will tolerate breaking this rule.
+
+        :param mods: (exposure 0/1, n_verts 3<=n<=12, x-center, y-center,
+            diameter of circumscribed circle >=0, rotation angle around origin)
+        :return:
+        """
+
+        pol, nverts, x, y, dia, angle = ApertureMacro.default2zero(6, mods)
+        points = [(0, 0)]*nverts
+
+        for i in nverts:
+            points[i] = (x + 0.5 * dia * cos(2*pi * i/nverts),
+                         y + 0.5 * dia * sin(2*pi * i/nverts))
+
+        poly = Polygon(points)
+        poly_rotated = affinity.rotate(poly, angle, origin=(0, 0))
+
+        return {"pol": int(pol), "geometry": poly_rotated}
+
+    @staticmethod
+    def make_moire(mods):
+        """
+        Note: Specs indicate that rotation is only allowed if the center
+        (x, y) == (0, 0). I will tolerate breaking this rule.
+
+        :param mods: (x-center, y-center, outer_dia_outer_ring, ring thickness,
+            gap, max_rings, crosshair_thickness, crosshair_len, rotation
+            angle around origin in degrees)
+        :return:
+        """
+
+        x, y, dia, thickness, gap, nrings, cross_th, cross_len, angle = ApertureMacro.default2zero(9, mods)
+
+        r = dia/2 - thickness/2
+        result = Point((x, y)).buffer(r).exterior.buffer(thickness/2.0)
+        ring = Point((x, y)).buffer(r).exterior.buffer(thickness/2.0)  # Need a copy!
+
+        i = 1  # Number of rings created so far
+
+        ## If the ring does not have an interior it means that it is
+        ## a disk. Then stop.
+        while len(ring.interiors) > 0 and i < nrings:
+            r -= thickness + gap
+            if r <= 0:
+                break
+            ring = Point((x, y)).buffer(r).exterior.buffer(thickness/2.0)
+            result = cascaded_union([result, ring])
+            i += 1
+
+        ## Crosshair
+        hor = LineString([(x - cross_len, y), (x + cross_len, y)]).buffer(cross_th/2.0, cap_style=2)
+        ver = LineString([(x, y-cross_len), (x, y + cross_len)]).buffer(cross_th/2.0, cap_style=2)
+        result = cascaded_union([result, hor, ver])
+
+        return {"pol": 1, "geometry": result}
+
+    @staticmethod
+    def make_thermal(mods):
+        """
+        Note: Specs indicate that rotation is only allowed if the center
+        (x, y) == (0, 0). I will tolerate breaking this rule.
+
+        :param mods: [x-center, y-center, diameter-outside, diameter-inside,
+            gap-thickness, rotation angle around origin]
+        :return:
+        """
+
+        x, y, dout, din, t, angle = ApertureMacro.default2zero(6, mods)
+
+        ring = Point((x, y)).buffer(dout/2.0).difference(Point((x, y)).buffer(din/2.0))
+        hline = LineString([(x - dout/2.0, y), (x + dout/2.0, y)]).buffer(t/2.0, cap_style=3)
+        vline = LineString([(x, y - dout/2.0), (x, y + dout/2.0)]).buffer(t/2.0, cap_style=3)
+        thermal = ring.difference(hline.union(vline))
+
+        return {"pol": 1, "geometry": thermal}
+
+    def make_geometry(self, modifiers):
+        """
+        Runs the macro for the given modifiers and generates
+        the corresponding geometry.
+
+        :param modifiers: Modifiers (parameters) for this macro
+        :type modifiers: list
+        """
+
+        ## Primitive makers
+        makers = {
+            "1": ApertureMacro.make_circle,
+            "2": ApertureMacro.make_vectorline,
+            "20": ApertureMacro.make_vectorline,
+            "21": ApertureMacro.make_centerline,
+            "22": ApertureMacro.make_lowerleftline,
+            "4": ApertureMacro.make_outline,
+            "5": ApertureMacro.make_polygon,
+            "6": ApertureMacro.make_moire,
+            "7": ApertureMacro.make_thermal
+        }
+
+        ## Store modifiers as local variables
+        modifiers = modifiers or []
+        modifiers = [float(m) for m in modifiers]
+        self.locvars = {}
+        for i in range(1, len(modifiers)+1):
+            self.locvars[str(i)] = modifiers[i]
+
+        ## Parse
+        self.primitives = []  # Cleanup
+        self.geometry = None
+        self.parse_content()
+
+        ## Make the geometry
+        for primitive in self.primitives:
+            # Make the primitive
+            prim_geo = makers[str(int(primitive[0]))](primitive[1:])
+
+            # Add it (according to polarity)
+            if self.geometry is None and prim_geo['pol'] == 1:
+                self.geometry = prim_geo['geometry']
+                continue
+            if prim_geo['pol'] == 1:
+                self.geometry = self.geometry.union(prim_geo['geometry'])
+                continue
+            if prim_geo['pol'] == 0:
+                self.geometry = self.geometry.difference(prim_geo['geometry'])
+                continue
+
+        return self.geometry
+
+
 class Gerber (Geometry):
     """
     **ATTRIBUTES**
@@ -191,7 +521,7 @@ class Gerber (Geometry):
     +-----------+-----------------------------------+
     | Key       | Value                             |
     +===========+===================================+
-    | type      | (str) "C", "R", or "O"            |
+    | type      | (str) "C", "R", "O", "P", or "AP" |
     +-----------+-----------------------------------+
     | others    | Depend on ``type``                |
     +-----------+-----------------------------------+
@@ -251,9 +581,11 @@ class Gerber (Geometry):
         """
         The constructor takes no parameters. Use ``gerber.parse_files()``
         or ``gerber.parse_lines()`` to populate the object from Gerber source.
+
         :return: Gerber object
         :rtype: Gerber
         """
+
         # Initialize parent
         Geometry.__init__(self)        
         
@@ -287,6 +619,10 @@ class Gerber (Geometry):
         # Geometry from flashes
         self.flash_geometry = []
 
+        # Aperture Macros
+        # TODO: Make sure these can be serialized
+        self.aperture_macros = {}
+
         # Attributes to be included in serialization
         # Always append to it because it carries contents
         # from Geometry.
@@ -308,7 +644,7 @@ class Gerber (Geometry):
         self.comm_re = re.compile(r'^G0?4(.*)$')
 
         # AD - Aperture definition
-        self.ad_re = re.compile(r'^%ADD(\d\d+)([a-zA-Z0-9]*),(.*)\*%$')
+        self.ad_re = re.compile(r'^%ADD(\d\d+)([a-zA-Z0-9]*)(?:,(.*))?\*%$')
 
         # AM - Aperture Macro
         # Beginning of macro (Ends with *%):
@@ -356,6 +692,16 @@ class Gerber (Geometry):
         # LP - Level polarity
         self.lpol_re = re.compile(r'^%LP([DC])\*%$')
 
+        # Units (OBSOLETE)
+        self.units_re = re.compile(r'^G7([01])\*$')
+
+        # Absolute/Relative G90/1 (OBSOLETE)
+        self.absrel_re = re.compile(r'^G9([01])\*$')
+
+        # Aperture macros
+        self.am1_re = re.compile(r'^%AM([^\*]+)\*(.+)?(%)?$')
+        self.am2_re = re.compile(r'(.*)%$')
+
         # TODO: This is bad.
         self.steps_per_circ = 40
 
@@ -376,9 +722,8 @@ class Gerber (Geometry):
         :rtype : None
         """
 
-        # Apertures
-        #print "Scaling apertures..."
-        #List of the non-dimension aperture parameters
+        ## Apertures
+        # List of the non-dimension aperture parameters
         nonDimensions = ["type", "nVertices", "rotation"]
         for apid in self.apertures:
             for param in self.apertures[apid]:
@@ -386,21 +731,18 @@ class Gerber (Geometry):
                     print "Tool:", apid, "Parameter:", param
                     self.apertures[apid][param] *= factor
 
-        # Paths
-        #print "Scaling paths..."
+        ## Paths
         for path in self.paths:
             path['linestring'] = affinity.scale(path['linestring'],
                                                 factor, factor, origin=(0, 0))
 
-        # Flashes
-        #print "Scaling flashes..."
+        ## Flashes
         for fl in self.flashes:
             # TODO: Shouldn't 'loc' be a numpy.array()?
             fl['loc'][0] *= factor
             fl['loc'][1] *= factor
 
-        # Regions
-        #print "Scaling regions..."
+        ## Regions
         for reg in self.regions:
             reg['polygon'] = affinity.scale(reg['polygon'], factor, factor,
                                             origin=(0, 0))
@@ -419,6 +761,7 @@ class Gerber (Geometry):
 
         Then ``buffered_paths``, ``flash_geometry`` and ``solid_geometry``
         are re-created with ``self.create_geometry()``.
+
         :param vect: (x, y) offset vector.
         :type vect: tuple
         :return: None
@@ -426,21 +769,18 @@ class Gerber (Geometry):
 
         dx, dy = vect
 
-        # Paths
-        #print "Shifting paths..."
+        ## Paths
         for path in self.paths:
             path['linestring'] = affinity.translate(path['linestring'],
                                                     xoff=dx, yoff=dy)
 
-        # Flashes
-        #print "Shifting flashes..."
+        ## Flashes
         for fl in self.flashes:
             # TODO: Shouldn't 'loc' be a numpy.array()?
             fl['loc'][0] += dx
             fl['loc'][1] += dy
 
-        # Regions
-        #print "Shifting regions..."
+        ## Regions
         for reg in self.regions:
             reg['polygon'] = affinity.translate(reg['polygon'],
                                                 xoff=dx, yoff=dy)
@@ -452,6 +792,8 @@ class Gerber (Geometry):
         """
         Overwrites the region polygons with fixed
         versions if found to be invalid (according to Shapely).
+
+        :return: None
         """
 
         for region in self.regions:
@@ -462,6 +804,7 @@ class Gerber (Geometry):
         """
         This is part of the parsing process. "Thickens" the paths
         by their appertures. This will only work for circular appertures.
+
         :return: None
         """
 
@@ -483,6 +826,7 @@ class Gerber (Geometry):
         * *Rectangle (R)*: width (float), height (float)
         * *Obround (O)*: width (float), height (float).
         * *Polygon (P)*: diameter(float), vertices(int), [rotation(float)]
+        * *Aperture Macro (AM)*: macro (ApertureMacro), modifiers (list)
 
         :param apertureId: Id of the aperture being defined.
         :param apertureType: Type of the aperture.
@@ -497,33 +841,43 @@ class Gerber (Geometry):
         # Found some Gerber with a leading zero in the aperture id and the
         # referenced it without the zero, so this is a hack to handle that.
         apid = str(int(apertureId))
-        paramList = apParameters.split('X')
 
-        if apertureType == "C" :  # Circle, example: %ADD11C,0.1*%
+        try:  # Could be empty for aperture macros
+            paramList = apParameters.split('X')
+        except:
+            paramList = None
+
+        if apertureType == "C":  # Circle, example: %ADD11C,0.1*%
             self.apertures[apid] = {"type": "C",
                                     "size": float(paramList[0])}
             return apid
         
-        if apertureType == "R" :  # Rectangle, example: %ADD15R,0.05X0.12*%
+        if apertureType == "R":  # Rectangle, example: %ADD15R,0.05X0.12*%
             self.apertures[apid] = {"type": "R",
                                     "width": float(paramList[0]),
                                     "height": float(paramList[1])}
             return apid
 
-        if apertureType == "O" :  # Obround
+        if apertureType == "O":  # Obround
             self.apertures[apid] = {"type": "O",
                                     "width": float(paramList[0]),
                                     "height": float(paramList[1])}
             return apid
         
-        if apertureType == "P" :
+        if apertureType == "P":  # Polygon (regular)
             self.apertures[apid] = {"type": "P",
                                     "diam": float(paramList[0]),
                                     "nVertices": int(paramList[1])}
-            if len(paramList) >= 3 :
+            if len(paramList) >= 3:
                 self.apertures[apid]["rotation"] = float(paramList[2])
             return apid
 
+        if apertureType in self.aperture_macros:
+            self.apertures[apid] = {"type": "AM",
+                                    "macro": self.aperture_macros[apertureType],
+                                    "modifiers": paramList}
+            return apid
+
         print "WARNING: Aperture not implemented:", apertureType
         return None
         
@@ -531,6 +885,10 @@ class Gerber (Geometry):
         """
         Calls Gerber.parse_lines() with array of lines
         read from the given file.
+
+        :param filename: Gerber file to parse.
+        :type filename: str
+        :return: None
         """
         gfile = open(filename, 'r')
         gstr = gfile.readlines()
@@ -566,24 +924,60 @@ class Gerber (Geometry):
         current_x = None
         current_y = None
 
-        # How to interprest circular interpolation: SINGLE or MULTI
+        # Absolute or Relative/Incremental coordinates
+        absolute = True
+
+        # How to interpret circular interpolation: SINGLE or MULTI
         quadrant_mode = None
 
+        # Indicates we are parsing an aperture macro
+        current_macro = None
+
+        #### Parsing starts here ####
         line_num = 0
         for gline in glines:
             line_num += 1
 
-            ## G01 - Linear interpolation plus flashes
+            ### Aperture Macros
+            # Having this at the beggining will slow things down
+            # but macros can have complicated statements than could
+            # be caught by other ptterns.
+            if current_macro is None:  # No macro started yet
+                match = self.am1_re.search(gline)
+                # Start macro if match, else not an AM, carry on.
+                if match:
+                    current_macro = match.group(1)
+                    self.aperture_macros[current_macro] = ApertureMacro(name=current_macro)
+                    if match.group(2):  # Append
+                        self.aperture_macros[current_macro].append(match.group(2))
+                    if match.group(3):  # Finish macro
+                        #self.aperture_macros[current_macro].parse_content()
+                        current_macro = None
+                    continue
+            else:  # Continue macro
+                match = self.am2_re.search(gline)
+                if match:  # Finish macro
+                    self.aperture_macros[current_macro].append(match.group(1))
+                    #self.aperture_macros[current_macro].parse_content()
+                    current_macro = None
+                else:  # Append
+                    self.aperture_macros[current_macro].append(gline)
+                continue
+
+            ### G01 - Linear interpolation plus flashes
             # Operation code (D0x) missing is deprecated... oh well I will support it.
+            # REGEX: r'^(?:G0?(1))?(?:X(-?\d+))?(?:Y(-?\d+))?(?:D0([123]))?\*$'
             match = self.lin_re.search(gline)
             if match:
-                # Dxx alone? Will ignore for now.
-                if match.group(1) is None and match.group(2) is None and match.group(3) is None:
-                    try:
-                        current_operation_code = int(match.group(4))
-                    except:
-                        pass  # A line with just * will match too.
-                    continue
+                # Dxx alone?
+                # if match.group(1) is None and match.group(2) is None and match.group(3) is None:
+                #     try:
+                #         current_operation_code = int(match.group(4))
+                #     except:
+                #         pass  # A line with just * will match too.
+                #     continue
+                # NOTE: Letting it continue allows it to react to the
+                #       operation code.
 
                 # Parse coordinates
                 if match.group(2) is not None:
@@ -616,7 +1010,7 @@ class Gerber (Geometry):
 
                 continue
 
-            ## G02/3 - Circular interpolation
+            ### G02/3 - Circular interpolation
             # 2-clockwise, 3-counterclockwise
             match = self.circ_re.search(gline)
             if match:
@@ -697,7 +1091,7 @@ class Gerber (Geometry):
                 if quadrant_mode == 'SINGLE':
                     print "Warning: Single quadrant arc are not implemented yet. (%d)" % line_num
 
-            ## G74/75* - Single or multiple quadrant arcs
+            ### G74/75* - Single or multiple quadrant arcs
             match = self.quad_re.search(gline)
             if match:
                 if match.group(1) == '4':
@@ -706,7 +1100,7 @@ class Gerber (Geometry):
                     quadrant_mode = 'MULTI'
                 continue
 
-            ## G37* - End region
+            ### G37* - End region
             if self.regionoff_re.search(gline):
                 # Only one path defines region?
                 if len(path) < 3:
@@ -723,14 +1117,13 @@ class Gerber (Geometry):
                 path = [[current_x, current_y]]  # Start new path
                 continue
             
-            #Parse an aperture.
+            ### Aperture definitions %ADD...
             match = self.ad_re.search(gline)
             if match:
-                self.aperture_parse(match.group(1),match.group(2),match.group(3))
+                self.aperture_parse(match.group(1), match.group(2), match.group(3))
                 continue
 
-
-            ## G01/2/3* - Interpolation mode change
+            ### G01/2/3* - Interpolation mode change
             # Can occur along with coordinates and operation code but
             # sometimes by itself (handled here).
             # Example: G01*
@@ -739,29 +1132,54 @@ class Gerber (Geometry):
                 current_interpolation_mode = int(match.group(1))
                 continue
 
-            ## Tool/aperture change
+            ### Tool/aperture change
             # Example: D12*
             match = self.tool_re.search(gline)
             if match:
                 current_aperture = match.group(1)
                 continue
 
-            ## Number format
+            ### Number format
             # Example: %FSLAX24Y24*%
             # TODO: This is ignoring most of the format. Implement the rest.
             match = self.fmt_re.search(gline)
             if match:
+                absolute = {'A': True, 'I': False}
                 self.int_digits = int(match.group(3))
                 self.frac_digits = int(match.group(4))
                 continue
 
-            ## Mode (IN/MM)
+            ### Mode (IN/MM)
             # Example: %MOIN*%
             match = self.mode_re.search(gline)
             if match:
                 self.units = match.group(1)
                 continue
 
+            ### Units (G70/1) OBSOLETE
+            match = self.units_re.search(gline)
+            if match:
+                self.units = {'0': 'IN', '1': 'MM'}[match.group(1)]
+                continue
+
+            ### Absolute/relative coordinates G90/1 OBSOLETE
+            match = self.absrel_re.search(gline)
+            if match:
+                absolute = {'0': True, '1': False}[match.group(1)]
+                continue
+
+            #### Ignored lines
+            ## Comments
+            match = self.comm_re.search(gline)
+            if match:
+                continue
+
+            ## EOF
+            match = self.eof_re.search(gline)
+            if match:
+                continue
+
+            ### Line did not match any pattern. Warn user.
             print "WARNING: Line ignored (%d):" % line_num, gline
         
         if len(path) > 1:
@@ -821,11 +1239,11 @@ class Gerber (Geometry):
             if aperture['type'] == 'P':  # Regular polygon
                 loc = flash['loc']
                 diam = aperture['diam']
-                nVertices = aperture['nVertices']
+                n_vertices = aperture['nVertices']
                 points = []
-                for i in range(0, nVertices):
-                    x = loc[0] + diam * (cos(2 * pi * i / nVertices))
-                    y = loc[1] + diam * (sin(2 * pi * i / nVertices))
+                for i in range(0, n_vertices):
+                    x = loc[0] + diam * (cos(2 * pi * i / n_vertices))
+                    y = loc[1] + diam * (sin(2 * pi * i / n_vertices))
                     points.append((x, y))
                 ply = Polygon(points)
                 if 'rotation' in aperture:
@@ -833,6 +1251,13 @@ class Gerber (Geometry):
                 self.flash_geometry.append(ply)
                 continue
 
+            if aperture['type'] == 'AM':  # Aperture Macro
+                loc = flash['loc']
+                flash_geo = aperture['macro'].make_geometry(aperture['modifiers'])
+                flash_geo_final = affinity.translate(flash_geo, xoff=loc[0], yoff=loc[1])
+                self.flash_geometry.append(flash_geo_final)
+                continue
+
             print "WARNING: Aperture type %s not implemented" % (aperture['type'])
     
     def create_geometry(self):
@@ -1261,7 +1686,7 @@ class CNCjob(Geometry):
             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)
+            tools = filter(lambda i: i in exobj.tools, tools)
         print "Tools are:", tools
 
         points = []
@@ -1374,7 +1799,8 @@ class CNCjob(Geometry):
             # NOTE: Limited to 1 bracket pair
             op = line.find("(")
             cl = line.find(")")
-            if op > -1 and  cl > op:
+            #if op > -1 and  cl > op:
+            if cl > op > -1:
                 #comment = line[op+1:cl]
                 line = line[:op] + line[(cl+1):]
 
@@ -1448,7 +1874,6 @@ class CNCjob(Geometry):
                                      "kind": kind})
                     path = [path[-1]]  # Start with the last point of last path.
 
-
             if 'G' in gobj:
                 current['G'] = int(gobj['G'])
                 
@@ -1677,6 +2102,7 @@ class CNCjob(Geometry):
 
         self.create_geometry()
 
+
 def get_bounds(geometry_set):
     xmin = Inf
     ymin = Inf
@@ -1776,7 +2202,14 @@ def find_polygon(poly_set, point):
 
 
 def to_dict(geo):
-    output = ''
+    """
+    Makes a Shapely geometry object into serializeable form.
+
+    :param geo: Shapely geometry.
+    :type geo: BaseGeometry
+    :return: Dictionary with serializable form if ``geo`` was
+        BaseGeometry, otherwise returns ``geo``.
+    """
     if isinstance(geo, BaseGeometry):
         return {
             "__class__": "Shply",
@@ -1840,6 +2273,7 @@ def parse_gerber_number(strnumber, frac_digits):
     """
     return int(strnumber)*(10**(-frac_digits))
 
+
 def parse_gerber_coords(gstr, int_digits, frac_digits):
     """
     Parse Gerber coordinates

+ 1 - 1
defaults.json

@@ -1 +1 @@
-{"geometry_paintoverlap": 0.15, "geometry_plot": true, "excellon_feedrate": 5.0, "gerber_plot": true, "gerber_mergepolys": true, "excellon_drillz": -0.1, "geometry_feedrate": 3.0, "units": "IN", "excellon_travelz": 0.1, "gerber_multicolored": false, "gerber_solid": true, "gerber_isopasses": 1, "excellon_plot": true, "gerber_isotooldia": 0.016, "cncjob_tooldia": 0.016, "geometry_travelz": 0.1, "gerber_cutoutmargin": 0.2, "excellon_solid": false, "geometry_paintmargin": 0.01, "geometry_cutz": -0.002, "geometry_cnctooldia": 0.016, "geometry_painttooldia": 0.0625, "gerber_gaps": "4", "excellon_multicolored": false, "gerber_bboxmargin": 0.0, "cncjob_plot": true, "gerber_cutoutgapsize": 0.15, "gerber_isooverlap": 0.17, "gerber_bboxrounded": false, "geometry_multicolored": false, "gerber_noncoppermargin": 0.0, "geometry_solid": false}
+{"gerber_cutouttooldia":0.07, "geometry_paintoverlap": 0.15, "geometry_plot": true, "excellon_feedrate": 5.0, "gerber_plot": true, "gerber_mergepolys": true, "excellon_drillz": -0.1, "geometry_feedrate": 3.0, "units": "IN", "excellon_travelz": 0.1, "gerber_multicolored": false, "gerber_solid": true, "gerber_isopasses": 1, "excellon_plot": true, "gerber_isotooldia": 0.016, "cncjob_tooldia": 0.016, "geometry_travelz": 0.1, "gerber_cutoutmargin": 0.2, "excellon_solid": false, "geometry_paintmargin": 0.01, "geometry_cutz": -0.002, "geometry_cnctooldia": 0.016, "geometry_painttooldia": 0.0625, "gerber_gaps": "4", "gerber_bboxmargin": 0.0, "cncjob_plot": true, "gerber_cutoutgapsize": 0.15, "gerber_isooverlap": 0.17, "gerber_bboxrounded": false, "gerber_noncopperrounded": false, "geometry_multicolored": false, "gerber_noncoppermargin": 0.0, "geometry_solid": false}

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio