Procházet zdrojové kódy

Added full support for Aperture Macros in Gerber parser.

Juan Pablo Caram před 12 roky
rodič
revize
04b9a0ecd7
4 změnil soubory, kde provedl 1002 přidání a 369 odebrání
  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
         :return: None
         :rtype: None
         :rtype: None
         """
         """
+
         if self.axes is None:
         if self.axes is None:
             print "New axes"
             print "New axes"
             self.axes = figure.add_axes([0.05, 0.05, 0.9, 0.9],
             self.axes = figure.add_axes([0.05, 0.05, 0.9, 0.9],
@@ -177,15 +178,17 @@ class FlatCAMObj:
             return
             return
         print "Unknown kind of form item:", fkind
         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):
     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:
         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'])
             self.axes = self.app.plotcanvas.new_axes(self.options['name'])
 
 
@@ -194,6 +197,9 @@ class FlatCAMObj:
             self.app.plotcanvas.auto_adjust_axes()
             self.app.plotcanvas.auto_adjust_axes()
             return False
             return False
 
 
+        # Clear axes or we will plot on top of them.
+        self.axes.cla()
+        # GLib.idle_add(self.axes.cla)
         return True
         return True
 
 
     def serialize(self):
     def serialize(self):
@@ -237,10 +243,12 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
             "isotooldia": 0.016,
             "isotooldia": 0.016,
             "isopasses": 1,
             "isopasses": 1,
             "isooverlap": 0.15,
             "isooverlap": 0.15,
+            "cutouttooldia": 0.07,
             "cutoutmargin": 0.2,
             "cutoutmargin": 0.2,
             "cutoutgapsize": 0.15,
             "cutoutgapsize": 0.15,
             "gaps": "tb",
             "gaps": "tb",
             "noncoppermargin": 0.0,
             "noncoppermargin": 0.0,
+            "noncopperrounded": False,
             "bboxmargin": 0.0,
             "bboxmargin": 0.0,
             "bboxrounded": False
             "bboxrounded": False
         })
         })
@@ -254,10 +262,12 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
             "isotooldia": "entry_eval",
             "isotooldia": "entry_eval",
             "isopasses": "entry_eval",
             "isopasses": "entry_eval",
             "isooverlap": "entry_eval",
             "isooverlap": "entry_eval",
+            "cutouttooldia": "entry_eval",
             "cutoutmargin": "entry_eval",
             "cutoutmargin": "entry_eval",
             "cutoutgapsize": "entry_eval",
             "cutoutgapsize": "entry_eval",
             "gaps": "radio",
             "gaps": "radio",
             "noncoppermargin": "entry_eval",
             "noncoppermargin": "entry_eval",
+            "noncopperrounded": "cb",
             "bboxmargin": "entry_eval",
             "bboxmargin": "entry_eval",
             "bboxrounded": "cb"
             "bboxrounded": "cb"
         })
         })
