Added full support for Aperture Macros in Gerber parser.

This commit is contained in:
Juan Pablo Caram
2014-03-12 19:45:40 -04:00
parent 5f3319dd7a
commit 04b9a0ecd7
4 changed files with 1093 additions and 491 deletions

View File

@@ -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: if self.options["solid"]:
x, y = geo.exterior.coords.xy for geo in self.solid_geometry:
self.axes.plot(x, y, 'r-') patch = PolygonPatch(geo,
for ints in geo.interiors: facecolor="#C40000",
x, y = ints.coords.xy edgecolor="#750000",
self.axes.plot(x, y, 'g-') 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() #app_obj.plotcanvas.auto_adjust_axes()
self.on_zoom_fit(None) #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"] margin = gerb.options["cutoutmargin"] + gerb.options["cutouttooldia"]/2
gap_size = gerb.options["cutoutgapsize"] 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

File diff suppressed because it is too large Load Diff

534
camlib.py
View File

@@ -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 ## Apertures
#print "Scaling apertures..." # List of the non-dimension aperture parameters
#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 ## Paths
#print "Scaling 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 ## Flashes
#print "Scaling 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 ## Regions
#print "Scaling 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 ## Paths
#print "Shifting 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 ## Flashes
#print "Shifting 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 ## Regions
#print "Shifting 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. # Dxx alone?
if match.group(1) is None and match.group(2) is None and match.group(3) is None: # if match.group(1) is None and match.group(2) is None and match.group(3) is None:
try: # try:
current_operation_code = int(match.group(4)) # current_operation_code = int(match.group(4))
except: # except:
pass # A line with just * will match too. # pass # A line with just * will match too.
continue # 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): for i in range(0, n_vertices):
x = loc[0] + diam * (cos(2 * pi * i / nVertices)) x = loc[0] + diam * (cos(2 * pi * i / n_vertices))
y = loc[1] + diam * (sin(2 * pi * i / nVertices)) 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

View File

@@ -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}