Added primitives and tests
This commit is contained in:
parent
208149d676
commit
360eddc3c4
3 changed files with 317 additions and 27 deletions
|
|
@ -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))
|
||||
|
||||
|
|
|
|||
|
|
@ -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)))
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue