From 724c2b3bced319ed0b50c4302fed9b0e1aa9ce9c Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Sat, 5 Nov 2016 20:56:47 -0400 Subject: Finish Merge, most tests passing --- gerber/primitives.py | 366 +++++++++++++++++++++++++-------------------------- 1 file changed, 183 insertions(+), 183 deletions(-) (limited to 'gerber/primitives.py') diff --git a/gerber/primitives.py b/gerber/primitives.py index a291c26..a66400a 100644 --- a/gerber/primitives.py +++ b/gerber/primitives.py @@ -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 + + @property def flashed(self): - return False + return False - @property + @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 '' % (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 -- cgit