From 360eddc3c421cc193716b17d33cc94d8444d64ce Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Sun, 1 Feb 2015 13:40:08 -0500 Subject: Added primitives and tests --- gerber/primitives.py | 203 +++++++++++++++++++++++++++++++++++----- gerber/tests/test_primitives.py | 134 +++++++++++++++++++++++++- gerber/tests/tests.py | 9 +- 3 files changed, 318 insertions(+), 28 deletions(-) (limited to 'gerber') diff --git a/gerber/primitives.py b/gerber/primitives.py index e13e37f..61df7c1 100644 --- a/gerber/primitives.py +++ b/gerber/primitives.py @@ -19,7 +19,21 @@ from operator import sub class Primitive(object): - + """ Base class for all Cam file primitives + + Parameters + --------- + level_polarity : string + Polarity of the parameter. May be 'dark' or 'clear'. Dark indicates + a "positive" primitive, i.e. indicating where coppper should remain, + and clear indicates a negative primitive, such as where copper should + be removed. clear primitives are often used to create cutouts in region + pours. + + rotation : float + Rotation of a primitive about its origin in degrees. Positive rotation + is counter-clockwise as viewed from the board top. + """ def __init__(self, level_polarity='dark', rotation=0): self.level_polarity = level_polarity self.rotation = rotation @@ -102,7 +116,6 @@ class Arc(Primitive): theta0 = (self.start_angle + two_pi) % two_pi theta1 = (self.end_angle + two_pi) % two_pi points = [self.start, self.end] - #Shit's about to get ugly... if self.direction == 'counterclockwise': # Passes through 0 degrees if theta0 > theta1: @@ -170,13 +183,20 @@ class Ellipse(Primitive): self.position = position self.width = width self.height = height + # Axis-aligned width and height + ux = (self.width / 2.) * math.cos(math.radians(self.rotation)) + uy = (self.width / 2.) * math.sin(math.radians(self.rotation)) + vx = (self.height / 2.) * math.cos(math.radians(self.rotation) + (math.pi / 2.)) + vy = (self.height / 2.) * math.sin(math.radians(self.rotation) + (math.pi / 2.)) + self._abs_width = 2 * math.sqrt((ux * ux) + (vx * vx)) + self._abs_height = 2 * math.sqrt((uy * uy) + (vy * vy)) @property def bounding_box(self): - min_x = self.position[0] - (self.width / 2.0) - max_x = self.position[0] + (self.width / 2.0) - min_y = self.position[1] - (self.height / 2.0) - max_y = self.position[1] + (self.height / 2.0) + min_x = self.position[0] - (self._abs_width / 2.0) + max_x = self.position[0] + (self._abs_width / 2.0) + min_y = self.position[1] - (self._abs_height / 2.0) + max_y = self.position[1] + (self._abs_height / 2.0) return ((min_x, max_x), (min_y, max_y)) @@ -188,16 +208,21 @@ class Rectangle(Primitive): self.position = position self.width = width self.height = height + # Axis-aligned width and height + self._abs_width = (math.cos(math.radians(self.rotation)) * self.width + + math.sin(math.radians(self.rotation)) * self.height) + self._abs_height = (math.cos(math.radians(self.rotation)) * self.height + + math.sin(math.radians(self.rotation)) * self.width) @property def lower_left(self): - return (self.position[0] - (self.width / 2.), - self.position[1] - (self.height / 2.)) + return (self.position[0] - (self._abs_width / 2.), + self.position[1] - (self._abs_height / 2.)) @property def upper_right(self): - return (self.position[0] + (self.width / 2.), - self.position[1] + (self.height / 2.)) + return (self.position[0] + (self._abs_width / 2.), + self.position[1] + (self._abs_height / 2.)) @property def bounding_box(self): @@ -207,21 +232,109 @@ class Rectangle(Primitive): max_y = self.upper_right[1] return ((min_x, max_x), (min_y, max_y)) - @property - def stroke_width(self): - return max((self.width, self.height)) class Diamond(Primitive): - pass + """ + """ + def __init__(self, position, width, height, **kwargs): + super(Diamond, self).__init__(**kwargs) + self.position = position + self.width = width + self.height = height + # Axis-aligned width and height + self._abs_width = (math.cos(math.radians(self.rotation)) * self.width + + math.sin(math.radians(self.rotation)) * self.height) + self._abs_height = (math.cos(math.radians(self.rotation)) * self.height + + math.sin(math.radians(self.rotation)) * self.width) + + @property + def lower_left(self): + return (self.position[0] - (self._abs_width / 2.), + self.position[1] - (self._abs_height / 2.)) + + @property + def upper_right(self): + return (self.position[0] + (self._abs_width / 2.), + self.position[1] + (self._abs_height / 2.)) + + @property + def bounding_box(self): + min_x = self.lower_left[0] + max_x = self.upper_right[0] + min_y = self.lower_left[1] + max_y = self.upper_right[1] + return ((min_x, max_x), (min_y, max_y)) class ChamferRectangle(Primitive): - pass + """ + """ + def __init__(self, position, width, height, chamfer, corners, **kwargs): + super(ChamferRectangle, self).__init__(**kwargs) + self.position = position + self.width = width + self.height = height + self.chamfer = chamfer + self.corners = corners + # Axis-aligned width and height + self._abs_width = (math.cos(math.radians(self.rotation)) * self.width + + math.sin(math.radians(self.rotation)) * self.height) + self._abs_height = (math.cos(math.radians(self.rotation)) * self.height + + math.sin(math.radians(self.rotation)) * self.width) + + @property + def lower_left(self): + return (self.position[0] - (self._abs_width / 2.), + self.position[1] - (self._abs_height / 2.)) + + @property + def upper_right(self): + return (self.position[0] + (self._abs_width / 2.), + self.position[1] + (self._abs_height / 2.)) + + @property + def bounding_box(self): + min_x = self.lower_left[0] + max_x = self.upper_right[0] + min_y = self.lower_left[1] + max_y = self.upper_right[1] + return ((min_x, max_x), (min_y, max_y)) class RoundRectangle(Primitive): - pass + """ + """ + def __init__(self, position, width, height, radius, corners, **kwargs): + super(RoundRectangle, self).__init__(**kwargs) + self.position = position + self.width = width + self.height = height + self.radius = radius + self.corners = corners + # Axis-aligned width and height + self._abs_width = (math.cos(math.radians(self.rotation)) * self.width + + math.sin(math.radians(self.rotation)) * self.height) + self._abs_height = (math.cos(math.radians(self.rotation)) * self.height + + math.sin(math.radians(self.rotation)) * self.width) + + @property + def lower_left(self): + return (self.position[0] - (self._abs_width / 2.), + self.position[1] - (self._abs_height / 2.)) + + @property + def upper_right(self): + return (self.position[0] + (self._abs_width / 2.), + self.position[1] + (self._abs_height / 2.)) + + @property + def bounding_box(self): + min_x = self.lower_left[0] + max_x = self.upper_right[0] + min_y = self.lower_left[1] + max_y = self.upper_right[1] + return ((min_x, max_x), (min_y, max_y)) class Obround(Primitive): @@ -310,7 +423,7 @@ class Region(Primitive): class RoundButterfly(Primitive): - """ + """ A circle with two diagonally-opposite quadrants removed """ def __init__(self, position, diameter, **kwargs): super(RoundButterfly, self).__init__(**kwargs) @@ -328,17 +441,64 @@ class RoundButterfly(Primitive): min_y = self.position[1] - self.radius max_y = self.position[1] + self.radius return ((min_x, max_x), (min_y, max_y)) - + class SquareButterfly(Primitive): - pass + """ A square with two diagonally-opposite quadrants removed + """ + def __init__(self, position, side, **kwargs): + super(SquareButterfly, self).__init__(**kwargs) + self.position = position + self.side = side + + + @property + def bounding_box(self): + min_x = self.position[0] - (self.side / 2.) + max_x = self.position[0] + (self.side / 2.) + min_y = self.position[1] - (self.side / 2.) + max_y = self.position[1] + (self.side / 2.) + return ((min_x, max_x), (min_y, max_y)) class Donut(Primitive): - pass + """ A Shape with an identical concentric shape removed from its center + """ + def __init__(self, position, shape, inner_diameter, outer_diameter, **kwargs): + super(Donut, self).__init__(**kwargs) + self.position = position + self.shape = shape + self.inner_diameter = inner_diameter + self.outer_diameter = outer_diameter + if self.shape in ('round', 'square', 'octagon'): + self.width = outer_diameter + self.height = outer_diameter + else: + # Hexagon + self.width = 0.5 * math.sqrt(3.) * outer_diameter + self.height = outer_diameter + + + @property + def lower_left(self): + return (self.position[0] - (self.width / 2.), + self.position[1] - (self.height / 2.)) + + @property + def upper_right(self): + return (self.position[0] + (self.width / 2.), + self.position[1] + (self.height / 2.)) + + @property + def bounding_box(self): + min_x = self.lower_left[0] + max_x = self.upper_right[0] + min_y = self.lower_left[1] + max_y = self.upper_right[1] + return ((min_x, max_x), (min_y, max_y)) class Drill(Primitive): - """ + """ A drill hole """ def __init__(self, position, diameter): super(Drill, self).__init__('dark') @@ -356,3 +516,4 @@ class Drill(Primitive): min_y = self.position[1] - self.radius max_y = self.position[1] + self.radius return ((min_x, max_x), (min_y, max_y)) + diff --git a/gerber/tests/test_primitives.py b/gerber/tests/test_primitives.py index 29036b4..4534484 100644 --- a/gerber/tests/test_primitives.py +++ b/gerber/tests/test_primitives.py @@ -6,7 +6,6 @@ from ..primitives import * from tests import * - def test_line_angle(): """ Test Line primitive angle calculation """ @@ -22,7 +21,8 @@ def test_line_angle(): l = Line(start, end, 0) line_angle = (l.angle + 2 * math.pi) % (2 * math.pi) assert_almost_equal(line_angle, expected) - + + def test_line_bounds(): """ Test Line primitive bounding box calculation """ @@ -34,6 +34,7 @@ def test_line_bounds(): l = Line(start, end, 0) assert_equal(l.bounding_box, expected) + def test_arc_radius(): """ Test Arc primitive radius calculation """ @@ -65,21 +66,144 @@ def test_arc_bounds(): ((1, 0), (0, 1), (0, 0), 'counterclockwise', ((0, 1), (0, 1))), #TODO: ADD MORE TEST CASES HERE ] - for start, end, center, direction, bounds in cases: a = Arc(start, end, center, direction, 0) assert_equal(a.bounding_box, bounds) - + + def test_circle_radius(): """ Test Circle primitive radius calculation """ c = Circle((1, 1), 2) assert_equal(c.radius, 1) - + + def test_circle_bounds(): """ Test Circle bounding box calculation """ c = Circle((1, 1), 2) assert_equal(c.bounding_box, ((0, 2), (0, 2))) + + +def test_ellipse_ctor(): + """ Test ellipse creation + """ + e = Ellipse((2, 2), 3, 2) + assert_equal(e.position, (2, 2)) + assert_equal(e.width, 3) + assert_equal(e.height, 2) + + +def test_ellipse_bounds(): + """ Test ellipse bounding box calculation + """ + e = Ellipse((2, 2), 4, 2) + assert_equal(e.bounding_box, ((0, 4), (1, 3))) + e = Ellipse((2, 2), 4, 2, rotation=90) + assert_equal(e.bounding_box, ((1, 3), (0, 4))) + e = Ellipse((2, 2), 4, 2, rotation=180) + assert_equal(e.bounding_box, ((0, 4), (1, 3))) + e = Ellipse((2, 2), 4, 2, rotation=270) + assert_equal(e.bounding_box, ((1, 3), (0, 4))) + +def test_rectangle_ctor(): + """ Test rectangle creation + """ + test_cases = (((0,0), 1, 1), ((0, 0), 1, 2), ((1,1), 1, 2)) + for pos, width, height in test_cases: + r = Rectangle(pos, width, height) + assert_equal(r.position, pos) + assert_equal(r.width, width) + assert_equal(r.height, height) + +def test_rectangle_bounds(): + """ Test rectangle bounding box calculation + """ + r = Rectangle((0,0), 2, 2) + xbounds, ybounds = r.bounding_box + assert_array_almost_equal(xbounds, (-1, 1)) + assert_array_almost_equal(ybounds, (-1, 1)) + r = Rectangle((0,0), 2, 2, rotation=45) + xbounds, ybounds = r.bounding_box + assert_array_almost_equal(xbounds, (-math.sqrt(2), math.sqrt(2))) + assert_array_almost_equal(ybounds, (-math.sqrt(2), math.sqrt(2))) + +def test_diamond_ctor(): + """ Test diamond creation + """ + test_cases = (((0,0), 1, 1), ((0, 0), 1, 2), ((1,1), 1, 2)) + for pos, width, height in test_cases: + d = Diamond(pos, width, height) + assert_equal(d.position, pos) + assert_equal(d.width, width) + assert_equal(d.height, height) + +def test_diamond_bounds(): + """ Test diamond bounding box calculation + """ + d = Diamond((0,0), 2, 2) + xbounds, ybounds = d.bounding_box + assert_array_almost_equal(xbounds, (-1, 1)) + assert_array_almost_equal(ybounds, (-1, 1)) + d = Diamond((0,0), math.sqrt(2), math.sqrt(2), rotation=45) + xbounds, ybounds = d.bounding_box + assert_array_almost_equal(xbounds, (-1, 1)) + assert_array_almost_equal(ybounds, (-1, 1)) + + +def test_chamfer_rectangle_ctor(): + """ Test chamfer rectangle creation + """ + test_cases = (((0,0), 1, 1, 0.2, (True, True, False, False)), + ((0, 0), 1, 2, 0.3, (True, True, True, True)), + ((1,1), 1, 2, 0.4, (False, False, False, False))) + for pos, width, height, chamfer, corners in test_cases: + r = ChamferRectangle(pos, width, height, chamfer, corners) + assert_equal(r.position, pos) + assert_equal(r.width, width) + assert_equal(r.height, height) + assert_equal(r.chamfer, chamfer) + assert_array_almost_equal(r.corners, corners) + +def test_chamfer_rectangle_bounds(): + """ Test chamfer rectangle bounding box calculation + """ + r = ChamferRectangle((0,0), 2, 2, 0.2, (True, True, False, False)) + xbounds, ybounds = r.bounding_box + assert_array_almost_equal(xbounds, (-1, 1)) + assert_array_almost_equal(ybounds, (-1, 1)) + r = ChamferRectangle((0,0), 2, 2, 0.2, (True, True, False, False), rotation=45) + xbounds, ybounds = r.bounding_box + assert_array_almost_equal(xbounds, (-math.sqrt(2), math.sqrt(2))) + assert_array_almost_equal(ybounds, (-math.sqrt(2), math.sqrt(2))) + + +def test_round_rectangle_ctor(): + """ Test round rectangle creation + """ + test_cases = (((0,0), 1, 1, 0.2, (True, True, False, False)), + ((0, 0), 1, 2, 0.3, (True, True, True, True)), + ((1,1), 1, 2, 0.4, (False, False, False, False))) + for pos, width, height, radius, corners in test_cases: + r = RoundRectangle(pos, width, height, radius, corners) + assert_equal(r.position, pos) + assert_equal(r.width, width) + assert_equal(r.height, height) + assert_equal(r.radius, radius) + assert_array_almost_equal(r.corners, corners) + +def test_round_rectangle_bounds(): + """ Test round rectangle bounding box calculation + """ + r = RoundRectangle((0,0), 2, 2, 0.2, (True, True, False, False)) + xbounds, ybounds = r.bounding_box + assert_array_almost_equal(xbounds, (-1, 1)) + assert_array_almost_equal(ybounds, (-1, 1)) + r = RoundRectangle((0,0), 2, 2, 0.2, (True, True, False, False), rotation=45) + xbounds, ybounds = r.bounding_box + assert_array_almost_equal(xbounds, (-math.sqrt(2), math.sqrt(2))) + assert_array_almost_equal(ybounds, (-math.sqrt(2), math.sqrt(2))) + + \ No newline at end of file diff --git a/gerber/tests/tests.py b/gerber/tests/tests.py index 222eea3..e7029e4 100644 --- a/gerber/tests/tests.py +++ b/gerber/tests/tests.py @@ -15,5 +15,10 @@ from nose.tools import raises from nose import with_setup __all__ = ['assert_in', 'assert_not_in', 'assert_equal', 'assert_not_equal', - 'assert_almost_equal', 'assert_true', 'assert_false', - 'assert_raises', 'raises', 'with_setup' ] + 'assert_almost_equal', 'assert_array_almost_equal', 'assert_true', + 'assert_false', 'assert_raises', 'raises', 'with_setup' ] + +def assert_array_almost_equal(arr1, arr2, decimal=6): + assert_equal(len(arr1), len(arr2)) + for i in xrange(len(arr1)): + assert_almost_equal(arr1[i], arr2[i], decimal) \ No newline at end of file -- cgit