Finish Merge, most tests passing
This commit is contained in:
parent
5af19af190
commit
724c2b3bce
10 changed files with 405 additions and 468 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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__)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue