From 04b9a0ecd7215e828997cc7e0a6f357db13249e3 Mon Sep 17 00:00:00 2001 From: Juan Pablo Caram Date: Wed, 12 Mar 2014 19:45:40 -0400 Subject: [PATCH] Added full support for Aperture Macros in Gerber parser. --- FlatCAM.py | 70 ++-- FlatCAM.ui | 978 +++++++++++++++++++++++++++++--------------------- camlib.py | 534 ++++++++++++++++++++++++--- defaults.json | 2 +- 4 files changed, 1093 insertions(+), 491 deletions(-) diff --git a/FlatCAM.py b/FlatCAM.py index 2f82859e..711fb9e7 100644 --- a/FlatCAM.py +++ b/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 diff --git a/FlatCAM.ui b/FlatCAM.ui index 9d2f8647..1feaeb7f 100644 --- a/FlatCAM.ui +++ b/FlatCAM.ui @@ -112,366 +112,6 @@ THE SOFTWARE. False gtk-open - - False - - - True - True - never - in - - - True - False - - - True - False - 5 - 5 - 5 - 5 - vertical - - - True - False - 6 - 3 - Double-Sided PCB Tool - - - - - - False - True - 0 - - - - - True - False - 3 - 3 - - - True - False - 1 - 3 - Bottom Layer: - - - 0 - 0 - 1 - 1 - - - - - 200 - True - False - start - 0 - 1 - - - 1 - 0 - 1 - 1 - - - - - True - False - 1 - 3 - Mirror Axis: - - - 0 - 1 - 1 - 1 - - - - - True - False - 10 - - - X - True - True - False - 0 - True - True - - - False - True - 0 - - - - - Y - True - True - False - 0 - True - rb_mirror_x - - - False - True - 1 - - - - - 1 - 1 - 1 - 1 - - - - - True - False - 1 - 3 - Axis location: - - - 0 - 2 - 1 - 1 - - - - - True - False - 10 - - - Point - True - True - False - 0 - True - True - - - - False - True - 0 - - - - - Box - True - True - False - 0 - True - rb_mirror_point - - - False - True - 1 - - - - - 1 - 2 - 1 - 1 - - - - - True - False - 1 - 3 - Point/Box: - - - 0 - 3 - 1 - 1 - - - - - True - False - vertical - - - - - - 1 - 3 - 1 - 1 - - - - - True - False - 1 - 3 - Algnmt holes: - - - 0 - 4 - 1 - 1 - - - - - True - True - - - - 1 - 4 - 1 - 1 - - - - - True - False - 1 - 3 - Drill diam.: - - - 0 - 5 - 1 - 1 - - - - - True - True - - True - - - 1 - 5 - 1 - 1 - - - - - False - True - 1 - - - - - True - False - end - 6 - 3 - - - Create Alignment Drill - 120 - True - True - True - end - - - - - False - False - 4 - 0 - - - - - Create Mirror - 120 - True - True - True - end - - - - - False - False - 4 - 1 - - - - - False - True - 2 - - - - - - - - - - - - - - - - - - - - - False @@ -540,6 +180,8 @@ THE SOFTWARE. True False + The Object's name. Only gets changed +after hitting Enter in the text box. 3 Name: @@ -593,6 +235,7 @@ THE SOFTWARE. True True False + Plot this object in the main window. 0 True True @@ -621,6 +264,7 @@ THE SOFTWARE. True False + Tool diameter used for plotting. 1 Tool diam: @@ -670,6 +314,8 @@ THE SOFTWARE. True False + <b>Export G-Code:</b> Save a +G-Code file for this CNC job. 3 0 3 @@ -690,6 +336,7 @@ THE SOFTWARE. True True True + Save a G-Code file for this CNC job. @@ -703,6 +350,9 @@ THE SOFTWARE. True False + <b>Scale:</b> Resizes the geometry +of the object. All sizes and coordinates +are multiplied by the given factor. 3 0 3 @@ -728,6 +378,8 @@ THE SOFTWARE. True False + Factor by which to multiply all +geometrical dimensions. 1 Factor: @@ -764,6 +416,8 @@ THE SOFTWARE. True True True + Scales the geometry +of this object. @@ -777,6 +431,8 @@ THE SOFTWARE. True False + <b>Offset:</b> Shift the geometry of +this object by the specified (x, y) vector. 3 0 3 @@ -802,6 +458,10 @@ THE SOFTWARE. True False + Vector by which to offset the geometry. +Format is <b>(x, y)</b>, where <b>x</b> and <b>y</b> are +decimal numbers representing the +distance in the corresponding axis. 1 Offset Vector: @@ -838,6 +498,8 @@ THE SOFTWARE. True True True + Offset the geometry of +this object. @@ -1010,19 +672,7 @@ THE SOFTWARE. - - Multi-colored - True - True - False - 0 - True - - - False - True - 5 - + @@ -1458,6 +1108,8 @@ THE SOFTWARE. True False + The Object's name. Only gets changed +after hitting Enter in the text box. 3 Name: @@ -1511,6 +1163,7 @@ THE SOFTWARE. True True False + Plot this object on the main window. 0 True True @@ -1528,6 +1181,7 @@ THE SOFTWARE. True True False + Show overlapping polygons as single. 0 True @@ -1543,6 +1197,7 @@ THE SOFTWARE. True True False + Draw polygons with different colors. 0 True @@ -1571,6 +1226,8 @@ THE SOFTWARE. True False + <b>Create CNC Job:</b> CNC Jobs from Geometry +Objects are cutting toolpaths along object contours. 5 0 3 @@ -1640,6 +1297,10 @@ THE SOFTWARE. True False + The contours are cut at the given Z-axis +position. Typically, the surface of the +workpiece is at z=0.0, and <b>Cut Z</b> +is specified as a negative number. 1 Cut Z: @@ -1654,6 +1315,9 @@ THE SOFTWARE. True False + This specifies the Z-axis position of the +tool for XY-plane motions when not +cutting. 1 Travel Z: @@ -1668,6 +1332,9 @@ THE SOFTWARE. True False + The feed rate is the speed in <b>units/minute</b> +when cutting. When not cutting, movement is +done at maximum speed. 1 Feed rate: @@ -1682,6 +1349,9 @@ THE SOFTWARE. True False + The tool diameter specified here is for +visual purposes only and does not affect +the resulting job. 1 Tool diam: @@ -1720,6 +1390,8 @@ THE SOFTWARE. True True True + Creates the CNC Job oobject from this +Geometry object with the specified options. @@ -1733,6 +1405,10 @@ THE SOFTWARE. True False + <b>Paint Area:</b> Creates contour +inside a polygon for a tool to cover +its entire surface. Use for clearing +large copper areas. 5 0 3 @@ -1757,6 +1433,8 @@ THE SOFTWARE. True False + Tool diameter for painting +inside a polygon. 1 Tool diam: @@ -1786,6 +1464,8 @@ THE SOFTWARE. True False + How much (fraction of tool diameter) +to overlap each toolpath. 1 Overlap: @@ -1815,6 +1495,10 @@ THE SOFTWARE. True False + Margin from the edge of the polygon. +Use this to stay away from edges +cut or to be cut with a finer, more +accurate tool. 1 Margin: @@ -1853,6 +1537,12 @@ THE SOFTWARE. True True True + Creates a Geometry Object with toolpaths +to cover the inside of a polygon. + +<b>Note:</b> After clicking generate +you have to click inside the desired +polygon. @@ -1866,6 +1556,9 @@ THE SOFTWARE. True False + <b>Scale:</b> Resizes the geometry +of the object. All sizes and coordinates +are multiplied by the given factor. 3 0 3 @@ -1891,6 +1584,8 @@ THE SOFTWARE. True False + Factor by which to multiply all +geometrical dimensions. 1 Factor: @@ -1927,6 +1622,8 @@ THE SOFTWARE. True True True + Scales the geometry +of this object. @@ -1940,6 +1637,8 @@ THE SOFTWARE. True False + <b>Offset:</b> Shift the geometry of +this object by the specified (x, y) vector. 3 0 3 @@ -1965,6 +1664,10 @@ THE SOFTWARE. True False + Vector by which to offset the geometry. +Format is <b>(x, y)</b>, where <b>x</b> and <b>y</b> are +decimal numbers representing the +distance in the corresponding axis. 1 Offset Vector: @@ -2001,6 +1704,8 @@ THE SOFTWARE. True True True + Offset the geometry of +this object. @@ -2091,6 +1796,8 @@ THE SOFTWARE. True False + The Object's name. Only gets changed +after hitting Enter in the text box. 3 Name: @@ -2412,7 +2119,7 @@ cutting tool path. 0 - 0 + 1 1 1 @@ -2428,7 +2135,7 @@ cutting tool path. 1 - 0 + 1 1 1 @@ -2444,7 +2151,7 @@ cutting tool path. 1 - 1 + 2 1 1 @@ -2461,7 +2168,7 @@ board in place until the job is complete. 0 - 1 + 2 1 1 @@ -2475,7 +2182,7 @@ board in place until the job is complete. 0 - 2 + 3 1 1 @@ -2543,7 +2250,39 @@ of the board cutout. 1 - 2 + 3 + 1 + 1 + + + + + True + False + True + Tool diameter to be used to cut out +the baord. + 1 + Tool diam.: + + + 0 + 0 + 1 + 1 + + + + + True + True + + 12 + True + + + 1 + 0 1 1 @@ -2644,6 +2383,28 @@ this boundary. 15 + + + Rounded corners + True + True + False + True + Whether to draw rounded corners on the bounding +box. The radius is the Boundary margin. + + Whether to draw rounded corners on the bounding +box. The radius is the Boundary margin. + + 0 + True + + + False + True + 16 + + Generate Geometry @@ -2658,7 +2419,7 @@ areas without copper. False True - 16 + 17 @@ -2678,7 +2439,7 @@ boundary around the PCB contents. False True - 17 + 18 @@ -2723,7 +2484,7 @@ bounding box. False True - 18 + 19 @@ -2741,7 +2502,7 @@ box. The radius is the Boundary margin. False True - 19 + 20 @@ -2758,7 +2519,7 @@ with the bounding box. False True - 20 + 21 @@ -2779,7 +2540,7 @@ are multiplied by the given factor. False True - 21 + 22 @@ -2822,7 +2583,7 @@ geometrical dimensions. False True - 22 + 23 @@ -2839,7 +2600,7 @@ of this object. False True - 23 + 24 @@ -2859,7 +2620,7 @@ this object by the specified (x, y) vector. False True - 24 + 25 @@ -2904,7 +2665,7 @@ distance in the corresponding axis. False True - 25 + 26 @@ -2921,7 +2682,364 @@ this object. False True - 26 + 27 + + + + + + + + + + + + + + + + + + + + False + + + True + True + never + in + + + True + False + + + True + False + 5 + 5 + 5 + 5 + vertical + + + True + False + 6 + 3 + Double-Sided PCB Tool + + + + + + False + True + 0 + + + + + True + False + 3 + 3 + + + True + False + 1 + 3 + Bottom Layer: + + + 0 + 0 + 1 + 1 + + + + + 200 + True + False + start + 0 + 1 + + + 1 + 0 + 1 + 1 + + + + + True + False + 1 + 3 + Mirror Axis: + + + 0 + 1 + 1 + 1 + + + + + True + False + 10 + + + X + True + True + False + 0 + True + True + + + False + True + 0 + + + + + Y + True + True + False + 0 + True + rb_mirror_x + + + False + True + 1 + + + + + 1 + 1 + 1 + 1 + + + + + True + False + 1 + 3 + Axis location: + + + 0 + 2 + 1 + 1 + + + + + True + False + 10 + + + Point + True + True + False + 0 + True + True + + + + False + True + 0 + + + + + Box + True + True + False + 0 + True + rb_mirror_point + + + False + True + 1 + + + + + 1 + 2 + 1 + 1 + + + + + True + False + 1 + 3 + Point/Box: + + + 0 + 3 + 1 + 1 + + + + + True + False + vertical + + + + + + 1 + 3 + 1 + 1 + + + + + True + False + 1 + 3 + Algnmt holes: + + + 0 + 4 + 1 + 1 + + + + + True + True + + + + 1 + 4 + 1 + 1 + + + + + True + False + 1 + 3 + Drill diam.: + + + 0 + 5 + 1 + 1 + + + + + True + True + + True + + + 1 + 5 + 1 + 1 + + + + + False + True + 1 + + + + + True + False + end + 6 + 3 + + + Create Alignment Drill + 120 + True + True + True + end + + + + + False + False + 4 + 0 + + + + + Create Mirror + 120 + True + True + True + end + + + + + False + False + 4 + 1 + + + + + False + True + 2 @@ -3807,7 +3925,7 @@ to overlap each pass. 0 - 0 + 1 1 1 @@ -3823,7 +3941,7 @@ to overlap each pass. 1 - 0 + 1 1 1 @@ -3839,7 +3957,7 @@ to overlap each pass. 1 - 1 + 2 1 1 @@ -3853,7 +3971,7 @@ to overlap each pass. 0 - 1 + 2 1 1 @@ -3867,7 +3985,7 @@ to overlap each pass. 0 - 2 + 3 1 1 @@ -3931,7 +4049,36 @@ to overlap each pass. 1 - 2 + 3 + 1 + 1 + + + + + True + False + 1 + Tool diam.: + + + 0 + 0 + 1 + 1 + + + + + True + True + + 12 + True + + + 1 + 0 1 1 @@ -4003,6 +4150,21 @@ to overlap each pass. 10 + + + Rounded corners + True + True + False + 0 + True + + + False + True + 11 + + True @@ -4018,7 +4180,7 @@ to overlap each pass. False True - 11 + 12 @@ -4060,7 +4222,7 @@ to overlap each pass. False True - 12 + 13 @@ -4076,7 +4238,7 @@ to overlap each pass. False True - 13 + 14 @@ -4085,9 +4247,6 @@ to overlap each pass. - - - @@ -4184,20 +4343,7 @@ to overlap each pass. - - Multi-colored - True - True - False - 0 - True - - - - False - True - 3 - + diff --git a/camlib.py b/camlib.py index 76c5e403..16ccb453 100644 --- a/camlib.py +++ b/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 diff --git a/defaults.json b/defaults.json index 433b5513..fec7aea9 100644 --- a/defaults.json +++ b/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} \ No newline at end of file +{"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} \ No newline at end of file