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
: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

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])
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

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}