Finish Merge, most tests passing

This commit is contained in:
Hamilton Kibbe 2016-11-05 20:56:47 -04:00
parent 5af19af190
commit 724c2b3bce
10 changed files with 405 additions and 468 deletions

View file

@ -75,7 +75,7 @@ class AMPrimitive(object):
def to_metric(self):
raise NotImplementedError('Subclass must implement `to-metric`')
@property
def _level_polarity(self):
if self.exposure == 'off':
@ -190,9 +190,9 @@ class AMCirclePrimitive(AMPrimitive):
diameter = float(modifiers[2])
position = (float(modifiers[3]), float(modifiers[4]))
return cls(code, exposure, diameter, position)
@classmethod
def from_primitive(cls, primitive):
def from_primitive(cls, primitive):
return cls(1, 'on', primitive.diameter, primitive.position)
def __init__(self, code, exposure, diameter, position):
@ -262,11 +262,11 @@ class AMVectorLinePrimitive(AMPrimitive):
------
ValueError, TypeError
"""
@classmethod
def from_primitive(cls, primitive):
return cls(2, 'on', primitive.aperture.width, primitive.start, primitive.end, 0)
@classmethod
def from_gerber(cls, primitive):
modifiers = primitive.strip(' *').split(',')
@ -310,27 +310,27 @@ class AMVectorLinePrimitive(AMPrimitive):
endy=self.end[1],
rotation=self.rotation)
return fmtstr.format(**data)
def to_primitive(self, units):
"""
Convert this to a primitive. We use the Outline to represent this (instead of Line)
because the behaviour of the end caps is different for aperture macros compared to Lines
when rotated.
"""
# Use a line to generate our vertices easily
line = Line(self.start, self.end, Rectangle(None, self.width, self.width))
vertices = line.vertices
aperture = Circle((0, 0), 0)
lines = []
prev_point = rotate_point(vertices[-1], self.rotation, (0, 0))
for point in vertices:
cur_point = rotate_point(point, self.rotation, (0, 0))
lines.append(Line(prev_point, cur_point, aperture))
return Outline(lines, units=units, level_polarity=self._level_polarity)
@ -372,19 +372,19 @@ class AMOutlinePrimitive(AMPrimitive):
------
ValueError, TypeError
"""
@classmethod
def from_primitive(cls, primitive):
start_point = (round(primitive.primitives[0].start[0], 6), round(primitive.primitives[0].start[1], 6))
points = []
for prim in primitive.primitives:
points.append((round(prim.end[0], 6), round(prim.end[1], 6)))
rotation = 0.0
return cls(4, 'on', start_point, points, rotation)
@classmethod
def from_gerber(cls, primitive):
modifiers = primitive.strip(' *').split(",")
@ -434,25 +434,25 @@ class AMOutlinePrimitive(AMPrimitive):
)
# TODO I removed a closing asterix - not sure if this works for items with multiple statements
return "{code},{exposure},{n_points},{start_point},{points},\n{rotation}*".format(**data)
def to_primitive(self, units):
"""
Convert this to a drawable primitive. This uses the Outline instead of Line
primitive to handle differences in end caps when rotated.
"""
lines = []
prev_point = rotate_point(self.start_point, self.rotation)
for point in self.points:
cur_point = rotate_point(point, self.rotation)
lines.append(Line(prev_point, cur_point, Circle((0,0), 0)))
prev_point = cur_point
if lines[0].start != lines[-1].end:
raise ValueError('Outline must be closed')
return Outline(lines, units=units, level_polarity=self._level_polarity)
@ -495,11 +495,11 @@ class AMPolygonPrimitive(AMPrimitive):
------
ValueError, TypeError
"""
@classmethod
def from_primitive(cls, primitive):
return cls(5, 'on', primitive.sides, primitive.position, primitive.diameter, primitive.rotation)
@classmethod
def from_gerber(cls, primitive):
modifiers = primitive.strip(' *').split(",")
@ -548,7 +548,7 @@ class AMPolygonPrimitive(AMPrimitive):
)
fmt = "{code},{exposure},{vertices},{position},{diameter},{rotation}*"
return fmt.format(**data)
def to_primitive(self, units):
return Polygon(self.position, self.vertices, self.diameter / 2.0, 0, rotation=math.radians(self.rotation), units=units, level_polarity=self._level_polarity)
@ -663,7 +663,8 @@ class AMMoirePrimitive(AMPrimitive):
return fmt.format(**data)
def to_primitive(self, units):
raise NotImplementedError()
#raise NotImplementedError()
return None
class AMThermalPrimitive(AMPrimitive):
@ -750,70 +751,70 @@ class AMThermalPrimitive(AMPrimitive):
)
fmt = "{code},{position},{outer_diameter},{inner_diameter},{gap},{rotation}*"
return fmt.format(**data)
def _approximate_arc_cw(self, start_angle, end_angle, radius, center):
"""
Get an arc as a series of points
Parameters
----------
start_angle : The start angle in radians
end_angle : The end angle in radians
radius`: Radius of the arc
center : The center point of the arc (x, y) tuple
Returns
-------
array of point tuples
"""
# The total sweep
sweep_angle = end_angle - start_angle
num_steps = 10
angle_step = sweep_angle / num_steps
radius = radius
center = center
points = []
for i in range(num_steps + 1):
current_angle = start_angle + (angle_step * i)
nextx = (center[0] + math.cos(current_angle) * radius)
nexty = (center[1] + math.sin(current_angle) * radius)
points.append((nextx, nexty))
return points
def to_primitive(self, units):
# We start with calculating the top right section, then duplicate it
inner_radius = self.inner_diameter / 2.0
outer_radius = self.outer_diameter / 2.0
# Calculate the start angle relative to the horizontal axis
inner_offset_angle = asin(self.gap / 2.0 / inner_radius)
outer_offset_angle = asin(self.gap / 2.0 / outer_radius)
rotation_rad = math.radians(self.rotation)
inner_start_angle = inner_offset_angle + rotation_rad
inner_end_angle = math.pi / 2 - inner_offset_angle + rotation_rad
outer_start_angle = outer_offset_angle + rotation_rad
outer_end_angle = math.pi / 2 - outer_offset_angle + rotation_rad
outlines = []
aperture = Circle((0, 0), 0)
points = (self._approximate_arc_cw(inner_start_angle, inner_end_angle, inner_radius, self.position)
+ list(reversed(self._approximate_arc_cw(outer_start_angle, outer_end_angle, outer_radius, self.position))))
# Add in the last point since outlines should be closed
points.append(points[0])
# There are four outlines at rotated sections
for rotation in [0, 90.0, 180.0, 270.0]:
@ -821,11 +822,11 @@ class AMThermalPrimitive(AMPrimitive):
prev_point = rotate_point(points[0], rotation, self.position)
for point in points[1:]:
cur_point = rotate_point(point, rotation, self.position)
lines.append(Line(prev_point, cur_point, aperture))
prev_point = cur_point
outlines.append(Outline(lines, units=units, level_polarity=self._level_polarity))
return outlines
@ -869,7 +870,7 @@ class AMCenterLinePrimitive(AMPrimitive):
------
ValueError, TypeError
"""
@classmethod
def from_primitive(cls, primitive):
width = primitive.width
@ -922,27 +923,27 @@ class AMCenterLinePrimitive(AMPrimitive):
return fmt.format(**data)
def to_primitive(self, units):
x = self.center[0]
y = self.center[1]
half_width = self.width / 2.0
half_height = self.height / 2.0
points = []
points.append((x - half_width, y + half_height))
points.append((x - half_width, y - half_height))
points.append((x + half_width, y - half_height))
points.append((x + half_width, y + half_height))
aperture = Circle((0, 0), 0)
lines = []
prev_point = rotate_point(points[3], self.rotation, self.center)
for point in points:
cur_point = rotate_point(point, self.rotation, self.center)
lines.append(Line(prev_point, cur_point, aperture))
return Outline(lines, units=units, level_polarity=self._level_polarity)
@ -1057,4 +1058,4 @@ class AMUnsupportPrimitive(AMPrimitive):
return self.primitive
def to_primitive(self, units):
return None
return None

View file

@ -95,10 +95,10 @@ class ParamStmt(Statement):
class FSParamStmt(ParamStmt):
""" FS - Gerber Format Specification Statement
"""
@classmethod
def from_settings(cls, settings):
return cls('FS', settings.zero_suppression, settings.notation, settings.format)
@classmethod
@ -173,7 +173,7 @@ class FSParamStmt(ParamStmt):
class MOParamStmt(ParamStmt):
""" MO - Gerber Mode (measurement units) Statement.
"""
@classmethod
def from_units(cls, units):
return cls(None, units)
@ -235,7 +235,7 @@ class LPParamStmt(ParamStmt):
param = stmt_dict['param']
lp = 'clear' if stmt_dict.get('lp') == 'C' else 'dark'
return cls(param, lp)
@classmethod
def from_region(cls, region):
#todo what is the first param?
@ -272,34 +272,34 @@ class LPParamStmt(ParamStmt):
class ADParamStmt(ParamStmt):
""" AD - Gerber Aperture Definition Statement
"""
@classmethod
def rect(cls, dcode, width, height):
'''Create a rectangular aperture definition statement'''
return cls('AD', dcode, 'R', ([width, height],))
@classmethod
def circle(cls, dcode, diameter, hole_diameter):
'''Create a circular aperture definition statement'''
if hole_diameter != None:
return cls('AD', dcode, 'C', ([diameter, hole_diameter],))
return cls('AD', dcode, 'C', ([diameter],))
@classmethod
def obround(cls, dcode, width, height):
'''Create an obround aperture definition statement'''
return cls('AD', dcode, 'O', ([width, height],))
@classmethod
def polygon(cls, dcode, diameter, num_vertices, rotation, hole_diameter):
'''Create a polygon aperture definition statement'''
return cls('AD', dcode, 'P', ([diameter, num_vertices, rotation, hole_diameter],))
@classmethod
def macro(cls, dcode, name):
return cls('AD', dcode, name, '')
@classmethod
def from_dict(cls, stmt_dict):
param = stmt_dict.get('param')
@ -436,7 +436,7 @@ class AMParamStmt(ParamStmt):
AMThermalPrimitive.from_gerber(primitive))
else:
self.primitives.append(AMUnsupportPrimitive.from_gerber(primitive))
return AMGroup(self.primitives, stmt=self, units=self.units)
def to_inch(self):
@ -452,7 +452,7 @@ class AMParamStmt(ParamStmt):
primitive.to_metric()
def to_gerber(self, settings=None):
return '%AM{0}*{1}%'.format(self.name, "".join([primitive.to_gerber() for primitive in self.primitives]))
return '%AM{0}*{1}*%'.format(self.name, self.macro)
def __str__(self):
return '<Aperture Macro %s: %s>' % (self.name, self.macro)
@ -864,10 +864,10 @@ class CoordStmt(Statement):
""" Coordinate Data Block
"""
OP_DRAW = 'D01'
OP_DRAW = 'D01'
OP_MOVE = 'D02'
OP_FLASH = 'D03'
FUNC_LINEAR = 'G01'
FUNC_ARC_CW = 'G02'
FUNC_ARC_CCW = 'G03'
@ -894,26 +894,26 @@ class CoordStmt(Statement):
j = parse_gerber_value(stmt_dict.get('j'), settings.format,
settings.zero_suppression)
return cls(function, x, y, i, j, op, settings)
@classmethod
def move(cls, func, point):
if point:
return cls(func, point[0], point[1], None, None, CoordStmt.OP_MOVE, None)
# No point specified, so just write the function. This is normally for ending a region (D02*)
return cls(func, None, None, None, None, CoordStmt.OP_MOVE, None)
@classmethod
def line(cls, func, point):
return cls(func, point[0], point[1], None, None, CoordStmt.OP_DRAW, None)
@classmethod
def mode(cls, func):
return cls(func, None, None, None, None, None, None)
@classmethod
def arc(cls, func, point, center):
return cls(func, point[0], point[1], center[0], center[1], CoordStmt.OP_DRAW, None)
@classmethod
def flash(cls, point):
if point:
@ -1043,13 +1043,13 @@ class CoordStmt(Statement):
coord_str += 'Op: %s' % op
return '<Coordinate Statement: %s>' % coord_str
@property
def only_function(self):
"""
Returns if the statement only set the function.
"""
# TODO I would like to refactor this so that the function is handled separately and then
# TODO this isn't required
return self.function != None and self.op == None and self.x == None and self.y == None and self.i == None and self.j == None
@ -1104,11 +1104,11 @@ class EofStmt(Statement):
class QuadrantModeStmt(Statement):
@classmethod
def single(cls):
return cls('single-quadrant')
@classmethod
def multi(cls):
return cls('multi-quadrant')
@ -1140,11 +1140,11 @@ class RegionModeStmt(Statement):
if 'G36' not in line and 'G37' not in line:
raise ValueError('%s is not a valid region mode statement' % line)
return (cls('on') if line[:3] == 'G36' else cls('off'))
@classmethod
def on(cls):
return cls('on')
@classmethod
def off(cls):
return cls('off')

View file

@ -16,14 +16,14 @@
# limitations under the License.
import math
from operator import add
from itertools import combinations
from .utils import validate_coordinates, inch, metric, convex_hull, rotate_point, nearly_equal
from .utils import validate_coordinates, inch, metric, convex_hull, rotate_point, nearly_equal
class Primitive(object):
""" Base class for all Cam file primitives
@ -50,9 +50,9 @@ class Primitive(object):
def __init__(self, level_polarity='dark', rotation=0, units=None, net_name=None):
self.level_polarity = level_polarity
self.net_name = net_name
self._to_convert = list()
self.id = id
self.net_name = net_name
self._to_convert = list()
self.id = id
self._memoized = list()
self._units = units
self._rotation = rotation
@ -60,21 +60,21 @@ class Primitive(object):
self._sin_theta = math.sin(math.radians(rotation))
self._bounding_box = None
self._vertices = None
self._segments = None
self._segments = None
@property
def flashed(self):
'''Is this a flashed primitive'''
raise NotImplementedError('Is flashed must be '
'implemented in subclass')
'implemented in subclass')
def __eq__(self, other):
return self.__dict__ == other.__dict__
@property
def units(self):
return self._units
return self._units
@units.setter
def units(self, value):
@ -84,7 +84,7 @@ class Primitive(object):
@property
def rotation(self):
return self._rotation
@rotation.setter
def rotation(self, value):
self._changed()
@ -103,7 +103,7 @@ class Primitive(object):
self._segments = [segment for segment in
combinations(self.vertices, 2)]
return self._segments
@property
def bounding_box(self):
""" Calculate axis-aligned bounding box
@ -114,14 +114,14 @@ class Primitive(object):
"""
raise NotImplementedError('Bounding box calculation must be '
'implemented in subclass')
@property
def bounding_box_no_aperture(self):
""" Calculate bouxing box without considering the aperture
for most objects, this is the same as the bounding_box, but is different for
Lines and Arcs (which are not flashed)
Return ((min x, max x), (min y, max y))
"""
return self.bounding_box
@ -175,7 +175,7 @@ class Primitive(object):
except:
if value is not None:
setattr(self, attr, metric(value))
def offset(self, x_offset=0, y_offset=0):
""" Move the primitive by the specified x and y offset amount.
@ -186,7 +186,7 @@ class Primitive(object):
self.position = tuple([coord + offset for coord, offset
in zip(self.position,
(x_offset, y_offset))])
def to_statement(self):
pass
@ -201,7 +201,7 @@ class Primitive(object):
self._bounding_box = None
self._vertices = None
self._segments = None
for attr in self._memoized:
for attr in self._memoized:
setattr(self, attr, None)
class Line(Primitive):
@ -214,8 +214,8 @@ class Line(Primitive):
self._end = end
self.aperture = aperture
self._to_convert = ['start', 'end', 'aperture']
@property
@property
def flashed(self):
return False
@ -244,8 +244,8 @@ class Line(Primitive):
angle = math.atan2(delta_y, delta_x)
return angle
@property
def bounding_box(self):
@property
def bounding_box(self):
if self._bounding_box is None:
if isinstance(self.aperture, Circle):
width_2 = self.aperture.radius
@ -267,7 +267,7 @@ class Line(Primitive):
max_x = max(self.start[0], self.end[0])
min_y = min(self.start[1], self.end[1])
max_y = max(self.start[1], self.end[1])
return ((min_x, max_x), (min_y, max_y))
return ((min_x, max_x), (min_y, max_y))
@property
def vertices(self):
@ -291,30 +291,30 @@ 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))
return self._vertices
def offset(self, x_offset=0, y_offset=0):
self._changed()
self._changed()
self.start = tuple([coord + offset for coord, offset
in zip(self.start, (x_offset, y_offset))])
self.end = tuple([coord + offset for coord, offset
in zip(self.end, (x_offset, y_offset))])
def equivalent(self, other, offset):
if not isinstance(other, Line):
return False
equiv_start = tuple(map(add, other.start, offset))
equiv_end = tuple(map(add, other.end, offset))
equiv_end = tuple(map(add, other.end, offset))
return nearly_equal(self.start, equiv_start) and nearly_equal(self.end, equiv_end)
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
@ -324,10 +324,10 @@ class Arc(Primitive):
self._quadrant_mode = quadrant_mode
self._to_convert = ['start', 'end', 'center', 'aperture']
@property
@property
def flashed(self):
return False
@property
def start(self):
return self._start
@ -354,11 +354,11 @@ class Arc(Primitive):
def center(self, value):
self._changed()
self._center = value
@property
def quadrant_mode(self):
return self._quadrant_mode
@quadrant_mode.setter
def quadrant_mode(self, quadrant_mode):
self._changed()
@ -436,8 +436,8 @@ class Arc(Primitive):
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
return self._bounding_box
@property
def bounding_box_no_aperture(self):
'''Gets the bounding box without considering the aperture'''
@ -472,12 +472,12 @@ class Arc(Primitive):
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)
max_x = max(x)
min_y = min(y)
max_y = max(y)
return ((min_x, max_x), (min_y, max_y))
return ((min_x, max_x), (min_y, max_y))
def offset(self, x_offset=0, y_offset=0):
self._changed()
@ -489,19 +489,19 @@ class Arc(Primitive):
class Circle(Primitive):
"""
"""
def __init__(self, position, diameter, hole_diameter = None, **kwargs):
def __init__(self, position, diameter, hole_diameter = None, **kwargs):
super(Circle, self).__init__(**kwargs)
validate_coordinates(position)
self._position = position
self._diameter = diameter
self._diameter = diameter
self.hole_diameter = hole_diameter
self._to_convert = ['position', 'diameter', 'hole_diameter']
self._to_convert = ['position', 'diameter', 'hole_diameter']
@property
@property
def flashed(self):
return True
@property
def position(self):
return self._position
@ -523,7 +523,7 @@ class Circle(Primitive):
@property
def radius(self):
return self.diameter / 2.
@property
def hole_radius(self):
if self.hole_diameter != None:
@ -538,23 +538,23 @@ class Circle(Primitive):
min_y = self.position[1] - self.radius
max_y = self.position[1] + self.radius
self._bounding_box = ((min_x, max_x), (min_y, max_y))
return self._bounding_box
return self._bounding_box
def offset(self, x_offset=0, y_offset=0):
self.position = tuple(map(add, self.position, (x_offset, y_offset)))
def equivalent(self, other, offset):
'''Is this the same as the other circle, ignoring the offiset?'''
if not isinstance(other, Circle):
return False
if self.diameter != other.diameter or self.hole_diameter != other.hole_diameter:
return False
equiv_position = tuple(map(add, other.position, offset))
return nearly_equal(self.position, equiv_position)
return nearly_equal(self.position, equiv_position)
class Ellipse(Primitive):
@ -568,19 +568,19 @@ class Ellipse(Primitive):
self._width = width
self._height = height
self._to_convert = ['position', 'width', 'height']
@property
@property
def flashed(self):
return True
@property
def position(self):
return self._position
@position.setter
def position(self, value):
self._changed()
self._position = value
self._position = value
@property
def width(self):
@ -626,29 +626,29 @@ class Ellipse(Primitive):
class Rectangle(Primitive):
"""
"""
When rotated, the rotation is about the center point.
Only aperture macro generated Rectangle objects can be rotated. If you aren't in a AMGroup,
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, **kwargs):
super(Rectangle, self).__init__(**kwargs)
validate_coordinates(position)
self._position = position
self._width = width
self._height = height
self._height = height
self.hole_diameter = hole_diameter
self._to_convert = ['position', 'width', 'height', 'hole_diameter']
# TODO These are probably wrong when rotated
self._lower_left = None
self._upper_right = None
@property
@property
def flashed(self):
return True
@property
def position(self):
return self._position
@ -658,14 +658,14 @@ class Rectangle(Primitive):
self._changed()
self._position = value
@property
@property
def width(self):
return self._width
@width.setter
def width(self, value):
self._changed()
self._width = value
self._width = value
@property
def height(self):
@ -675,7 +675,7 @@ class Rectangle(Primitive):
def height(self, value):
self._changed()
self._height = value
@property
def hole_radius(self):
"""The radius of the hole. If there is no hole, returns None"""
@ -683,12 +683,12 @@ class Rectangle(Primitive):
return self.hole_diameter / 2.
return None
@property
@property
def upper_right(self):
return (self.position[0] + (self._abs_width / 2.),
self.position[1] + (self._abs_height / 2.))
@property
@property
def lower_left(self):
return (self.position[0] - (self.axis_aligned_width / 2.),
self.position[1] - (self.axis_aligned_height / 2.))
@ -721,27 +721,27 @@ class Rectangle(Primitive):
def axis_aligned_width(self):
return (self._cos_theta * self.width + self._sin_theta * self.height)
@property
@property
def _abs_height(self):
return (math.cos(math.radians(self.rotation)) * self.height +
math.sin(math.radians(self.rotation)) * self.width)
@property
@property
def axis_aligned_height(self):
return (self._cos_theta * self.height + self._sin_theta * self.width)
def equivalent(self, other, offset):
"""Is this the same as the other rect, ignoring the offset?"""
if not isinstance(other, Rectangle):
return False
if self.width != other.width or self.height != other.height or self.rotation != other.rotation or self.hole_diameter != other.hole_diameter:
return False
equiv_position = tuple(map(add, other.position, offset))
return nearly_equal(self.position, equiv_position)
return nearly_equal(self.position, equiv_position)
class Diamond(Primitive):
@ -755,8 +755,8 @@ class Diamond(Primitive):
self._width = width
self._height = height
self._to_convert = ['position', 'width', 'height']
@property
@property
def flashed(self):
return True
@ -767,7 +767,7 @@ class Diamond(Primitive):
@position.setter
def position(self, value):
self._changed()
self._position = value
self._position = value
@property
def width(self):
@ -778,7 +778,7 @@ class Diamond(Primitive):
self._changed()
self._width = value
@property
@property
def height(self):
return self._height
@ -833,8 +833,8 @@ class ChamferRectangle(Primitive):
self._chamfer = chamfer
self._corners = corners
self._to_convert = ['position', 'width', 'height', 'chamfer']
@property
@property
def flashed(self):
return True
@ -922,8 +922,8 @@ class RoundRectangle(Primitive):
self._radius = radius
self._corners = corners
self._to_convert = ['position', 'width', 'height', 'radius']
@property
@property
def flashed(self):
return True
@ -952,7 +952,7 @@ class RoundRectangle(Primitive):
@height.setter
def height(self, value):
self._changed()
self._height = value
self._height = value
@property
def radius(self):
@ -987,28 +987,28 @@ class RoundRectangle(Primitive):
return (self._cos_theta * self.width +
self._sin_theta * self.height)
@property
@property
def axis_aligned_height(self):
return (self._cos_theta * self.height +
self._sin_theta * self.width)
class Obround(Primitive):
"""
"""
def __init__(self, position, width, height, hole_diameter=0, **kwargs):
"""
def __init__(self, position, width, height, hole_diameter=0, **kwargs):
super(Obround, self).__init__(**kwargs)
validate_coordinates(position)
self._position = position
self._width = width
self._height = height
self._height = height
self.hole_diameter = hole_diameter
self._to_convert = ['position', 'width', 'height', 'hole_diameter']
@property
@property
def flashed(self):
return True
return True
@property
def position(self):
@ -1017,7 +1017,7 @@ class Obround(Primitive):
@position.setter
def position(self, value):
self._changed()
self._position = value
self._position = value
@property
def width(self):
@ -1028,7 +1028,7 @@ class Obround(Primitive):
self._changed()
self._width = value
@property
@property
def upper_right(self):
return (self.position[0] + (self._abs_width / 2.),
self.position[1] + (self._abs_height / 2.))
@ -1047,8 +1047,8 @@ class Obround(Primitive):
"""The radius of the hole. If there is no hole, returns None"""
if self.hole_diameter != None:
return self.hole_diameter / 2.
return None
return None
@property
def orientation(self):
@ -1096,31 +1096,31 @@ class Obround(Primitive):
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, **kwargs):
super(Polygon, self).__init__(**kwargs)
validate_coordinates(position)
self._position = position
self.sides = sides
self.sides = sides
self._radius = radius
self.hole_diameter = hole_diameter
self._to_convert = ['position', 'radius', 'hole_diameter']
@property
@property
def flashed(self):
return True
@property
def diameter(self):
return self.radius * 2
@property
def hole_radius(self):
if self.hole_diameter != None:
return self.hole_diameter / 2.
return None
return None
@property
def position(self):
@ -1129,7 +1129,7 @@ class Polygon(Primitive):
@position.setter
def position(self, value):
self._changed()
self._position = value
self._position = value
@property
def radius(self):
@ -1149,22 +1149,22 @@ class Polygon(Primitive):
max_y = self.position[1] + self.radius
self._bounding_box = ((min_x, max_x), (min_y, max_y))
return self._bounding_box
def offset(self, x_offset=0, y_offset=0):
self.position = tuple(map(add, self.position, (x_offset, y_offset)))
@property
def vertices(self):
offset = self.rotation
da = 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))
return points
@property
def vertices(self):
if self._vertices is None:
@ -1175,17 +1175,17 @@ class Polygon(Primitive):
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
return self._vertices
def equivalent(self, other, offset):
"""
Is this the outline the same as the other, ignoring the position offset?
"""
# Quick check if it even makes sense to compare them
if type(self) != type(other) or self.sides != other.sides or self.radius != other.radius:
return False
equiv_pos = tuple(map(add, other.position, offset))
return nearly_equal(self.position, equiv_pos)
@ -1193,14 +1193,14 @@ class Polygon(Primitive):
class AMGroup(Primitive):
"""
"""
"""
def __init__(self, amprimitives, stmt = None, **kwargs):
"""
stmt : The original statment that generated this, since it is really hard to re-generate from primitives
"""
super(AMGroup, self).__init__(**kwargs)
self.primitives = []
for amprim in amprimitives:
prim = amprim.to_primitive(self.units)
@ -1212,11 +1212,11 @@ class AMGroup(Primitive):
self._position = None
self._to_convert = ['_position', 'primitives']
self.stmt = stmt
def to_inch(self):
if self.units == 'metric':
super(AMGroup, self).to_inch()
# If we also have a stmt, convert that too
if self.stmt:
self.stmt.to_inch()
@ -1225,15 +1225,15 @@ class AMGroup(Primitive):
def to_metric(self):
if self.units == 'inch':
super(AMGroup, self).to_metric()
# If we also have a stmt, convert that too
if self.stmt:
self.stmt.to_metric()
@property
def flashed(self):
return True
@property
def bounding_box(self):
# TODO Make this cached like other items
@ -1245,49 +1245,49 @@ class AMGroup(Primitive):
min_y = min(miny)
max_y = max(maxy)
return ((min_x, max_x), (min_y, max_y))
@property
def position(self):
return self._position
def offset(self, x_offset=0, y_offset=0):
self._position = tuple(map(add, self._position, (x_offset, y_offset)))
for primitive in self.primitives:
primitive.offset(x_offset, y_offset)
@position.setter
def position(self, new_pos):
'''
Sets the position of the AMGroup.
This offset all of the objects by the specified distance.
'''
if self._position:
dx = new_pos[0] - self._position[0]
dy = new_pos[1] - self._position[1]
else:
dx = new_pos[0]
dy = new_pos[1]
for primitive in self.primitives:
primitive.offset(dx, dy)
self._position = new_pos
def equivalent(self, other, offset):
'''
Is this the macro group the same as the other, ignoring the position offset?
'''
if len(self.primitives) != len(other.primitives):
return False
# We know they have the same number of primitives, so now check them all
for i in range(0, len(self.primitives)):
if not self.primitives[i].equivalent(other.primitives[i], offset):
return False
# If we didn't find any differences, then they are the same
return True
@ -1296,16 +1296,16 @@ class Outline(Primitive):
Outlines only exist as the rendering for a apeture macro outline.
They don't exist outside of AMGroup objects
"""
def __init__(self, primitives, **kwargs):
super(Outline, self).__init__(**kwargs)
self.primitives = primitives
self._to_convert = ['primitives']
if self.primitives[0].start != self.primitives[-1].end:
raise ValueError('Outline must be closed')
@property
@property
def flashed(self):
return True
@ -1326,7 +1326,7 @@ class Outline(Primitive):
self._changed()
for p in self.primitives:
p.offset(x_offset, y_offset)
@property
def vertices(self):
if self._vertices is None:
@ -1337,7 +1337,7 @@ class Outline(Primitive):
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
return self._vertices
@property
def width(self):
@ -1348,15 +1348,15 @@ class Outline(Primitive):
'''
Is this the outline the same as the other, ignoring the position offset?
'''
# Quick check if it even makes sense to compare them
if type(self) != type(other) or len(self.primitives) != len(other.primitives):
return False
for i in range(0, len(self.primitives)):
if not self.primitives[i].equivalent(other.primitives[i], offset):
return False
return True
class Region(Primitive):
@ -1367,13 +1367,13 @@ class Region(Primitive):
super(Region, self).__init__(**kwargs)
self.primitives = primitives
self._to_convert = ['primitives']
@property
@property
def flashed(self):
return False
@property
def bounding_box(self):
def bounding_box(self):
if self._bounding_box is None:
xlims, ylims = zip(*[p.bounding_box_no_aperture for p in self.primitives])
minx, maxx = zip(*xlims)
@ -1383,7 +1383,7 @@ class Region(Primitive):
min_y = min(miny)
max_y = max(maxy)
self._bounding_box = ((min_x, max_x), (min_y, max_y))
return self._bounding_box
return self._bounding_box
def offset(self, x_offset=0, y_offset=0):
self._changed()
@ -1401,10 +1401,10 @@ class RoundButterfly(Primitive):
self.position = position
self.diameter = diameter
self._to_convert = ['position', 'diameter']
# TODO This does not reset bounding box correctly
@property
@property
def flashed(self):
return True
@ -1433,13 +1433,13 @@ class SquareButterfly(Primitive):
self.position = position
self.side = side
self._to_convert = ['position', 'side']
# TODO This does not reset bounding box correctly
@property
@property
def flashed(self):
return True
return True
@property
def bounding_box(self):
if self._bounding_box is None:
@ -1475,14 +1475,14 @@ class Donut(Primitive):
else:
# Hexagon
self.width = 0.5 * math.sqrt(3.) * outer_diameter
self.height = outer_diameter
self.height = outer_diameter
self._to_convert = ['position', 'width',
'height', 'inner_diameter', 'outer_diameter']
# TODO This does not reset bounding box correctly
@property
@property
def flashed(self):
return True
@ -1494,7 +1494,7 @@ class Donut(Primitive):
@property
def upper_right(self):
return (self.position[0] + (self.width / 2.),
self.position[1] + (self.height / 2.)
self.position[1] + (self.height / 2.))
@property
def bounding_box(self):
@ -1521,11 +1521,11 @@ class SquareRoundDonut(Primitive):
self.inner_diameter = inner_diameter
self.outer_diameter = outer_diameter
self._to_convert = ['position', 'inner_diameter', 'outer_diameter']
@property
@property
def flashed(self):
return True
@property
def bounding_box(self):
if self._bounding_box is None:
@ -1537,7 +1537,7 @@ class SquareRoundDonut(Primitive):
class Drill(Primitive):
""" A drill hole
"""
"""
def __init__(self, position, diameter, hit, **kwargs):
super(Drill, self).__init__('dark', **kwargs)
validate_coordinates(position)
@ -1545,14 +1545,14 @@ class Drill(Primitive):
self._diameter = diameter
self.hit = hit
self._to_convert = ['position', 'diameter', 'hit']
# TODO Ths won't handle the hit updates correctly
@property
def flashed(self):
return False
@property
# TODO Ths won't handle the hit updates correctly
@property
def flashed(self):
return False
@property
def position(self):
return self._position
@ -1583,15 +1583,15 @@ class Drill(Primitive):
max_y = self.position[1] + self.radius
self._bounding_box = ((min_x, max_x), (min_y, max_y))
return self._bounding_box
def offset(self, x_offset=0, y_offset=0):
self._changed()
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)
class Slot(Primitive):
""" A drilled slot
"""
@ -1604,13 +1604,13 @@ class Slot(Primitive):
self.diameter = diameter
self.hit = hit
self._to_convert = ['start', 'end', 'diameter', 'hit']
# TODO this needs to use cached bounding box
@property
@property
def flashed(self):
return False
def bounding_box(self):
if self._bounding_box is None:
ll = tuple([c - self.outer_diameter / 2. for c in self.position])
@ -1621,7 +1621,7 @@ class Slot(Primitive):
def offset(self, x_offset=0, y_offset=0):
self.start = tuple(map(add, self.start, (x_offset, y_offset)))
self.end = tuple(map(add, self.end, (x_offset, y_offset)))
class TestRecord(Primitive):
""" Netlist Test record

View file

@ -12,15 +12,15 @@
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# See the License for the specific language governing permissions and
# limitations under the License.
try:
import cairo
except ImportError:
import cairocffi as cairo
import math
from operator import mul, div
import tempfile
@ -139,35 +139,20 @@ class GerberCairoContext(GerberContext):
start = [pos * scale for pos, scale in zip(line.start, self.scale)]
end = [pos * scale for pos, scale in zip(line.end, self.scale)]
if not self.invert:
<<<<<<< HEAD
self.ctx.set_source_rgba(color[0], color[1], color[2], alpha=self.alpha)
self.ctx.set_operator(cairo.OPERATOR_OVER
if line.level_polarity == "dark"
else cairo.OPERATOR_CLEAR)
=======
self.ctx.set_source_rgba(*color, alpha=self.alpha)
self.ctx.set_operator(cairo.OPERATOR_OVER
if line.level_polarity == 'dark'
else cairo.OPERATOR_CLEAR)
>>>>>>> 5476da8... Fix a bunch of rendering bugs.
else cairo.OPERATOR_CLEAR)
else:
self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0)
self.ctx.set_operator(cairo.OPERATOR_CLEAR)
if isinstance(line.aperture, Circle):
<<<<<<< HEAD
width = line.aperture.diameter
=======
width = line.aperture.diameter
>>>>>>> 5476da8... Fix a bunch of rendering bugs.
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)
<<<<<<< HEAD
self.ctx.stroke()
=======
self.ctx.stroke()
>>>>>>> 5476da8... Fix a bunch of rendering bugs.
elif isinstance(line.aperture, Rectangle):
points = [self.scale_point(x) for x in line.vertices]
self.ctx.set_line_width(0)
@ -190,9 +175,8 @@ class GerberCairoContext(GerberContext):
width = arc.aperture.diameter if arc.aperture.diameter != 0 else 0.001
else:
width = max(arc.aperture.width, arc.aperture.height, 0.001)
if not self.invert:
<<<<<<< HEAD
self.ctx.set_source_rgba(color[0], color[1], color[2], alpha=self.alpha)
self.ctx.set_operator(cairo.OPERATOR_OVER
if arc.level_polarity == "dark"\
@ -200,51 +184,26 @@ class GerberCairoContext(GerberContext):
else:
self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0)
self.ctx.set_operator(cairo.OPERATOR_CLEAR)
=======
self.ctx.set_source_rgba(*color, alpha=self.alpha)
self.ctx.set_operator(cairo.OPERATOR_OVER
if arc.level_polarity == 'dark'
else cairo.OPERATOR_CLEAR)
else:
self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0)
self.ctx.set_operator(cairo.OPERATOR_CLEAR)
>>>>>>> 5476da8... Fix a bunch of rendering bugs.
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':
<<<<<<< HEAD
self.ctx.arc(center[0], center[1], radius, angle1, angle2)
else:
self.ctx.arc_negative(center[0], center[1], radius, angle1, angle2)
=======
self.ctx.arc(*center, radius=radius, angle1=angle1, angle2=angle2)
else:
self.ctx.arc_negative(*center, radius=radius,
angle1=angle1, angle2=angle2)
>>>>>>> 5476da8... Fix a bunch of rendering bugs.
self.ctx.move_to(*end) # ...lame
def _render_region(self, region, color):
if not self.invert:
<<<<<<< HEAD
self.ctx.set_source_rgba(color[0], color[1], color[2], alpha=self.alpha)
self.ctx.set_operator(cairo.OPERATOR_OVER
if region.level_polarity == "dark"
=======
self.ctx.set_source_rgba(*color, alpha=self.alpha)
self.ctx.set_operator(cairo.OPERATOR_OVER
if region.level_polarity == 'dark'
>>>>>>> 5476da8... Fix a bunch of rendering bugs.
else cairo.OPERATOR_CLEAR)
else:
self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0)
self.ctx.set_operator(cairo.OPERATOR_CLEAR)
<<<<<<< HEAD
=======
>>>>>>> 5476da8... Fix a bunch of rendering bugs.
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))
@ -262,8 +221,9 @@ class GerberCairoContext(GerberContext):
else:
self.ctx.arc_negative(*center, radius=radius,
angle1=angle1, angle2=angle2)
<<<<<<< HEAD
self.ctx.fill()
self.ctx.fill()
def _render_circle(self, circle, color):
center = self.scale_point(circle.position)
if not self.invert:
@ -274,47 +234,30 @@ class GerberCairoContext(GerberContext):
else:
self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0)
self.ctx.set_operator(cairo.OPERATOR_CLEAR)
if circle.hole_diameter > 0:
self.ctx.push_group()
self.ctx.set_line_width(0)
self.ctx.arc(center[0], center[1], 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.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.pop_group_to_source()
self.ctx.paint_with_alpha(1)
=======
self.ctx.fill()
def _render_circle(self, circle, color):
center = self.scale_point(circle.position)
if not self.invert:
self.ctx.set_source_rgba(*color, alpha=self.alpha)
self.ctx.set_operator(
cairo.OPERATOR_OVER if circle.level_polarity == 'dark' else cairo.OPERATOR_CLEAR)
else:
self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0)
self.ctx.set_operator(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()
>>>>>>> 5476da8... Fix a bunch of rendering bugs.
self.ctx.pop_group_to_source()
self.ctx.paint_with_alpha(1)
def _render_rectangle(self, rectangle, color):
lower_left = self.scale_point(rectangle.lower_left)
width, height = tuple([abs(coord) for coord in self.scale_point((rectangle.width, rectangle.height))])
<<<<<<< HEAD
if not self.invert:
self.ctx.set_source_rgba(color[0], color[1], color[2], alpha=self.alpha)
self.ctx.set_operator(cairo.OPERATOR_OVER
@ -323,10 +266,10 @@ class GerberCairoContext(GerberContext):
else:
self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0)
self.ctx.set_operator(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])
@ -335,14 +278,14 @@ class GerberCairoContext(GerberContext):
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[0], lower_left[1], width, 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)
@ -350,42 +293,30 @@ class GerberCairoContext(GerberContext):
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()
self.ctx.pop_group_to_source()
self.ctx.paint_with_alpha(1)
if rectangle.rotation != 0:
self.ctx.restore()
=======
if not self.invert:
self.ctx.set_source_rgba(*color, alpha=self.alpha)
self.ctx.set_operator(
cairo.OPERATOR_OVER if rectangle.level_polarity == 'dark' else cairo.OPERATOR_CLEAR)
else:
self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0)
self.ctx.set_operator(cairo.OPERATOR_CLEAR)
self.ctx.set_line_width(0)
self.ctx.rectangle(*lower_left, width=width, height=height)
self.ctx.fill()
>>>>>>> 5476da8... Fix a bunch of rendering bugs.
if rectangle.rotation != 0:
self.ctx.restore()
def _render_obround(self, obround, color):
if not self.invert:
self.ctx.set_source_rgba(color[0], color[1], color[2], alpha=self.alpha)
self.ctx.set_operator(cairo.OPERATOR_OVER if obround.level_polarity == "dark" else cairo.OPERATOR_CLEAR)
else:
self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0)
self.ctx.set_operator(cairo.OPERATOR_CLEAR)
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)
@ -393,12 +324,12 @@ class GerberCairoContext(GerberContext):
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)
def _render_polygon(self, polygon, color):
# TODO Ths does not handle rotation of a polygon
if not self.invert:
self.ctx.set_source_rgba(color[0], color[1], color[2], alpha=self.alpha)
@ -406,44 +337,44 @@ class GerberCairoContext(GerberContext):
else:
self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0)
self.ctx.set_operator(cairo.OPERATOR_CLEAR)
if polygon.hole_radius > 0:
self.ctx.push_group()
vertices = polygon.vertices
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)
self.ctx.set_operator(cairo.OPERATOR_CLEAR)
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.pop_group_to_source()
self.ctx.paint_with_alpha(1)
def _render_drill(self, circle, color=None):
color = color if color is not None else self.drill_color
self._render_circle(circle, color)
def _render_slot(self, slot, color):
start = map(mul, slot.start, self.scale)
end = map(mul, slot.end, self.scale)
width = slot.diameter
if not self.invert:
self.ctx.set_source_rgba(color[0], color[1], color[2], alpha=self.alpha)
self.ctx.set_operator(cairo.OPERATOR_OVER if slot.level_polarity == "dark" else cairo.OPERATOR_CLEAR)
@ -456,7 +387,7 @@ class GerberCairoContext(GerberContext):
self.ctx.move_to(*start)
self.ctx.line_to(*end)
self.ctx.stroke()
def _render_amgroup(self, amgroup, color):
self.ctx.push_group()
for primitive in amgroup.primitives:
@ -478,7 +409,7 @@ class GerberCairoContext(GerberContext):
for coord in position])
self.ctx.scale(1, -1)
self.ctx.show_text(primitive.net_name)
self.ctx.scale(1, -1)
self.ctx.scale(1, -1)
def _new_render_layer(self, color=None):
size_in_pixels = self.scale_point(self.size_in_inch)
@ -498,11 +429,7 @@ class GerberCairoContext(GerberContext):
def _flatten(self):
self.output_ctx.set_operator(cairo.OPERATOR_OVER)
<<<<<<< HEAD
ptn = cairo.SurfacePattern(self.active_layer)
=======
ptn = cairo.SurfacePattern(self.active_layer)
>>>>>>> 5476da8... Fix a bunch of rendering bugs.
ptn.set_matrix(self._xform_matrix)
self.output_ctx.set_source(ptn)
self.output_ctx.paint()
@ -510,19 +437,11 @@ class GerberCairoContext(GerberContext):
self.active_layer = None
def _paint_background(self, force=False):
if (not self.bg) or force:
if (not self.bg) or force:
self.bg = True
self.output_ctx.set_operator(cairo.OPERATOR_OVER)
<<<<<<< HEAD
self.output_ctx.set_source_rgba(self.background_color[0], self.background_color[1], self.background_color[2], alpha=1.0)
self.output_ctx.paint()
def scale_point(self, point):
return tuple([coord * scale for coord, scale in zip(point, self.scale)])
=======
self.output_ctx.set_source_rgba(*self.background_color, alpha=1.0)
self.output_ctx.paint()
def scale_point(self, point):
return tuple([coord * scale for coord, scale in zip(point, self.scale)])
>>>>>>> 5476da8... Fix a bunch of rendering bugs.

View file

@ -98,7 +98,7 @@ class GerberFile(CamFile):
def __init__(self, statements, settings, primitives, apertures, filename=None):
super(GerberFile, self).__init__(statements, settings, primitives, filename)
self.apertures = apertures
@property
@ -115,15 +115,18 @@ class GerberFile(CamFile):
def bounds(self):
min_x = min_y = 1000000
max_x = max_y = -1000000
for stmt in [stmt for stmt in self.statements if isinstance(stmt, CoordStmt)]:
if stmt.x is not None:
min_x = min(stmt.x, min_x)
max_x = max(stmt.x, max_x)
if stmt.y is not None:
min_y = min(stmt.y, min_y)
max_y = max(stmt.y, max_y)
return ((min_x, max_x), (min_y, max_y))
@property
def bounding_box(self):
min_x = min_y = 1000000
@ -258,7 +261,7 @@ class GerberParser(object):
stmt.units = self.settings.units
return GerberFile(self.statements, self.settings, self.primitives, self.apertures.values(), filename)
def _split_commands(self, data):
"""
Split the data into commands. Commands end with * (and also newline to help with some badly formatted files)
@ -267,24 +270,24 @@ class GerberParser(object):
length = len(data)
start = 0
in_header = True
for cur in range(0, length):
val = data[cur]
if val == '%' and start == cur:
in_header = True
continue
if val == '\r' or val == '\n':
if start != cur:
yield data[start:cur]
start = cur + 1
elif not in_header and val == '*':
yield data[start:cur + 1]
start = cur + 1
elif in_header and val == '%':
yield data[start:cur + 1]
start = cur + 1
@ -318,13 +321,13 @@ class GerberParser(object):
did_something = True # make sure we do at least one loop
while did_something and len(line) > 0:
did_something = False
# consume empty data blocks
if line[0] == '*':
line = line[1:]
did_something = True
continue
# coord
(coord, r) = _match_one(self.COORD_STMT, line)
if coord:
@ -332,7 +335,7 @@ class GerberParser(object):
line = r
did_something = True
continue
# aperture selection
(aperture, r) = _match_one(self.APERTURE_STMT, line)
if aperture:
@ -485,32 +488,32 @@ class GerberParser(object):
aperture = None
if shape == 'C':
diameter = modifiers[0][0]
if len(modifiers[0]) >= 2:
hole_diameter = modifiers[0][1]
else:
hole_diameter = None
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 = modifiers[0][2]
else:
hole_diameter = None
aperture = Rectangle(position=None, width=width, height=height, hole_diameter=hole_diameter, units=self.settings.units)
elif shape == 'O':
width = modifiers[0][0]
height = modifiers[0][1]
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 shape == 'P':
outer_diameter = modifiers[0][0]
@ -519,7 +522,7 @@ class GerberParser(object):
rotation = modifiers[0][2]
else:
rotation = 0
if len(modifiers[0]) > 3:
hole_diameter = modifiers[0][3]
else:
@ -636,7 +639,7 @@ class GerberParser(object):
units=self.settings.units))
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:
@ -663,32 +666,32 @@ class GerberParser(object):
if renderable is not None:
self.primitives.append(renderable)
self.x, self.y = x, y
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
"""
if self.quadrant_mode == 'single-quadrant':
# The Gerber spec says single quadrant only has one possible center, and you can detect
# 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])
sqdist_start = sq_distance(start, test_center)
sqdist_end = sq_distance(end, test_center)
if abs(sqdist_start - sqdist_end) < sqdist_diff_min:
center = test_center
sqdist_diff_min = abs(sqdist_start - sqdist_end)
return center
else:
return (start[0] + offsets[0], start[1] + offsets[1])

File diff suppressed because one or more lines are too long

View file

@ -23,21 +23,21 @@ def test_render_single_quadrant():
def test_render_simple_contour():
"""Umaco exapmle of a simple arrow-shaped contour"""
gerber = _test_render('resources/example_simple_contour.gbr', 'golden/example_simple_contour.png')
# Check the resulting dimensions
assert_tuple_equal(((2.0, 11.0), (1.0, 9.0)), gerber.bounding_box)
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.png')
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.png')
@ -45,12 +45,11 @@ 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.png')
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.png')
def test_render_not_overlapping_touching():
"""Umaco example of D02 staring a second contour"""
@ -69,7 +68,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.png')
@ -98,7 +97,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.png')
def test_flash_circle():
"""Umaco example a simple circular flash with and without a hole"""
@ -143,7 +142,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
@ -152,14 +151,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 PNG image to the memory stream
@ -167,7 +166,7 @@ def _test_render(gerber_path, png_expected_path, create_output_path = None):
gerber.render(ctx)
actual_bytes = ctx.dump(None)
# 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:
@ -176,14 +175,14 @@ 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, 'rb') as expected_file:
expected_bytes = expected_file.read()
# Don't directly use assert_equal otherwise any failure pollutes the test results
equal = (expected_bytes == actual_bytes)
assert_true(equal)
return gerber

View file

@ -116,33 +116,22 @@ def test_zeros():
def test_filesettings_validation():
""" Test FileSettings constructor argument validation
"""
<<<<<<< HEAD
# absolute-ish is not a valid notation
assert_raises(ValueError, FileSettings, 'absolute-ish',
'inch', None, (2, 5), None)
# degrees kelvin isn't a valid unit for a CAM file
assert_raises(ValueError, FileSettings, 'absolute',
'degrees kelvin', None, (2, 5), None)
assert_raises(ValueError, FileSettings, 'absolute',
'inch', 'leading', (2, 5), 'leading')
# Technnically this should be an error, but Eangle files often do this incorrectly so we
# allow it
#assert_raises(ValueError, FileSettings, 'absolute',
# 'inch', 'following', (2, 5), None)
=======
assert_raises(ValueError, FileSettings, 'absolute-ish',
'inch', None, (2, 5), None)
assert_raises(ValueError, FileSettings, 'absolute',
'degrees kelvin', None, (2, 5), None)
assert_raises(ValueError, FileSettings, 'absolute',
'inch', 'leading', (2, 5), 'leading')
assert_raises(ValueError, FileSettings, 'absolute',
'inch', 'following', (2, 5), None)
>>>>>>> 5476da8... Fix a bunch of rendering bugs.
assert_raises(ValueError, FileSettings, 'absolute',
'inch', None, (2, 5), 'following')
assert_raises(ValueError, FileSettings, 'absolute',

View file

@ -38,4 +38,4 @@ def test_load_from_string():
def test_file_type_validation():
""" Test file format validation
"""
assert_raises(ParseError, read, 'LICENSE')
assert_raises(ParseError, read, __file__)

View file

@ -13,17 +13,17 @@ def test_primitive_smoketest():
try:
p.bounding_box
assert_false(True, 'should have thrown the exception')
except NotImplementedError:
except NotImplementedError:
pass
#assert_raises(NotImplementedError, p.bounding_box)
p.to_metric()
p.to_inch()
try:
p.offset(1, 1)
assert_false(True, 'should have thrown the exception')
except NotImplementedError:
pass
#try:
# p.offset(1, 1)
# assert_false(True, 'should have thrown the exception')
#except NotImplementedError:
# pass
def test_line_angle():
@ -291,7 +291,7 @@ def test_circle_conversion():
assert_equal(c.position, (0.1, 1.))
assert_equal(c.diameter, 10.)
assert_equal(c.hole_diameter, None)
# Circle initially metric, with hole
c = Circle((2.54, 25.4), 254.0, 127.0, units='metric')
@ -310,7 +310,7 @@ def test_circle_conversion():
assert_equal(c.position, (0.1, 1.))
assert_equal(c.diameter, 10.)
assert_equal(c.hole_diameter, 5.)
# Circle initially inch, no hole
c = Circle((0.1, 1.0), 10.0, units='inch')
# No effect
@ -437,13 +437,13 @@ def test_rectangle_ctor():
assert_equal(r.position, pos)
assert_equal(r.width, width)
assert_equal(r.height, height)
def test_rectangle_hole_radius():
""" Test rectangle hole diameter calculation
"""
r = Rectangle((0,0), 2, 2)
assert_equal(0, r.hole_radius)
r = Rectangle((0,0), 2, 2, 1)
assert_equal(0.5, r.hole_radius)
@ -464,7 +464,7 @@ def test_rectangle_bounds():
def test_rectangle_conversion():
"""Test converting rectangles between units"""
# Initially metric no hole
r = Rectangle((2.54, 25.4), 254.0, 2540.0, units='metric')
@ -482,7 +482,7 @@ def test_rectangle_conversion():
assert_equal(r.position, (0.1, 1.0))
assert_equal(r.width, 10.0)
assert_equal(r.height, 100.0)
# Initially metric with hole
r = Rectangle((2.54, 25.4), 254.0, 2540.0, 127.0, units='metric')
@ -520,7 +520,7 @@ def test_rectangle_conversion():
assert_equal(r.position, (2.54, 25.4))
assert_equal(r.width, 254.0)
assert_equal(r.height, 2540.0)
# Initially inch with hole
r = Rectangle((0.1, 1.0), 10.0, 100.0, 5.0, units='inch')
r.to_inch()
@ -903,7 +903,7 @@ def test_polygon_bounds():
def test_polygon_conversion():
p = Polygon((2.54, 25.4), 3, 254.0, 0, units='metric')
# No effect
p.to_metric()
assert_equal(p.position, (2.54, 25.4))