summaryrefslogtreecommitdiff
path: root/gerber
diff options
context:
space:
mode:
Diffstat (limited to 'gerber')
-rwxr-xr-xgerber/excellon.py45
-rw-r--r--gerber/excellon_statements.py66
-rw-r--r--gerber/excellon_tool.py70
-rw-r--r--gerber/gerber_statements.py24
-rw-r--r--gerber/primitives.py232
-rw-r--r--gerber/render/cairo_backend.py488
-rw-r--r--gerber/render/rs274x_backend.py51
-rw-r--r--gerber/rs274x.py141
-rw-r--r--gerber/tests/test_excellon.py154
-rw-r--r--gerber/tests/test_primitives.py111
-rw-r--r--gerber/tests/test_rs274x_backend.py38
-rw-r--r--gerber/utils.py7
12 files changed, 956 insertions, 471 deletions
diff --git a/gerber/excellon.py b/gerber/excellon.py
index 9825c5a..c3de948 100755
--- a/gerber/excellon.py
+++ b/gerber/excellon.py
@@ -100,12 +100,12 @@ class DrillHit(object):
self.position = position
def to_inch(self):
- if self.tool.units == 'metric':
+ if self.tool.settings.units == 'metric':
self.tool.to_inch()
self.position = tuple(map(inch, self.position))
def to_metric(self):
- if self.tool.units == 'inch':
+ if self.tool.settings.units == 'inch':
self.tool.to_metric()
self.position = tuple(map(metric, self.position))
@@ -120,7 +120,7 @@ class DrillHit(object):
max_y = position[1] + radius
return ((min_x, max_x), (min_y, max_y))
- def offset(self, x_offset, y_offset):
+ def offset(self, x_offset=0, y_offset=0):
self.position = tuple(map(operator.add, self.position, (x_offset, y_offset)))
def __str__(self):
@@ -141,13 +141,13 @@ class DrillSlot(object):
self.slot_type = slot_type
def to_inch(self):
- if self.tool.units == 'metric':
+ if self.tool.settings.units == 'metric':
self.tool.to_inch()
self.start = tuple(map(inch, self.start))
self.end = tuple(map(inch, self.end))
def to_metric(self):
- if self.tool.units == 'inch':
+ if self.tool.settings.units == 'inch':
self.tool.to_metric()
self.start = tuple(map(metric, self.start))
self.end = tuple(map(metric, self.end))
@@ -163,7 +163,7 @@ class DrillSlot(object):
max_y = max(start[1], end[1]) + radius
return ((min_x, max_x), (min_y, max_y))
- def offset(self, x_offset, y_offset):
+ def offset(self, x_offset=0, y_offset=0):
self.start = tuple(map(operator.add, self.start, (x_offset, y_offset)))
self.end = tuple(map(operator.add, self.end, (x_offset, y_offset)))
@@ -183,6 +183,7 @@ class ExcellonFile(CamFile):
hits : list of tuples
list of drill hits as (<Tool>, (x, y))
+
settings : dict
Dictionary of gerber file settings
@@ -211,16 +212,17 @@ class ExcellonFile(CamFile):
primitives = []
for hit in self.hits:
if isinstance(hit, DrillHit):
- primitives.append(Drill(hit.position, hit.tool.diameter, hit, units=self.settings.units))
+ primitives.append(Drill(hit.position, hit.tool.diameter,
+ units=self.settings.units))
elif isinstance(hit, DrillSlot):
- primitives.append(Slot(hit.start, hit.end, hit.tool.diameter, hit, units=self.settings.units))
+ primitives.append(Slot(hit.start, hit.end, hit.tool.diameter,
+ units=self.settings.units))
else:
raise ValueError('Unknown hit type')
-
return primitives
@property
- def bounds(self):
+ def bounding_box(self):
xmin = ymin = 100000000000
xmax = ymax = -100000000000
for hit in self.hits:
@@ -282,29 +284,31 @@ class ExcellonFile(CamFile):
Convert units to inches
"""
if self.units != 'inch':
- self.units = 'inch'
for statement in self.statements:
statement.to_inch()
for tool in iter(self.tools.values()):
tool.to_inch()
- for primitive in self.primitives:
- primitive.to_inch()
- for hit in self.hits:
- hit.to_inch()
+ #for primitive in self.primitives:
+ # primitive.to_inch()
+ #for hit in self.hits:
+ # hit.to_inch()
+ self.units = 'inch'
def to_metric(self):
""" Convert units to metric
"""
if self.units != 'metric':
- self.units = 'metric'
for statement in self.statements:
statement.to_metric()
for tool in iter(self.tools.values()):
tool.to_metric()
- for primitive in self.primitives:
- primitive.to_metric()
+ #for primitive in self.primitives:
+ # print("Converting to metric: {}".format(primitive))
+ # primitive.to_metric()
+ # print(primitive)
for hit in self.hits:
hit.to_metric()
+ self.units = 'metric'
def offset(self, x_offset=0, y_offset=0):
for statement in self.statements:
@@ -663,7 +667,8 @@ class ExcellonParser(object):
if 'G85' in line:
stmt = SlotStmt.from_excellon(line, self._settings())
- # I don't know if this is actually correct, but it makes sense that this is where the tool would end
+ # I don't know if this is actually correct, but it makes sense
+ # that this is where the tool would end
x = stmt.x_end
y = stmt.y_end
@@ -835,7 +840,7 @@ def detect_excellon_format(data=None, filename=None):
try:
p = ExcellonParser(settings)
ef = p.parse_raw(data)
- size = tuple([t[0] - t[1] for t in ef.bounds])
+ size = tuple([t[0] - t[1] for t in ef.bounding_box])
hole_area = 0.0
for hit in p.hits:
tool = hit.tool
diff --git a/gerber/excellon_statements.py b/gerber/excellon_statements.py
index ac9c528..bcf35e4 100644
--- a/gerber/excellon_statements.py
+++ b/gerber/excellon_statements.py
@@ -113,16 +113,16 @@ class ExcellonTool(ExcellonStatement):
hit_count : integer
Number of tool hits in excellon file.
"""
-
+
PLATED_UNKNOWN = None
PLATED_YES = 'plated'
PLATED_NO = 'nonplated'
PLATED_OPTIONAL = 'optional'
-
+
@classmethod
def from_tool(cls, tool):
args = {}
-
+
args['depth_offset'] = tool.depth_offset
args['diameter'] = tool.diameter
args['feed_rate'] = tool.feed_rate
@@ -131,7 +131,7 @@ class ExcellonTool(ExcellonStatement):
args['plated'] = tool.plated
args['retract_rate'] = tool.retract_rate
args['rpm'] = tool.rpm
-
+
return cls(None, **args)
@classmethod
@@ -172,9 +172,9 @@ class ExcellonTool(ExcellonStatement):
args['number'] = int(val)
elif cmd == 'Z':
args['depth_offset'] = parse_gerber_value(val, nformat, zero_suppression)
-
+
if plated != ExcellonTool.PLATED_UNKNOWN:
- # Sometimees we can can parse the
+ # Sometimees we can can parse the plating status
args['plated'] = plated
return cls(settings, **args)
@@ -209,7 +209,7 @@ class ExcellonTool(ExcellonStatement):
self.max_hit_count = kwargs.get('max_hit_count')
self.depth_offset = kwargs.get('depth_offset')
self.plated = kwargs.get('plated')
-
+
self.hit_count = 0
def to_excellon(self, settings=None):
@@ -249,15 +249,15 @@ class ExcellonTool(ExcellonStatement):
def _hit(self):
self.hit_count += 1
-
+
def equivalent(self, other):
"""
Is the other tool equal to this, ignoring the tool number, and other file specified properties
"""
-
+
if type(self) != type(other):
return False
-
+
return (self.diameter == other.diameter
and self.feed_rate == other.feed_rate
and self.retract_rate == other.retract_rate
@@ -314,12 +314,12 @@ class ToolSelectionStmt(ExcellonStatement):
if self.compensation_index is not None:
stmt += '%02d' % self.compensation_index
return stmt
-
+
class NextToolSelectionStmt(ExcellonStatement):
-
+
# TODO the statement exists outside of the context of the file,
# so it is imposible to know that it is really the next tool
-
+
def __init__(self, cur_tool, next_tool, **kwargs):
"""
Select the next tool in the wheel.
@@ -329,10 +329,10 @@ class NextToolSelectionStmt(ExcellonStatement):
next_tool : the that that is now selected
"""
super(NextToolSelectionStmt, self).__init__(**kwargs)
-
+
self.cur_tool = cur_tool
self.next_tool = next_tool
-
+
def to_excellon(self, settings=None):
stmt = 'M00'
return stmt
@@ -651,11 +651,11 @@ class EndOfProgramStmt(ExcellonStatement):
class UnitStmt(ExcellonStatement):
-
+
@classmethod
def from_settings(cls, settings):
"""Create the unit statement from the FileSettings"""
-
+
return cls(settings.units, settings.zeros)
@classmethod
@@ -742,7 +742,7 @@ class FormatStmt(ExcellonStatement):
def to_excellon(self, settings=None):
return 'FMAT,%d' % self.format
-
+
@property
def format_tuple(self):
return (self.format, 6 - self.format)
@@ -844,38 +844,38 @@ class UnknownStmt(ExcellonStatement):
class SlotStmt(ExcellonStatement):
"""
G85 statement. Defines a slot created by multiple drills between two specified points.
-
+
Format is two coordinates, split by G85in the middle, for example, XnY0nG85XnYn
"""
-
+
@classmethod
def from_points(cls, start, end):
-
+
return cls(start[0], start[1], end[0], end[1])
-
+
@classmethod
def from_excellon(cls, line, settings, **kwargs):
# Split the line based on the G85 separator
sub_coords = line.split('G85')
(x_start_coord, y_start_coord) = SlotStmt.parse_sub_coords(sub_coords[0], settings)
(x_end_coord, y_end_coord) = SlotStmt.parse_sub_coords(sub_coords[1], settings)
-
+
# Some files seem to specify only one of the coordinates
if x_end_coord == None:
x_end_coord = x_start_coord
if y_end_coord == None:
y_end_coord = y_start_coord
-
+
c = cls(x_start_coord, y_start_coord, x_end_coord, y_end_coord, **kwargs)
c.units = settings.units
- return c
-
+ return c
+
@staticmethod
def parse_sub_coords(line, settings):
-
+
x_coord = None
y_coord = None
-
+
if line[0] == 'X':
splitline = line.strip('X').split('Y')
x_coord = parse_gerber_value(splitline[0], settings.format,
@@ -886,7 +886,7 @@ class SlotStmt(ExcellonStatement):
else:
y_coord = parse_gerber_value(line.strip(' Y'), settings.format,
settings.zero_suppression)
-
+
return (x_coord, y_coord)
@@ -907,16 +907,16 @@ class SlotStmt(ExcellonStatement):
if self.y_start is not None:
stmt += 'Y%s' % write_gerber_value(self.y_start, settings.format,
settings.zero_suppression)
-
+
stmt += 'G85'
-
+
if self.x_end is not None:
stmt += 'X%s' % write_gerber_value(self.x_end, settings.format,
settings.zero_suppression)
if self.y_end is not None:
stmt += 'Y%s' % write_gerber_value(self.y_end, settings.format,
settings.zero_suppression)
-
+
return stmt
def to_inch(self):
@@ -959,7 +959,7 @@ class SlotStmt(ExcellonStatement):
start_str += 'X: %g ' % self.x_start
if self.y_start is not None:
start_str += 'Y: %g ' % self.y_start
-
+
end_str = ''
if self.x_end is not None:
end_str += 'X: %g ' % self.x_end
diff --git a/gerber/excellon_tool.py b/gerber/excellon_tool.py
index bd76e54..a9ac450 100644
--- a/gerber/excellon_tool.py
+++ b/gerber/excellon_tool.py
@@ -28,9 +28,9 @@ try:
from cStringIO import StringIO
except(ImportError):
from io import StringIO
-
+
from .excellon_statements import ExcellonTool
-
+
def loads(data, settings=None):
""" Read tool file information and return a map of tools
Parameters
@@ -52,13 +52,13 @@ class ExcellonToolDefinitionParser(object):
----------
None
"""
-
+
allegro_tool = re.compile(r'(?P<size>[0-9/.]+)\s+(?P<plated>P|N)\s+T(?P<toolid>[0-9]{2})\s+(?P<xtol>[0-9/.]+)\s+(?P<ytol>[0-9/.]+)')
allegro_comment_mils = re.compile('Holesize (?P<toolid>[0-9]{1,2})\. = (?P<size>[0-9/.]+) Tolerance = \+(?P<xtol>[0-9/.]+)/-(?P<ytol>[0-9/.]+) (?P<plated>(PLATED)|(NON_PLATED)|(OPTIONAL)) MILS Quantity = [0-9]+')
allegro2_comment_mils = re.compile('T(?P<toolid>[0-9]{1,2}) Holesize (?P<toolid2>[0-9]{1,2})\. = (?P<size>[0-9/.]+) Tolerance = \+(?P<xtol>[0-9/.]+)/-(?P<ytol>[0-9/.]+) (?P<plated>(PLATED)|(NON_PLATED)|(OPTIONAL)) MILS Quantity = [0-9]+')
allegro_comment_mm = re.compile('Holesize (?P<toolid>[0-9]{1,2})\. = (?P<size>[0-9/.]+) Tolerance = \+(?P<xtol>[0-9/.]+)/-(?P<ytol>[0-9/.]+) (?P<plated>(PLATED)|(NON_PLATED)|(OPTIONAL)) MM Quantity = [0-9]+')
allegro2_comment_mm = re.compile('T(?P<toolid>[0-9]{1,2}) Holesize (?P<toolid2>[0-9]{1,2})\. = (?P<size>[0-9/.]+) Tolerance = \+(?P<xtol>[0-9/.]+)/-(?P<ytol>[0-9/.]+) (?P<plated>(PLATED)|(NON_PLATED)|(OPTIONAL)) MM Quantity = [0-9]+')
-
+
matchers = [
(allegro_tool, 'mils'),
(allegro_comment_mils, 'mils'),
@@ -66,34 +66,34 @@ class ExcellonToolDefinitionParser(object):
(allegro_comment_mm, 'mm'),
(allegro2_comment_mm, 'mm'),
]
-
+
def __init__(self, settings=None):
self.tools = {}
self.settings = settings
-
+
def parse_raw(self, data):
for line in StringIO(data):
self._parse(line.strip())
-
+
return self.tools
-
+
def _parse(self, line):
-
+
for matcher in ExcellonToolDefinitionParser.matchers:
m = matcher[0].match(line)
if m:
unit = matcher[1]
-
+
size = float(m.group('size'))
platedstr = m.group('plated')
toolid = int(m.group('toolid'))
xtol = float(m.group('xtol'))
ytol = float(m.group('ytol'))
-
+
size = self._convert_length(size, unit)
xtol = self._convert_length(xtol, unit)
ytol = self._convert_length(ytol, unit)
-
+
if platedstr == 'PLATED':
plated = ExcellonTool.PLATED_YES
elif platedstr == 'NON_PLATED':
@@ -102,19 +102,20 @@ class ExcellonToolDefinitionParser(object):
plated = ExcellonTool.PLATED_OPTIONAL
else:
plated = ExcellonTool.PLATED_UNKNOWN
-
- tool = ExcellonTool(None, number=toolid, diameter=size, plated=plated)
-
+
+ tool = ExcellonTool(None, number=toolid, diameter=size,
+ plated=plated)
+
self.tools[tool.number] = tool
-
+
break
-
+
def _convert_length(self, value, unit):
-
+
# Convert the value to mm
if unit == 'mils':
value /= 39.3700787402
-
+
# Now convert to the settings unit
if self.settings.units == 'inch':
return value / 25.4
@@ -137,34 +138,35 @@ def loads_rep(data, settings=None):
return ExcellonReportParser(settings).parse_raw(data)
class ExcellonReportParser(object):
-
+
# We sometimes get files with different encoding, so we can't actually
# match the text - the best we can do it detect the table header
header = re.compile(r'====\s+====\s+====\s+====\s+=====\s+===')
-
+
def __init__(self, settings=None):
self.tools = {}
self.settings = settings
-
+
self.found_header = False
-
+
def parse_raw(self, data):
for line in StringIO(data):
self._parse(line.strip())
-
+
return self.tools
-
+
def _parse(self, line):
-
+
# skip empty lines and "comments"
if not line.strip():
return
-
+
if not self.found_header:
- # Try to find the heaader, since we need that to be sure we understand the contents correctly.
+ # Try to find the heaader, since we need that to be sure we
+ # understand the contents correctly.
if ExcellonReportParser.header.match(line):
self.found_header = True
-
+
elif line[0] != '=':
# Already found the header, so we know to to map the contents
parts = line.split()
@@ -180,7 +182,9 @@ class ExcellonReportParser(object):
feedrate = int(parts[3])
speed = int(parts[4])
qty = int(parts[5])
-
- tool = ExcellonTool(None, number=toolid, diameter=size, plated=plated, feed_rate=feedrate, rpm=speed)
-
- self.tools[tool.number] = tool \ No newline at end of file
+
+ tool = ExcellonTool(None, number=toolid, diameter=size,
+ plated=plated, feed_rate=feedrate,
+ rpm=speed)
+
+ self.tools[tool.number] = tool
diff --git a/gerber/gerber_statements.py b/gerber/gerber_statements.py
index 7322b3c..43596be 100644
--- a/gerber/gerber_statements.py
+++ b/gerber/gerber_statements.py
@@ -279,22 +279,36 @@ class ADParamStmt(ParamStmt):
return cls('AD', dcode, 'R', ([width, height],))
@classmethod
- def circle(cls, dcode, diameter, hole_diameter):
+ def circle(cls, dcode, diameter, hole_diameter=None, hole_width=None, hole_height=None):
'''Create a circular aperture definition statement'''
- if hole_diameter != None:
+ if hole_diameter is not None and hole_diameter > 0:
return cls('AD', dcode, 'C', ([diameter, hole_diameter],))
+ elif (hole_width is not None and hole_width > 0
+ and hole_height is not None and hole_height > 0):
+ return cls('AD', dcode, 'C', ([diameter, hole_width, hole_height],))
return cls('AD', dcode, 'C', ([diameter],))
@classmethod
- def obround(cls, dcode, width, height):
+ def obround(cls, dcode, width, height, hole_diameter=None, hole_width=None, hole_height=None):
'''Create an obround aperture definition statement'''
+ if hole_diameter is not None and hole_diameter > 0:
+ return cls('AD', dcode, 'O', ([width, height, hole_diameter],))
+ elif (hole_width is not None and hole_width > 0
+ and hole_height is not None and hole_height > 0):
+ return cls('AD', dcode, 'O', ([width, height, hole_width, hole_height],))
return cls('AD', dcode, 'O', ([width, height],))
@classmethod
- def polygon(cls, dcode, diameter, num_vertices, rotation, hole_diameter):
+ def polygon(cls, dcode, diameter, num_vertices, rotation, hole_diameter=None, hole_width=None, hole_height=None):
'''Create a polygon aperture definition statement'''
- return cls('AD', dcode, 'P', ([diameter, num_vertices, rotation, hole_diameter],))
+ if hole_diameter is not None and hole_diameter > 0:
+ return cls('AD', dcode, 'P', ([diameter, num_vertices, rotation, hole_diameter],))
+ elif (hole_width is not None and hole_width > 0
+ and hole_height is not None and hole_height > 0):
+ return cls('AD', dcode, 'P', ([diameter, num_vertices, rotation, hole_width, hole_height],))
+ return cls('AD', dcode, 'P', ([diameter, num_vertices, rotation],))
+
@classmethod
def macro(cls, dcode, name):
diff --git a/gerber/primitives.py b/gerber/primitives.py
index bd93e04..f583ca9 100644
--- a/gerber/primitives.py
+++ b/gerber/primitives.py
@@ -64,7 +64,6 @@ class Primitive(object):
@property
def flashed(self):
'''Is this a flashed primitive'''
-
raise NotImplementedError('Is flashed must be '
'implemented in subclass')
@@ -271,9 +270,9 @@ class Line(Primitive):
@property
def vertices(self):
if self._vertices is None:
+ start = self.start
+ end = self.end
if isinstance(self.aperture, Rectangle):
- start = self.start
- end = self.end
width = self.aperture.width
height = self.aperture.height
@@ -289,6 +288,11 @@ class Line(Primitive):
# The line is defined by the convex hull of the points
self._vertices = convex_hull((start_ll, start_lr, start_ul, start_ur, end_ll, end_lr, end_ul, end_ur))
+ elif isinstance(self.aperture, Polygon):
+ points = [map(add, point, vertex)
+ for vertex in self.aperture.vertices
+ for point in (start, end)]
+ self._vertices = convex_hull(points)
return self._vertices
def offset(self, x_offset=0, y_offset=0):
@@ -309,11 +313,18 @@ class Line(Primitive):
return nearly_equal(self.start, equiv_start) and nearly_equal(self.end, equiv_end)
+ def __str__(self):
+ return "<Line {} to {}>".format(self.start, self.end)
+
+ def __repr__(self):
+ return str(self)
+
class Arc(Primitive):
"""
"""
- def __init__(self, start, end, center, direction, aperture, quadrant_mode, **kwargs):
+ def __init__(self, start, end, center, direction, aperture, quadrant_mode,
+ **kwargs):
super(Arc, self).__init__(**kwargs)
self._start = start
self._end = end
@@ -371,15 +382,15 @@ class Arc(Primitive):
@property
def start_angle(self):
- dy, dx = tuple([start - center for start, center
+ dx, dy = tuple([start - center for start, center
in zip(self.start, self.center)])
- return math.atan2(dx, dy)
+ return math.atan2(dy, dx)
@property
def end_angle(self):
- dy, dx = tuple([end - center for end, center
+ dx, dy = tuple([end - center for end, center
in zip(self.end, self.center)])
- return math.atan2(dx, dy)
+ return math.atan2(dy, dx)
@property
def sweep_angle(self):
@@ -399,77 +410,98 @@ class Arc(Primitive):
theta0 = (self.start_angle + two_pi) % two_pi
theta1 = (self.end_angle + two_pi) % two_pi
points = [self.start, self.end]
+ if self.quadrant_mode == 'multi-quadrant':
+ if self.direction == 'counterclockwise':
+ # Passes through 0 degrees
+ if theta0 >= theta1:
+ points.append((self.center[0] + self.radius, self.center[1]))
+ # Passes through 90 degrees
+ if (((theta0 <= math.pi / 2.) and ((theta1 >= math.pi / 2.) or (theta1 <= theta0)))
+ or ((theta1 > math.pi / 2.) and (theta1 <= theta0))):
+ points.append((self.center[0], self.center[1] + self.radius))
+ # Passes through 180 degrees
+ if ((theta0 <= math.pi and (theta1 >= math.pi or theta1 <= theta0))
+ or ((theta1 > math.pi) and (theta1 <= theta0))):
+ points.append((self.center[0] - self.radius, self.center[1]))
+ # Passes through 270 degrees
+ if (theta0 <= math.pi * 1.5 and (theta1 >= math.pi * 1.5 or theta1 <= theta0)
+ or ((theta1 > math.pi * 1.5) and (theta1 <= theta0))):
+ points.append((self.center[0], self.center[1] - self.radius))
+ else:
+ # Passes through 0 degrees
+ if theta1 >= theta0:
+ points.append((self.center[0] + self.radius, self.center[1]))
+ # Passes through 90 degrees
+ if (((theta1 <= math.pi / 2.) and (theta0 >= math.pi / 2. or theta0 <= theta1))
+ or ((theta0 > math.pi / 2.) and (theta0 <= theta1))):
+ points.append((self.center[0], self.center[1] + self.radius))
+ # Passes through 180 degrees
+ if (((theta1 <= math.pi) and (theta0 >= math.pi or theta0 <= theta1))
+ or ((theta0 > math.pi) and (theta0 <= theta1))):
+ points.append((self.center[0] - self.radius, self.center[1]))
+ # Passes through 270 degrees
+ if (((theta1 <= math.pi * 1.5) and (theta0 >= math.pi * 1.5 or theta0 <= theta1))
+ or ((theta0 > math.pi * 1.5) and (theta0 <= theta1))):
+ points.append((self.center[0], self.center[1] - self.radius))
+ x, y = zip(*points)
+ if hasattr(self.aperture, 'radius'):
+ min_x = min(x) - self.aperture.radius
+ max_x = max(x) + self.aperture.radius
+ min_y = min(y) - self.aperture.radius
+ max_y = max(y) + self.aperture.radius
+ else:
+ min_x = min(x) - self.aperture.width
+ max_x = max(x) + self.aperture.width
+ min_y = min(y) - self.aperture.height
+ max_y = max(y) + self.aperture.height
+
+ self._bounding_box = ((min_x, max_x), (min_y, max_y))
+ return self._bounding_box
+
+ @property
+ def bounding_box_no_aperture(self):
+ '''Gets the bounding box without considering the aperture'''
+ two_pi = 2 * math.pi
+ theta0 = (self.start_angle + two_pi) % two_pi
+ theta1 = (self.end_angle + two_pi) % two_pi
+ points = [self.start, self.end]
+ if self.quadrant_mode == 'multi-quadrant':
if self.direction == 'counterclockwise':
# Passes through 0 degrees
- if theta0 > theta1:
+ if theta0 >= theta1:
points.append((self.center[0] + self.radius, self.center[1]))
# Passes through 90 degrees
- if theta0 <= math.pi / \
- 2. and (theta1 >= math.pi / 2. or theta1 < theta0):
+ if (((theta0 <= math.pi / 2.) and (
+ (theta1 >= math.pi / 2.) or (theta1 <= theta0)))
+ or ((theta1 > math.pi / 2.) and (theta1 <= theta0))):
points.append((self.center[0], self.center[1] + self.radius))
# Passes through 180 degrees
- if theta0 <= math.pi and (theta1 >= math.pi or theta1 < theta0):
+ if ((theta0 <= math.pi and (theta1 >= math.pi or theta1 <= theta0))
+ or ((theta1 > math.pi) and (theta1 <= theta0))):
points.append((self.center[0] - self.radius, self.center[1]))
# Passes through 270 degrees
- if theta0 <= math.pi * \
- 1.5 and (theta1 >= math.pi * 1.5 or theta1 < theta0):
+ if (theta0 <= math.pi * 1.5 and (
+ theta1 >= math.pi * 1.5 or theta1 <= theta0)
+ or ((theta1 > math.pi * 1.5) and (theta1 <= theta0))):
points.append((self.center[0], self.center[1] - self.radius))
else:
# Passes through 0 degrees
- if theta1 > theta0:
+ if theta1 >= theta0:
points.append((self.center[0] + self.radius, self.center[1]))
# Passes through 90 degrees
- if theta1 <= math.pi / \
- 2. and (theta0 >= math.pi / 2. or theta0 < theta1):
+ if (((theta1 <= math.pi / 2.) and (
+ theta0 >= math.pi / 2. or theta0 <= theta1))
+ or ((theta0 > math.pi / 2.) and (theta0 <= theta1))):
points.append((self.center[0], self.center[1] + self.radius))
# Passes through 180 degrees
- if theta1 <= math.pi and (theta0 >= math.pi or theta0 < theta1):
+ if (((theta1 <= math.pi) and (theta0 >= math.pi or theta0 <= theta1))
+ or ((theta0 > math.pi) and (theta0 <= theta1))):
points.append((self.center[0] - self.radius, self.center[1]))
# Passes through 270 degrees
- if theta1 <= math.pi * \
- 1.5 and (theta0 >= math.pi * 1.5 or theta0 < theta1):
+ if (((theta1 <= math.pi * 1.5) and (
+ theta0 >= math.pi * 1.5 or theta0 <= theta1))
+ or ((theta0 > math.pi * 1.5) and (theta0 <= theta1))):
points.append((self.center[0], self.center[1] - self.radius))
- x, y = zip(*points)
- min_x = min(x) - self.aperture.radius
- max_x = max(x) + self.aperture.radius
- min_y = min(y) - self.aperture.radius
- max_y = max(y) + self.aperture.radius
- self._bounding_box = ((min_x, max_x), (min_y, max_y))
- return self._bounding_box
-
- @property
- def bounding_box_no_aperture(self):
- '''Gets the bounding box without considering the aperture'''
- two_pi = 2 * math.pi
- theta0 = (self.start_angle + two_pi) % two_pi
- theta1 = (self.end_angle + two_pi) % two_pi
- points = [self.start, self.end]
- if self.direction == 'counterclockwise':
- # Passes through 0 degrees
- if theta0 > theta1:
- points.append((self.center[0] + self.radius, self.center[1]))
- # Passes through 90 degrees
- if theta0 <= math.pi / 2. and (theta1 >= math.pi / 2. or theta1 < theta0):
- points.append((self.center[0], self.center[1] + self.radius))
- # Passes through 180 degrees
- if theta0 <= math.pi and (theta1 >= math.pi or theta1 < theta0):
- points.append((self.center[0] - self.radius, self.center[1]))
- # Passes through 270 degrees
- if theta0 <= math.pi * 1.5 and (theta1 >= math.pi * 1.5 or theta1 < theta0):
- points.append((self.center[0], self.center[1] - self.radius ))
- else:
- # Passes through 0 degrees
- if theta1 > theta0:
- points.append((self.center[0] + self.radius, self.center[1]))
- # Passes through 90 degrees
- if theta1 <= math.pi / 2. and (theta0 >= math.pi / 2. or theta0 < theta1):
- points.append((self.center[0], self.center[1] + self.radius))
- # Passes through 180 degrees
- if theta1 <= math.pi and (theta0 >= math.pi or theta0 < theta1):
- points.append((self.center[0] - self.radius, self.center[1]))
- # Passes through 270 degrees
- if theta1 <= math.pi * 1.5 and (theta0 >= math.pi * 1.5 or theta0 < theta1):
- points.append((self.center[0], self.center[1] - self.radius ))
x, y = zip(*points)
min_x = min(x)
@@ -489,13 +521,16 @@ class Circle(Primitive):
"""
"""
- def __init__(self, position, diameter, hole_diameter = None, **kwargs):
+ def __init__(self, position, diameter, hole_diameter=None,
+ hole_width=0, hole_height=0, **kwargs):
super(Circle, self).__init__(**kwargs)
validate_coordinates(position)
self._position = position
self._diameter = diameter
self.hole_diameter = hole_diameter
- self._to_convert = ['position', 'diameter', 'hole_diameter']
+ self.hole_width = hole_width
+ self.hole_height = hole_height
+ self._to_convert = ['position', 'diameter', 'hole_diameter', 'hole_width', 'hole_height']
@property
def flashed(self):
@@ -631,14 +666,18 @@ class Rectangle(Primitive):
then you don't need to worry about rotation
"""
- def __init__(self, position, width, height, hole_diameter=0, **kwargs):
+ def __init__(self, position, width, height, hole_diameter=0,
+ hole_width=0, hole_height=0, **kwargs):
super(Rectangle, self).__init__(**kwargs)
validate_coordinates(position)
self._position = position
self._width = width
self._height = height
self.hole_diameter = hole_diameter
- self._to_convert = ['position', 'width', 'height', 'hole_diameter']
+ self.hole_width = hole_width
+ self.hole_height = hole_height
+ self._to_convert = ['position', 'width', 'height', 'hole_diameter',
+ 'hole_width', 'hole_height']
# TODO These are probably wrong when rotated
self._lower_left = None
self._upper_right = None
@@ -736,6 +775,12 @@ class Rectangle(Primitive):
return nearly_equal(self.position, equiv_position)
+ def __str__(self):
+ return "<Rectangle W {} H {} R {}>".format(self.width, self.height, self.rotation * 180/math.pi)
+
+ def __repr__(self):
+ return self.__str__()
+
class Diamond(Primitive):
"""
@@ -898,7 +943,8 @@ class ChamferRectangle(Primitive):
((self.position[0] - delta_w), (self.position[1] - delta_h)),
((self.position[0] + delta_w), (self.position[1] - delta_h))
]
- for idx, corner, chamfered in enumerate((rect_corners, self.corners)):
+ for idx, params in enumerate(zip(rect_corners, self.corners)):
+ corner, chamfered = params
x, y = corner
if chamfered:
if idx == 0:
@@ -1019,14 +1065,18 @@ class Obround(Primitive):
"""
"""
- def __init__(self, position, width, height, hole_diameter=0, **kwargs):
+ def __init__(self, position, width, height, hole_diameter=0,
+ hole_width=0,hole_height=0, **kwargs):
super(Obround, self).__init__(**kwargs)
validate_coordinates(position)
self._position = position
self._width = width
self._height = height
self.hole_diameter = hole_diameter
- self._to_convert = ['position', 'width', 'height', 'hole_diameter']
+ self.hole_width = hole_width
+ self.hole_height = hole_height
+ self._to_convert = ['position', 'width', 'height', 'hole_diameter',
+ 'hole_width', 'hole_height' ]
@property
def flashed(self):
@@ -1116,14 +1166,18 @@ class Polygon(Primitive):
"""
Polygon flash defined by a set number of sides.
"""
- def __init__(self, position, sides, radius, hole_diameter, **kwargs):
+ def __init__(self, position, sides, radius, hole_diameter=0,
+ hole_width=0, hole_height=0, **kwargs):
super(Polygon, self).__init__(**kwargs)
validate_coordinates(position)
self._position = position
self.sides = sides
self._radius = radius
self.hole_diameter = hole_diameter
- self._to_convert = ['position', 'radius', 'hole_diameter']
+ self.hole_width = hole_width
+ self.hole_height = hole_height
+ self._to_convert = ['position', 'radius', 'hole_diameter',
+ 'hole_width', 'hole_height']
@property
def flashed(self):
@@ -1174,25 +1228,14 @@ class Polygon(Primitive):
def vertices(self):
offset = self.rotation
- da = 360.0 / self.sides
+ delta_angle = 360.0 / self.sides
points = []
- for i in xrange(self.sides):
- points.append(rotate_point((self.position[0] + self.radius, self.position[1]), offset + da * i, self.position))
-
+ for i in range(self.sides):
+ points.append(
+ rotate_point((self.position[0] + self.radius, self.position[1]), offset + delta_angle * i, self.position))
return points
- @property
- def vertices(self):
- if self._vertices is None:
- theta = math.radians(360/self.sides)
- vertices = [(self.position[0] + (math.cos(theta * side) * self.radius),
- self.position[1] + (math.sin(theta * side) * self.radius))
- for side in range(self.sides)]
- self._vertices = [(((x * self._cos_theta) - (y * self._sin_theta)),
- ((x * self._sin_theta) + (y * self._cos_theta)))
- for x, y in vertices]
- return self._vertices
def equivalent(self, other, offset):
"""
@@ -1555,15 +1598,12 @@ class SquareRoundDonut(Primitive):
class Drill(Primitive):
""" A drill hole
"""
- def __init__(self, position, diameter, hit, **kwargs):
+ def __init__(self, position, diameter, **kwargs):
super(Drill, self).__init__('dark', **kwargs)
validate_coordinates(position)
self._position = position
self._diameter = diameter
- self.hit = hit
- self._to_convert = ['position', 'diameter', 'hit']
-
- # TODO Ths won't handle the hit updates correctly
+ self._to_convert = ['position', 'diameter']
@property
def flashed(self):
@@ -1606,23 +1646,21 @@ class Drill(Primitive):
self.position = tuple(map(add, self.position, (x_offset, y_offset)))
def __str__(self):
- return '<Drill %f (%f, %f) [%s]>' % (self.diameter, self.position[0], self.position[1], self.hit)
+ return '<Drill %f %s (%f, %f)>' % (self.diameter, self.units, self.position[0], self.position[1])
class Slot(Primitive):
""" A drilled slot
"""
- def __init__(self, start, end, diameter, hit, **kwargs):
+ def __init__(self, start, end, diameter, **kwargs):
super(Slot, self).__init__('dark', **kwargs)
validate_coordinates(start)
validate_coordinates(end)
self.start = start
self.end = end
self.diameter = diameter
- self.hit = hit
- self._to_convert = ['start', 'end', 'diameter', 'hit']
+ self._to_convert = ['start', 'end', 'diameter']
- # TODO this needs to use cached bounding box
@property
def flashed(self):
@@ -1630,8 +1668,8 @@ class Slot(Primitive):
def bounding_box(self):
if self._bounding_box is None:
- ll = tuple([c - self.outer_diameter / 2. for c in self.position])
- ur = tuple([c + self.outer_diameter / 2. for c in self.position])
+ ll = tuple([c - self.diameter / 2. for c in self.position])
+ ur = tuple([c + self.diameter / 2. for c in self.position])
self._bounding_box = ((ll[0], ur[0]), (ll[1], ur[1]))
return self._bounding_box
diff --git a/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py
index 31a1e77..a2baa47 100644
--- a/gerber/render/cairo_backend.py
+++ b/gerber/render/cairo_backend.py
@@ -1,4 +1,4 @@
-#! /usr/bin/env python
+#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be>
@@ -29,6 +29,7 @@ import os
from .render import GerberContext, RenderSettings
from .theme import THEMES
from ..primitives import *
+from ..utils import rotate_point
from io import BytesIO
@@ -67,16 +68,13 @@ class GerberCairoContext(GerberContext):
size_in_pixels = self.scale_point(size_in_inch)
self.origin_in_inch = origin_in_inch if self.origin_in_inch is None else self.origin_in_inch
self.size_in_inch = size_in_inch if self.size_in_inch is None else self.size_in_inch
+ self._xform_matrix = cairo.Matrix(xx=1.0, yy=-1.0,
+ x0=-self.origin_in_pixels[0],
+ y0=self.size_in_pixels[1])
if (self.surface is None) or new_surface:
self.surface_buffer = tempfile.NamedTemporaryFile()
self.surface = cairo.SVGSurface(self.surface_buffer, size_in_pixels[0], size_in_pixels[1])
self.output_ctx = cairo.Context(self.surface)
- self.output_ctx.scale(1, -1)
- self.output_ctx.translate(-(origin_in_inch[0] * self.scale[0]),
- (-origin_in_inch[1] * self.scale[0]) - size_in_pixels[1])
- self._xform_matrix = cairo.Matrix(xx=1.0, yy=-1.0,
- x0=-self.origin_in_pixels[0],
- y0=self.size_in_pixels[1] + self.origin_in_pixels[1])
def render_layer(self, layer, filename=None, settings=None, bgsettings=None,
verbose=False):
@@ -155,6 +153,23 @@ class GerberCairoContext(GerberContext):
self.surface_buffer.close()
self.surface_buffer = None
+ def _new_mask(self):
+ class Mask:
+ def __enter__(msk):
+ size_in_pixels = self.size_in_pixels
+ msk.surface = cairo.SVGSurface(None, size_in_pixels[0],
+ size_in_pixels[1])
+ msk.ctx = cairo.Context(msk.surface)
+ msk.ctx.translate(-self.origin_in_pixels[0], -self.origin_in_pixels[1])
+ return msk
+
+
+ def __exit__(msk, exc_type, exc_val, traceback):
+ if hasattr(msk.surface, 'finish'):
+ msk.surface.finish()
+
+ return Mask()
+
def _render_layer(self, layer, settings):
self.invert = settings.invert
# Get a new clean layer to render on
@@ -167,31 +182,36 @@ class GerberCairoContext(GerberContext):
def _render_line(self, line, color):
start = [pos * scale for pos, scale in zip(line.start, self.scale)]
end = [pos * scale for pos, scale in zip(line.end, self.scale)]
- self.ctx.set_operator(cairo.OPERATOR_SOURCE
- if line.level_polarity == 'dark' and
- (not self.invert) else cairo.OPERATOR_CLEAR)
- if isinstance(line.aperture, Circle):
- width = line.aperture.diameter
- self.ctx.set_line_width(width * self.scale[0])
- self.ctx.set_line_cap(cairo.LINE_CAP_ROUND)
- self.ctx.move_to(*start)
- self.ctx.line_to(*end)
- self.ctx.stroke()
- elif isinstance(line.aperture, Rectangle):
- points = [self.scale_point(x) for x in line.vertices]
- self.ctx.set_line_width(0)
- self.ctx.move_to(*points[0])
- for point in points[1:]:
- self.ctx.line_to(*point)
- self.ctx.fill()
+ self.ctx.set_operator(cairo.OPERATOR_OVER
+ if (not self.invert)
+ and line.level_polarity == 'dark'
+ else cairo.OPERATOR_CLEAR)
+ with self._new_mask() as mask:
+ if isinstance(line.aperture, Circle):
+ width = line.aperture.diameter
+ mask.ctx.set_line_width(width * self.scale[0])
+ mask.ctx.set_line_cap(cairo.LINE_CAP_ROUND)
+ mask.ctx.move_to(*start)
+ mask.ctx.line_to(*end)
+ mask.ctx.stroke()
+
+ elif hasattr(line, 'vertices') and line.vertices is not None:
+ points = [self.scale_point(x) for x in line.vertices]
+ mask.ctx.set_line_width(0)
+ mask.ctx.move_to(*points[-1])
+ for point in points:
+ mask.ctx.line_to(*point)
+ mask.ctx.fill()
+ self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0])
def _render_arc(self, arc, color):
center = self.scale_point(arc.center)
start = self.scale_point(arc.start)
end = self.scale_point(arc.end)
radius = self.scale[0] * arc.radius
- angle1 = arc.start_angle
- angle2 = arc.end_angle
+ two_pi = 2 * math.pi
+ angle1 = (arc.start_angle + two_pi) % two_pi
+ angle2 = (arc.end_angle + two_pi) % two_pi
if angle1 == angle2 and arc.quadrant_mode != 'single-quadrant':
# Make the angles slightly different otherwise Cario will draw nothing
angle2 -= 0.000000001
@@ -200,61 +220,111 @@ class GerberCairoContext(GerberContext):
else:
width = max(arc.aperture.width, arc.aperture.height, 0.001)
- self.ctx.set_operator(cairo.OPERATOR_SOURCE
- if arc.level_polarity == 'dark' and
- (not self.invert) else cairo.OPERATOR_CLEAR)
- self.ctx.set_line_width(width * self.scale[0])
- self.ctx.set_line_cap(cairo.LINE_CAP_ROUND)
- self.ctx.move_to(*start) # You actually have to do this...
- if arc.direction == 'counterclockwise':
- self.ctx.arc(*center, radius=radius, angle1=angle1, angle2=angle2)
- else:
- self.ctx.arc_negative(*center, radius=radius,
- angle1=angle1, angle2=angle2)
- self.ctx.move_to(*end) # ...lame
+ self.ctx.set_operator(cairo.OPERATOR_OVER
+ if (not self.invert)
+ and arc.level_polarity == 'dark'
+ else cairo.OPERATOR_CLEAR)
+
+ with self._new_mask() as mask:
+ mask.ctx.set_line_width(width * self.scale[0])
+ mask.ctx.set_line_cap(cairo.LINE_CAP_ROUND if isinstance(arc.aperture, Circle) else cairo.LINE_CAP_SQUARE)
+ mask.ctx.move_to(*start) # You actually have to do this...
+ if arc.direction == 'counterclockwise':
+ mask.ctx.arc(*center, radius=radius, angle1=angle1, angle2=angle2)
+ else:
+ mask.ctx.arc_negative(*center, radius=radius,
+ angle1=angle1, angle2=angle2)
+ mask.ctx.move_to(*end) # ...lame
+ mask.ctx.stroke()
+
+ #if isinstance(arc.aperture, Rectangle):
+ # print("Flash Rectangle Ends")
+ # print(arc.aperture.rotation * 180/math.pi)
+ # rect = arc.aperture
+ # width = self.scale[0] * rect.width
+ # height = self.scale[1] * rect.height
+ # for point, angle in zip((start, end), (angle1, angle2)):
+ # print("{} w {} h{}".format(point, rect.width, rect.height))
+ # mask.ctx.rectangle(point[0] - width/2.0,
+ # point[1] - height/2.0, width, height)
+ # mask.ctx.fill()
+
+ self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0])
+
def _render_region(self, region, color):
- self.ctx.set_operator(cairo.OPERATOR_SOURCE
- if region.level_polarity == 'dark' and
- (not self.invert) else cairo.OPERATOR_CLEAR)
- self.ctx.set_line_width(0)
- self.ctx.set_line_cap(cairo.LINE_CAP_ROUND)
- self.ctx.move_to(*self.scale_point(region.primitives[0].start))
- for prim in region.primitives:
- if isinstance(prim, Line):
- self.ctx.line_to(*self.scale_point(prim.end))
- else:
- center = self.scale_point(prim.center)
- radius = self.scale[0] * prim.radius
- angle1 = prim.start_angle
- angle2 = prim.end_angle
- if prim.direction == 'counterclockwise':
- self.ctx.arc(*center, radius=radius,
- angle1=angle1, angle2=angle2)
+ self.ctx.set_operator(cairo.OPERATOR_OVER
+ if (not self.invert) and region.level_polarity == 'dark'
+ else cairo.OPERATOR_CLEAR)
+ with self._new_mask() as mask:
+ mask.ctx.set_line_width(0)
+ mask.ctx.set_line_cap(cairo.LINE_CAP_ROUND)
+ mask.ctx.move_to(*self.scale_point(region.primitives[0].start))
+ for prim in region.primitives:
+ if isinstance(prim, Line):
+ mask.ctx.line_to(*self.scale_point(prim.end))
else:
- self.ctx.arc_negative(*center, radius=radius,
- angle1=angle1, angle2=angle2)
- self.ctx.fill()
+ center = self.scale_point(prim.center)
+ radius = self.scale[0] * prim.radius
+ angle1 = prim.start_angle
+ angle2 = prim.end_angle
+ if prim.direction == 'counterclockwise':
+ mask.ctx.arc(*center, radius=radius,
+ angle1=angle1, angle2=angle2)
+ else:
+ mask.ctx.arc_negative(*center, radius=radius,
+ angle1=angle1, angle2=angle2)
+ mask.ctx.fill()
+ self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0])
def _render_circle(self, circle, color):
center = self.scale_point(circle.position)
- self.ctx.set_operator(cairo.OPERATOR_SOURCE
- if circle.level_polarity == 'dark' and
- (not self.invert) else cairo.OPERATOR_CLEAR)
- self.ctx.set_line_width(0)
- self.ctx.arc(*center, radius=(circle.radius * self.scale[0]), angle1=0,
- angle2=(2 * math.pi))
- self.ctx.fill()
-
- if circle.hole_diameter > 0:
- # Render the center clear
-
- self.ctx.set_source_rgba(color[0], color[1], color[2], self.alpha)
- self.ctx.set_operator(cairo.OPERATOR_CLEAR)
- self.ctx.arc(center[0], center[1],
- radius=circle.hole_radius * self.scale[0], angle1=0,
- angle2=2 * math.pi)
- self.ctx.fill()
+ self.ctx.set_operator(cairo.OPERATOR_OVER
+ if (not self.invert)
+ and circle.level_polarity == 'dark'
+ else cairo.OPERATOR_CLEAR)
+
+ with self._new_mask() as mask:
+ mask.ctx.set_line_width(0)
+ mask.ctx.arc(center[0],
+ center[1],
+ radius=(circle.radius * self.scale[0]),
+ angle1=0,
+ angle2=(2 * math.pi))
+ mask.ctx.fill()
+
+ if hasattr(circle, 'hole_diameter') and circle.hole_diameter > 0:
+ mask.ctx.set_operator(cairo.OPERATOR_CLEAR)
+ mask.ctx.arc(center[0],
+ center[1],
+ radius=circle.hole_radius * self.scale[0],
+ angle1=0,
+ angle2=2 * math.pi)
+ mask.ctx.fill()
+
+ if (hasattr(circle, 'hole_width') and hasattr(circle, 'hole_height')
+ and circle.hole_width > 0 and circle.hole_height > 0):
+ mask.ctx.set_operator(cairo.OPERATOR_CLEAR
+ if circle.level_polarity == 'dark'
+ and (not self.invert)
+ else cairo.OPERATOR_OVER)
+ width, height = self.scale_point((circle.hole_width, circle.hole_height))
+ lower_left = rotate_point(
+ (center[0] - width / 2.0, center[1] - height / 2.0),
+ circle.rotation, center)
+ lower_right = rotate_point((center[0] + width / 2.0, center[1] - height / 2.0),
+ circle.rotation, center)
+ upper_left = rotate_point((center[0] - width / 2.0, center[1] + height / 2.0),
+ circle.rotation, center)
+ upper_right = rotate_point((center[0] + width / 2.0, center[1] + height / 2.0),
+ circle.rotation, center)
+ points = (lower_left, lower_right, upper_right, upper_left)
+ mask.ctx.move_to(*points[-1])
+ for point in points:
+ mask.ctx.line_to(*point)
+ mask.ctx.fill()
+
+ self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0])
def _render_rectangle(self, rectangle, color):
@@ -262,101 +332,156 @@ class GerberCairoContext(GerberContext):
width, height = tuple([abs(coord) for coord in
self.scale_point((rectangle.width,
rectangle.height))])
- self.ctx.set_operator(cairo.OPERATOR_SOURCE
- if rectangle.level_polarity == 'dark' and
- (not self.invert) else cairo.OPERATOR_CLEAR)
-
- if rectangle.rotation != 0:
- self.ctx.save()
-
- center = map(mul, rectangle.position, self.scale)
- matrix = cairo.Matrix()
- matrix.translate(center[0], center[1])
- # For drawing, we already handles the translation
- lower_left[0] = lower_left[0] - center[0]
- lower_left[1] = lower_left[1] - center[1]
- matrix.rotate(rectangle.rotation)
- self.ctx.transform(matrix)
-
- if rectangle.hole_diameter > 0:
- self.ctx.push_group()
-
- self.ctx.set_line_width(0)
- self.ctx.rectangle(*lower_left, width=width, height=height)
- self.ctx.fill()
-
- if rectangle.hole_diameter > 0:
- # Render the center clear
- self.ctx.set_source_rgba(color[0], color[1], color[2], self.alpha)
- self.ctx.set_operator(cairo.OPERATOR_CLEAR
- if rectangle.level_polarity == 'dark'
- and (not self.invert)
- else cairo.OPERATOR_SOURCE)
- center = map(mul, rectangle.position, self.scale)
- self.ctx.arc(center[0], center[1],
- radius=rectangle.hole_radius * self.scale[0], angle1=0,
- angle2=2 * math.pi)
- self.ctx.fill()
-
- if rectangle.rotation != 0:
- self.ctx.restore()
+ self.ctx.set_operator(cairo.OPERATOR_OVER
+ if (not self.invert)
+ and rectangle.level_polarity == 'dark'
+ else cairo.OPERATOR_CLEAR)
+ with self._new_mask() as mask:
+
+ mask.ctx.set_line_width(0)
+ mask.ctx.rectangle(*lower_left, width=width, height=height)
+ mask.ctx.fill()
+
+ center = self.scale_point(rectangle.position)
+ if rectangle.hole_diameter > 0:
+ # Render the center clear
+ mask.ctx.set_operator(cairo.OPERATOR_CLEAR
+ if rectangle.level_polarity == 'dark'
+ and (not self.invert)
+ else cairo.OPERATOR_OVER)
+
+ mask.ctx.arc(center[0], center[1],
+ radius=rectangle.hole_radius * self.scale[0], angle1=0,
+ angle2=2 * math.pi)
+ mask.ctx.fill()
+
+ if rectangle.hole_width > 0 and rectangle.hole_height > 0:
+ mask.ctx.set_operator(cairo.OPERATOR_CLEAR
+ if rectangle.level_polarity == 'dark'
+ and (not self.invert)
+ else cairo.OPERATOR_OVER)
+ width, height = self.scale_point((rectangle.hole_width, rectangle.hole_height))
+ lower_left = rotate_point((center[0] - width/2.0, center[1] - height/2.0), rectangle.rotation, center)
+ lower_right = rotate_point((center[0] + width/2.0, center[1] - height/2.0), rectangle.rotation, center)
+ upper_left = rotate_point((center[0] - width / 2.0, center[1] + height / 2.0), rectangle.rotation, center)
+ upper_right = rotate_point((center[0] + width / 2.0, center[1] + height / 2.0), rectangle.rotation, center)
+ points = (lower_left, lower_right, upper_right, upper_left)
+ mask.ctx.move_to(*points[-1])
+ for point in points:
+ mask.ctx.line_to(*point)
+ mask.ctx.fill()
+
+ self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0])
def _render_obround(self, obround, color):
- if obround.hole_diameter > 0:
- self.ctx.push_group()
-
- self._render_circle(obround.subshapes['circle1'], color)
- self._render_circle(obround.subshapes['circle2'], color)
- self._render_rectangle(obround.subshapes['rectangle'], color)
-
- if obround.hole_diameter > 0:
- # Render the center clear
- self.ctx.set_source_rgba(color[0], color[1], color[2], self.alpha)
- self.ctx.set_operator(cairo.OPERATOR_CLEAR)
- center = map(mul, obround.position, self.scale)
- self.ctx.arc(center[0], center[1],
- radius=obround.hole_radius * self.scale[0], angle1=0,
- angle2=2 * math.pi)
- self.ctx.fill()
-
- self.ctx.pop_group_to_source()
- self.ctx.paint_with_alpha(1)
-
+ self.ctx.set_operator(cairo.OPERATOR_OVER
+ if (not self.invert)
+ and obround.level_polarity == 'dark'
+ else cairo.OPERATOR_CLEAR)
+ with self._new_mask() as mask:
+ mask.ctx.set_line_width(0)
+
+ # Render circles
+ for circle in (obround.subshapes['circle1'], obround.subshapes['circle2']):
+ center = self.scale_point(circle.position)
+ mask.ctx.arc(center[0],
+ center[1],
+ radius=(circle.radius * self.scale[0]),
+ angle1=0,
+ angle2=(2 * math.pi))
+ mask.ctx.fill()
+
+ # Render Rectangle
+ rectangle = obround.subshapes['rectangle']
+ lower_left = self.scale_point(rectangle.lower_left)
+ width, height = tuple([abs(coord) for coord in
+ self.scale_point((rectangle.width,
+ rectangle.height))])
+ mask.ctx.rectangle(*lower_left, width=width, height=height)
+ mask.ctx.fill()
+
+ center = self.scale_point(obround.position)
+ if obround.hole_diameter > 0:
+ # Render the center clear
+ mask.ctx.set_operator(cairo.OPERATOR_CLEAR)
+ mask.ctx.arc(center[0], center[1],
+ radius=obround.hole_radius * self.scale[0], angle1=0,
+ angle2=2 * math.pi)
+ mask.ctx.fill()
+
+ if obround.hole_width > 0 and obround.hole_height > 0:
+ mask.ctx.set_operator(cairo.OPERATOR_CLEAR
+ if rectangle.level_polarity == 'dark'
+ and (not self.invert)
+ else cairo.OPERATOR_OVER)
+ width, height =self.scale_point((obround.hole_width, obround.hole_height))
+ lower_left = rotate_point((center[0] - width / 2.0, center[1] - height / 2.0),
+ obround.rotation, center)
+ lower_right = rotate_point((center[0] + width / 2.0, center[1] - height / 2.0),
+ obround.rotation, center)
+ upper_left = rotate_point((center[0] - width / 2.0, center[1] + height / 2.0),
+ obround.rotation, center)
+ upper_right = rotate_point((center[0] + width / 2.0, center[1] + height / 2.0),
+ obround.rotation, center)
+ points = (lower_left, lower_right, upper_right, upper_left)
+ mask.ctx.move_to(*points[-1])
+ for point in points:
+ mask.ctx.line_to(*point)
+ mask.ctx.fill()
+
+ self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0])
def _render_polygon(self, polygon, color):
-
- # TODO Ths does not handle rotation of a polygon
- self.ctx.set_operator(cairo.OPERATOR_SOURCE
- if polygon.level_polarity == 'dark' and
- (not self.invert) else cairo.OPERATOR_CLEAR)
- if polygon.hole_radius > 0:
- self.ctx.push_group()
-
- vertices = polygon.vertices
-
- self.ctx.set_line_width(0)
- self.ctx.set_line_cap(cairo.LINE_CAP_ROUND)
-
- # Start from before the end so it is easy to iterate and make sure it is closed
- self.ctx.move_to(*map(mul, vertices[-1], self.scale))
- for v in vertices:
- self.ctx.line_to(*map(mul, v, self.scale))
-
- self.ctx.fill()
-
- if polygon.hole_radius > 0:
- # Render the center clear
- center = tuple(map(mul, polygon.position, self.scale))
- self.ctx.set_source_rgba(color[0], color[1], color[2], self.alpha)
- self.ctx.set_operator(cairo.OPERATOR_CLEAR
- if polygon.level_polarity == 'dark'
- and (not self.invert)
- else cairo.OPERATOR_SOURCE)
- self.ctx.set_line_width(0)
- self.ctx.arc(center[0],
- center[1],
- polygon.hole_radius * self.scale[0], 0, 2 * math.pi)
- self.ctx.fill()
+ self.ctx.set_operator(cairo.OPERATOR_OVER
+ if (not self.invert)
+ and polygon.level_polarity == 'dark'
+ else cairo.OPERATOR_CLEAR)
+ with self._new_mask() as mask:
+
+ vertices = polygon.vertices
+ mask.ctx.set_line_width(0)
+ mask.ctx.set_line_cap(cairo.LINE_CAP_ROUND)
+ # Start from before the end so it is easy to iterate and make sure
+ # it is closed
+ mask.ctx.move_to(*self.scale_point(vertices[-1]))
+ for v in vertices:
+ mask.ctx.line_to(*self.scale_point(v))
+ mask.ctx.fill()
+
+ center = self.scale_point(polygon.position)
+ if polygon.hole_radius > 0:
+ # Render the center clear
+ mask.ctx.set_operator(cairo.OPERATOR_CLEAR
+ if polygon.level_polarity == 'dark'
+ and (not self.invert)
+ else cairo.OPERATOR_OVER)
+ mask.ctx.set_line_width(0)
+ mask.ctx.arc(center[0],
+ center[1],
+ polygon.hole_radius * self.scale[0], 0, 2 * math.pi)
+ mask.ctx.fill()
+
+ if polygon.hole_width > 0 and polygon.hole_height > 0:
+ mask.ctx.set_operator(cairo.OPERATOR_CLEAR
+ if polygon.level_polarity == 'dark'
+ and (not self.invert)
+ else cairo.OPERATOR_OVER)
+ width, height = self.scale_point((polygon.hole_width, polygon.hole_height))
+ lower_left = rotate_point((center[0] - width / 2.0, center[1] - height / 2.0),
+ polygon.rotation, center)
+ lower_right = rotate_point((center[0] + width / 2.0, center[1] - height / 2.0),
+ polygon.rotation, center)
+ upper_left = rotate_point((center[0] - width / 2.0, center[1] + height / 2.0),
+ polygon.rotation, center)
+ upper_right = rotate_point((center[0] + width / 2.0, center[1] + height / 2.0),
+ polygon.rotation, center)
+ points = (lower_left, lower_right, upper_right, upper_left)
+ mask.ctx.move_to(*points[-1])
+ for point in points:
+ mask.ctx.line_to(*point)
+ mask.ctx.fill()
+
+ self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0])
def _render_drill(self, circle, color=None):
color = color if color is not None else self.drill_color
@@ -368,22 +493,20 @@ class GerberCairoContext(GerberContext):
width = slot.diameter
- self.ctx.set_operator(cairo.OPERATOR_SOURCE
+ self.ctx.set_operator(cairo.OPERATOR_OVER
if slot.level_polarity == 'dark' and
(not self.invert) else cairo.OPERATOR_CLEAR)
-
- self.ctx.set_line_width(width * self.scale[0])
- self.ctx.set_line_cap(cairo.LINE_CAP_ROUND)
- self.ctx.move_to(*start)
- self.ctx.line_to(*end)
- self.ctx.stroke()
+ with self._new_mask() as mask:
+ mask.ctx.set_line_width(width * self.scale[0])
+ mask.ctx.set_line_cap(cairo.LINE_CAP_ROUND)
+ mask.ctx.move_to(*start)
+ mask.ctx.line_to(*end)
+ mask.ctx.stroke()
+ self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0])
def _render_amgroup(self, amgroup, color):
- self.ctx.push_group()
for primitive in amgroup.primitives:
self.render(primitive)
- self.ctx.pop_group_to_source()
- self.ctx.paint_with_alpha(1)
def _render_test_record(self, primitive, color):
position = [pos + origin for pos, origin in
@@ -392,7 +515,7 @@ class GerberCairoContext(GerberContext):
'monospace', cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_BOLD)
self.ctx.set_font_size(13)
self._render_circle(Circle(position, 0.015), color)
- self.ctx.set_operator(cairo.OPERATOR_SOURCE
+ self.ctx.set_operator(cairo.OPERATOR_OVER
if primitive.level_polarity == 'dark' and
(not self.invert) else cairo.OPERATOR_CLEAR)
self.ctx.move_to(*[self.scale[0] * (coord + 0.015) for coord in position])
@@ -405,26 +528,25 @@ class GerberCairoContext(GerberContext):
matrix = copy.copy(self._xform_matrix)
layer = cairo.SVGSurface(None, size_in_pixels[0], size_in_pixels[1])
ctx = cairo.Context(layer)
- ctx.scale(1, -1)
- ctx.translate(-(self.origin_in_inch[0] * self.scale[0]),
- (-self.origin_in_inch[1] * self.scale[0]) - size_in_pixels[1])
+
if self.invert:
+ ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0)
ctx.set_operator(cairo.OPERATOR_OVER)
ctx.paint()
if mirror:
matrix.xx = -1.0
matrix.x0 = self.origin_in_pixels[0] + self.size_in_pixels[0]
self.ctx = ctx
+ self.ctx.set_matrix(matrix)
self.active_layer = layer
self.active_matrix = matrix
+
def _flatten(self, color=None, alpha=None):
color = color if color is not None else self.color
alpha = alpha if alpha is not None else self.alpha
- ptn = cairo.SurfacePattern(self.active_layer)
- ptn.set_matrix(self.active_matrix)
self.output_ctx.set_source_rgba(*color, alpha=alpha)
- self.output_ctx.mask(ptn)
+ self.output_ctx.mask_surface(self.active_layer)
self.ctx = None
self.active_layer = None
self.active_matrix = None
diff --git a/gerber/render/rs274x_backend.py b/gerber/render/rs274x_backend.py
index 13e871c..d32602a 100644
--- a/gerber/render/rs274x_backend.py
+++ b/gerber/render/rs274x_backend.py
@@ -159,7 +159,7 @@ class Rs274xContext(GerberContext):
# Select the right aperture if not already selected
if aperture:
if isinstance(aperture, Circle):
- aper = self._get_circle(aperture.diameter, aperture.hole_diameter)
+ aper = self._get_circle(aperture.diameter, aperture.hole_diameter, aperture.hole_width, aperture.hole_height)
elif isinstance(aperture, Rectangle):
aper = self._get_rectangle(aperture.width, aperture.height)
elif isinstance(aperture, Obround):
@@ -283,10 +283,12 @@ class Rs274xContext(GerberContext):
self._pos = primitive.position
- def _get_circle(self, diameter, hole_diameter, dcode = None):
+ def _get_circle(self, diameter, hole_diameter=None, hole_width=None,
+ hole_height=None, dcode = None):
'''Define a circlar aperture'''
- aper = self._circles.get((diameter, hole_diameter), None)
+ key = (diameter, hole_diameter, hole_width, hole_height)
+ aper = self._circles.get(key, None)
if not aper:
if not dcode:
@@ -295,21 +297,22 @@ class Rs274xContext(GerberContext):
else:
self._next_dcode = max(dcode + 1, self._next_dcode)
- aper = ADParamStmt.circle(dcode, diameter, hole_diameter)
- self._circles[(diameter, hole_diameter)] = aper
+ aper = ADParamStmt.circle(dcode, diameter, hole_diameter, hole_width, hole_height)
+ self._circles[(diameter, hole_diameter, hole_width, hole_height)] = aper
self.header.append(aper)
return aper
def _render_circle(self, circle, color):
- aper = self._get_circle(circle.diameter, circle.hole_diameter)
+ aper = self._get_circle(circle.diameter, circle.hole_diameter, circle.hole_width, circle.hole_height)
self._render_flash(circle, aper)
- def _get_rectangle(self, width, height, dcode = None):
+ def _get_rectangle(self, width, height, hole_diameter=None, hole_width=None,
+ hole_height=None, dcode = None):
'''Get a rectanglar aperture. If it isn't defined, create it'''
- key = (width, height)
+ key = (width, height, hole_diameter, hole_width, hole_height)
aper = self._rects.get(key, None)
if not aper:
@@ -319,20 +322,23 @@ class Rs274xContext(GerberContext):
else:
self._next_dcode = max(dcode + 1, self._next_dcode)
- aper = ADParamStmt.rect(dcode, width, height)
- self._rects[(width, height)] = aper
+ aper = ADParamStmt.rect(dcode, width, height, hole_diameter, hole_width, hole_height)
+ self._rects[(width, height, hole_diameter, hole_width, hole_height)] = aper
self.header.append(aper)
return aper
def _render_rectangle(self, rectangle, color):
- aper = self._get_rectangle(rectangle.width, rectangle.height)
+ aper = self._get_rectangle(rectangle.width, rectangle.height,
+ rectangle.hole_diameter,
+ rectangle.hole_width, rectangle.hole_height)
self._render_flash(rectangle, aper)
- def _get_obround(self, width, height, dcode = None):
+ def _get_obround(self, width, height, hole_diameter=None, hole_width=None,
+ hole_height=None, dcode = None):
- key = (width, height)
+ key = (width, height, hole_diameter, hole_width, hole_height)
aper = self._obrounds.get(key, None)
if not aper:
@@ -342,7 +348,7 @@ class Rs274xContext(GerberContext):
else:
self._next_dcode = max(dcode + 1, self._next_dcode)
- aper = ADParamStmt.obround(dcode, width, height)
+ aper = ADParamStmt.obround(dcode, width, height, hole_diameter, hole_width, hole_height)
self._obrounds[key] = aper
self.header.append(aper)
@@ -350,17 +356,22 @@ class Rs274xContext(GerberContext):
def _render_obround(self, obround, color):
- aper = self._get_obround(obround.width, obround.height)
+ aper = self._get_obround(obround.width, obround.height,
+ obround.hole_diameter, obround.hole_width,
+ obround.hole_height)
self._render_flash(obround, aper)
def _render_polygon(self, polygon, color):
- aper = self._get_polygon(polygon.radius, polygon.sides, polygon.rotation, polygon.hole_radius)
+ aper = self._get_polygon(polygon.radius, polygon.sides,
+ polygon.rotation, polygon.hole_diameter,
+ polygon.hole_width, polygon.hole_height)
self._render_flash(polygon, aper)
- def _get_polygon(self, radius, num_vertices, rotation, hole_radius, dcode = None):
+ def _get_polygon(self, radius, num_vertices, rotation, hole_diameter=None,
+ hole_width=None, hole_height=None, dcode = None):
- key = (radius, num_vertices, rotation, hole_radius)
+ key = (radius, num_vertices, rotation, hole_diameter, hole_width, hole_height)
aper = self._polygons.get(key, None)
if not aper:
@@ -370,7 +381,9 @@ class Rs274xContext(GerberContext):
else:
self._next_dcode = max(dcode + 1, self._next_dcode)
- aper = ADParamStmt.polygon(dcode, radius * 2, num_vertices, rotation, hole_radius * 2)
+ aper = ADParamStmt.polygon(dcode, radius * 2, num_vertices,
+ rotation, hole_diameter, hole_width,
+ hole_height)
self._polygons[key] = aper
self.header.append(aper)
diff --git a/gerber/rs274x.py b/gerber/rs274x.py
index 5d64597..5191fb7 100644
--- a/gerber/rs274x.py
+++ b/gerber/rs274x.py
@@ -20,6 +20,7 @@
import copy
import json
+import os
import re
import sys
@@ -146,7 +147,7 @@ class GerberFile(CamFile):
return ((min_x, max_x), (min_y, max_y))
def write(self, filename, settings=None):
- """ Write data out to a gerber file
+ """ Write data out to a gerber file.
"""
with open(filename, 'w') as f:
for statement in self.statements:
@@ -193,6 +194,9 @@ class GerberParser(object):
AD_POLY = r"(?P<param>AD)D(?P<d>\d+)(?P<shape>P)[,](?P<modifiers>[^,%]*)"
AD_MACRO = r"(?P<param>AD)D(?P<d>\d+)(?P<shape>{name})[,]?(?P<modifiers>[^,%]*)".format(name=NAME)
AM = r"(?P<param>AM)(?P<name>{name})\*(?P<macro>[^%]*)".format(name=NAME)
+ # Include File
+ IF = r"(?P<param>IF)(?P<filename>.*)"
+
# begin deprecated
AS = r"(?P<param>AS)(?P<mode>(AXBY)|(AYBX))"
@@ -208,7 +212,7 @@ class GerberParser(object):
# end deprecated
PARAMS = (FS, MO, LP, AD_CIRCLE, AD_RECT, AD_OBROUND, AD_POLY,
- AD_MACRO, AM, AS, IN, IP, IR, MI, OF, SF, LN)
+ AD_MACRO, AM, AS, IF, IN, IP, IR, MI, OF, SF, LN)
PARAM_STMT = [re.compile(r"%?{0}\*%?".format(p)) for p in PARAMS]
@@ -230,7 +234,11 @@ class GerberParser(object):
REGION_MODE_STMT = re.compile(r'(?P<mode>G3[67])\*')
QUAD_MODE_STMT = re.compile(r'(?P<mode>G7[45])\*')
+ # Keep include loop from crashing us
+ INCLUDE_FILE_RECURSION_LIMIT = 10
+
def __init__(self):
+ self.filename = None
self.settings = FileSettings()
self.statements = []
self.primitives = []
@@ -248,13 +256,16 @@ class GerberParser(object):
self.region_mode = 'off'
self.quadrant_mode = 'multi-quadrant'
self.step_and_repeat = (1, 1, 0, 0)
+ self._recursion_depth = 0
def parse(self, filename):
+ self.filename = filename
with open(filename, "rU") as fp:
data = fp.read()
return self.parse_raw(data, filename)
def parse_raw(self, data, filename=None):
+ self.filename = filename
for stmt in self._parse(self._split_commands(data)):
self.evaluate(stmt)
self.statements.append(stmt)
@@ -371,6 +382,17 @@ class GerberParser(object):
yield stmt
elif param["param"] == "OF":
yield OFParamStmt.from_dict(param)
+ elif param["param"] == "IF":
+ # Don't crash on include loop
+ if self._recursion_depth < self.INCLUDE_FILE_RECURSION_LIMIT:
+ self._recursion_depth += 1
+ with open(os.path.join(os.path.dirname(self.filename), param["filename"]), 'r') as f:
+ inc_data = f.read()
+ for stmt in self._parse(self._split_commands(inc_data)):
+ yield stmt
+ self._recursion_depth -= 1
+ else:
+ raise IOError("Include file nesting depth limit exceeded.")
elif param["param"] == "IN":
yield INParamStmt.from_dict(param)
elif param["param"] == "LN":
@@ -492,32 +514,51 @@ class GerberParser(object):
if shape == 'C':
diameter = modifiers[0][0]
- if len(modifiers[0]) >= 2:
+ hole_diameter = 0
+ rectangular_hole = (0, 0)
+ if len(modifiers[0]) == 2:
hole_diameter = modifiers[0][1]
- else:
- hole_diameter = None
+ elif len(modifiers[0]) == 3:
+ rectangular_hole = modifiers[0][1:3]
+
+ aperture = Circle(position=None, diameter=diameter,
+ hole_diameter=hole_diameter,
+ hole_width=rectangular_hole[0],
+ hole_height=rectangular_hole[1],
+ units=self.settings.units)
- aperture = Circle(position=None, diameter=diameter, hole_diameter=hole_diameter, units=self.settings.units)
elif shape == 'R':
width = modifiers[0][0]
height = modifiers[0][1]
- if len(modifiers[0]) >= 3:
+ hole_diameter = 0
+ rectangular_hole = (0, 0)
+ if len(modifiers[0]) == 3:
hole_diameter = modifiers[0][2]
- else:
- hole_diameter = None
-
- aperture = Rectangle(position=None, width=width, height=height, hole_diameter=hole_diameter, units=self.settings.units)
+ elif len(modifiers[0]) == 4:
+ rectangular_hole = modifiers[0][2:4]
+
+ aperture = Rectangle(position=None, width=width, height=height,
+ hole_diameter=hole_diameter,
+ hole_width=rectangular_hole[0],
+ hole_height=rectangular_hole[1],
+ units=self.settings.units)
elif shape == 'O':
width = modifiers[0][0]
height = modifiers[0][1]
- if len(modifiers[0]) >= 3:
+ hole_diameter = 0
+ rectangular_hole = (0, 0)
+ if len(modifiers[0]) == 3:
hole_diameter = modifiers[0][2]
- else:
- hole_diameter = None
-
- aperture = Obround(position=None, width=width, height=height, hole_diameter=hole_diameter, units=self.settings.units)
+ elif len(modifiers[0]) == 4:
+ rectangular_hole = modifiers[0][2:4]
+
+ aperture = Obround(position=None, width=width, height=height,
+ hole_diameter=hole_diameter,
+ hole_width=rectangular_hole[0],
+ hole_height=rectangular_hole[1],
+ units=self.settings.units)
elif shape == 'P':
outer_diameter = modifiers[0][0]
number_vertices = int(modifiers[0][1])
@@ -526,11 +567,19 @@ class GerberParser(object):
else:
rotation = 0
- if len(modifiers[0]) > 3:
+ hole_diameter = 0
+ rectangular_hole = (0, 0)
+ if len(modifiers[0]) == 4:
hole_diameter = modifiers[0][3]
- else:
- hole_diameter = None
- aperture = Polygon(position=None, sides=number_vertices, radius=outer_diameter/2.0, hole_diameter=hole_diameter, rotation=rotation)
+ elif len(modifiers[0]) >= 5:
+ rectangular_hole = modifiers[0][3:5]
+
+ aperture = Polygon(position=None, sides=number_vertices,
+ radius=outer_diameter/2.0,
+ hole_diameter=hole_diameter,
+ hole_width=rectangular_hole[0],
+ hole_height=rectangular_hole[1],
+ rotation=rotation)
else:
aperture = self.macros[shape].build(modifiers)
@@ -641,13 +690,18 @@ class GerberParser(object):
quadrant_mode=self.quadrant_mode,
level_polarity=self.level_polarity,
units=self.settings.units))
+ # Gerbv seems to reset interpolation mode in regions..
+ # TODO: Make sure this is right.
+ self.interpolation = 'linear'
elif self.op == "D02" or self.op == "D2":
if self.region_mode == "on":
# D02 in the middle of a region finishes that region and starts a new one
if self.current_region and len(self.current_region) > 1:
- self.primitives.append(Region(self.current_region, level_polarity=self.level_polarity, units=self.settings.units))
+ self.primitives.append(Region(self.current_region,
+ level_polarity=self.level_polarity,
+ units=self.settings.units))
self.current_region = None
elif self.op == "D03" or self.op == "D3":
@@ -672,29 +726,53 @@ class GerberParser(object):
def _find_center(self, start, end, offsets):
"""
- In single quadrant mode, the offsets are always positive, which means there are 4 possible centers.
- The correct center is the only one that results in an arc with sweep angle of less than or equal to 90 degrees
+ In single quadrant mode, the offsets are always positive, which means
+ there are 4 possible centers. The correct center is the only one that
+ results in an arc with sweep angle of less than or equal to 90 degrees
+ in the specified direction
"""
-
+ two_pi = 2 * math.pi
if self.quadrant_mode == 'single-quadrant':
+ # The Gerber spec says single quadrant only has one possible center,
+ # and you can detect it based on the angle. But for real files, this
+ # seems to work better - there is usually only one option that makes
+ # sense for the center (since the distance should be the same
+ # from start and end). We select the center with the least error in
+ # radius from all the options with a valid sweep angle.
- # The Gerber spec says single quadrant only has one possible center, and you can detect
- # based on the angle. But for real files, this seems to work better - there is usually
- # only one option that makes sense for the center (since the distance should be the same
- # from start and end). Find the center that makes the most sense
sqdist_diff_min = sys.maxint
center = None
for factors in [(1, 1), (1, -1), (-1, 1), (-1, -1)]:
- test_center = (start[0] + offsets[0] * factors[0], start[1] + offsets[1] * factors[1])
+ test_center = (start[0] + offsets[0] * factors[0],
+ start[1] + offsets[1] * factors[1])
+
+ # Find angle from center to start and end points
+ start_angle = math.atan2(*reversed([_start - _center for _start, _center in zip(start, test_center)]))
+ end_angle = math.atan2(*reversed([_end - _center for _end, _center in zip(end, test_center)]))
+
+ # Clamp angles to 0, 2pi
+ theta0 = (start_angle + two_pi) % two_pi
+ theta1 = (end_angle + two_pi) % two_pi
+ # Determine sweep angle in the current arc direction
+ if self.direction == 'counterclockwise':
+ sweep_angle = abs(theta1 - theta0)
+ else:
+ theta0 += two_pi
+ sweep_angle = abs(theta0 - theta1) % two_pi
+
+ # Calculate the radius error
sqdist_start = sq_distance(start, test_center)
sqdist_end = sq_distance(end, test_center)
- if abs(sqdist_start - sqdist_end) < sqdist_diff_min:
+ # Take the option with the lowest radius error from the set of
+ # options with a valid sweep angle
+ if ((abs(sqdist_start - sqdist_end) < sqdist_diff_min)
+ and (sweep_angle >= 0)
+ and (sweep_angle <= math.pi / 2.0)):
center = test_center
sqdist_diff_min = abs(sqdist_start - sqdist_end)
-
return center
else:
return (start[0] + offsets[0], start[1] + offsets[1])
@@ -702,7 +780,6 @@ class GerberParser(object):
def _evaluate_aperture(self, stmt):
self.aperture = stmt.d
-
def _match_one(expr, data):
match = expr.match(data)
if match is None:
diff --git a/gerber/tests/test_excellon.py b/gerber/tests/test_excellon.py
index 1402938..6cddb60 100644
--- a/gerber/tests/test_excellon.py
+++ b/gerber/tests/test_excellon.py
@@ -6,6 +6,7 @@ import os
from ..cam import FileSettings
from ..excellon import read, detect_excellon_format, ExcellonFile, ExcellonParser
+from ..excellon import DrillHit, DrillSlot
from ..excellon_statements import ExcellonTool
from .tests import *
@@ -50,29 +51,28 @@ def test_read_settings():
assert_equal(ncdrill.settings['zeros'], 'trailing')
-def test_bounds():
+def test_bounding_box():
ncdrill = read(NCDRILL_FILE)
- xbound, ybound = ncdrill.bounds
+ xbound, ybound = ncdrill.bounding_box
assert_array_almost_equal(xbound, (0.1300, 2.1430))
assert_array_almost_equal(ybound, (0.3946, 1.7164))
def test_report():
ncdrill = read(NCDRILL_FILE)
-
+ rprt = ncdrill.report()
def test_conversion():
import copy
ncdrill = read(NCDRILL_FILE)
assert_equal(ncdrill.settings.units, 'inch')
ncdrill_inch = copy.deepcopy(ncdrill)
+
ncdrill.to_metric()
assert_equal(ncdrill.settings.units, 'metric')
- inch_primitives = ncdrill_inch.primitives
for tool in iter(ncdrill_inch.tools.values()):
tool.to_metric()
- for primitive in inch_primitives:
- primitive.to_metric()
+
for statement in ncdrill_inch.statements:
statement.to_metric()
@@ -80,7 +80,8 @@ def test_conversion():
iter(ncdrill_inch.tools.values())):
assert_equal(i_tool, m_tool)
- for m, i in zip(ncdrill.primitives, inch_primitives):
+ for m, i in zip(ncdrill.primitives, ncdrill_inch.primitives):
+
assert_equal(m.position, i.position, '%s not equal to %s' % (m, i))
assert_equal(m.diameter, i.diameter, '%s not equal to %s' % (m, i))
@@ -197,3 +198,142 @@ def test_parse_unknown():
p = ExcellonParser(FileSettings())
p._parse_line('Not A Valid Statement')
assert_equal(p.statements[0].stmt, 'Not A Valid Statement')
+
+def test_drill_hit_units_conversion():
+ """ Test unit conversion for drill hits
+ """
+ # Inch hit
+ settings = FileSettings(units='inch')
+ tool = ExcellonTool(settings, diameter=1.0)
+ hit = DrillHit(tool, (1.0, 1.0))
+
+ assert_equal(hit.tool.settings.units, 'inch')
+ assert_equal(hit.tool.diameter, 1.0)
+ assert_equal(hit.position, (1.0, 1.0))
+
+ # No Effect
+ hit.to_inch()
+
+ assert_equal(hit.tool.settings.units, 'inch')
+ assert_equal(hit.tool.diameter, 1.0)
+ assert_equal(hit.position, (1.0, 1.0))
+
+ # Should convert
+ hit.to_metric()
+
+ assert_equal(hit.tool.settings.units, 'metric')
+ assert_equal(hit.tool.diameter, 25.4)
+ assert_equal(hit.position, (25.4, 25.4))
+
+ # No Effect
+ hit.to_metric()
+
+ assert_equal(hit.tool.settings.units, 'metric')
+ assert_equal(hit.tool.diameter, 25.4)
+ assert_equal(hit.position, (25.4, 25.4))
+
+ # Convert back to inch
+ hit.to_inch()
+
+ assert_equal(hit.tool.settings.units, 'inch')
+ assert_equal(hit.tool.diameter, 1.0)
+ assert_equal(hit.position, (1.0, 1.0))
+
+def test_drill_hit_offset():
+ TEST_VECTORS = [
+ ((0.0 ,0.0), (0.0, 1.0), (0.0, 1.0)),
+ ((0.0, 0.0), (1.0, 1.0), (1.0, 1.0)),
+ ((1.0, 1.0), (0.0, -1.0), (1.0, 0.0)),
+ ((1.0, 1.0), (-1.0, -1.0), (0.0, 0.0)),
+
+ ]
+ for position, offset, expected in TEST_VECTORS:
+ settings = FileSettings(units='inch')
+ tool = ExcellonTool(settings, diameter=1.0)
+ hit = DrillHit(tool, position)
+
+ assert_equal(hit.position, position)
+
+ hit.offset(offset[0], offset[1])
+
+ assert_equal(hit.position, expected)
+
+
+def test_drill_slot_units_conversion():
+ """ Test unit conversion for drill hits
+ """
+ # Inch hit
+ settings = FileSettings(units='inch')
+ tool = ExcellonTool(settings, diameter=1.0)
+ hit = DrillSlot(tool, (1.0, 1.0), (10.0, 10.0), DrillSlot.TYPE_ROUT)
+
+ assert_equal(hit.tool.settings.units, 'inch')
+ assert_equal(hit.tool.diameter, 1.0)
+ assert_equal(hit.start, (1.0, 1.0))
+ assert_equal(hit.end, (10.0, 10.0))
+
+ # No Effect
+ hit.to_inch()
+
+ assert_equal(hit.tool.settings.units, 'inch')
+ assert_equal(hit.tool.diameter, 1.0)
+ assert_equal(hit.start, (1.0, 1.0))
+ assert_equal(hit.end, (10.0, 10.0))
+
+ # Should convert
+ hit.to_metric()
+
+ assert_equal(hit.tool.settings.units, 'metric')
+ assert_equal(hit.tool.diameter, 25.4)
+ assert_equal(hit.start, (25.4, 25.4))
+ assert_equal(hit.end, (254.0, 254.0))
+
+ # No Effect
+ hit.to_metric()
+
+ assert_equal(hit.tool.settings.units, 'metric')
+ assert_equal(hit.tool.diameter, 25.4)
+ assert_equal(hit.start, (25.4, 25.4))
+ assert_equal(hit.end, (254.0, 254.0))
+
+ # Convert back to inch
+ hit.to_inch()
+
+ assert_equal(hit.tool.settings.units, 'inch')
+ assert_equal(hit.tool.diameter, 1.0)
+ assert_equal(hit.start, (1.0, 1.0))
+ assert_equal(hit.end, (10.0, 10.0))
+
+def test_drill_slot_offset():
+ TEST_VECTORS = [
+ ((0.0 ,0.0), (1.0, 1.0), (0.0, 0.0), (0.0, 0.0), (1.0, 1.0)),
+ ((0.0, 0.0), (1.0, 1.0), (1.0, 0.0), (1.0, 0.0), (2.0, 1.0)),
+ ((0.0, 0.0), (1.0, 1.0), (1.0, 1.0), (1.0, 1.0), (2.0, 2.0)),
+ ((0.0, 0.0), (1.0, 1.0), (-1.0, 1.0), (-1.0, 1.0), (0.0, 2.0)),
+ ]
+ for start, end, offset, expected_start, expected_end in TEST_VECTORS:
+ settings = FileSettings(units='inch')
+ tool = ExcellonTool(settings, diameter=1.0)
+ slot = DrillSlot(tool, start, end, DrillSlot.TYPE_ROUT)
+
+ assert_equal(slot.start, start)
+ assert_equal(slot.end, end)
+
+ slot.offset(offset[0], offset[1])
+
+ assert_equal(slot.start, expected_start)
+ assert_equal(slot.end, expected_end)
+
+def test_drill_slot_bounds():
+ TEST_VECTORS = [
+ ((0.0, 0.0), (1.0, 1.0), 1.0, ((-0.5, 1.5), (-0.5, 1.5))),
+ ((0.0, 0.0), (1.0, 1.0), 0.5, ((-0.25, 1.25), (-0.25, 1.25))),
+ ]
+ for start, end, diameter, expected, in TEST_VECTORS:
+ settings = FileSettings(units='inch')
+ tool = ExcellonTool(settings, diameter=diameter)
+ slot = DrillSlot(tool, start, end, DrillSlot.TYPE_ROUT)
+
+ assert_equal(slot.bounding_box, expected)
+
+#def test_exce
diff --git a/gerber/tests/test_primitives.py b/gerber/tests/test_primitives.py
index 52d774c..2fe5a4b 100644
--- a/gerber/tests/test_primitives.py
+++ b/gerber/tests/test_primitives.py
@@ -192,16 +192,53 @@ def test_arc_sweep_angle():
def test_arc_bounds():
""" Test Arc primitive bounding box calculation
"""
- cases = [((1, 0), (0, 1), (0, 0), 'clockwise', ((-1.5, 1.5), (-1.5, 1.5))),
- ((1, 0), (0, 1), (0, 0), 'counterclockwise',
- ((-0.5, 1.5), (-0.5, 1.5))),
- # TODO: ADD MORE TEST CASES HERE
- ]
+ cases = [
+ ((1, 0), (0, 1), (0, 0), 'clockwise', ((-1.5, 1.5), (-1.5, 1.5))),
+ ((1, 0), (0, 1), (0, 0), 'counterclockwise',((-0.5, 1.5), (-0.5, 1.5))),
+
+ ((0, 1), (-1, 0), (0, 0), 'clockwise', ((-1.5, 1.5), (-1.5, 1.5))),
+ ((0, 1), (-1, 0), (0, 0), 'counterclockwise', ((-1.5, 0.5), (-0.5, 1.5))),
+
+ ((-1, 0), (0, -1), (0, 0), 'clockwise', ((-1.5, 1.5), (-1.5, 1.5))),
+ ((-1, 0), (0, -1), (0, 0), 'counterclockwise', ((-1.5, 0.5), (-1.5, 0.5))),
+
+ ((0, -1), (1, 0), (0, 0), 'clockwise', ((-1.5, 1.5), (-1.5, 1.5))),
+ ((0, -1), (1, 0), (0, 0), 'counterclockwise',((-0.5, 1.5), (-1.5, 0.5))),
+
+ # Arcs with the same start and end point render a full circle
+ ((1, 0), (1, 0), (0, 0), 'clockwise', ((-1.5, 1.5), (-1.5, 1.5))),
+ ((1, 0), (1, 0), (0, 0), 'counterclockwise', ((-1.5, 1.5), (-1.5, 1.5))),
+ ]
for start, end, center, direction, bounds in cases:
c = Circle((0,0), 1)
- a = Arc(start, end, center, direction, c, 'single-quadrant')
+ a = Arc(start, end, center, direction, c, 'multi-quadrant')
assert_equal(a.bounding_box, bounds)
+def test_arc_bounds_no_aperture():
+ """ Test Arc primitive bounding box calculation ignoring aperture
+ """
+ cases = [
+ ((1, 0), (0, 1), (0, 0), 'clockwise', ((-1.0, 1.0), (-1.0, 1.0))),
+ ((1, 0), (0, 1), (0, 0), 'counterclockwise',((0.0, 1.0), (0.0, 1.0))),
+
+ ((0, 1), (-1, 0), (0, 0), 'clockwise', ((-1.0, 1.0), (-1.0, 1.0))),
+ ((0, 1), (-1, 0), (0, 0), 'counterclockwise', ((-1.0, 0.0), (0.0, 1.0))),
+
+ ((-1, 0), (0, -1), (0, 0), 'clockwise', ((-1.0, 1.0), (-1.0, 1.0))),
+ ((-1, 0), (0, -1), (0, 0), 'counterclockwise', ((-1.0, 0.0), (-1.0, 0.0))),
+
+ ((0, -1), (1, 0), (0, 0), 'clockwise', ((-1.0, 1.0), (-1.0, 1.0))),
+ ((0, -1), (1, 0), (0, 0), 'counterclockwise',((-0.0, 1.0), (-1.0, 0.0))),
+
+ # Arcs with the same start and end point render a full circle
+ ((1, 0), (1, 0), (0, 0), 'clockwise', ((-1.0, 1.0), (-1.0, 1.0))),
+ ((1, 0), (1, 0), (0, 0), 'counterclockwise', ((-1.0, 1.0), (-1.0, 1.0))),
+ ]
+ for start, end, center, direction, bounds in cases:
+ c = Circle((0,0), 1)
+ a = Arc(start, end, center, direction, c, 'multi-quadrant')
+ assert_equal(a.bounding_box_no_aperture, bounds)
+
def test_arc_conversion():
c = Circle((0, 0), 25.4, units='metric')
@@ -438,6 +475,7 @@ def test_rectangle_ctor():
assert_equal(r.width, width)
assert_equal(r.height, height)
+
def test_rectangle_hole_radius():
""" Test rectangle hole diameter calculation
"""
@@ -448,7 +486,6 @@ def test_rectangle_hole_radius():
assert_equal(0.5, r.hole_radius)
-
def test_rectangle_bounds():
""" Test rectangle bounding box calculation
"""
@@ -461,6 +498,32 @@ def test_rectangle_bounds():
assert_array_almost_equal(xbounds, (-math.sqrt(2), math.sqrt(2)))
assert_array_almost_equal(ybounds, (-math.sqrt(2), math.sqrt(2)))
+def test_rectangle_vertices():
+ sqrt2 = math.sqrt(2.0)
+ TEST_VECTORS = [
+ ((0, 0), 2.0, 2.0, 0.0, ((-1.0, -1.0), (-1.0, 1.0), (1.0, 1.0), (1.0, -1.0))),
+ ((0, 0), 2.0, 3.0, 0.0, ((-1.0, -1.5), (-1.0, 1.5), (1.0, 1.5), (1.0, -1.5))),
+ ((0, 0), 2.0, 2.0, 90.0,((-1.0, -1.0), (-1.0, 1.0), (1.0, 1.0), (1.0, -1.0))),
+ ((0, 0), 3.0, 2.0, 90.0,((-1.0, -1.5), (-1.0, 1.5), (1.0, 1.5), (1.0, -1.5))),
+ ((0, 0), 2.0, 2.0, 45.0,((-sqrt2, 0.0), (0.0, sqrt2), (sqrt2, 0), (0, -sqrt2))),
+ ]
+ for pos, width, height, rotation, expected in TEST_VECTORS:
+ r = Rectangle(pos, width, height, rotation=rotation)
+ for test, expect in zip(sorted(r.vertices), sorted(expected)):
+ assert_array_almost_equal(test, expect)
+
+ r = Rectangle((0, 0), 2.0, 2.0, rotation=0.0)
+ r.rotation = 45.0
+ for test, expect in zip(sorted(r.vertices), sorted(((-sqrt2, 0.0), (0.0, sqrt2), (sqrt2, 0), (0, -sqrt2)))):
+ assert_array_almost_equal(test, expect)
+
+def test_rectangle_segments():
+
+ r = Rectangle((0, 0), 2.0, 2.0)
+ expected = [vtx for segment in r.segments for vtx in segment]
+ for vertex in r.vertices:
+ assert_in(vertex, expected)
+
def test_rectangle_conversion():
"""Test converting rectangles between units"""
@@ -697,6 +760,18 @@ def test_chamfer_rectangle_offset():
r.offset(0, 1)
assert_equal(r.position, (1., 1.))
+def test_chamfer_rectangle_vertices():
+ TEST_VECTORS = [
+ (1.0, (True, True, True, True), ((-2.5, -1.5), (-2.5, 1.5), (-1.5, 2.5), (1.5, 2.5), (2.5, 1.5), (2.5, -1.5), (1.5, -2.5), (-1.5, -2.5))),
+ (1.0, (True, False, False, False), ((-2.5, -2.5), (-2.5, 2.5), (1.5, 2.5), (2.5, 1.5), (2.5, -2.5))),
+ (1.0, (False, True, False, False), ((-2.5, -2.5), (-2.5, 1.5), (-1.5, 2.5), (2.5, 2.5), (2.5, -2.5))),
+ (1.0, (False, False, True, False), ((-2.5, -1.5), (-2.5, 2.5), (2.5, 2.5), (2.5, -2.5), (-1.5, -2.5))),
+ (1.0, (False, False, False, True), ((-2.5, -2.5), (-2.5, 2.5), (2.5, 2.5), (2.5, -1.5), (1.5, -2.5))),
+ ]
+ for chamfer, corners, expected in TEST_VECTORS:
+ r = ChamferRectangle((0, 0), 5, 5, chamfer, corners)
+ assert_equal(set(r.vertices), set(expected))
+
def test_round_rectangle_ctor():
""" Test round rectangle creation
@@ -1195,7 +1270,7 @@ def test_drill_ctor():
"""
test_cases = (((0, 0), 2), ((1, 1), 3), ((2, 2), 5))
for position, diameter in test_cases:
- d = Drill(position, diameter, None)
+ d = Drill(position, diameter)
assert_equal(d.position, position)
assert_equal(d.diameter, diameter)
assert_equal(d.radius, diameter / 2.)
@@ -1204,24 +1279,24 @@ def test_drill_ctor():
def test_drill_ctor_validation():
""" Test drill argument validation
"""
- assert_raises(TypeError, Drill, 3, 5, None)
- assert_raises(TypeError, Drill, (3,4,5), 5, None)
+ assert_raises(TypeError, Drill, 3, 5)
+ assert_raises(TypeError, Drill, (3,4,5), 5)
def test_drill_bounds():
- d = Drill((0, 0), 2, None)
+ d = Drill((0, 0), 2)
xbounds, ybounds = d.bounding_box
assert_array_almost_equal(xbounds, (-1, 1))
assert_array_almost_equal(ybounds, (-1, 1))
- d = Drill((1, 2), 2, None)
+ d = Drill((1, 2), 2)
xbounds, ybounds = d.bounding_box
assert_array_almost_equal(xbounds, (0, 2))
assert_array_almost_equal(ybounds, (1, 3))
def test_drill_conversion():
- d = Drill((2.54, 25.4), 254., None, units='metric')
+ d = Drill((2.54, 25.4), 254., units='metric')
#No effect
d.to_metric()
@@ -1237,7 +1312,7 @@ def test_drill_conversion():
assert_equal(d.position, (0.1, 1.0))
assert_equal(d.diameter, 10.0)
- d = Drill((0.1, 1.0), 10., None, units='inch')
+ d = Drill((0.1, 1.0), 10., units='inch')
# No effect
d.to_inch()
@@ -1255,7 +1330,7 @@ def test_drill_conversion():
def test_drill_offset():
- d = Drill((0, 0), 1., None)
+ d = Drill((0, 0), 1.)
d.offset(1, 0)
assert_equal(d.position, (1., 0.))
d.offset(0, 1)
@@ -1263,8 +1338,8 @@ def test_drill_offset():
def test_drill_equality():
- d = Drill((2.54, 25.4), 254., None)
- d1 = Drill((2.54, 25.4), 254., None)
+ d = Drill((2.54, 25.4), 254.)
+ d1 = Drill((2.54, 25.4), 254.)
assert_equal(d, d1)
- d1 = Drill((2.54, 25.4), 254.2, None)
+ d1 = Drill((2.54, 25.4), 254.2)
assert_not_equal(d, d1)
diff --git a/gerber/tests/test_rs274x_backend.py b/gerber/tests/test_rs274x_backend.py
index 89512f0..e128841 100644
--- a/gerber/tests/test_rs274x_backend.py
+++ b/gerber/tests/test_rs274x_backend.py
@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*-
# Author: Garret Fick <garret@ficksworkshop.com>
-import io
+
import os
from ..render.rs274x_backend import Rs274xContext
@@ -16,7 +16,7 @@ def test_render_two_boxes():
def _test_render_single_quadrant():
"""Umaco exapmle of a single quadrant arc"""
-
+
# TODO there is probably a bug here
_test_render('resources/example_single_quadrant.gbr', 'golden/example_single_quadrant.gbr')
@@ -25,17 +25,17 @@ def _test_render_simple_contour():
"""Umaco exapmle of a simple arrow-shaped contour"""
_test_render('resources/example_simple_contour.gbr', 'golden/example_simple_contour.gbr')
-
+
def _test_render_single_contour_1():
"""Umaco example of a single contour
-
+
The resulting image for this test is used by other tests because they must generate the same output."""
_test_render('resources/example_single_contour_1.gbr', 'golden/example_single_contour.gbr')
def _test_render_single_contour_2():
"""Umaco exapmle of a single contour, alternate contour end order
-
+
The resulting image for this test is used by other tests because they must generate the same output."""
_test_render('resources/example_single_contour_2.gbr', 'golden/example_single_contour.gbr')
@@ -43,12 +43,12 @@ def _test_render_single_contour_2():
def _test_render_single_contour_3():
"""Umaco exapmle of a single contour with extra line"""
_test_render('resources/example_single_contour_3.gbr', 'golden/example_single_contour_3.gbr')
-
-
+
+
def _test_render_not_overlapping_contour():
"""Umaco example of D02 staring a second contour"""
_test_render('resources/example_not_overlapping_contour.gbr', 'golden/example_not_overlapping_contour.gbr')
-
+
def _test_render_not_overlapping_touching():
"""Umaco example of D02 staring a second contour"""
@@ -67,7 +67,7 @@ def _test_render_overlapping_contour():
def _DISABLED_test_render_level_holes():
"""Umaco example of using multiple levels to create multiple holes"""
-
+
# TODO This is clearly rendering wrong. I'm temporarily checking this in because there are more
# rendering fixes in the related repository that may resolve these.
_test_render('resources/example_level_holes.gbr', 'golden/example_overlapping_contour.gbr')
@@ -96,7 +96,7 @@ def _test_render_cutin_multiple():
"""Umaco example of a region with multiple cutins"""
_test_render('resources/example_cutin_multiple.gbr', 'golden/example_cutin_multiple.gbr')
-
+
def _test_flash_circle():
"""Umaco example a simple circular flash with and without a hole"""
@@ -141,7 +141,7 @@ def _resolve_path(path):
def _test_render(gerber_path, png_expected_path, create_output_path = None):
"""Render the gerber file and compare to the expected PNG output.
-
+
Parameters
----------
gerber_path : string
@@ -150,14 +150,14 @@ def _test_render(gerber_path, png_expected_path, create_output_path = None):
Path to the PNG file to compare to
create_output : string|None
If not None, write the generated PNG to the specified path.
- This is primarily to help with
+ This is primarily to help with
"""
-
+
gerber_path = _resolve_path(gerber_path)
png_expected_path = _resolve_path(png_expected_path)
if create_output_path:
create_output_path = _resolve_path(create_output_path)
-
+
gerber = read(gerber_path)
# Create GBR output from the input file
@@ -165,7 +165,7 @@ def _test_render(gerber_path, png_expected_path, create_output_path = None):
gerber.render(ctx)
actual_contents = ctx.dump()
-
+
# If we want to write the file bytes, do it now. This happens
if create_output_path:
with open(create_output_path, 'wb') as out_file:
@@ -174,12 +174,12 @@ def _test_render(gerber_path, png_expected_path, create_output_path = None):
# So if we are creating the output, we make the test fail on purpose so you
# won't forget to disable this
assert_false(True, 'Test created the output %s. This needs to be disabled to make sure the test behaves correctly' % (create_output_path,))
-
+
# Read the expected PNG file
-
+
with open(png_expected_path, 'r') as expected_file:
expected_contents = expected_file.read()
-
+
assert_equal(expected_contents, actual_contents.getvalue())
-
+
return gerber
diff --git a/gerber/utils.py b/gerber/utils.py
index c62ad2a..06adfd7 100644
--- a/gerber/utils.py
+++ b/gerber/utils.py
@@ -25,9 +25,7 @@ files.
import os
from math import radians, sin, cos
-from operator import sub
-from copy import deepcopy
-from pyhull.convex_hull import ConvexHull
+from scipy.spatial import ConvexHull
MILLIMETERS_PER_INCH = 25.4
@@ -344,5 +342,4 @@ def listdir(directory, ignore_hidden=True, ignore_os=True):
def convex_hull(points):
vertices = ConvexHull(points).vertices
- return [points[idx] for idx in
- set([point for pair in vertices for point in pair])]
+ return [points[idx] for idx in vertices]