@@ -325,7 +335,8 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
                     x, y = ints.coords.xy
                     x, y = ints.coords.xy
                     self.axes.plot(x, y, linespec)
                     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):
     def serialize(self):
         return {
         return {
@@ -348,7 +359,6 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
         self.options.update({
         self.options.update({
             "plot": True,
             "plot": True,
             "solid": False,
             "solid": False,
-            "multicolored": False,
             "drillz": -0.1,
             "drillz": -0.1,
             "travelz": 0.1,
             "travelz": 0.1,
             "feedrate": 5.0,
             "feedrate": 5.0,
@@ -358,7 +368,6 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
         self.form_kinds.update({
         self.form_kinds.update({
             "plot": "cb",
             "plot": "cb",
             "solid": "cb",
             "solid": "cb",
-            "multicolored": "cb",
             "drillz": "entry_eval",
             "drillz": "entry_eval",
             "travelz": "entry_eval",
             "travelz": "entry_eval",
             "feedrate": "entry_eval",
             "feedrate": "entry_eval",
@@ -393,12 +402,21 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
             self.solid_geometry = [self.solid_geometry]
             self.solid_geometry = [self.solid_geometry]
 
 
         # Plot excellon (All polygons?)
         # 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()
         self.app.plotcanvas.auto_adjust_axes()
 
 
@@ -914,8 +932,10 @@ class App:
                 percentage += delta
                 percentage += delta
                 GLib.idle_add(lambda: app_obj.set_progress_bar(percentage, "Re-plotting..."))
                 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, ""))
             GLib.timeout_add(300, lambda: app_obj.set_progress_bar(0.0, ""))
 
 
         t = threading.Thread(target=thread_func, args=(self,))
         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_plot": "Plot this object on the main window.",
             "cb_gerber_mergepolys": "Show overlapping polygons as single.",
             "cb_gerber_mergepolys": "Show overlapping polygons as single.",
             "cb_gerber_solid": "Paint inside polygons.",
             "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:
         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.set_progress_bar(0.5, "Plotting..."))
             #GLib.idle_add(lambda: app_obj.get_current().plot(app_obj.figure))
             #GLib.idle_add(lambda: app_obj.get_current().plot(app_obj.figure))
             obj.plot()
             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"))
             GLib.timeout_add(300, lambda: app_obj.set_progress_bar(0.0, "Idle"))
 
 
         t = threading.Thread(target=thread_func, args=(self,))
         t = threading.Thread(target=thread_func, args=(self,))
@@ -2058,6 +2078,8 @@ class App:
         def geo_init(geo_obj, app_obj):
         def geo_init(geo_obj, app_obj):
             assert isinstance(geo_obj, FlatCAMGeometry)
             assert isinstance(geo_obj, FlatCAMGeometry)
             bounding_box = gerb.solid_geometry.envelope.buffer(gerb.options["noncoppermargin"])
             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)
             non_copper = bounding_box.difference(gerb.solid_geometry)
             geo_obj.solid_geometry = non_copper
             geo_obj.solid_geometry = non_copper
 
 
@@ -2078,8 +2100,8 @@ class App:
         name = gerb.options["name"] + "_cutout"
         name = gerb.options["name"] + "_cutout"
 
 
         def geo_init(geo_obj, app_obj):
         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, miny, maxx, maxy = gerb.bounds()
             minx -= margin
             minx -= margin
             maxx += margin
             maxx += margin
@@ -2990,7 +3012,7 @@ class PlotCanvas:
         width = xmax - xmin
         width = xmax - xmin
         height = ymax - ymin
         height = ymax - ymin
 
 
-        if center is None:
+        if center is None or center == [None, None]:
             center = [(xmin + xmax) / 2.0, (ymin + ymax) / 2.0]
             center = [(xmin + xmax) / 2.0, (ymin + ymax) / 2.0]
 
 
         # For keeping the point at the pointer location
         # For keeping the point at the pointer location

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 471 - 294
FlatCAM.ui


+ 484 - 50
camlib.py

@@ -180,6 +180,336 @@ class Geometry:
             setattr(self, attr, d[attr])
             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):
 class Gerber (Geometry):
     """
     """
     **ATTRIBUTES**
     **ATTRIBUTES**
@@ -191,7 +521,7 @@ class Gerber (Geometry):
     +-----------+-----------------------------------+
     +-----------+-----------------------------------+
     | Key       | Value                             |
     | Key       | Value                             |
     +===========+===================================+
     +===========+===================================+
-    | type      | (str) "C", "R", or "O"            |
+    | type      | (str) "C", "R", "O", "P", or "AP" |
     +-----------+-----------------------------------+
     +-----------+-----------------------------------+
     | others    | Depend on ``type``                |
     | others    | Depend on ``type``                |
     +-----------+-----------------------------------+
     +-----------+-----------------------------------+
@@ -251,9 +581,11 @@ class Gerber (Geometry):
         """
         """
         The constructor takes no parameters. Use ``gerber.parse_files()``
         The constructor takes no parameters. Use ``gerber.parse_files()``
         or ``gerber.parse_lines()`` to populate the object from Gerber source.
         or ``gerber.parse_lines()`` to populate the object from Gerber source.
+
         :return: Gerber object
         :return: Gerber object
         :rtype: Gerber
         :rtype: Gerber
         """
         """
+
         # Initialize parent
         # Initialize parent
         Geometry.__init__(self)        
         Geometry.__init__(self)        
         
         
@@ -287,6 +619,10 @@ class Gerber (Geometry):
         # Geometry from flashes
         # Geometry from flashes
         self.flash_geometry = []
         self.flash_geometry = []
 
 
+        # Aperture Macros
+        # TODO: Make sure these can be serialized
+        self.aperture_macros = {}
+
         # Attributes to be included in serialization
         # Attributes to be included in serialization
         # Always append to it because it carries contents
         # Always append to it because it carries contents
         # from Geometry.
         # from Geometry.
@@ -308,7 +644,7 @@ class Gerber (Geometry):
         self.comm_re = re.compile(r'^G0?4(.*)$')
         self.comm_re = re.compile(r'^G0?4(.*)$')
 
 
         # AD - Aperture definition
         # 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
         # AM - Aperture Macro
         # Beginning of macro (Ends with *%):
         # Beginning of macro (Ends with *%):
@@ -356,6 +692,16 @@ class Gerber (Geometry):
         # LP - Level polarity
         # LP - Level polarity
         self.lpol_re = re.compile(r'^%LP([DC])\*%$')
         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.
         # TODO: This is bad.
         self.steps_per_circ = 40
         self.steps_per_circ = 40
 
 
@@ -376,9 +722,8 @@ class Gerber (Geometry):
         :rtype : None
         :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"]
         nonDimensions = ["type", "nVertices", "rotation"]
         for apid in self.apertures:
         for apid in self.apertures:
             for param in self.apertures[apid]:
             for param in self.apertures[apid]:
@@ -386,21 +731,18 @@ class Gerber (Geometry):
                     print "Tool:", apid, "Parameter:", param
                     print "Tool:", apid, "Parameter:", param
                     self.apertures[apid][param] *= factor
                     self.apertures[apid][param] *= factor
 
 
-        # Paths
-        #print "Scaling paths..."
+        ## Paths
         for path in self.paths:
         for path in self.paths:
             path['linestring'] = affinity.scale(path['linestring'],
             path['linestring'] = affinity.scale(path['linestring'],
                                                 factor, factor, origin=(0, 0))
                                                 factor, factor, origin=(0, 0))
 
 
-        # Flashes
-        #print "Scaling flashes..."
+        ## Flashes
         for fl in self.flashes:
         for fl in self.flashes:
             # TODO: Shouldn't 'loc' be a numpy.array()?
             # TODO: Shouldn't 'loc' be a numpy.array()?
             fl['loc'][0] *= factor
             fl['loc'][0] *= factor
             fl['loc'][1] *= factor
             fl['loc'][1] *= factor
 
 
-        # Regions
-        #print "Scaling regions..."
+        ## Regions
         for reg in self.regions:
         for reg in self.regions:
             reg['polygon'] = affinity.scale(reg['polygon'], factor, factor,
             reg['polygon'] = affinity.scale(reg['polygon'], factor, factor,
                                             origin=(0, 0))
                                             origin=(0, 0))
@@ -419,6 +761,7 @@ class Gerber (Geometry):
 
 
         Then ``buffered_paths``, ``flash_geometry`` and ``solid_geometry``
         Then ``buffered_paths``, ``flash_geometry`` and ``solid_geometry``
         are re-created with ``self.create_geometry()``.
         are re-created with ``self.create_geometry()``.
+
         :param vect: (x, y) offset vector.
         :param vect: (x, y) offset vector.
         :type vect: tuple
         :type vect: tuple
         :return: None
         :return: None
@@ -426,21 +769,18 @@ class Gerber (Geometry):
 
 
         dx, dy = vect
         dx, dy = vect
 
 
-        # Paths
-        #print "Shifting paths..."
+        ## Paths
         for path in self.paths:
         for path in self.paths:
             path['linestring'] = affinity.translate(path['linestring'],
             path['linestring'] = affinity.translate(path['linestring'],
                                                     xoff=dx, yoff=dy)
                                                     xoff=dx, yoff=dy)
 
 
-        # Flashes
-        #print "Shifting flashes..."
+        ## Flashes
         for fl in self.flashes:
         for fl in self.flashes:
             # TODO: Shouldn't 'loc' be a numpy.array()?
             # TODO: Shouldn't 'loc' be a numpy.array()?
             fl['loc'][0] += dx
             fl['loc'][0] += dx
             fl['loc'][1] += dy
             fl['loc'][1] += dy
 
 
-        # Regions
-        #print "Shifting regions..."
+        ## Regions
         for reg in self.regions:
         for reg in self.regions:
             reg['polygon'] = affinity.translate(reg['polygon'],
             reg['polygon'] = affinity.translate(reg['polygon'],
                                                 xoff=dx, yoff=dy)
                                                 xoff=dx, yoff=dy)
@@ -452,6 +792,8 @@ class Gerber (Geometry):
         """
         """
         Overwrites the region polygons with fixed
         Overwrites the region polygons with fixed
         versions if found to be invalid (according to Shapely).
         versions if found to be invalid (according to Shapely).
+
+        :return: None
         """
         """
 
 
         for region in self.regions:
         for region in self.regions:
@@ -462,6 +804,7 @@ class Gerber (Geometry):
         """
         """
         This is part of the parsing process. "Thickens" the paths
         This is part of the parsing process. "Thickens" the paths
         by their appertures. This will only work for circular appertures.
         by their appertures. This will only work for circular appertures.
+
         :return: None
         :return: None
         """
         """
 
 
@@ -483,6 +826,7 @@ class Gerber (Geometry):
         * *Rectangle (R)*: width (float), height (float)
         * *Rectangle (R)*: width (float), height (float)
         * *Obround (O)*: width (float), height (float).
         * *Obround (O)*: width (float), height (float).
         * *Polygon (P)*: diameter(float), vertices(int), [rotation(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 apertureId: Id of the aperture being defined.
         :param apertureType: Type of the aperture.
         :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
         # 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.
         # referenced it without the zero, so this is a hack to handle that.
         apid = str(int(apertureId))
         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",
             self.apertures[apid] = {"type": "C",
                                     "size": float(paramList[0])}
                                     "size": float(paramList[0])}
             return apid
             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",
             self.apertures[apid] = {"type": "R",
                                     "width": float(paramList[0]),
                                     "width": float(paramList[0]),
                                     "height": float(paramList[1])}
                                     "height": float(paramList[1])}
             return apid
             return apid
 
 
-        if apertureType == "O" :  # Obround
+        if apertureType == "O":  # Obround
             self.apertures[apid] = {"type": "O",
             self.apertures[apid] = {"type": "O",
                                     "width": float(paramList[0]),
                                     "width": float(paramList[0]),
                                     "height": float(paramList[1])}
                                     "height": float(paramList[1])}
             return apid
             return apid
         
         
-        if apertureType == "P" :
+        if apertureType == "P":  # Polygon (regular)
             self.apertures[apid] = {"type": "P",
             self.apertures[apid] = {"type": "P",
                                     "diam": float(paramList[0]),
                                     "diam": float(paramList[0]),
                                     "nVertices": int(paramList[1])}
                                     "nVertices": int(paramList[1])}
-            if len(paramList) >= 3 :
+            if len(paramList) >= 3:
                 self.apertures[apid]["rotation"] = float(paramList[2])
                 self.apertures[apid]["rotation"] = float(paramList[2])
             return apid
             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
         print "WARNING: Aperture not implemented:", apertureType
         return None
         return None
         
         
@@ -531,6 +885,10 @@ class Gerber (Geometry):
         """
         """
         Calls Gerber.parse_lines() with array of lines
         Calls Gerber.parse_lines() with array of lines
         read from the given file.
         read from the given file.
+
+        :param filename: Gerber file to parse.
+        :type filename: str
+        :return: None
         """
         """
         gfile = open(filename, 'r')
         gfile = open(filename, 'r')
         gstr = gfile.readlines()
         gstr = gfile.readlines()
@@ -566,24 +924,60 @@ class Gerber (Geometry):
         current_x = None
         current_x = None
         current_y = 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
         quadrant_mode = None
 
 
+        # Indicates we are parsing an aperture macro
+        current_macro = None
+
+        #### Parsing starts here ####
         line_num = 0
         line_num = 0
         for gline in glines:
         for gline in glines:
             line_num += 1
             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.
             # 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)
             match = self.lin_re.search(gline)
             if match:
             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
                 # Parse coordinates
                 if match.group(2) is not None:
                 if match.group(2) is not None:
@@ -616,7 +1010,7 @@ class Gerber (Geometry):
 
 
                 continue
                 continue
 
 
-            ## G02/3 - Circular interpolation
+            ### G02/3 - Circular interpolation
             # 2-clockwise, 3-counterclockwise
             # 2-clockwise, 3-counterclockwise
             match = self.circ_re.search(gline)
             match = self.circ_re.search(gline)
             if match:
             if match:
@@ -697,7 +1091,7 @@ class Gerber (Geometry):
                 if quadrant_mode == 'SINGLE':
                 if quadrant_mode == 'SINGLE':
                     print "Warning: Single quadrant arc are not implemented yet. (%d)" % line_num
                     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)
             match = self.quad_re.search(gline)
             if match:
             if match:
                 if match.group(1) == '4':
                 if match.group(1) == '4':
@@ -706,7 +1100,7 @@ class Gerber (Geometry):
                     quadrant_mode = 'MULTI'
                     quadrant_mode = 'MULTI'
                 continue
                 continue
 
 
-            ## G37* - End region
+            ### G37* - End region
             if self.regionoff_re.search(gline):
             if self.regionoff_re.search(gline):
                 # Only one path defines region?
                 # Only one path defines region?
                 if len(path) < 3:
                 if len(path) < 3:
@@ -723,14 +1117,13 @@ class Gerber (Geometry):
                 path = [[current_x, current_y]]  # Start new path
                 path = [[current_x, current_y]]  # Start new path
                 continue
                 continue
             
             
-            #Parse an aperture.
+            ### Aperture definitions %ADD...
             match = self.ad_re.search(gline)
             match = self.ad_re.search(gline)
             if match:
             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
                 continue
 
 
-
-            ## G01/2/3* - Interpolation mode change
+            ### G01/2/3* - Interpolation mode change
             # Can occur along with coordinates and operation code but
             # Can occur along with coordinates and operation code but
             # sometimes by itself (handled here).
             # sometimes by itself (handled here).
             # Example: G01*
             # Example: G01*
@@ -739,29 +1132,54 @@ class Gerber (Geometry):
                 current_interpolation_mode = int(match.group(1))
                 current_interpolation_mode = int(match.group(1))
                 continue
                 continue
 
 
-            ## Tool/aperture change
+            ### Tool/aperture change
             # Example: D12*
             # Example: D12*
             match = self.tool_re.search(gline)
             match = self.tool_re.search(gline)
             if match:
             if match:
                 current_aperture = match.group(1)
                 current_aperture = match.group(1)
                 continue
                 continue
 
 
-            ## Number format
+            ### Number format
             # Example: %FSLAX24Y24*%
             # Example: %FSLAX24Y24*%
             # TODO: This is ignoring most of the format. Implement the rest.
             # TODO: This is ignoring most of the format. Implement the rest.
             match = self.fmt_re.search(gline)
             match = self.fmt_re.search(gline)
             if match:
             if match:
+                absolute = {'A': True, 'I': False}
                 self.int_digits = int(match.group(3))
                 self.int_digits = int(match.group(3))
                 self.frac_digits = int(match.group(4))
                 self.frac_digits = int(match.group(4))
                 continue
                 continue
 
 
-            ## Mode (IN/MM)
+            ### Mode (IN/MM)
             # Example: %MOIN*%
             # Example: %MOIN*%
             match = self.mode_re.search(gline)
             match = self.mode_re.search(gline)
             if match:
             if match:
                 self.units = match.group(1)
                 self.units = match.group(1)
                 continue
                 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
             print "WARNING: Line ignored (%d):" % line_num, gline
         
         
         if len(path) > 1:
         if len(path) > 1:
@@ -821,11 +1239,11 @@ class Gerber (Geometry):
             if aperture['type'] == 'P':  # Regular polygon
             if aperture['type'] == 'P':  # Regular polygon
                 loc = flash['loc']
                 loc = flash['loc']
                 diam = aperture['diam']
                 diam = aperture['diam']
-                nVertices = aperture['nVertices']
+                n_vertices = aperture['nVertices']
                 points = []
                 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))
                     points.append((x, y))
                 ply = Polygon(points)
                 ply = Polygon(points)
                 if 'rotation' in aperture:
                 if 'rotation' in aperture:
@@ -833,6 +1251,13 @@ class Gerber (Geometry):
                 self.flash_geometry.append(ply)
                 self.flash_geometry.append(ply)
                 continue
                 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'])
             print "WARNING: Aperture type %s not implemented" % (aperture['type'])
     
     
     def create_geometry(self):
     def create_geometry(self):
@@ -1261,7 +1686,7 @@ class CNCjob(Geometry):
             tools = [tool for tool in exobj.tools]
             tools = [tool for tool in exobj.tools]
         else:
         else:
             tools = [x.strip() for x in tools.split(",")]
             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
         print "Tools are:", tools
 
 
         points = []
         points = []
@@ -1374,7 +1799,8 @@ class CNCjob(Geometry):
             # NOTE: Limited to 1 bracket pair
             # NOTE: Limited to 1 bracket pair
             op = line.find("(")
             op = line.find("(")
             cl = 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]
                 #comment = line[op+1:cl]
                 line = line[:op] + line[(cl+1):]
                 line = line[:op] + line[(cl+1):]
 
 
@@ -1448,7 +1874,6 @@ class CNCjob(Geometry):
                                      "kind": kind})
                                      "kind": kind})
                     path = [path[-1]]  # Start with the last point of last path.
                     path = [path[-1]]  # Start with the last point of last path.
 
 
-
             if 'G' in gobj:
             if 'G' in gobj:
                 current['G'] = int(gobj['G'])
                 current['G'] = int(gobj['G'])
                 
                 
@@ -1677,6 +2102,7 @@ class CNCjob(Geometry):
 
 
         self.create_geometry()
         self.create_geometry()
 
 
+
 def get_bounds(geometry_set):
 def get_bounds(geometry_set):
     xmin = Inf
     xmin = Inf
     ymin = Inf
     ymin = Inf
@@ -1776,7 +2202,14 @@ def find_polygon(poly_set, point):
 
 
 
 
 def to_dict(geo):
 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):
     if isinstance(geo, BaseGeometry):
         return {
         return {
             "__class__": "Shply",
             "__class__": "Shply",
@@ -1840,6 +2273,7 @@ def parse_gerber_number(strnumber, frac_digits):
     """
     """
     return int(strnumber)*(10**(-frac_digits))
     return int(strnumber)*(10**(-frac_digits))
 
 
+
 def parse_gerber_coords(gstr, int_digits, frac_digits):
 def parse_gerber_coords(gstr, int_digits, frac_digits):
     """
     """
     Parse Gerber coordinates
     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}

Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů