From f5abd5b0bdc0b9f524456dc9216bd0f3732e82a0 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Tue, 28 Oct 2014 22:11:43 -0400 Subject: Add arc rendering and tests --- gerber/primitives.py | 63 +++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 58 insertions(+), 5 deletions(-) (limited to 'gerber/primitives.py') diff --git a/gerber/primitives.py b/gerber/primitives.py index b3869e1..f934f74 100644 --- a/gerber/primitives.py +++ b/gerber/primitives.py @@ -44,8 +44,8 @@ class Line(Primitive): @property def angle(self): - delta_x, delta_y = tuple(map(sub, end, start)) - angle = degrees(math.tan(delta_y/delta_x)) + delta_x, delta_y = tuple(map(sub, self.end, self.start)) + angle = math.atan2(delta_y, delta_x) return angle @property @@ -69,19 +69,72 @@ class Arc(Primitive): self.direction = direction self.width = width + @property + def radius(self): + dy, dx = map(sub, self.start, self.center) + return math.sqrt(dy**2 + dx**2) + @property def start_angle(self): dy, dx = map(sub, self.start, self.center) - return math.atan2(dy, dx) + return math.atan2(dx, dy) @property def end_angle(self): dy, dx = map(sub, self.end, self.center) - return math.atan2(dy, dx) + return math.atan2(dx, dy) + + @property + def sweep_angle(self): + two_pi = 2 * math.pi + theta0 = (self.start_angle + two_pi) % two_pi + theta1 = (self.end_angle + two_pi) % two_pi + if self.direction == 'counterclockwise': + return abs(theta1 - theta0) + else: + theta0 += two_pi + return abs(theta0 - theta1) % two_pi @property def bounding_box(self): - pass + two_pi = 2 * math.pi + 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: + points.append((self.center[0] + self.radius, self.center[1])) + # Passes through 90 degrees + if theta0 <= math.pi / 2. and (theta1 >= math.pi / 2. or theta1 < theta0): + points.append((self.center[0], self.center[1] + self.radius)) + # Passes through 180 degrees + if theta0 <= math.pi and (theta1 >= math.pi or theta1 < theta0): + points.append((self.center[0] - self.radius, self.center[1])) + # Passes through 270 degrees + if theta0 <= math.pi * 1.5 and (theta1 >= math.pi * 1.5 or theta1 < theta0): + points.append((self.center[0], self.center[1] - self.radius )) + else: + # Passes through 0 degrees + if theta1 > theta0: + points.append((self.center[0] + self.radius, self.center[1])) + # Passes through 90 degrees + if theta1 <= math.pi / 2. and (theta0 >= math.pi / 2. or theta0 < theta1): + points.append((self.center[0], self.center[1] + self.radius)) + # Passes through 180 degrees + if theta1 <= math.pi and (theta0 >= math.pi or theta0 < theta1): + points.append((self.center[0] - self.radius, self.center[1])) + # Passes through 270 degrees + 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)) + class Circle(Primitive): """ -- cgit From 29deffcf77e963ae81aec9f8cbc61b029f3052d5 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Fri, 5 Dec 2014 23:59:28 -0500 Subject: add ipc2581 primitives --- gerber/primitives.py | 94 ++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 77 insertions(+), 17 deletions(-) (limited to 'gerber/primitives.py') diff --git a/gerber/primitives.py b/gerber/primitives.py index f934f74..e13e37f 100644 --- a/gerber/primitives.py +++ b/gerber/primitives.py @@ -19,10 +19,11 @@ from operator import sub class Primitive(object): - - def __init__(self, level_polarity='dark'): + + def __init__(self, level_polarity='dark', rotation=0): self.level_polarity = level_polarity - + self.rotation = rotation + def bounding_box(self): """ Calculate bounding box @@ -36,8 +37,8 @@ class Primitive(object): class Line(Primitive): """ """ - def __init__(self, start, end, width, level_polarity='dark'): - super(Line, self).__init__(level_polarity) + def __init__(self, start, end, width, **kwargs): + super(Line, self).__init__(**kwargs) self.start = start self.end = end self.width = width @@ -61,8 +62,8 @@ class Line(Primitive): class Arc(Primitive): """ """ - def __init__(self, start, end, center, direction, width, level_polarity='dark'): - super(Arc, self).__init__(level_polarity) + def __init__(self, start, end, center, direction, width, **kwargs): + super(Arc, self).__init__(**kwargs) self.start = start self.end = end self.center = center @@ -139,8 +140,8 @@ class Arc(Primitive): class Circle(Primitive): """ """ - def __init__(self, position, diameter, level_polarity='dark'): - super(Circle, self).__init__(level_polarity) + def __init__(self, position, diameter, **kwargs): + super(Circle, self).__init__(**kwargs) self.position = position self.diameter = diameter @@ -161,11 +162,29 @@ class Circle(Primitive): return self.diameter +class Ellipse(Primitive): + """ + """ + def __init__(self, position, width, height, **kwargs): + super(Ellipse, self).__init__(**kwargs) + self.position = position + self.width = width + self.height = height + + @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) + return ((min_x, max_x), (min_y, max_y)) + + class Rectangle(Primitive): """ """ - def __init__(self, position, width, height, level_polarity='dark'): - super(Rectangle, self).__init__(level_polarity) + def __init__(self, position, width, height, **kwargs): + super(Rectangle, self).__init__(**kwargs) self.position = position self.width = width self.height = height @@ -193,11 +212,23 @@ class Rectangle(Primitive): return max((self.width, self.height)) +class Diamond(Primitive): + pass + + +class ChamferRectangle(Primitive): + pass + + +class RoundRectangle(Primitive): + pass + + class Obround(Primitive): """ """ - def __init__(self, position, width, height, level_polarity='dark'): - super(Obround, self).__init__(level_polarity) + def __init__(self, position, width, height, **kwargs): + super(Obround, self).__init__(**kwargs) self.position = position self.width = width self.height = height @@ -242,11 +273,12 @@ class Obround(Primitive): self.height) return {'circle1': circle1, 'circle2': circle2, 'rectangle': rect} + class Polygon(Primitive): """ """ - def __init__(self, position, sides, radius, level_polarity='dark'): - super(Polygon, self).__init__(level_polarity) + def __init__(self, position, sides, radius, **kwargs): + super(Polygon, self).__init__(**kwargs) self.position = position self.sides = sides self.radius = radius @@ -263,8 +295,8 @@ class Polygon(Primitive): class Region(Primitive): """ """ - def __init__(self, points, level_polarity='dark'): - super(Region, self).__init__(level_polarity) + def __init__(self, points, **kwargs): + super(Region, self).__init__(**kwargs) self.points = points @property @@ -277,6 +309,34 @@ class Region(Primitive): return ((min_x, max_x), (min_y, max_y)) +class RoundButterfly(Primitive): + """ + """ + def __init__(self, position, diameter, **kwargs): + super(RoundButterfly, self).__init__(**kwargs) + self.position = position + self.diameter = diameter + + @property + def radius(self): + return self.diameter / 2. + + @property + def bounding_box(self): + min_x = self.position[0] - self.radius + max_x = self.position[0] + self.radius + 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 + + +class Donut(Primitive): + pass + + class Drill(Primitive): """ """ -- cgit 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 +++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 182 insertions(+), 21 deletions(-) (limited to 'gerber/primitives.py') 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)) + -- cgit From d98d23f8b5d61bb9d20e743a3c44bf04b6b2330a Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Mon, 2 Feb 2015 00:43:08 -0500 Subject: More tests and bugfixes --- gerber/primitives.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) (limited to 'gerber/primitives.py') diff --git a/gerber/primitives.py b/gerber/primitives.py index 61df7c1..da05127 100644 --- a/gerber/primitives.py +++ b/gerber/primitives.py @@ -345,20 +345,25 @@ class Obround(Primitive): self.position = position self.width = width self.height = height - - @property - def orientation(self): - return 'vertical' if self.height > self.width else 'horizontal' + # 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 orientation(self): + return 'vertical' if self.height > self.width else 'horizontal' @property def bounding_box(self): @@ -380,7 +385,7 @@ class Obround(Primitive): else: circle1 = Circle((self.position[0] - (self.height - self.width) / 2., self.position[1]), self.height) - circle2 = Circle((self.position[0] - (self.height - self.width) / 2., + circle2 = Circle((self.position[0] + (self.height - self.width) / 2., self.position[1]), self.height) rect = Rectangle(self.position, (self.width - self.height), self.height) -- cgit From 1cc20b351c10b1fa19817f29edd8c54a27aeee4b Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Mon, 2 Feb 2015 11:42:47 -0500 Subject: tests --- gerber/primitives.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) (limited to 'gerber/primitives.py') diff --git a/gerber/primitives.py b/gerber/primitives.py index da05127..2d666b8 100644 --- a/gerber/primitives.py +++ b/gerber/primitives.py @@ -16,6 +16,7 @@ # limitations under the License. import math from operator import sub +from .utils import validate_coordinates class Primitive(object): @@ -45,7 +46,7 @@ class Primitive(object): Return ((min x, max x), (min y, max y)) """ - pass + raise NotImplementedError('Bounding box calculation must be implemented in subclass') class Line(Primitive): @@ -155,6 +156,7 @@ class Circle(Primitive): """ def __init__(self, position, diameter, **kwargs): super(Circle, self).__init__(**kwargs) + validate_coordinates(position) self.position = position self.diameter = diameter @@ -180,6 +182,7 @@ class Ellipse(Primitive): """ def __init__(self, position, width, height, **kwargs): super(Ellipse, self).__init__(**kwargs) + validate_coordinates(position) self.position = position self.width = width self.height = height @@ -205,6 +208,7 @@ class Rectangle(Primitive): """ def __init__(self, position, width, height, **kwargs): super(Rectangle, self).__init__(**kwargs) + validate_coordinates(position) self.position = position self.width = width self.height = height @@ -239,6 +243,7 @@ class Diamond(Primitive): """ def __init__(self, position, width, height, **kwargs): super(Diamond, self).__init__(**kwargs) + validate_coordinates(position) self.position = position self.width = width self.height = height @@ -272,6 +277,7 @@ class ChamferRectangle(Primitive): """ def __init__(self, position, width, height, chamfer, corners, **kwargs): super(ChamferRectangle, self).__init__(**kwargs) + validate_coordinates(position) self.position = position self.width = width self.height = height @@ -307,6 +313,7 @@ class RoundRectangle(Primitive): """ def __init__(self, position, width, height, radius, corners, **kwargs): super(RoundRectangle, self).__init__(**kwargs) + validate_coordinates(position) self.position = position self.width = width self.height = height @@ -342,6 +349,7 @@ class Obround(Primitive): """ def __init__(self, position, width, height, **kwargs): super(Obround, self).__init__(**kwargs) + validate_coordinates(position) self.position = position self.width = width self.height = height @@ -397,6 +405,7 @@ class Polygon(Primitive): """ def __init__(self, position, sides, radius, **kwargs): super(Polygon, self).__init__(**kwargs) + validate_coordinates(position) self.position = position self.sides = sides self.radius = radius @@ -432,6 +441,7 @@ class RoundButterfly(Primitive): """ def __init__(self, position, diameter, **kwargs): super(RoundButterfly, self).__init__(**kwargs) + validate_coordinates(position) self.position = position self.diameter = diameter @@ -452,6 +462,7 @@ class SquareButterfly(Primitive): """ def __init__(self, position, side, **kwargs): super(SquareButterfly, self).__init__(**kwargs) + validate_coordinates(position) self.position = position self.side = side @@ -470,8 +481,14 @@ class Donut(Primitive): """ def __init__(self, position, shape, inner_diameter, outer_diameter, **kwargs): super(Donut, self).__init__(**kwargs) + if len(position) != 2: + raise TypeError('Position must be a tuple (n=2) of coordinates') self.position = position + if shape not in ('round', 'square', 'hexagon', 'octagon'): + raise ValueError('Valid shapes are round, square, hexagon or octagon') self.shape = shape + if inner_diameter >= outer_diameter: + raise ValueError('Outer diameter must be larger than inner diameter.') self.inner_diameter = inner_diameter self.outer_diameter = outer_diameter if self.shape in ('round', 'square', 'octagon'): @@ -507,6 +524,8 @@ class Drill(Primitive): """ def __init__(self, position, diameter): super(Drill, self).__init__('dark') + if len(position) != 2: + raise TypeError('Position must be a tuple (n=2) of coordinates') self.position = position self.diameter = diameter -- cgit From 8f69c1dfa281b6486c8fce16c1d58acef70c7ae7 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Thu, 12 Feb 2015 11:28:50 -0500 Subject: Update line primitive to take aperture parameter This fixes the exception referenced in #12. Still need to add rendering code for rectangle aperture lines and arcs. Rectangle strokes will be drawn as polygons by the rendering backends. --- gerber/primitives.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) (limited to 'gerber/primitives.py') diff --git a/gerber/primitives.py b/gerber/primitives.py index 2d666b8..a239cab 100644 --- a/gerber/primitives.py +++ b/gerber/primitives.py @@ -52,11 +52,11 @@ class Primitive(object): class Line(Primitive): """ """ - def __init__(self, start, end, width, **kwargs): + def __init__(self, start, end, aperture, **kwargs): super(Line, self).__init__(**kwargs) self.start = start self.end = end - self.width = width + self.aperture = aperture @property def angle(self): @@ -64,26 +64,26 @@ class Line(Primitive): angle = math.atan2(delta_y, delta_x) return angle - @property - def bounding_box(self): - width_2 = self.width / 2. - min_x = min(self.start[0], self.end[0]) - width_2 - max_x = max(self.start[0], self.end[0]) + width_2 - min_y = min(self.start[1], self.end[1]) - width_2 - max_y = max(self.start[1], self.end[1]) + width_2 - return ((min_x, max_x), (min_y, max_y)) + #@property + #def bounding_box(self): + # width_2 = self.width / 2. + # min_x = min(self.start[0], self.end[0]) - width_2 + # max_x = max(self.start[0], self.end[0]) + width_2 + # min_y = min(self.start[1], self.end[1]) - width_2 + # max_y = max(self.start[1], self.end[1]) + width_2 + # return ((min_x, max_x), (min_y, max_y)) class Arc(Primitive): """ """ - def __init__(self, start, end, center, direction, width, **kwargs): + def __init__(self, start, end, center, direction, aperture, **kwargs): super(Arc, self).__init__(**kwargs) self.start = start self.end = end self.center = center self.direction = direction - self.width = width + self.aperture = aperture @property def radius(self): -- cgit From 5e23d07bcb5103b4607c6ad591a2a547c97ee1f6 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Fri, 13 Feb 2015 09:37:27 -0500 Subject: Fix rendering for line with rectangular aperture per #12. Still need to do the same for arcs. --- gerber/primitives.py | 72 ++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 64 insertions(+), 8 deletions(-) (limited to 'gerber/primitives.py') diff --git a/gerber/primitives.py b/gerber/primitives.py index a239cab..1663a53 100644 --- a/gerber/primitives.py +++ b/gerber/primitives.py @@ -64,14 +64,70 @@ class Line(Primitive): angle = math.atan2(delta_y, delta_x) return angle - #@property - #def bounding_box(self): - # width_2 = self.width / 2. - # min_x = min(self.start[0], self.end[0]) - width_2 - # max_x = max(self.start[0], self.end[0]) + width_2 - # min_y = min(self.start[1], self.end[1]) - width_2 - # max_y = max(self.start[1], self.end[1]) + width_2 - # return ((min_x, max_x), (min_y, max_y)) + @property + def bounding_box(self): + if isinstance(self.aperture, Circle): + width_2 = self.aperture.radius + height_2 = width_2 + else: + width_2 = self.aperture.width / 2. + height_2 = self.aperture.height / 2. + min_x = min(self.start[0], self.end[0]) - width_2 + max_x = max(self.start[0], self.end[0]) + width_2 + min_y = min(self.start[1], self.end[1]) - height_2 + max_y = max(self.start[1], self.end[1]) + height_2 + return ((min_x, max_x), (min_y, max_y)) + + @property + def vertices(self): + if not isinstance(self.aperture, Rectangle): + return None + else: + start = self.start + end = self.end + width = self.aperture.width + height = self.aperture.height + + # Find all the corners of the start and end position + start_ll = (start[0] - (width / 2.), + start[1] - (height / 2.)) + start_lr = (start[0] - (width / 2.), + start[1] + (height / 2.)) + start_ul = (start[0] + (width / 2.), + start[1] - (height / 2.)) + start_ur = (start[0] + (width / 2.), + start[1] + (height / 2.)) + end_ll = (end[0] - (width / 2.), + end[1] - (height / 2.)) + end_lr = (end[0] - (width / 2.), + end[1] + (height / 2.)) + end_ul = (end[0] + (width / 2.), + end[1] - (height / 2.)) + end_ur = (end[0] + (width / 2.), + end[1] + (height / 2.)) + + if end[0] == start[0] and end[1] == start[1]: + return (start_ll, start_lr, start_ur, start_ul) + elif end[0] == start[0] and end[1] > start[1]: + return (start_ll, start_lr, end_ur, end_ul) + elif end[0] > start[0] and end[1] > start[1]: + return (start_ll, start_lr, end_lr, end_ur, end_ul, start_ul) + elif end[0] > start[0] and end[1] == start[1]: + return (start_ll, end_lr, end_ur, start_ul) + elif end[0] > start[0] and end[1] < start[1]: + return (start_ll, end_ll, end_lr, end_ur, start_ur, start_ul) + elif end[0] == start[0] and end[1] < start[1]: + return (end_ll, end_lr, start_ur, start_ul) + elif end[0] < start[0] and end[1] < start[1]: + return (end_ll, end_lr, start_lr, start_ur, start_ul, end_ul) + elif end[0] < start[0] and end[1] == start[1]: + return (end_ll, start_lr, start_ur, end_ul) + elif end[0] < start[0] and end[1] > start[1]: + return (start_ll, start_lr, start_ur, end_ur, end_ul, end_ll) + else: + return None + + class Arc(Primitive): -- cgit From 5cf1fa74b42eb8feaab23078bef6f31f6d647c33 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Sun, 15 Feb 2015 02:20:02 -0500 Subject: Tests and bugfixes --- gerber/primitives.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'gerber/primitives.py') diff --git a/gerber/primitives.py b/gerber/primitives.py index 1663a53..ffdbea7 100644 --- a/gerber/primitives.py +++ b/gerber/primitives.py @@ -200,10 +200,10 @@ 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) + min_x = min(x) - self.aperture.radius + max_x = max(x) + self.aperture.radius + min_y = min(y) - self.aperture.radius + max_y = max(y) + self.aperture.radius return ((min_x, max_x), (min_y, max_y)) -- cgit From 288ac27084b47166ac662402ea340d0aa25d8f56 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Wed, 18 Feb 2015 04:31:23 -0500 Subject: Get unit conversion working for Gerber/Excellon files Started operations module for file operations/transforms --- gerber/primitives.py | 186 ++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 161 insertions(+), 25 deletions(-) (limited to 'gerber/primitives.py') diff --git a/gerber/primitives.py b/gerber/primitives.py index ffdbea7..cb6e4ea 100644 --- a/gerber/primitives.py +++ b/gerber/primitives.py @@ -16,7 +16,8 @@ # limitations under the License. import math from operator import sub -from .utils import validate_coordinates + +from .utils import validate_coordinates, inch, metric class Primitive(object): @@ -46,7 +47,11 @@ class Primitive(object): Return ((min x, max x), (min y, max y)) """ - raise NotImplementedError('Bounding box calculation must be implemented in subclass') + raise NotImplementedError('Bounding box calculation must be ' + 'implemented in subclass') + + def __eq__(self, other): + return self.__dict__ == other.__dict__ class Line(Primitive): @@ -91,18 +96,18 @@ class Line(Primitive): # Find all the corners of the start and end position start_ll = (start[0] - (width / 2.), start[1] - (height / 2.)) - start_lr = (start[0] - (width / 2.), - start[1] + (height / 2.)) - start_ul = (start[0] + (width / 2.), + start_lr = (start[0] + (width / 2.), start[1] - (height / 2.)) + start_ul = (start[0] - (width / 2.), + start[1] + (height / 2.)) start_ur = (start[0] + (width / 2.), start[1] + (height / 2.)) end_ll = (end[0] - (width / 2.), end[1] - (height / 2.)) - end_lr = (end[0] - (width / 2.), - end[1] + (height / 2.)) - end_ul = (end[0] + (width / 2.), + end_lr = (end[0] + (width / 2.), end[1] - (height / 2.)) + end_ul = (end[0] - (width / 2.), + end[1] + (height / 2.)) end_ur = (end[0] + (width / 2.), end[1] + (height / 2.)) @@ -124,10 +129,17 @@ class Line(Primitive): return (end_ll, start_lr, start_ur, end_ul) elif end[0] < start[0] and end[1] > start[1]: return (start_ll, start_lr, start_ur, end_ur, end_ul, end_ll) - else: - return None + def to_inch(self): + self.aperture.to_inch() + self.start = tuple(map(inch, self.start)) + self.end = tuple(map(inch, self.end)) + + def to_metric(self): + self.aperture.to_metric() + self.start = tuple(map(metric, self.start)) + self.end = tuple(map(metric, self.end)) class Arc(Primitive): @@ -206,6 +218,18 @@ class Arc(Primitive): max_y = max(y) + self.aperture.radius return ((min_x, max_x), (min_y, max_y)) + def to_inch(self): + self.aperture.to_inch() + self.start = tuple(map(inch, self.start)) + self.end = tuple(map(inch, self.end)) + self.center = tuple(map(inch, self.center)) + + def to_metric(self): + self.aperture.to_metric() + self.start = tuple(map(metric, self.start)) + self.end = tuple(map(metric, self.end)) + self.center = tuple(map(metric, self.center)) + class Circle(Primitive): """ @@ -228,9 +252,15 @@ class Circle(Primitive): max_y = self.position[1] + self.radius return ((min_x, max_x), (min_y, max_y)) - @property - def stroke_width(self): - return self.diameter + def to_inch(self): + if self.position is not None: + self.position = tuple(map(inch, self.position)) + self.diameter = inch(self.diameter) + + def to_metric(self): + if self.position is not None: + self.position = tuple(map(metric, self.position)) + self.diameter = metric(self.diameter) class Ellipse(Primitive): @@ -276,12 +306,12 @@ class Rectangle(Primitive): @property def lower_left(self): - return (self.position[0] - (self._abs_width / 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._abs_width / 2.), + return (self.position[0] + (self._abs_width / 2.), self.position[1] + (self._abs_height / 2.)) @property @@ -292,6 +322,15 @@ class Rectangle(Primitive): max_y = self.upper_right[1] return ((min_x, max_x), (min_y, max_y)) + def to_inch(self): + self.position = tuple(map(inch, self.position)) + self.width = inch(self.width) + self.height = inch(self.height) + + def to_metric(self): + self.position = tuple(map(metric, self.position)) + self.width = metric(self.width) + self.height = metric(self.height) class Diamond(Primitive): @@ -311,12 +350,12 @@ class Diamond(Primitive): @property def lower_left(self): - return (self.position[0] - (self._abs_width / 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._abs_width / 2.), + return (self.position[0] + (self._abs_width / 2.), self.position[1] + (self._abs_height / 2.)) @property @@ -327,6 +366,16 @@ class Diamond(Primitive): max_y = self.upper_right[1] return ((min_x, max_x), (min_y, max_y)) + def to_inch(self): + self.position = tuple(map(inch, self.position)) + self.width = inch(self.width) + self.height = inch(self.height) + + def to_metric(self): + self.position = tuple(map(metric, self.position)) + self.width = metric(self.width) + self.height = metric(self.height) + class ChamferRectangle(Primitive): """ @@ -347,12 +396,12 @@ class ChamferRectangle(Primitive): @property def lower_left(self): - return (self.position[0] - (self._abs_width / 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._abs_width / 2.), + return (self.position[0] + (self._abs_width / 2.), self.position[1] + (self._abs_height / 2.)) @property @@ -363,6 +412,18 @@ class ChamferRectangle(Primitive): max_y = self.upper_right[1] return ((min_x, max_x), (min_y, max_y)) + def to_inch(self): + self.position = tuple(map(inch, self.position)) + self.width = inch(self.width) + self.height = inch(self.height) + self.chamfer = inch(self.chamfer) + + def to_metric(self): + self.position = tuple(map(metric, self.position)) + self.width = metric(self.width) + self.height = metric(self.height) + self.chamfer = metric(self.chamfer) + class RoundRectangle(Primitive): """ @@ -383,12 +444,12 @@ class RoundRectangle(Primitive): @property def lower_left(self): - return (self.position[0] - (self._abs_width / 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._abs_width / 2.), + return (self.position[0] + (self._abs_width / 2.), self.position[1] + (self._abs_height / 2.)) @property @@ -399,6 +460,18 @@ class RoundRectangle(Primitive): max_y = self.upper_right[1] return ((min_x, max_x), (min_y, max_y)) + def to_inch(self): + self.position = tuple(map(inch, self.position)) + self.width = inch(self.width) + self.height = inch(self.height) + self.radius = inch(self.radius) + + def to_metric(self): + self.position = tuple(map(metric, self.position)) + self.width = metric(self.width) + self.height = metric(self.height) + self.radius = metric(self.radius) + class Obround(Primitive): """ @@ -417,12 +490,12 @@ class Obround(Primitive): @property def lower_left(self): - return (self.position[0] - (self._abs_width / 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._abs_width / 2.), + return (self.position[0] + (self._abs_width / 2.), self.position[1] + (self._abs_height / 2.)) @property @@ -455,6 +528,16 @@ class Obround(Primitive): self.height) return {'circle1': circle1, 'circle2': circle2, 'rectangle': rect} + def to_inch(self): + self.position = tuple(map(inch, self.position)) + self.width = inch(self.width) + self.height = inch(self.height) + + def to_metric(self): + self.position = tuple(map(metric, self.position)) + self.width = metric(self.width) + self.height = metric(self.height) + class Polygon(Primitive): """ @@ -474,6 +557,14 @@ class Polygon(Primitive): max_y = self.position[1] + self.radius return ((min_x, max_x), (min_y, max_y)) + def to_inch(self): + self.position = tuple(map(inch, self.position)) + self.radius = inch(self.radius) + + def to_metric(self): + self.position = tuple(map(metric, self.position)) + self.radius = metric(self.radius) + class Region(Primitive): """ @@ -491,6 +582,12 @@ class Region(Primitive): max_y = max(y_list) return ((min_x, max_x), (min_y, max_y)) + def to_inch(self): + self.points = [tuple(map(inch, point)) for point in self.points] + + def to_metric(self): + self.points = [tuple(map(metric, point)) for point in self.points] + class RoundButterfly(Primitive): """ A circle with two diagonally-opposite quadrants removed @@ -513,6 +610,15 @@ class RoundButterfly(Primitive): max_y = self.position[1] + self.radius return ((min_x, max_x), (min_y, max_y)) + def to_inch(self): + self.position = tuple(map(inch, self.position)) + self.diameter = inch(self.diameter) + + def to_metric(self): + self.position = tuple(map(metric, self.position)) + self.diameter = metric(self.diameter) + + class SquareButterfly(Primitive): """ A square with two diagonally-opposite quadrants removed """ @@ -531,6 +637,14 @@ class SquareButterfly(Primitive): max_y = self.position[1] + (self.side / 2.) return ((min_x, max_x), (min_y, max_y)) + def to_inch(self): + self.position = tuple(map(inch, self.position)) + self.side = inch(self.side) + + def to_metric(self): + self.position = tuple(map(metric, self.position)) + self.side = metric(self.side) + class Donut(Primitive): """ A Shape with an identical concentric shape removed from its center @@ -558,12 +672,12 @@ class Donut(Primitive): @property def lower_left(self): - return (self.position[0] - (self.width / 2.), + 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.), + return (self.position[0] + (self.width / 2.), self.position[1] + (self.height / 2.)) @property @@ -574,6 +688,20 @@ class Donut(Primitive): max_y = self.upper_right[1] return ((min_x, max_x), (min_y, max_y)) + def to_inch(self): + self.position = tuple(map(inch, self.position)) + self.width = inch(self.width) + self.height = inch(self.height) + self.inner_diameter = inch(self.inner_diameter) + self.outer_diaemter = inch(self.outer_diameter) + + def to_metric(self): + self.position = tuple(map(metric, self.position)) + self.width = metric(self.width) + self.height = metric(self.height) + self.inner_diameter = metric(self.inner_diameter) + self.outer_diaemter = metric(self.outer_diameter) + class Drill(Primitive): """ A drill hole @@ -597,3 +725,11 @@ class Drill(Primitive): max_y = self.position[1] + self.radius return ((min_x, max_x), (min_y, max_y)) + def to_inch(self): + self.position = tuple(map(inch, self.position)) + self.diameter = inch(self.diameter) + + def to_metric(self): + self.position = tuple(map(metric, self.position)) + self.diameter = metric(self.diameter) + -- cgit From 5966d7830bda7f37ed5ddcc1bfccb93e7f780eaa Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Wed, 18 Feb 2015 23:13:23 -0500 Subject: Add offset operation --- gerber/primitives.py | 70 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) (limited to 'gerber/primitives.py') diff --git a/gerber/primitives.py b/gerber/primitives.py index cb6e4ea..3469880 100644 --- a/gerber/primitives.py +++ b/gerber/primitives.py @@ -15,7 +15,7 @@ # See the License for the specific language governing permissions and # limitations under the License. import math -from operator import sub +from operator import add, sub from .utils import validate_coordinates, inch, metric @@ -50,6 +50,15 @@ class Primitive(object): raise NotImplementedError('Bounding box calculation must be ' 'implemented in subclass') + def to_inch(self): + pass + + def to_metric(self): + pass + + def offset(self, x_offset=0, y_offset=0): + pass + def __eq__(self, other): return self.__dict__ == other.__dict__ @@ -141,6 +150,10 @@ class Line(Primitive): self.start = tuple(map(metric, self.start)) self.end = tuple(map(metric, self.end)) + 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 Arc(Primitive): """ @@ -230,6 +243,11 @@ class Arc(Primitive): self.end = tuple(map(metric, self.end)) self.center = tuple(map(metric, self.center)) + 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))) + self.center = tuple(map(add, self.center, (x_offset, y_offset))) + class Circle(Primitive): """ @@ -262,6 +280,9 @@ class Circle(Primitive): self.position = tuple(map(metric, self.position)) self.diameter = metric(self.diameter) + def offset(self, x_offset=0, y_offset=0): + self.position = tuple(map(add, self.position, (x_offset, y_offset))) + class Ellipse(Primitive): """ @@ -288,6 +309,19 @@ class Ellipse(Primitive): max_y = self.position[1] + (self._abs_height / 2.0) return ((min_x, max_x), (min_y, max_y)) + def to_inch(self): + self.position = tuple(map(inch, self.position)) + self.width = inch(self.width) + self.height = inch(self.height) + + def to_metric(self): + self.position = tuple(map(metric, self.position)) + self.width = metric(self.width) + self.height = metric(self.height) + + def offset(self, x_offset=0, y_offset=0): + self.position = tuple(map(add, self.position, (x_offset, y_offset))) + class Rectangle(Primitive): """ @@ -332,6 +366,9 @@ class Rectangle(Primitive): self.width = metric(self.width) self.height = metric(self.height) + def offset(self, x_offset=0, y_offset=0): + self.position = tuple(map(add, self.position, (x_offset, y_offset))) + class Diamond(Primitive): """ @@ -376,6 +413,9 @@ class Diamond(Primitive): self.width = metric(self.width) self.height = metric(self.height) + def offset(self, x_offset=0, y_offset=0): + self.position = tuple(map(add, self.position, (x_offset, y_offset))) + class ChamferRectangle(Primitive): """ @@ -424,6 +464,9 @@ class ChamferRectangle(Primitive): self.height = metric(self.height) self.chamfer = metric(self.chamfer) + def offset(self, x_offset=0, y_offset=0): + self.position = tuple(map(add, self.position, (x_offset, y_offset))) + class RoundRectangle(Primitive): """ @@ -472,6 +515,9 @@ class RoundRectangle(Primitive): self.height = metric(self.height) self.radius = metric(self.radius) + def offset(self, x_offset=0, y_offset=0): + self.position = tuple(map(add, self.position, (x_offset, y_offset))) + class Obround(Primitive): """ @@ -538,6 +584,9 @@ class Obround(Primitive): self.width = metric(self.width) self.height = metric(self.height) + def offset(self, x_offset=0, y_offset=0): + self.position = tuple(map(add, self.position, (x_offset, y_offset))) + class Polygon(Primitive): """ @@ -565,6 +614,9 @@ class Polygon(Primitive): self.position = tuple(map(metric, self.position)) self.radius = metric(self.radius) + def offset(self, x_offset=0, y_offset=0): + self.position = tuple(map(add, self.position, (x_offset, y_offset))) + class Region(Primitive): """ @@ -588,6 +640,10 @@ class Region(Primitive): def to_metric(self): self.points = [tuple(map(metric, point)) for point in self.points] + def offset(self, x_offset=0, y_offset=0): + self.points = [tuple(map(add, point, (x_offset, y_offset))) + for point in self.points] + class RoundButterfly(Primitive): """ A circle with two diagonally-opposite quadrants removed @@ -618,6 +674,9 @@ class RoundButterfly(Primitive): self.position = tuple(map(metric, self.position)) self.diameter = metric(self.diameter) + def offset(self, x_offset=0, y_offset=0): + self.position = tuple(map(add, self.position, (x_offset, y_offset))) + class SquareButterfly(Primitive): """ A square with two diagonally-opposite quadrants removed @@ -645,6 +704,9 @@ class SquareButterfly(Primitive): self.position = tuple(map(metric, self.position)) self.side = metric(self.side) + def offset(self, x_offset=0, y_offset=0): + self.position = tuple(map(add, self.position, (x_offset, y_offset))) + class Donut(Primitive): """ A Shape with an identical concentric shape removed from its center @@ -702,6 +764,9 @@ class Donut(Primitive): self.inner_diameter = metric(self.inner_diameter) self.outer_diaemter = metric(self.outer_diameter) + def offset(self, x_offset=0, y_offset=0): + self.position = tuple(map(add, self.position, (x_offset, y_offset))) + class Drill(Primitive): """ A drill hole @@ -733,3 +798,6 @@ class Drill(Primitive): self.position = tuple(map(metric, self.position)) self.diameter = metric(self.diameter) + def offset(self, x_offset=0, y_offset=0): + self.position = tuple(map(add, self.position, (x_offset, y_offset))) + -- cgit From 68619d4d5a7beb38dc81d953b43bf4196ca1d3a6 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Thu, 5 Mar 2015 22:42:42 -0500 Subject: Fix parsing for multiline ipc-d-356 records --- gerber/primitives.py | 63 ++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 56 insertions(+), 7 deletions(-) (limited to 'gerber/primitives.py') diff --git a/gerber/primitives.py b/gerber/primitives.py index 3469880..5d0b8cf 100644 --- a/gerber/primitives.py +++ b/gerber/primitives.py @@ -713,8 +713,7 @@ class Donut(Primitive): """ def __init__(self, position, shape, inner_diameter, outer_diameter, **kwargs): super(Donut, self).__init__(**kwargs) - if len(position) != 2: - raise TypeError('Position must be a tuple (n=2) of coordinates') + validate_coordinates(position) self.position = position if shape not in ('round', 'square', 'hexagon', 'octagon'): raise ValueError('Valid shapes are round, square, hexagon or octagon') @@ -731,7 +730,6 @@ class Donut(Primitive): 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.), @@ -755,14 +753,56 @@ class Donut(Primitive): self.width = inch(self.width) self.height = inch(self.height) self.inner_diameter = inch(self.inner_diameter) - self.outer_diaemter = inch(self.outer_diameter) + self.outer_diameter = inch(self.outer_diameter) def to_metric(self): self.position = tuple(map(metric, self.position)) self.width = metric(self.width) self.height = metric(self.height) self.inner_diameter = metric(self.inner_diameter) - self.outer_diaemter = metric(self.outer_diameter) + self.outer_diameter = metric(self.outer_diameter) + + def offset(self, x_offset=0, y_offset=0): + self.position = tuple(map(add, self.position, (x_offset, y_offset))) + + +class SquareRoundDonut(Primitive): + """ A Square with a circular cutout in the center + """ + def __init__(self, position, inner_diameter, outer_diameter, **kwargs): + super(SquareRoundDonut, self).__init__(**kwargs) + validate_coordinates(position) + self.position = position + if inner_diameter >= outer_diameter: + raise ValueError('Outer diameter must be larger than inner diameter.') + self.inner_diameter = inner_diameter + self.outer_diameter = outer_diameter + + @property + def lower_left(self): + return tuple([c - self.outer_diameter / 2. for c in self.position]) + + @property + def upper_right(self): + return tuple([c + self.outer_diameter / 2. for c in self.position]) + + @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)) + + def to_inch(self): + self.position = tuple(map(inch, self.position)) + self.inner_diameter = inch(self.inner_diameter) + self.outer_diameter = inch(self.outer_diameter) + + def to_metric(self): + self.position = tuple(map(metric, self.position)) + self.inner_diameter = metric(self.inner_diameter) + self.outer_diameter = metric(self.outer_diameter) def offset(self, x_offset=0, y_offset=0): self.position = tuple(map(add, self.position, (x_offset, y_offset))) @@ -773,8 +813,7 @@ class Drill(Primitive): """ def __init__(self, position, diameter): super(Drill, self).__init__('dark') - if len(position) != 2: - raise TypeError('Position must be a tuple (n=2) of coordinates') + validate_coordinates(position) self.position = position self.diameter = diameter @@ -801,3 +840,13 @@ class Drill(Primitive): def offset(self, x_offset=0, y_offset=0): self.position = tuple(map(add, self.position, (x_offset, y_offset))) +class TestRecord(Primitive): + """ Netlist Test record + """ + def __init__(self, position, net_name, layer, **kwargs): + super(TestRecord, self).__init__(**kwargs) + validate_coordinates(position) + self.position = position + self.net_name = net_name + self.layer = layer + -- cgit From 390838fc8b70c9b105fdc1d3e35a4533b27faa83 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Fri, 24 Apr 2015 10:54:13 -0400 Subject: Fix for #25. Checking was happening at the gerber/excellon file level, but I added units checking at the primitive level so the use case shown in the example is covered. Might want to throw a bunch more assertions in the test code (i started doing a few) to cover multiple calls to unit conversion functions --- gerber/primitives.py | 255 ++++++++++++++++++++++++++++++++------------------- 1 file changed, 160 insertions(+), 95 deletions(-) (limited to 'gerber/primitives.py') diff --git a/gerber/primitives.py b/gerber/primitives.py index 5d0b8cf..c0a6259 100644 --- a/gerber/primitives.py +++ b/gerber/primitives.py @@ -36,9 +36,10 @@ class Primitive(object): 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): + def __init__(self, level_polarity='dark', rotation=0, units=None): self.level_polarity = level_polarity self.rotation = rotation + self.units = units def bounding_box(self): """ Calculate bounding box @@ -141,14 +142,18 @@ class Line(Primitive): def to_inch(self): - self.aperture.to_inch() - self.start = tuple(map(inch, self.start)) - self.end = tuple(map(inch, self.end)) + if self.units == 'metric': + self.units = 'inch' + self.aperture.to_inch() + self.start = tuple(map(inch, self.start)) + self.end = tuple(map(inch, self.end)) def to_metric(self): - self.aperture.to_metric() - self.start = tuple(map(metric, self.start)) - self.end = tuple(map(metric, self.end)) + if self.units == 'inch': + self.units = 'metric' + self.aperture.to_metric() + self.start = tuple(map(metric, self.start)) + self.end = tuple(map(metric, self.end)) def offset(self, x_offset=0, y_offset=0): self.start = tuple(map(add, self.start, (x_offset, y_offset))) @@ -232,16 +237,20 @@ class Arc(Primitive): return ((min_x, max_x), (min_y, max_y)) def to_inch(self): - self.aperture.to_inch() - self.start = tuple(map(inch, self.start)) - self.end = tuple(map(inch, self.end)) - self.center = tuple(map(inch, self.center)) + if self.units == 'metric': + self.units = 'inch' + self.aperture.to_inch() + self.start = tuple(map(inch, self.start)) + self.end = tuple(map(inch, self.end)) + self.center = tuple(map(inch, self.center)) def to_metric(self): - self.aperture.to_metric() - self.start = tuple(map(metric, self.start)) - self.end = tuple(map(metric, self.end)) - self.center = tuple(map(metric, self.center)) + if self.units == 'inch': + self.units = 'metric' + self.aperture.to_metric() + self.start = tuple(map(metric, self.start)) + self.end = tuple(map(metric, self.end)) + self.center = tuple(map(metric, self.center)) def offset(self, x_offset=0, y_offset=0): self.start = tuple(map(add, self.start, (x_offset, y_offset))) @@ -271,14 +280,18 @@ class Circle(Primitive): return ((min_x, max_x), (min_y, max_y)) def to_inch(self): - if self.position is not None: - self.position = tuple(map(inch, self.position)) - self.diameter = inch(self.diameter) + if self.units == 'metric': + self.units = 'inch' + if self.position is not None: + self.position = tuple(map(inch, self.position)) + self.diameter = inch(self.diameter) def to_metric(self): - if self.position is not None: - self.position = tuple(map(metric, self.position)) - self.diameter = metric(self.diameter) + if self.units == 'inch': + self.units = 'metric' + if self.position is not None: + self.position = tuple(map(metric, self.position)) + self.diameter = metric(self.diameter) def offset(self, x_offset=0, y_offset=0): self.position = tuple(map(add, self.position, (x_offset, y_offset))) @@ -310,14 +323,18 @@ class Ellipse(Primitive): return ((min_x, max_x), (min_y, max_y)) def to_inch(self): - self.position = tuple(map(inch, self.position)) - self.width = inch(self.width) - self.height = inch(self.height) + if self.units == 'metric': + self.units = 'inch' + self.position = tuple(map(inch, self.position)) + self.width = inch(self.width) + self.height = inch(self.height) def to_metric(self): - self.position = tuple(map(metric, self.position)) - self.width = metric(self.width) - self.height = metric(self.height) + if self.units == 'inch': + self.units = 'metric' + self.position = tuple(map(metric, self.position)) + self.width = metric(self.width) + self.height = metric(self.height) def offset(self, x_offset=0, y_offset=0): self.position = tuple(map(add, self.position, (x_offset, y_offset))) @@ -357,14 +374,18 @@ class Rectangle(Primitive): return ((min_x, max_x), (min_y, max_y)) def to_inch(self): - self.position = tuple(map(inch, self.position)) - self.width = inch(self.width) - self.height = inch(self.height) + if self.units == 'metric': + self.units = 'inch' + self.position = tuple(map(inch, self.position)) + self.width = inch(self.width) + self.height = inch(self.height) def to_metric(self): - self.position = tuple(map(metric, self.position)) - self.width = metric(self.width) - self.height = metric(self.height) + if self.units == 'inch': + self.units = 'metric' + self.position = tuple(map(metric, self.position)) + self.width = metric(self.width) + self.height = metric(self.height) def offset(self, x_offset=0, y_offset=0): self.position = tuple(map(add, self.position, (x_offset, y_offset))) @@ -404,14 +425,18 @@ class Diamond(Primitive): return ((min_x, max_x), (min_y, max_y)) def to_inch(self): - self.position = tuple(map(inch, self.position)) - self.width = inch(self.width) - self.height = inch(self.height) + if self.units == 'metric': + self.units = 'inch' + self.position = tuple(map(inch, self.position)) + self.width = inch(self.width) + self.height = inch(self.height) def to_metric(self): - self.position = tuple(map(metric, self.position)) - self.width = metric(self.width) - self.height = metric(self.height) + if self.units == 'inch': + self.units = 'metric' + self.position = tuple(map(metric, self.position)) + self.width = metric(self.width) + self.height = metric(self.height) def offset(self, x_offset=0, y_offset=0): self.position = tuple(map(add, self.position, (x_offset, y_offset))) @@ -453,16 +478,20 @@ class ChamferRectangle(Primitive): return ((min_x, max_x), (min_y, max_y)) def to_inch(self): - self.position = tuple(map(inch, self.position)) - self.width = inch(self.width) - self.height = inch(self.height) - self.chamfer = inch(self.chamfer) + if self.units == 'metric': + self.units = 'inch' + self.position = tuple(map(inch, self.position)) + self.width = inch(self.width) + self.height = inch(self.height) + self.chamfer = inch(self.chamfer) def to_metric(self): - self.position = tuple(map(metric, self.position)) - self.width = metric(self.width) - self.height = metric(self.height) - self.chamfer = metric(self.chamfer) + if self.units == 'inch': + self.units = 'metric' + self.position = tuple(map(metric, self.position)) + self.width = metric(self.width) + self.height = metric(self.height) + self.chamfer = metric(self.chamfer) def offset(self, x_offset=0, y_offset=0): self.position = tuple(map(add, self.position, (x_offset, y_offset))) @@ -504,16 +533,20 @@ class RoundRectangle(Primitive): return ((min_x, max_x), (min_y, max_y)) def to_inch(self): - self.position = tuple(map(inch, self.position)) - self.width = inch(self.width) - self.height = inch(self.height) - self.radius = inch(self.radius) + if self.units == 'metric': + self.units = 'inch' + self.position = tuple(map(inch, self.position)) + self.width = inch(self.width) + self.height = inch(self.height) + self.radius = inch(self.radius) def to_metric(self): - self.position = tuple(map(metric, self.position)) - self.width = metric(self.width) - self.height = metric(self.height) - self.radius = metric(self.radius) + if self.units == 'inch': + self.units = 'metric' + self.position = tuple(map(metric, self.position)) + self.width = metric(self.width) + self.height = metric(self.height) + self.radius = metric(self.radius) def offset(self, x_offset=0, y_offset=0): self.position = tuple(map(add, self.position, (x_offset, y_offset))) @@ -575,14 +608,18 @@ class Obround(Primitive): return {'circle1': circle1, 'circle2': circle2, 'rectangle': rect} def to_inch(self): - self.position = tuple(map(inch, self.position)) - self.width = inch(self.width) - self.height = inch(self.height) + if self.units == 'metric': + self.units = 'inch' + self.position = tuple(map(inch, self.position)) + self.width = inch(self.width) + self.height = inch(self.height) def to_metric(self): - self.position = tuple(map(metric, self.position)) - self.width = metric(self.width) - self.height = metric(self.height) + if self.units == 'inch': + self.units = 'metric' + self.position = tuple(map(metric, self.position)) + self.width = metric(self.width) + self.height = metric(self.height) def offset(self, x_offset=0, y_offset=0): self.position = tuple(map(add, self.position, (x_offset, y_offset))) @@ -607,12 +644,16 @@ class Polygon(Primitive): return ((min_x, max_x), (min_y, max_y)) def to_inch(self): - self.position = tuple(map(inch, self.position)) - self.radius = inch(self.radius) + if self.units == 'metric': + self.units = 'inch' + self.position = tuple(map(inch, self.position)) + self.radius = inch(self.radius) def to_metric(self): - self.position = tuple(map(metric, self.position)) - self.radius = metric(self.radius) + if self.units == 'inch': + self.units = 'metric' + self.position = tuple(map(metric, self.position)) + self.radius = metric(self.radius) def offset(self, x_offset=0, y_offset=0): self.position = tuple(map(add, self.position, (x_offset, y_offset))) @@ -635,10 +676,14 @@ class Region(Primitive): return ((min_x, max_x), (min_y, max_y)) def to_inch(self): - self.points = [tuple(map(inch, point)) for point in self.points] + if self.units == 'metric': + self.units = 'inch' + self.points = [tuple(map(inch, point)) for point in self.points] def to_metric(self): - self.points = [tuple(map(metric, point)) for point in self.points] + if self.units == 'inch': + self.units = 'metric' + self.points = [tuple(map(metric, point)) for point in self.points] def offset(self, x_offset=0, y_offset=0): self.points = [tuple(map(add, point, (x_offset, y_offset))) @@ -667,12 +712,16 @@ class RoundButterfly(Primitive): return ((min_x, max_x), (min_y, max_y)) def to_inch(self): - self.position = tuple(map(inch, self.position)) - self.diameter = inch(self.diameter) + if self.units == 'metric': + self.units = 'inch' + self.position = tuple(map(inch, self.position)) + self.diameter = inch(self.diameter) def to_metric(self): - self.position = tuple(map(metric, self.position)) - self.diameter = metric(self.diameter) + if self.units == 'inch': + self.units = 'metric' + self.position = tuple(map(metric, self.position)) + self.diameter = metric(self.diameter) def offset(self, x_offset=0, y_offset=0): self.position = tuple(map(add, self.position, (x_offset, y_offset))) @@ -697,12 +746,16 @@ class SquareButterfly(Primitive): return ((min_x, max_x), (min_y, max_y)) def to_inch(self): - self.position = tuple(map(inch, self.position)) - self.side = inch(self.side) + if self.units == 'metric': + self.units = 'inch' + self.position = tuple(map(inch, self.position)) + self.side = inch(self.side) def to_metric(self): - self.position = tuple(map(metric, self.position)) - self.side = metric(self.side) + if self.units == 'inch': + self.units = 'metric' + self.position = tuple(map(metric, self.position)) + self.side = metric(self.side) def offset(self, x_offset=0, y_offset=0): self.position = tuple(map(add, self.position, (x_offset, y_offset))) @@ -749,18 +802,22 @@ class Donut(Primitive): return ((min_x, max_x), (min_y, max_y)) def to_inch(self): - self.position = tuple(map(inch, self.position)) - self.width = inch(self.width) - self.height = inch(self.height) - self.inner_diameter = inch(self.inner_diameter) - self.outer_diameter = inch(self.outer_diameter) + if self.units == 'metric': + self.units = 'inch' + self.position = tuple(map(inch, self.position)) + self.width = inch(self.width) + self.height = inch(self.height) + self.inner_diameter = inch(self.inner_diameter) + self.outer_diameter = inch(self.outer_diameter) def to_metric(self): - self.position = tuple(map(metric, self.position)) - self.width = metric(self.width) - self.height = metric(self.height) - self.inner_diameter = metric(self.inner_diameter) - self.outer_diameter = metric(self.outer_diameter) + if self.units == 'inch': + self.units = 'metric' + self.position = tuple(map(metric, self.position)) + self.width = metric(self.width) + self.height = metric(self.height) + self.inner_diameter = metric(self.inner_diameter) + self.outer_diameter = metric(self.outer_diameter) def offset(self, x_offset=0, y_offset=0): self.position = tuple(map(add, self.position, (x_offset, y_offset))) @@ -795,14 +852,18 @@ class SquareRoundDonut(Primitive): return ((min_x, max_x), (min_y, max_y)) def to_inch(self): - self.position = tuple(map(inch, self.position)) - self.inner_diameter = inch(self.inner_diameter) - self.outer_diameter = inch(self.outer_diameter) + if self.units == 'metric': + self.units = 'inch' + self.position = tuple(map(inch, self.position)) + self.inner_diameter = inch(self.inner_diameter) + self.outer_diameter = inch(self.outer_diameter) def to_metric(self): - self.position = tuple(map(metric, self.position)) - self.inner_diameter = metric(self.inner_diameter) - self.outer_diameter = metric(self.outer_diameter) + if self.units == 'inch': + self.units = 'metric' + self.position = tuple(map(metric, self.position)) + self.inner_diameter = metric(self.inner_diameter) + self.outer_diameter = metric(self.outer_diameter) def offset(self, x_offset=0, y_offset=0): self.position = tuple(map(add, self.position, (x_offset, y_offset))) @@ -811,8 +872,8 @@ class SquareRoundDonut(Primitive): class Drill(Primitive): """ A drill hole """ - def __init__(self, position, diameter): - super(Drill, self).__init__('dark') + def __init__(self, position, diameter, **kwargs): + super(Drill, self).__init__('dark', **kwargs) validate_coordinates(position) self.position = position self.diameter = diameter @@ -830,10 +891,14 @@ class Drill(Primitive): return ((min_x, max_x), (min_y, max_y)) def to_inch(self): - self.position = tuple(map(inch, self.position)) - self.diameter = inch(self.diameter) + if self.units == 'metric': + self.units = 'inch' + self.position = tuple(map(inch, self.position)) + self.diameter = inch(self.diameter) def to_metric(self): + if self.units == 'inch': + self.units = 'metric' self.position = tuple(map(metric, self.position)) self.diameter = metric(self.diameter) -- cgit From a518043ae861a7c96042059857c6364fd0780bd5 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Silva Date: Fri, 24 Apr 2015 14:00:35 -0300 Subject: Fix indentation after PR #26 --- gerber/primitives.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'gerber/primitives.py') diff --git a/gerber/primitives.py b/gerber/primitives.py index c0a6259..4c027d2 100644 --- a/gerber/primitives.py +++ b/gerber/primitives.py @@ -899,8 +899,8 @@ class Drill(Primitive): def to_metric(self): if self.units == 'inch': self.units = 'metric' - self.position = tuple(map(metric, self.position)) - self.diameter = metric(self.diameter) + self.position = tuple(map(metric, self.position)) + self.diameter = metric(self.diameter) def offset(self, x_offset=0, y_offset=0): self.position = tuple(map(add, self.position, (x_offset, y_offset))) -- cgit From e34e1078b67f43be9b678a67cf30d3c53fdea171 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Sun, 26 Apr 2015 02:58:12 -0400 Subject: Refactor primitive unit conversion and add regression coverage to tests --- gerber/primitives.py | 360 +++++++++++++++------------------------------------ 1 file changed, 104 insertions(+), 256 deletions(-) (limited to 'gerber/primitives.py') diff --git a/gerber/primitives.py b/gerber/primitives.py index 4c027d2..bdd49f7 100644 --- a/gerber/primitives.py +++ b/gerber/primitives.py @@ -40,6 +40,7 @@ class Primitive(object): self.level_polarity = level_polarity self.rotation = rotation self.units = units + self._to_convert = list() def bounding_box(self): """ Calculate bounding box @@ -52,10 +53,39 @@ class Primitive(object): 'implemented in subclass') def to_inch(self): - pass + if self.units == 'metric': + self.units = 'inch' + for attr, value in [(attr, getattr(self, attr)) for attr in self._to_convert]: + if hasattr(value, 'to_inch'): + value.to_inch() + else: + try: + if len(value) > 1: + if isinstance(value[0], tuple): + setattr(self, attr, [tuple(map(inch, point)) for point in value]) + else: + setattr(self, attr, tuple(map(inch, value))) + except: + if value is not None: + setattr(self, attr, inch(value)) + def to_metric(self): - pass + if self.units == 'inch': + self.units = 'metric' + for attr, value in [(attr, getattr(self, attr)) for attr in self._to_convert]: + if hasattr(value, 'to_metric'): + value.to_metric() + else: + try: + if len(value) > 1: + if isinstance(value[0], tuple): + setattr(self, attr, [tuple(map(metric, point)) for point in value]) + else: + setattr(self, attr, tuple(map(metric, value))) + except: + if value is not None: + setattr(self, attr, metric(value)) def offset(self, x_offset=0, y_offset=0): pass @@ -72,6 +102,7 @@ class Line(Primitive): self.start = start self.end = end self.aperture = aperture + self._to_convert = ['start', 'end', 'aperture'] @property def angle(self): @@ -141,20 +172,6 @@ class Line(Primitive): return (start_ll, start_lr, start_ur, end_ur, end_ul, end_ll) - def to_inch(self): - if self.units == 'metric': - self.units = 'inch' - self.aperture.to_inch() - self.start = tuple(map(inch, self.start)) - self.end = tuple(map(inch, self.end)) - - def to_metric(self): - if self.units == 'inch': - self.units = 'metric' - self.aperture.to_metric() - self.start = tuple(map(metric, self.start)) - self.end = tuple(map(metric, self.end)) - 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))) @@ -170,6 +187,7 @@ class Arc(Primitive): self.center = center self.direction = direction self.aperture = aperture + self._to_convert = ['start', 'end', 'center', 'aperture'] @property def radius(self): @@ -236,22 +254,6 @@ class Arc(Primitive): max_y = max(y) + self.aperture.radius return ((min_x, max_x), (min_y, max_y)) - def to_inch(self): - if self.units == 'metric': - self.units = 'inch' - self.aperture.to_inch() - self.start = tuple(map(inch, self.start)) - self.end = tuple(map(inch, self.end)) - self.center = tuple(map(inch, self.center)) - - def to_metric(self): - if self.units == 'inch': - self.units = 'metric' - self.aperture.to_metric() - self.start = tuple(map(metric, self.start)) - self.end = tuple(map(metric, self.end)) - self.center = tuple(map(metric, self.center)) - 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))) @@ -266,6 +268,7 @@ class Circle(Primitive): validate_coordinates(position) self.position = position self.diameter = diameter + self._to_convert = ['position', 'diameter'] @property def radius(self): @@ -279,20 +282,6 @@ class Circle(Primitive): max_y = self.position[1] + self.radius return ((min_x, max_x), (min_y, max_y)) - def to_inch(self): - if self.units == 'metric': - self.units = 'inch' - if self.position is not None: - self.position = tuple(map(inch, self.position)) - self.diameter = inch(self.diameter) - - def to_metric(self): - if self.units == 'inch': - self.units = 'metric' - if self.position is not None: - self.position = tuple(map(metric, self.position)) - self.diameter = metric(self.diameter) - def offset(self, x_offset=0, y_offset=0): self.position = tuple(map(add, self.position, (x_offset, y_offset))) @@ -306,13 +295,8 @@ 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)) + self._to_convert = ['position', 'width', 'height'] + @property def bounding_box(self): @@ -322,23 +306,21 @@ class Ellipse(Primitive): max_y = self.position[1] + (self._abs_height / 2.0) return ((min_x, max_x), (min_y, max_y)) - def to_inch(self): - if self.units == 'metric': - self.units = 'inch' - self.position = tuple(map(inch, self.position)) - self.width = inch(self.width) - self.height = inch(self.height) - - def to_metric(self): - if self.units == 'inch': - self.units = 'metric' - self.position = tuple(map(metric, self.position)) - self.width = metric(self.width) - self.height = metric(self.height) - def offset(self, x_offset=0, y_offset=0): self.position = tuple(map(add, self.position, (x_offset, y_offset))) + @property + def _abs_width(self): + ux = (self.width / 2.) * math.cos(math.radians(self.rotation)) + vx = (self.height / 2.) * math.cos(math.radians(self.rotation) + (math.pi / 2.)) + return 2 * math.sqrt((ux * ux) + (vx * vx)) + + @property + def _abs_height(self): + uy = (self.width / 2.) * math.sin(math.radians(self.rotation)) + vy = (self.height / 2.) * math.sin(math.radians(self.rotation) + (math.pi / 2.)) + return 2 * math.sqrt((uy * uy) + (vy * vy)) + class Rectangle(Primitive): """ @@ -349,11 +331,8 @@ 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) + self._to_convert = ['position', 'width', 'height'] + @property def lower_left(self): @@ -373,23 +352,18 @@ class Rectangle(Primitive): max_y = self.upper_right[1] return ((min_x, max_x), (min_y, max_y)) - def to_inch(self): - if self.units == 'metric': - self.units = 'inch' - self.position = tuple(map(inch, self.position)) - self.width = inch(self.width) - self.height = inch(self.height) - - def to_metric(self): - if self.units == 'inch': - self.units = 'metric' - self.position = tuple(map(metric, self.position)) - self.width = metric(self.width) - self.height = metric(self.height) - def offset(self, x_offset=0, y_offset=0): self.position = tuple(map(add, self.position, (x_offset, y_offset))) + @property + def _abs_width(self): + return (math.cos(math.radians(self.rotation)) * self.width + + math.sin(math.radians(self.rotation)) * self.height) + @property + def _abs_height(self): + return (math.cos(math.radians(self.rotation)) * self.height + + math.sin(math.radians(self.rotation)) * self.width) + class Diamond(Primitive): """ @@ -400,11 +374,7 @@ class Diamond(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) + self._to_convert = ['position', 'width', 'height'] @property def lower_left(self): @@ -424,23 +394,18 @@ class Diamond(Primitive): max_y = self.upper_right[1] return ((min_x, max_x), (min_y, max_y)) - def to_inch(self): - if self.units == 'metric': - self.units = 'inch' - self.position = tuple(map(inch, self.position)) - self.width = inch(self.width) - self.height = inch(self.height) - - def to_metric(self): - if self.units == 'inch': - self.units = 'metric' - self.position = tuple(map(metric, self.position)) - self.width = metric(self.width) - self.height = metric(self.height) - def offset(self, x_offset=0, y_offset=0): self.position = tuple(map(add, self.position, (x_offset, y_offset))) + @property + def _abs_width(self): + return (math.cos(math.radians(self.rotation)) * self.width + + math.sin(math.radians(self.rotation)) * self.height) + @property + def _abs_height(self): + return (math.cos(math.radians(self.rotation)) * self.height + + math.sin(math.radians(self.rotation)) * self.width) + class ChamferRectangle(Primitive): """ @@ -453,11 +418,7 @@ class ChamferRectangle(Primitive): 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) + self._to_convert = ['position', 'width', 'height', 'chamfer'] @property def lower_left(self): @@ -477,25 +438,17 @@ class ChamferRectangle(Primitive): max_y = self.upper_right[1] return ((min_x, max_x), (min_y, max_y)) - def to_inch(self): - if self.units == 'metric': - self.units = 'inch' - self.position = tuple(map(inch, self.position)) - self.width = inch(self.width) - self.height = inch(self.height) - self.chamfer = inch(self.chamfer) - - def to_metric(self): - if self.units == 'inch': - self.units = 'metric' - self.position = tuple(map(metric, self.position)) - self.width = metric(self.width) - self.height = metric(self.height) - self.chamfer = metric(self.chamfer) - def offset(self, x_offset=0, y_offset=0): self.position = tuple(map(add, self.position, (x_offset, y_offset))) + @property + def _abs_width(self): + return (math.cos(math.radians(self.rotation)) * self.width + + math.sin(math.radians(self.rotation)) * self.height) + @property + def _abs_height(self): + return (math.cos(math.radians(self.rotation)) * self.height + + math.sin(math.radians(self.rotation)) * self.width) class RoundRectangle(Primitive): """ @@ -508,11 +461,7 @@ class RoundRectangle(Primitive): 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) + self._to_convert = ['position', 'width', 'height', 'radius'] @property def lower_left(self): @@ -532,25 +481,17 @@ class RoundRectangle(Primitive): max_y = self.upper_right[1] return ((min_x, max_x), (min_y, max_y)) - def to_inch(self): - if self.units == 'metric': - self.units = 'inch' - self.position = tuple(map(inch, self.position)) - self.width = inch(self.width) - self.height = inch(self.height) - self.radius = inch(self.radius) - - def to_metric(self): - if self.units == 'inch': - self.units = 'metric' - self.position = tuple(map(metric, self.position)) - self.width = metric(self.width) - self.height = metric(self.height) - self.radius = metric(self.radius) - def offset(self, x_offset=0, y_offset=0): self.position = tuple(map(add, self.position, (x_offset, y_offset))) + @property + def _abs_width(self): + return (math.cos(math.radians(self.rotation)) * self.width + + math.sin(math.radians(self.rotation)) * self.height) + @property + def _abs_height(self): + return (math.cos(math.radians(self.rotation)) * self.height + + math.sin(math.radians(self.rotation)) * self.width) class Obround(Primitive): """ @@ -561,11 +502,7 @@ class Obround(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) + self._to_convert = ['position', 'width', 'height'] @property def lower_left(self): @@ -607,23 +544,17 @@ class Obround(Primitive): self.height) return {'circle1': circle1, 'circle2': circle2, 'rectangle': rect} - def to_inch(self): - if self.units == 'metric': - self.units = 'inch' - self.position = tuple(map(inch, self.position)) - self.width = inch(self.width) - self.height = inch(self.height) - - def to_metric(self): - if self.units == 'inch': - self.units = 'metric' - self.position = tuple(map(metric, self.position)) - self.width = metric(self.width) - self.height = metric(self.height) - def offset(self, x_offset=0, y_offset=0): self.position = tuple(map(add, self.position, (x_offset, y_offset))) + @property + def _abs_width(self): + return (math.cos(math.radians(self.rotation)) * self.width + + math.sin(math.radians(self.rotation)) * self.height) + @property + def _abs_height(self): + return (math.cos(math.radians(self.rotation)) * self.height + + math.sin(math.radians(self.rotation)) * self.width) class Polygon(Primitive): """ @@ -634,6 +565,7 @@ class Polygon(Primitive): self.position = position self.sides = sides self.radius = radius + self._to_convert = ['position', 'radius'] @property def bounding_box(self): @@ -643,18 +575,6 @@ class Polygon(Primitive): max_y = self.position[1] + self.radius return ((min_x, max_x), (min_y, max_y)) - def to_inch(self): - if self.units == 'metric': - self.units = 'inch' - self.position = tuple(map(inch, self.position)) - self.radius = inch(self.radius) - - def to_metric(self): - if self.units == 'inch': - self.units = 'metric' - self.position = tuple(map(metric, self.position)) - self.radius = metric(self.radius) - def offset(self, x_offset=0, y_offset=0): self.position = tuple(map(add, self.position, (x_offset, y_offset))) @@ -665,6 +585,7 @@ class Region(Primitive): def __init__(self, points, **kwargs): super(Region, self).__init__(**kwargs) self.points = points + self._to_convert = ['points'] @property def bounding_box(self): @@ -675,16 +596,6 @@ class Region(Primitive): max_y = max(y_list) return ((min_x, max_x), (min_y, max_y)) - def to_inch(self): - if self.units == 'metric': - self.units = 'inch' - self.points = [tuple(map(inch, point)) for point in self.points] - - def to_metric(self): - if self.units == 'inch': - self.units = 'metric' - self.points = [tuple(map(metric, point)) for point in self.points] - def offset(self, x_offset=0, y_offset=0): self.points = [tuple(map(add, point, (x_offset, y_offset))) for point in self.points] @@ -698,6 +609,7 @@ class RoundButterfly(Primitive): validate_coordinates(position) self.position = position self.diameter = diameter + self._to_convert = ['position', 'diameter'] @property def radius(self): @@ -711,18 +623,6 @@ class RoundButterfly(Primitive): max_y = self.position[1] + self.radius return ((min_x, max_x), (min_y, max_y)) - def to_inch(self): - if self.units == 'metric': - self.units = 'inch' - self.position = tuple(map(inch, self.position)) - self.diameter = inch(self.diameter) - - def to_metric(self): - if self.units == 'inch': - self.units = 'metric' - self.position = tuple(map(metric, self.position)) - self.diameter = metric(self.diameter) - def offset(self, x_offset=0, y_offset=0): self.position = tuple(map(add, self.position, (x_offset, y_offset))) @@ -735,6 +635,7 @@ class SquareButterfly(Primitive): validate_coordinates(position) self.position = position self.side = side + self._to_convert = ['position', 'side'] @property @@ -745,18 +646,6 @@ class SquareButterfly(Primitive): max_y = self.position[1] + (self.side / 2.) return ((min_x, max_x), (min_y, max_y)) - def to_inch(self): - if self.units == 'metric': - self.units = 'inch' - self.position = tuple(map(inch, self.position)) - self.side = inch(self.side) - - def to_metric(self): - if self.units == 'inch': - self.units = 'metric' - self.position = tuple(map(metric, self.position)) - self.side = metric(self.side) - def offset(self, x_offset=0, y_offset=0): self.position = tuple(map(add, self.position, (x_offset, y_offset))) @@ -782,6 +671,7 @@ class Donut(Primitive): # Hexagon self.width = 0.5 * math.sqrt(3.) * outer_diameter self.height = outer_diameter + self._to_convert = ['position', 'width', 'height', 'inner_diameter', 'outer_diameter'] @property def lower_left(self): @@ -801,24 +691,6 @@ class Donut(Primitive): max_y = self.upper_right[1] return ((min_x, max_x), (min_y, max_y)) - def to_inch(self): - if self.units == 'metric': - self.units = 'inch' - self.position = tuple(map(inch, self.position)) - self.width = inch(self.width) - self.height = inch(self.height) - self.inner_diameter = inch(self.inner_diameter) - self.outer_diameter = inch(self.outer_diameter) - - def to_metric(self): - if self.units == 'inch': - self.units = 'metric' - self.position = tuple(map(metric, self.position)) - self.width = metric(self.width) - self.height = metric(self.height) - self.inner_diameter = metric(self.inner_diameter) - self.outer_diameter = metric(self.outer_diameter) - def offset(self, x_offset=0, y_offset=0): self.position = tuple(map(add, self.position, (x_offset, y_offset))) @@ -834,6 +706,7 @@ class SquareRoundDonut(Primitive): raise ValueError('Outer diameter must be larger than inner diameter.') self.inner_diameter = inner_diameter self.outer_diameter = outer_diameter + self._to_convert = ['position', 'inner_diameter', 'outer_diameter'] @property def lower_left(self): @@ -851,20 +724,6 @@ class SquareRoundDonut(Primitive): max_y = self.upper_right[1] return ((min_x, max_x), (min_y, max_y)) - def to_inch(self): - if self.units == 'metric': - self.units = 'inch' - self.position = tuple(map(inch, self.position)) - self.inner_diameter = inch(self.inner_diameter) - self.outer_diameter = inch(self.outer_diameter) - - def to_metric(self): - if self.units == 'inch': - self.units = 'metric' - self.position = tuple(map(metric, self.position)) - self.inner_diameter = metric(self.inner_diameter) - self.outer_diameter = metric(self.outer_diameter) - def offset(self, x_offset=0, y_offset=0): self.position = tuple(map(add, self.position, (x_offset, y_offset))) @@ -877,6 +736,7 @@ class Drill(Primitive): validate_coordinates(position) self.position = position self.diameter = diameter + self._to_convert = ['position', 'diameter'] @property def radius(self): @@ -890,18 +750,6 @@ class Drill(Primitive): max_y = self.position[1] + self.radius return ((min_x, max_x), (min_y, max_y)) - def to_inch(self): - if self.units == 'metric': - self.units = 'inch' - self.position = tuple(map(inch, self.position)) - self.diameter = inch(self.diameter) - - def to_metric(self): - if self.units == 'inch': - self.units = 'metric' - self.position = tuple(map(metric, self.position)) - self.diameter = metric(self.diameter) - def offset(self, x_offset=0, y_offset=0): self.position = tuple(map(add, self.position, (x_offset, y_offset))) -- cgit From 94f3976915d64a77135a1fdc8983085ee8d2e1f9 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Thu, 11 Jun 2015 11:20:56 -0400 Subject: Add keys to statements for linking to primitives. Add some API features to ExcellonFile, such as getting a tool path length and changing tool parameters. Excellonfiles write method generates statements based on the drill hits in the hits member, so drill hits in a generated file can be re-ordered by re-ordering the drill hits in ExcellonFile.hits. see #30 --- gerber/primitives.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'gerber/primitives.py') diff --git a/gerber/primitives.py b/gerber/primitives.py index bdd49f7..00ecb12 100644 --- a/gerber/primitives.py +++ b/gerber/primitives.py @@ -36,11 +36,13 @@ class Primitive(object): 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, units=None): + def __init__(self, level_polarity='dark', rotation=0, units=None, id=None, statement_id=None): self.level_polarity = level_polarity self.rotation = rotation self.units = units self._to_convert = list() + self.id = id + self.statement_id = statement_id def bounding_box(self): """ Calculate bounding box -- cgit From cb2fa34e881a389cf8a4bc98fd12be662ff687f8 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Sun, 9 Aug 2015 15:11:13 -0400 Subject: Add support for arcs in regions. This fixes the circular cutout issue described in #32. Regions were previously stored as a collection of points, now they are stored as a collection of line and arc primitives. --- gerber/primitives.py | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) (limited to 'gerber/primitives.py') diff --git a/gerber/primitives.py b/gerber/primitives.py index bdd49f7..e207fa8 100644 --- a/gerber/primitives.py +++ b/gerber/primitives.py @@ -61,7 +61,10 @@ class Primitive(object): else: try: if len(value) > 1: - if isinstance(value[0], tuple): + if hasattr(value[0], 'to_inch'): + for v in value: + v.to_inch() + elif isinstance(value[0], tuple): setattr(self, attr, [tuple(map(inch, point)) for point in value]) else: setattr(self, attr, tuple(map(inch, value))) @@ -79,7 +82,10 @@ class Primitive(object): else: try: if len(value) > 1: - if isinstance(value[0], tuple): + if hasattr(value[0], 'to_metric'): + for v in value: + v.to_metric() + elif isinstance(value[0], tuple): setattr(self, attr, [tuple(map(metric, point)) for point in value]) else: setattr(self, attr, tuple(map(metric, value))) @@ -582,23 +588,25 @@ class Polygon(Primitive): class Region(Primitive): """ """ - def __init__(self, points, **kwargs): + def __init__(self, primitives, **kwargs): super(Region, self).__init__(**kwargs) - self.points = points - self._to_convert = ['points'] + self.primitives = primitives + self._to_convert = ['primitives'] @property def bounding_box(self): - x_list, y_list = zip(*self.points) - min_x = min(x_list) - max_x = max(x_list) - min_y = min(y_list) - max_y = max(y_list) + xlims, ylims = zip(*[p.bounding_box for p in self.primitives]) + minx, maxx = zip(*xlims) + miny, maxy = zip(*ylims) + min_x = min(minx) + max_x = max(maxx) + min_y = min(miny) + max_y = max(maxy) return ((min_x, max_x), (min_y, max_y)) def offset(self, x_offset=0, y_offset=0): - self.points = [tuple(map(add, point, (x_offset, y_offset))) - for point in self.points] + for p in self.primitives: + p.offset(x_offset, y_offset) class RoundButterfly(Primitive): -- cgit From d69f50e0f62570a4c327cb8fe4f886f439196010 Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Wed, 2 Dec 2015 12:44:30 +0800 Subject: Make the hit accessible from the drawable Hit, fix crash with cario drawing rect --- gerber/primitives.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'gerber/primitives.py') diff --git a/gerber/primitives.py b/gerber/primitives.py index 0ac12af..1e26f19 100644 --- a/gerber/primitives.py +++ b/gerber/primitives.py @@ -741,11 +741,12 @@ class SquareRoundDonut(Primitive): class Drill(Primitive): """ A drill hole """ - def __init__(self, position, diameter, **kwargs): + def __init__(self, position, diameter, hit, **kwargs): super(Drill, self).__init__('dark', **kwargs) validate_coordinates(position) self.position = position self.diameter = diameter + self.hit = hit self._to_convert = ['position', 'diameter'] @property -- cgit From 206f4c57ab66f8a6753015340315991b40178c9b Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Wed, 16 Dec 2015 18:59:25 +0800 Subject: Fix drawing arcs. Dont crash for arcs with rectangular apertures. Fix crash with board size of zero for only one drill --- gerber/primitives.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) (limited to 'gerber/primitives.py') diff --git a/gerber/primitives.py b/gerber/primitives.py index 1e26f19..3f68496 100644 --- a/gerber/primitives.py +++ b/gerber/primitives.py @@ -256,10 +256,19 @@ 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) - self.aperture.radius - max_x = max(x) + self.aperture.radius - min_y = min(y) - self.aperture.radius - max_y = max(y) + self.aperture.radius + + if isinstance(self.aperture, Circle): + radius = self.aperture.radius + else: + # TODO this is actually not valid, but files contain it + width = self.aperture.width + height = self.aperture.height + radius = max(width, height) + + min_x = min(x) - radius + max_x = max(x) + radius + min_y = min(y) - radius + max_y = max(y) + radius return ((min_x, max_x), (min_y, max_y)) def offset(self, x_offset=0, y_offset=0): -- cgit From 4e838df32ac6d283429e30d2a3151b7d7e8e82b2 Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Sat, 19 Dec 2015 11:44:12 +0800 Subject: Parse misc nc drill files --- gerber/primitives.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'gerber/primitives.py') diff --git a/gerber/primitives.py b/gerber/primitives.py index 3f68496..3c630f0 100644 --- a/gerber/primitives.py +++ b/gerber/primitives.py @@ -755,7 +755,7 @@ class Drill(Primitive): validate_coordinates(position) self.position = position self.diameter = diameter - self.hit = hit + self.hit = hit self._to_convert = ['position', 'diameter'] @property -- cgit From cd0ed5aed07279c7ec6991043eeefadeb1620d5c Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Fri, 25 Dec 2015 14:43:44 +0800 Subject: Identify flashes and bounding box without aperture --- gerber/primitives.py | 136 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 133 insertions(+), 3 deletions(-) (limited to 'gerber/primitives.py') diff --git a/gerber/primitives.py b/gerber/primitives.py index 3c630f0..d964192 100644 --- a/gerber/primitives.py +++ b/gerber/primitives.py @@ -43,7 +43,15 @@ class Primitive(object): self._to_convert = list() self.id = id self.statement_id = statement_id + + @property + def flashed(self): + '''Is this a flashed primitive''' + + raise NotImplementedError('Is flashed must be ' + 'implemented in subclass') + @property def bounding_box(self): """ Calculate bounding box @@ -53,6 +61,17 @@ 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 def to_inch(self): if self.units == 'metric': @@ -111,6 +130,10 @@ class Line(Primitive): self.end = end self.aperture = aperture self._to_convert = ['start', 'end', 'aperture'] + + @property + def flashed(self): + return False @property def angle(self): @@ -131,6 +154,15 @@ class Line(Primitive): min_y = min(self.start[1], self.end[1]) - height_2 max_y = max(self.start[1], self.end[1]) + height_2 return ((min_x, max_x), (min_y, max_y)) + + @property + def bounding_box_no_aperture(self): + '''Gets the bounding box without the aperture''' + min_x = min(self.start[0], self.end[0]) + 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)) @property def vertices(self): @@ -197,6 +229,10 @@ class Arc(Primitive): self.aperture = aperture self._to_convert = ['start', 'end', 'center', 'aperture'] + @property + def flashed(self): + return False + @property def radius(self): dy, dx = map(sub, self.start, self.center) @@ -270,6 +306,47 @@ class Arc(Primitive): min_y = min(y) - radius max_y = max(y) + radius return ((min_x, max_x), (min_y, max_y)) + + @property + def bounding_box_no_aperture(self): + '''Gets the bounding box without considering the aperture''' + two_pi = 2 * math.pi + theta0 = (self.start_angle + two_pi) % two_pi + theta1 = (self.end_angle + two_pi) % two_pi + points = [self.start, self.end] + if self.direction == 'counterclockwise': + # Passes through 0 degrees + if theta0 > theta1: + points.append((self.center[0] + self.radius, self.center[1])) + # Passes through 90 degrees + if theta0 <= math.pi / 2. and (theta1 >= math.pi / 2. or theta1 < theta0): + points.append((self.center[0], self.center[1] + self.radius)) + # Passes through 180 degrees + if theta0 <= math.pi and (theta1 >= math.pi or theta1 < theta0): + points.append((self.center[0] - self.radius, self.center[1])) + # Passes through 270 degrees + if theta0 <= math.pi * 1.5 and (theta1 >= math.pi * 1.5 or theta1 < theta0): + points.append((self.center[0], self.center[1] - self.radius )) + else: + # Passes through 0 degrees + if theta1 > theta0: + points.append((self.center[0] + self.radius, self.center[1])) + # Passes through 90 degrees + if theta1 <= math.pi / 2. and (theta0 >= math.pi / 2. or theta0 < theta1): + points.append((self.center[0], self.center[1] + self.radius)) + # Passes through 180 degrees + if theta1 <= math.pi and (theta0 >= math.pi or theta0 < theta1): + points.append((self.center[0] - self.radius, self.center[1])) + # Passes through 270 degrees + 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)) def offset(self, x_offset=0, y_offset=0): self.start = tuple(map(add, self.start, (x_offset, y_offset))) @@ -287,6 +364,10 @@ class Circle(Primitive): self.diameter = diameter self._to_convert = ['position', 'diameter'] + @property + def flashed(self): + return True + @property def radius(self): return self.diameter / 2. @@ -314,6 +395,9 @@ class Ellipse(Primitive): self.height = height self._to_convert = ['position', 'width', 'height'] + @property + def flashed(self): + return True @property def bounding_box(self): @@ -350,7 +434,10 @@ class Rectangle(Primitive): self.height = height self._to_convert = ['position', 'width', 'height'] - + @property + def flashed(self): + return True + @property def lower_left(self): return (self.position[0] - (self._abs_width / 2.), @@ -392,6 +479,10 @@ class Diamond(Primitive): self.width = width self.height = height self._to_convert = ['position', 'width', 'height'] + + @property + def flashed(self): + return True @property def lower_left(self): @@ -436,6 +527,10 @@ class ChamferRectangle(Primitive): self.chamfer = chamfer self.corners = corners self._to_convert = ['position', 'width', 'height', 'chamfer'] + + @property + def flashed(self): + return True @property def lower_left(self): @@ -479,6 +574,10 @@ class RoundRectangle(Primitive): self.radius = radius self.corners = corners self._to_convert = ['position', 'width', 'height', 'radius'] + + @property + def flashed(self): + return True @property def lower_left(self): @@ -520,6 +619,10 @@ class Obround(Primitive): self.width = width self.height = height self._to_convert = ['position', 'width', 'height'] + + @property + def flashed(self): + return True @property def lower_left(self): @@ -583,6 +686,10 @@ class Polygon(Primitive): self.sides = sides self.radius = radius self._to_convert = ['position', 'radius'] + + @property + def flashed(self): + return True @property def bounding_box(self): @@ -603,6 +710,10 @@ class Region(Primitive): super(Region, self).__init__(**kwargs) self.primitives = primitives self._to_convert = ['primitives'] + + @property + def flashed(self): + return False @property def bounding_box(self): @@ -629,6 +740,10 @@ class RoundButterfly(Primitive): self.position = position self.diameter = diameter self._to_convert = ['position', 'diameter'] + + @property + def flashed(self): + return True @property def radius(self): @@ -655,7 +770,10 @@ class SquareButterfly(Primitive): self.position = position self.side = side self._to_convert = ['position', 'side'] - + + @property + def flashed(self): + return True @property def bounding_box(self): @@ -691,6 +809,10 @@ class Donut(Primitive): self.width = 0.5 * math.sqrt(3.) * outer_diameter self.height = outer_diameter self._to_convert = ['position', 'width', 'height', 'inner_diameter', 'outer_diameter'] + + @property + def flashed(self): + return True @property def lower_left(self): @@ -726,7 +848,11 @@ class SquareRoundDonut(Primitive): self.inner_diameter = inner_diameter self.outer_diameter = outer_diameter self._to_convert = ['position', 'inner_diameter', 'outer_diameter'] - + + @property + def flashed(self): + return True + @property def lower_left(self): return tuple([c - self.outer_diameter / 2. for c in self.position]) @@ -757,6 +883,10 @@ class Drill(Primitive): self.diameter = diameter self.hit = hit self._to_convert = ['position', 'diameter'] + + @property + def flashed(self): + return False @property def radius(self): -- cgit From 4a815bf25ddd1d378ec6ad5af008e5bbcd362b51 Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Wed, 30 Dec 2015 14:05:00 +0800 Subject: First time any macro renders --- gerber/primitives.py | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) (limited to 'gerber/primitives.py') diff --git a/gerber/primitives.py b/gerber/primitives.py index d964192..85035d2 100644 --- a/gerber/primitives.py +++ b/gerber/primitives.py @@ -18,6 +18,7 @@ import math from operator import add, sub from .utils import validate_coordinates, inch, metric +from jsonpickle.util import PRIMITIVES class Primitive(object): @@ -425,6 +426,10 @@ 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, **kwargs): super(Rectangle, self).__init__(**kwargs) @@ -702,6 +707,57 @@ class Polygon(Primitive): def offset(self, x_offset=0, y_offset=0): self.position = tuple(map(add, self.position, (x_offset, y_offset))) +class AMGroup(Primitive): + """ + """ + def __init__(self, amprimitives, **kwargs): + super(AMGroup, self).__init__(**kwargs) + + self.primitives = [] + for amprim in amprimitives: + prim = amprim.to_primitive(self.units) + if prim: + self.primitives.append(prim) + self._position = None + self._to_convert = ['arimitives'] + + @property + def flashed(self): + return True + + @property + def bounding_box(self): + xlims, ylims = zip(*[p.bounding_box for p in self.primitives]) + minx, maxx = zip(*xlims) + miny, maxy = zip(*ylims) + min_x = min(minx) + max_x = max(maxx) + min_y = min(miny) + max_y = max(maxy) + return ((min_x, max_x), (min_y, max_y)) + + @property + def position(self): + return self._position + + @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[0] - self._position[0] + else: + dx = new_pos[0] + dy = new_pos[1] + + for primitive in self.primitives: + primitive.offset(dx, dy) + + self._position = new_pos class Region(Primitive): """ -- cgit From 96692b22216fdfe11f2ded104ac0bdba3b7866a5 Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Wed, 30 Dec 2015 15:32:44 +0800 Subject: Render primitives for some aperture macros --- gerber/primitives.py | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) (limited to 'gerber/primitives.py') diff --git a/gerber/primitives.py b/gerber/primitives.py index 85035d2..86fd322 100644 --- a/gerber/primitives.py +++ b/gerber/primitives.py @@ -758,6 +758,47 @@ class AMGroup(Primitive): primitive.offset(dx, dy) self._position = new_pos + + +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'] + + @property + def flashed(self): + return True + + @property + def bounding_box(self): + xlims, ylims = zip(*[p.bounding_box for p in self.primitives]) + minx, maxx = zip(*xlims) + miny, maxy = zip(*ylims) + min_x = min(minx) + max_x = max(maxx) + min_y = min(miny) + max_y = max(maxy) + return ((min_x, max_x), (min_y, max_y)) + + def offset(self, x_offset=0, y_offset=0): + for p in self.primitives: + p.offset(x_offset, y_offset) + + @property + def width(self): + bounding_box = self.bounding_box() + return bounding_box[0][1] - bounding_box[0][0] + + @property + def width(self): + bounding_box = self.bounding_box() + return bounding_box[1][1] - bounding_box[1][0] + class Region(Primitive): """ -- cgit From f61eee807f87c329f6f88645ecdb48f01b887c52 Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Wed, 30 Dec 2015 18:44:07 +0800 Subject: Render polygon flashes --- gerber/primitives.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) (limited to 'gerber/primitives.py') diff --git a/gerber/primitives.py b/gerber/primitives.py index 86fd322..b0e17e9 100644 --- a/gerber/primitives.py +++ b/gerber/primitives.py @@ -17,7 +17,7 @@ import math from operator import add, sub -from .utils import validate_coordinates, inch, metric +from .utils import validate_coordinates, inch, metric, rotate_point from jsonpickle.util import PRIMITIVES @@ -683,6 +683,7 @@ class Obround(Primitive): class Polygon(Primitive): """ + Polygon flash defined by a set number of sized. """ def __init__(self, position, sides, radius, **kwargs): super(Polygon, self).__init__(**kwargs) @@ -706,6 +707,18 @@ class Polygon(Primitive): 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 = math.degrees(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 class AMGroup(Primitive): """ -- cgit From e84f131720e5952ba0dc20de8729bfd1d7aa0fe7 Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Sun, 31 Jan 2016 14:17:35 +0800 Subject: Add support for more excellon formats. Dont consider line width when determinging region bounding box --- gerber/primitives.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'gerber/primitives.py') diff --git a/gerber/primitives.py b/gerber/primitives.py index b0e17e9..81c5837 100644 --- a/gerber/primitives.py +++ b/gerber/primitives.py @@ -827,7 +827,7 @@ class Region(Primitive): @property def bounding_box(self): - xlims, ylims = zip(*[p.bounding_box for p in self.primitives]) + xlims, ylims = zip(*[p.bounding_box_no_aperture for p in self.primitives]) minx, maxx = zip(*xlims) miny, maxy = zip(*ylims) min_x = min(minx) -- cgit From 96bdd0f59dbda9b755b0eb28eb44cb9a6eae1410 Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Sun, 31 Jan 2016 15:24:57 +0800 Subject: Keep track of quadrant mode so we can draw full circles --- gerber/primitives.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'gerber/primitives.py') diff --git a/gerber/primitives.py b/gerber/primitives.py index 81c5837..944e34a 100644 --- a/gerber/primitives.py +++ b/gerber/primitives.py @@ -221,13 +221,14 @@ class Line(Primitive): class Arc(Primitive): """ """ - def __init__(self, start, end, center, direction, aperture, **kwargs): + def __init__(self, start, end, center, direction, aperture, quadrant_mode, **kwargs): super(Arc, self).__init__(**kwargs) self.start = start self.end = end self.center = center self.direction = direction self.aperture = aperture + self.quadrant_mode = quadrant_mode self._to_convert = ['start', 'end', 'center', 'aperture'] @property -- cgit From 5b93db47cd29e384ead918db1893f4cf58326f82 Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Tue, 2 Feb 2016 00:11:55 +0800 Subject: Draw thermal aperture macros (as approximation) --- gerber/primitives.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'gerber/primitives.py') diff --git a/gerber/primitives.py b/gerber/primitives.py index 944e34a..84115a6 100644 --- a/gerber/primitives.py +++ b/gerber/primitives.py @@ -730,7 +730,10 @@ class AMGroup(Primitive): self.primitives = [] for amprim in amprimitives: prim = amprim.to_primitive(self.units) - if prim: + if isinstance(prim, list): + for p in prim: + self.primitives.append(p) + elif prim: self.primitives.append(prim) self._position = None self._to_convert = ['arimitives'] -- cgit From 29c0d82bf53907030d11df9eb09471b716a0be2e Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Sat, 27 Feb 2016 15:24:36 +0800 Subject: RS274X backend for rendering. Incompelte still --- gerber/primitives.py | 46 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) (limited to 'gerber/primitives.py') diff --git a/gerber/primitives.py b/gerber/primitives.py index 84115a6..21efb55 100644 --- a/gerber/primitives.py +++ b/gerber/primitives.py @@ -17,8 +17,9 @@ import math from operator import add, sub -from .utils import validate_coordinates, inch, metric, rotate_point +from .utils import validate_coordinates, inch, metric, rotate_point, nearly_equal from jsonpickle.util import PRIMITIVES +from __builtin__ import False class Primitive(object): @@ -120,6 +121,9 @@ class Primitive(object): def __eq__(self, other): return self.__dict__ == other.__dict__ + + def to_statement(self): + pass class Line(Primitive): @@ -216,7 +220,16 @@ class Line(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))) + + 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)) + return nearly_equal(self.start, equiv_start) and nearly_equal(self.end, equiv_end) class Arc(Primitive): """ @@ -736,7 +749,7 @@ class AMGroup(Primitive): elif prim: self.primitives.append(prim) self._position = None - self._to_convert = ['arimitives'] + self._to_convert = ['primitives'] @property def flashed(self): @@ -776,6 +789,21 @@ class AMGroup(Primitive): 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 class Outline(Primitive): """ @@ -816,6 +844,20 @@ class Outline(Primitive): bounding_box = self.bounding_box() return bounding_box[1][1] - bounding_box[1][0] + 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 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): """ -- cgit From 223a010831f0d9dae4bd6d2e626a603a78eb0b1d Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Sat, 27 Feb 2016 18:18:04 +0800 Subject: Fix critical issue with rotatin points (when the angle is zero the y would be flipped). Render AM with outline to gerber --- gerber/primitives.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'gerber/primitives.py') diff --git a/gerber/primitives.py b/gerber/primitives.py index 21efb55..07a28db 100644 --- a/gerber/primitives.py +++ b/gerber/primitives.py @@ -779,7 +779,7 @@ class AMGroup(Primitive): if self._position: dx = new_pos[0] - self._position[0] - dy = new_pos[0] - self._position[0] + dy = new_pos[1] - self._position[1] else: dx = new_pos[0] dy = new_pos[1] -- cgit From 20a9af279ac2217a39b73903ff94b916a3025be2 Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Tue, 1 Mar 2016 00:06:14 +0800 Subject: More rendering of AMGroup to statements --- gerber/primitives.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) (limited to 'gerber/primitives.py') diff --git a/gerber/primitives.py b/gerber/primitives.py index 07a28db..3c85f17 100644 --- a/gerber/primitives.py +++ b/gerber/primitives.py @@ -397,6 +397,19 @@ class Circle(Primitive): 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: + return False + + equiv_position = tuple(map(add, other.position, offset)) + + return nearly_equal(self.position, equiv_position) class Ellipse(Primitive): @@ -487,6 +500,19 @@ class Rectangle(Primitive): return (math.cos(math.radians(self.rotation)) * self.height + math.sin(math.radians(self.rotation)) * self.width) + def equivalent(self, other, offset): + '''Is this the same as the other rect, ignoring the offiset?''' + + if not isinstance(other, Rectangle): + return False + + if self.width != other.width or self.height != other.height or self.rotation != other.rotation: + return False + + equiv_position = tuple(map(add, other.position, offset)) + + return nearly_equal(self.position, equiv_position) + class Diamond(Primitive): """ @@ -815,6 +841,9 @@ class Outline(Primitive): self.primitives = primitives self._to_convert = ['primitives'] + if self.primitives[0].start != self.primitives[-1].end: + raise ValueError('Outline must be closed') + @property def flashed(self): return True @@ -833,6 +862,9 @@ class Outline(Primitive): def offset(self, x_offset=0, y_offset=0): for p in self.primitives: p.offset(x_offset, y_offset) + + if self.primitives[0].start != self.primitives[-1].end: + raise ValueError('Outline must be closed') @property def width(self): -- cgit From 7f47aea332ee1df45c87baa304d95ed03cc59865 Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Sat, 5 Mar 2016 10:04:58 +0800 Subject: Write polygons to macros --- gerber/primitives.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) (limited to 'gerber/primitives.py') diff --git a/gerber/primitives.py b/gerber/primitives.py index 3c85f17..08aa634 100644 --- a/gerber/primitives.py +++ b/gerber/primitives.py @@ -736,6 +736,10 @@ class Polygon(Primitive): @property def flashed(self): return True + + @property + def diameter(self): + return self.radius * 2 @property def bounding_box(self): @@ -759,6 +763,20 @@ class Polygon(Primitive): points.append(rotate_point((self.position[0] + self.radius, self.position[1]), offset + da * i, self.position)) return points + + 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) + class AMGroup(Primitive): """ -- cgit From 5cb60d6385f167e814df7a608321a4f33da0e193 Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Sat, 5 Mar 2016 11:44:20 +0800 Subject: AM group hasn't implemented offset --- gerber/primitives.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) (limited to 'gerber/primitives.py') diff --git a/gerber/primitives.py b/gerber/primitives.py index 08aa634..a5ed491 100644 --- a/gerber/primitives.py +++ b/gerber/primitives.py @@ -117,7 +117,7 @@ class Primitive(object): setattr(self, attr, metric(value)) def offset(self, x_offset=0, y_offset=0): - pass + raise NotImplementedError('The offset member must be implemented') def __eq__(self, other): return self.__dict__ == other.__dict__ @@ -814,6 +814,12 @@ class AMGroup(Primitive): 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): ''' @@ -880,9 +886,6 @@ class Outline(Primitive): def offset(self, x_offset=0, y_offset=0): for p in self.primitives: p.offset(x_offset, y_offset) - - if self.primitives[0].start != self.primitives[-1].end: - raise ValueError('Outline must be closed') @property def width(self): -- cgit From 97924d188bf8fcc7d7537007464e65cbdc8c7bbb Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Sat, 5 Mar 2016 16:26:30 +0800 Subject: More robust writing, even for bad files. Remove accidentally added imports --- gerber/primitives.py | 2 -- 1 file changed, 2 deletions(-) (limited to 'gerber/primitives.py') diff --git a/gerber/primitives.py b/gerber/primitives.py index a5ed491..e5ff35f 100644 --- a/gerber/primitives.py +++ b/gerber/primitives.py @@ -18,8 +18,6 @@ import math from operator import add, sub from .utils import validate_coordinates, inch, metric, rotate_point, nearly_equal -from jsonpickle.util import PRIMITIVES -from __builtin__ import False class Primitive(object): -- cgit From acde19f205898188c03a46e5d8a7a6a4d4637a2d Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Sat, 26 Mar 2016 15:59:42 +0800 Subject: Support for the G85 slot statement --- gerber/primitives.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) (limited to 'gerber/primitives.py') diff --git a/gerber/primitives.py b/gerber/primitives.py index e5ff35f..3ecf0db 100644 --- a/gerber/primitives.py +++ b/gerber/primitives.py @@ -1109,6 +1109,42 @@ class Drill(Primitive): def offset(self, x_offset=0, y_offset=0): self.position = tuple(map(add, self.position, (x_offset, y_offset))) + + +class Slot(Primitive): + """ A drilled slot + """ + def __init__(self, start, end, diameter, hit, **kwargs): + super(Slot, self).__init__('dark', **kwargs) + validate_coordinates(start) + validate_coordinates(end) + self.start = start + self.end = end + self.diameter = diameter + self.hit = hit + self._to_convert = ['start', 'end', 'diameter'] + + @property + def flashed(self): + return False + + @property + def radius(self): + return self.diameter / 2. + + @property + def bounding_box(self): + radius = self.radius + min_x = min(self.start[0], self.end[0]) - radius + max_x = max(self.start[0], self.end[0]) + radius + min_y = min(self.start[1], self.end[1]) - radius + max_y = max(self.start[1], self.end[1]) + radius + return ((min_x, max_x), (min_y, max_y)) + + 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 From c9c1313d598d5afa8cb387a2cfcd4a4281086e01 Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Sat, 28 May 2016 12:36:31 +0800 Subject: Fix units statement. Keep track of original macro statement in the AMGroup --- gerber/primitives.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) (limited to 'gerber/primitives.py') diff --git a/gerber/primitives.py b/gerber/primitives.py index 3ecf0db..d74226d 100644 --- a/gerber/primitives.py +++ b/gerber/primitives.py @@ -763,9 +763,9 @@ class Polygon(Primitive): return points 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: @@ -779,7 +779,11 @@ class Polygon(Primitive): class AMGroup(Primitive): """ """ - def __init__(self, amprimitives, **kwargs): + 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 = [] @@ -792,6 +796,7 @@ class AMGroup(Primitive): self.primitives.append(prim) self._position = None self._to_convert = ['primitives'] + self.stmt = stmt @property def flashed(self): -- cgit From 5a20b2b92dc7ab82e1f196d1efbf4bb52a163720 Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Sat, 28 May 2016 14:14:49 +0800 Subject: Fix converting amgroup units --- gerber/primitives.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) (limited to 'gerber/primitives.py') diff --git a/gerber/primitives.py b/gerber/primitives.py index d74226d..a5c3055 100644 --- a/gerber/primitives.py +++ b/gerber/primitives.py @@ -795,9 +795,26 @@ class AMGroup(Primitive): elif prim: self.primitives.append(prim) self._position = None - self._to_convert = ['primitives'] + 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() + + + 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 -- cgit From 265aec83f6152387514eea75ee60241d55f702fd Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Sun, 19 Jun 2016 12:06:19 +0800 Subject: Offsetting amgroup was doubly offseting --- gerber/primitives.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'gerber/primitives.py') diff --git a/gerber/primitives.py b/gerber/primitives.py index a5c3055..aa6e661 100644 --- a/gerber/primitives.py +++ b/gerber/primitives.py @@ -835,7 +835,7 @@ class AMGroup(Primitive): return self._position def offset(self, x_offset=0, y_offset=0): - self.position = tuple(map(add, self.position, (x_offset, y_offset))) + self._position = tuple(map(add, self._position, (x_offset, y_offset))) for primitive in self.primitives: primitive.offset(x_offset, y_offset) -- cgit From ccb6eb7a766bd6edf314978f3ec4fc0dcd61652d Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Sat, 25 Jun 2016 16:46:44 +0800 Subject: Add support for polygon apertures --- gerber/primitives.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) (limited to 'gerber/primitives.py') diff --git a/gerber/primitives.py b/gerber/primitives.py index aa6e661..211acb8 100644 --- a/gerber/primitives.py +++ b/gerber/primitives.py @@ -721,14 +721,15 @@ class Obround(Primitive): class Polygon(Primitive): """ - Polygon flash defined by a set number of sized. + Polygon flash defined by a set number of sides. """ - def __init__(self, position, sides, radius, **kwargs): + def __init__(self, position, sides, radius, hole_radius, **kwargs): super(Polygon, self).__init__(**kwargs) validate_coordinates(position) self.position = position self.sides = sides self.radius = radius + self.hole_radius = hole_radius self._to_convert = ['position', 'radius'] @property @@ -753,7 +754,7 @@ class Polygon(Primitive): @property def vertices(self): - offset = math.degrees(self.rotation) + offset = self.rotation da = 360.0 / self.sides points = [] -- cgit From 52c6d4928a1b5fc65b95cf5b0784a560cec2ca1d Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Sat, 16 Jul 2016 15:49:48 +0800 Subject: Fix most broken tests so that I can safely merge into changes with known expected test result --- gerber/primitives.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) (limited to 'gerber/primitives.py') diff --git a/gerber/primitives.py b/gerber/primitives.py index 211acb8..90b6fb9 100644 --- a/gerber/primitives.py +++ b/gerber/primitives.py @@ -1112,7 +1112,7 @@ class Drill(Primitive): self.position = position self.diameter = diameter self.hit = hit - self._to_convert = ['position', 'diameter'] + self._to_convert = ['position', 'diameter', 'hit'] @property def flashed(self): @@ -1133,6 +1133,9 @@ class Drill(Primitive): def offset(self, x_offset=0, y_offset=0): 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 @@ -1145,7 +1148,7 @@ class Slot(Primitive): self.end = end self.diameter = diameter self.hit = hit - self._to_convert = ['start', 'end', 'diameter'] + self._to_convert = ['start', 'end', 'diameter', 'hit'] @property def flashed(self): -- cgit From 7cd6acf12670f3773113f67ed2acb35cb21c32a0 Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Sun, 24 Jul 2016 17:08:47 +0800 Subject: Add many render tests based on the Umaco gerger specification. Fix multiple rendering bugs, especially related to holes in flashed apertures --- gerber/primitives.py | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) (limited to 'gerber/primitives.py') diff --git a/gerber/primitives.py b/gerber/primitives.py index 90b6fb9..b8ee344 100644 --- a/gerber/primitives.py +++ b/gerber/primitives.py @@ -370,12 +370,13 @@ class Arc(Primitive): class Circle(Primitive): """ """ - def __init__(self, position, diameter, **kwargs): + def __init__(self, position, diameter, hole_diameter = 0, **kwargs): super(Circle, self).__init__(**kwargs) validate_coordinates(position) self.position = position self.diameter = diameter - self._to_convert = ['position', 'diameter'] + self.hole_diameter = hole_diameter + self._to_convert = ['position', 'diameter', 'hole_diameter'] @property def flashed(self): @@ -384,6 +385,10 @@ class Circle(Primitive): @property def radius(self): return self.diameter / 2. + + @property + def hole_radius(self): + return self.hole_diameter / 2. @property def bounding_box(self): @@ -402,7 +407,7 @@ class Circle(Primitive): if not isinstance(other, Circle): return False - if self.diameter != other.diameter: + if self.diameter != other.diameter or self.hole_diameter != other.hole_diameter: return False equiv_position = tuple(map(add, other.position, offset)) @@ -456,13 +461,14 @@ class Rectangle(Primitive): 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, **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._to_convert = ['position', 'width', 'height'] + self.hole_diameter = hole_diameter + self._to_convert = ['position', 'width', 'height', 'hole_diameter'] @property def flashed(self): @@ -477,6 +483,11 @@ class Rectangle(Primitive): def upper_right(self): return (self.position[0] + (self._abs_width / 2.), self.position[1] + (self._abs_height / 2.)) + + @property + def hole_radius(self): + """The radius of the hole. If there is no hole, returns 0""" + return self.hole_diameter / 2. @property def bounding_box(self): @@ -499,12 +510,12 @@ class Rectangle(Primitive): math.sin(math.radians(self.rotation)) * self.width) def equivalent(self, other, offset): - '''Is this the same as the other rect, ignoring the offiset?''' + """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: + 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)) @@ -655,13 +666,14 @@ class RoundRectangle(Primitive): class Obround(Primitive): """ """ - def __init__(self, position, width, height, **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._to_convert = ['position', 'width', 'height'] + self.hole_diameter = hole_diameter + self._to_convert = ['position', 'width', 'height', 'hole_diameter'] @property def flashed(self): @@ -676,6 +688,11 @@ class Obround(Primitive): def upper_right(self): return (self.position[0] + (self._abs_width / 2.), self.position[1] + (self._abs_height / 2.)) + + @property + def hole_radius(self): + """The radius of the hole. If there is no hole, returns 0""" + return self.hole_diameter / 2. @property def orientation(self): -- cgit From 965d3ce23b92f8aff1063debd6d3364de15791fe Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Sun, 24 Jul 2016 22:08:31 +0800 Subject: Add more tests for rendering to PNG. Start adding tests for rendering to Gerber format. Changed definition of no hole to use None instead of 0 so we can differentiate when writing to Gerber format. Makde polygon use hole diameter instead of hole radius to match other primitives --- gerber/primitives.py | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) (limited to 'gerber/primitives.py') diff --git a/gerber/primitives.py b/gerber/primitives.py index b8ee344..f259eff 100644 --- a/gerber/primitives.py +++ b/gerber/primitives.py @@ -370,7 +370,7 @@ class Arc(Primitive): class Circle(Primitive): """ """ - def __init__(self, position, diameter, hole_diameter = 0, **kwargs): + def __init__(self, position, diameter, hole_diameter = None, **kwargs): super(Circle, self).__init__(**kwargs) validate_coordinates(position) self.position = position @@ -388,7 +388,9 @@ class Circle(Primitive): @property def hole_radius(self): - return self.hole_diameter / 2. + if self.hole_diameter != None: + return self.hole_diameter / 2. + return None @property def bounding_box(self): @@ -486,8 +488,10 @@ class Rectangle(Primitive): @property def hole_radius(self): - """The radius of the hole. If there is no hole, returns 0""" - return self.hole_diameter / 2. + """The radius of the hole. If there is no hole, returns None""" + if self.hole_diameter != None: + return self.hole_diameter / 2. + return None @property def bounding_box(self): @@ -691,8 +695,10 @@ class Obround(Primitive): @property def hole_radius(self): - """The radius of the hole. If there is no hole, returns 0""" - return self.hole_diameter / 2. + """The radius of the hole. If there is no hole, returns None""" + if self.hole_diameter != None: + return self.hole_diameter / 2. + return None @property def orientation(self): @@ -740,14 +746,14 @@ class Polygon(Primitive): """ Polygon flash defined by a set number of sides. """ - def __init__(self, position, sides, radius, hole_radius, **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.radius = radius - self.hole_radius = hole_radius - self._to_convert = ['position', 'radius'] + self.hole_diameter = hole_diameter + self._to_convert = ['position', 'radius', 'hole_diameter'] @property def flashed(self): @@ -756,6 +762,12 @@ class Polygon(Primitive): @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 @property def bounding_box(self): -- cgit From 8cd842a41a55ab3d8f558a2e3e198beba7da58a1 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Thu, 21 Jan 2016 03:57:44 -0500 Subject: Manually mere rendering changes --- gerber/primitives.py | 1159 ++++++++++++++++++++++++++++++++++---------------- 1 file changed, 781 insertions(+), 378 deletions(-) (limited to 'gerber/primitives.py') diff --git a/gerber/primitives.py b/gerber/primitives.py index f259eff..98b3e1c 100644 --- a/gerber/primitives.py +++ b/gerber/primitives.py @@ -1,7 +1,7 @@ #! /usr/bin/env python # -*- coding: utf-8 -*- -# copyright 2014 Hamilton Kibbe +# copyright 2016 Hamilton Kibbe # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,10 +14,13 @@ # 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. -import math -from operator import add, sub -from .utils import validate_coordinates, inch, metric, rotate_point, nearly_equal + +import math +from operator import add +from itertools import combinations + +from .utils import validate_coordinates, inch, metric, convex_hull, rotate_point, nearly_equal class Primitive(object): @@ -35,14 +38,27 @@ class Primitive(object): rotation : float Rotation of a primitive about its origin in degrees. Positive rotation is counter-clockwise as viewed from the board top. + + units : string + Units in which primitive was defined. 'inch' or 'metric' + + net_name : string + Name of the electrical net the primitive belongs to """ - def __init__(self, level_polarity='dark', rotation=0, units=None, id=None, statement_id=None): + + def __init__(self, level_polarity='dark', rotation=0, units=None, net_name=None): self.level_polarity = level_polarity - self.rotation = rotation - self.units = units - self._to_convert = list() + self.net_name = net_name + self._to_convert = list() self.id = id - self.statement_id = statement_id + self._memoized = list() + self._units = units + self._rotation = rotation + self._cos_theta = math.cos(math.radians(rotation)) + self._sin_theta = math.sin(math.radians(rotation)) + self._bounding_box = None + self._vertices = None + self._segments = None @property def flashed(self): @@ -51,9 +67,41 @@ class Primitive(object): raise NotImplementedError('Is flashed must be ' 'implemented in subclass') + @property + def units(self): + return self._units + + @units.setter + def units(self, value): + self._changed() + self._units = value + + @property + def rotation(self): + return self._rotation + + @rotation.setter + def rotation(self, value): + self._changed() + self._rotation = value + self._cos_theta = math.cos(math.radians(value)) + self._sin_theta = math.sin(math.radians(value)) + + @property + def vertices(self): + return None + + @property + def segments(self): + if self._segments is None: + if self.vertices is not None and len(self.vertices): + self._segments = [segment for segment in + combinations(self.vertices, 2)] + return self._segments + @property def bounding_box(self): - """ Calculate bounding box + """ Calculate axis-aligned bounding box will be helpful for sweep & prune during DRC clearance checks. @@ -74,9 +122,12 @@ class Primitive(object): return self.bounding_box def to_inch(self): + """ Convert primitive units to inches. + """ if self.units == 'metric': self.units = 'inch' - for attr, value in [(attr, getattr(self, attr)) for attr in self._to_convert]: + for attr, value in [(attr, getattr(self, attr)) + for attr in self._to_convert]: if hasattr(value, 'to_inch'): value.to_inch() else: @@ -86,18 +137,22 @@ class Primitive(object): for v in value: v.to_inch() elif isinstance(value[0], tuple): - setattr(self, attr, [tuple(map(inch, point)) for point in value]) + setattr(self, attr, + [tuple(map(inch, point)) + for point in value]) else: setattr(self, attr, tuple(map(inch, value))) except: if value is not None: setattr(self, attr, inch(value)) - def to_metric(self): + """ Convert primitive units to metric. + """ if self.units == 'inch': self.units = 'metric' - for attr, value in [(attr, getattr(self, attr)) for attr in self._to_convert]: + for attr, value in [(attr, getattr(self, attr)) + for attr in self._to_convert]: if hasattr(value, 'to_metric'): value.to_metric() else: @@ -107,15 +162,25 @@ class Primitive(object): for v in value: v.to_metric() elif isinstance(value[0], tuple): - setattr(self, attr, [tuple(map(metric, point)) for point in value]) + setattr(self, attr, + [tuple(map(metric, point)) + for point in value]) else: setattr(self, attr, tuple(map(metric, value))) except: if value is not None: setattr(self, attr, metric(value)) - def offset(self, x_offset=0, y_offset=0): - raise NotImplementedError('The offset member must be implemented') + def offset(self, x_offset=0, y_offset=0): + """ Move the primitive by the specified x and y offset amount. + + values are specified in the primitive's native units + """ + if hasattr(self, 'position'): + self._changed() + self.position = tuple([coord + offset for coord, offset + in zip(self.position, + (x_offset, y_offset))]) def __eq__(self, other): return self.__dict__ == other.__dict__ @@ -123,14 +188,29 @@ class Primitive(object): def to_statement(self): pass + def _changed(self): + """ Clear memoized properties. + + Forces a recalculation next time any memoized propery is queried. + This must be called from a subclass every time a parameter that affects + a memoized property is changed. The easiest way to do this is to call + _changed() from property.setter methods. + """ + self._bounding_box = None + self._vertices = None + self._segments = None + for attr in self._memoized: + setattr(self, attr, None) + class Line(Primitive): """ """ + def __init__(self, start, end, aperture, **kwargs): super(Line, self).__init__(**kwargs) - self.start = start - self.end = end + self._start = start + self._end = end self.aperture = aperture self._to_convert = ['start', 'end', 'aperture'] @@ -138,25 +218,47 @@ class Line(Primitive): def flashed(self): return False + @property + def start(self): + return self._start + + @start.setter + def start(self, value): + self._changed() + self._start = value + + @property + def end(self): + return self._end + + @end.setter + def end(self, value): + self._changed() + self._end = value + + @property def angle(self): - delta_x, delta_y = tuple(map(sub, self.end, self.start)) + delta_x, delta_y = tuple( + [end - start for end, start in zip(self.end, self.start)]) angle = math.atan2(delta_y, delta_x) return angle @property - def bounding_box(self): - if isinstance(self.aperture, Circle): - width_2 = self.aperture.radius - height_2 = width_2 - else: - width_2 = self.aperture.width / 2. - height_2 = self.aperture.height / 2. - min_x = min(self.start[0], self.end[0]) - width_2 - max_x = max(self.start[0], self.end[0]) + width_2 - min_y = min(self.start[1], self.end[1]) - height_2 - max_y = max(self.start[1], self.end[1]) + height_2 - return ((min_x, max_x), (min_y, max_y)) + def bounding_box(self): + if self._bounding_box is None: + if isinstance(self.aperture, Circle): + width_2 = self.aperture.radius + height_2 = width_2 + else: + width_2 = self.aperture.width / 2. + height_2 = self.aperture.height / 2. + min_x = min(self.start[0], self.end[0]) - width_2 + max_x = max(self.start[0], self.end[0]) + width_2 + min_y = min(self.start[1], self.end[1]) - height_2 + max_y = max(self.start[1], self.end[1]) + height_2 + self._bounding_box = ((min_x, max_x), (min_y, max_y)) + return self._bounding_box @property def bounding_box_no_aperture(self): @@ -165,59 +267,37 @@ 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): - if not isinstance(self.aperture, Rectangle): - return None - else: - start = self.start - end = self.end - width = self.aperture.width - height = self.aperture.height - - # Find all the corners of the start and end position - start_ll = (start[0] - (width / 2.), - start[1] - (height / 2.)) - start_lr = (start[0] + (width / 2.), - start[1] - (height / 2.)) - start_ul = (start[0] - (width / 2.), - start[1] + (height / 2.)) - start_ur = (start[0] + (width / 2.), - start[1] + (height / 2.)) - end_ll = (end[0] - (width / 2.), - end[1] - (height / 2.)) - end_lr = (end[0] + (width / 2.), - end[1] - (height / 2.)) - end_ul = (end[0] - (width / 2.), - end[1] + (height / 2.)) - end_ur = (end[0] + (width / 2.), - end[1] + (height / 2.)) - - if end[0] == start[0] and end[1] == start[1]: - return (start_ll, start_lr, start_ur, start_ul) - elif end[0] == start[0] and end[1] > start[1]: - return (start_ll, start_lr, end_ur, end_ul) - elif end[0] > start[0] and end[1] > start[1]: - return (start_ll, start_lr, end_lr, end_ur, end_ul, start_ul) - elif end[0] > start[0] and end[1] == start[1]: - return (start_ll, end_lr, end_ur, start_ul) - elif end[0] > start[0] and end[1] < start[1]: - return (start_ll, end_ll, end_lr, end_ur, start_ur, start_ul) - elif end[0] == start[0] and end[1] < start[1]: - return (end_ll, end_lr, start_ur, start_ul) - elif end[0] < start[0] and end[1] < start[1]: - return (end_ll, end_lr, start_lr, start_ur, start_ul, end_ul) - elif end[0] < start[0] and end[1] == start[1]: - return (end_ll, start_lr, start_ur, end_ul) - elif end[0] < start[0] and end[1] > start[1]: - return (start_ll, start_lr, start_ur, end_ur, end_ul, end_ll) - - - 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))) + if self._vertices is None: + if isinstance(self.aperture, Rectangle): + start = self.start + end = self.end + width = self.aperture.width + height = self.aperture.height + + # Find all the corners of the start and end position + start_ll = (start[0] - (width / 2.), start[1] - (height / 2.)) + start_lr = (start[0] + (width / 2.), start[1] - (height / 2.)) + start_ul = (start[0] - (width / 2.), start[1] + (height / 2.)) + start_ur = (start[0] + (width / 2.), start[1] + (height / 2.)) + end_ll = (end[0] - (width / 2.), end[1] - (height / 2.)) + end_lr = (end[0] + (width / 2.), end[1] - (height / 2.)) + end_ul = (end[0] - (width / 2.), end[1] + (height / 2.)) + end_ur = (end[0] + (width / 2.), end[1] + (height / 2.)) + + # 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.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))]) + self._changed() def equivalent(self, other, offset): @@ -225,40 +305,79 @@ class Line(Primitive): 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 - self.center = center + self._start = start + self._end = end + self._center = center self.direction = direction self.aperture = aperture - self.quadrant_mode = quadrant_mode + self._quadrant_mode = quadrant_mode self._to_convert = ['start', 'end', 'center', 'aperture'] @property def flashed(self): return False + @property + def start(self): + return self._start + + @start.setter + def start(self, value): + self._changed() + self._start = value + + @property + def end(self): + return self._end + + @end.setter + def end(self, value): + self._changed() + self._end = value + + @property + def center(self): + return self._center + + @center.setter + 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() + self._quadrant_mode = quadrant_mode + @property def radius(self): - dy, dx = map(sub, self.start, self.center) - return math.sqrt(dy**2 + dx**2) + dy, dx = tuple([start - center for start, center + in zip(self.start, self.center)]) + return math.sqrt(dy ** 2 + dx ** 2) @property def start_angle(self): - dy, dx = map(sub, self.start, self.center) + dy, dx = tuple([start - center for start, center + in zip(self.start, self.center)]) return math.atan2(dx, dy) @property def end_angle(self): - dy, dx = map(sub, self.end, self.center) + dy, dx = tuple([end - center for end, center + in zip(self.end, self.center)]) return math.atan2(dx, dy) @property @@ -274,51 +393,48 @@ class Arc(Primitive): @property def bounding_box(self): - two_pi = 2 * math.pi - theta0 = (self.start_angle + two_pi) % two_pi - theta1 = (self.end_angle + two_pi) % two_pi - points = [self.start, self.end] - if self.direction == 'counterclockwise': - # Passes through 0 degrees - if theta0 > theta1: - points.append((self.center[0] + self.radius, self.center[1])) - # Passes through 90 degrees - if theta0 <= math.pi / 2. and (theta1 >= math.pi / 2. or theta1 < theta0): - points.append((self.center[0], self.center[1] + self.radius)) - # Passes through 180 degrees - if theta0 <= math.pi and (theta1 >= math.pi or theta1 < theta0): - points.append((self.center[0] - self.radius, self.center[1])) - # Passes through 270 degrees - if theta0 <= math.pi * 1.5 and (theta1 >= math.pi * 1.5 or theta1 < theta0): - points.append((self.center[0], self.center[1] - self.radius )) - else: - # Passes through 0 degrees - if theta1 > theta0: - points.append((self.center[0] + self.radius, self.center[1])) - # Passes through 90 degrees - if theta1 <= math.pi / 2. and (theta0 >= math.pi / 2. or theta0 < theta1): - points.append((self.center[0], self.center[1] + self.radius)) - # Passes through 180 degrees - if theta1 <= math.pi and (theta0 >= math.pi or theta0 < theta1): - points.append((self.center[0] - self.radius, self.center[1])) - # Passes through 270 degrees - 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) - - if isinstance(self.aperture, Circle): - radius = self.aperture.radius - else: - # TODO this is actually not valid, but files contain it - width = self.aperture.width - height = self.aperture.height - radius = max(width, height) - - min_x = min(x) - radius - max_x = max(x) + radius - min_y = min(y) - radius - max_y = max(y) + radius - return ((min_x, max_x), (min_y, max_y)) + if self._bounding_box is None: + two_pi = 2 * math.pi + theta0 = (self.start_angle + two_pi) % two_pi + theta1 = (self.end_angle + two_pi) % two_pi + points = [self.start, self.end] + if self.direction == 'counterclockwise': + # Passes through 0 degrees + if theta0 > theta1: + points.append((self.center[0] + self.radius, self.center[1])) + # Passes through 90 degrees + if theta0 <= math.pi / \ + 2. and (theta1 >= math.pi / 2. or theta1 < theta0): + points.append((self.center[0], self.center[1] + self.radius)) + # Passes through 180 degrees + if theta0 <= math.pi and (theta1 >= math.pi or theta1 < theta0): + points.append((self.center[0] - self.radius, self.center[1])) + # Passes through 270 degrees + if theta0 <= math.pi * \ + 1.5 and (theta1 >= math.pi * 1.5 or theta1 < theta0): + points.append((self.center[0], self.center[1] - self.radius)) + else: + # Passes through 0 degrees + if theta1 > theta0: + points.append((self.center[0] + self.radius, self.center[1])) + # Passes through 90 degrees + if theta1 <= math.pi / \ + 2. and (theta0 >= math.pi / 2. or theta0 < theta1): + points.append((self.center[0], self.center[1] + self.radius)) + # Passes through 180 degrees + if theta1 <= math.pi and (theta0 >= math.pi or theta0 < theta1): + points.append((self.center[0] - self.radius, self.center[1])) + # Passes through 270 degrees + 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) - self.aperture.radius + max_x = max(x) + self.aperture.radius + 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 @property def bounding_box_no_aperture(self): @@ -341,7 +457,7 @@ class Arc(Primitive): if theta0 <= math.pi * 1.5 and (theta1 >= math.pi * 1.5 or theta1 < theta0): points.append((self.center[0], self.center[1] - self.radius )) else: - # Passes through 0 degrees + # Passes through 0 degrees if theta1 > theta0: points.append((self.center[0] + self.radius, self.center[1])) # Passes through 90 degrees @@ -359,9 +475,10 @@ class Arc(Primitive): 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() self.start = tuple(map(add, self.start, (x_offset, y_offset))) self.end = tuple(map(add, self.end, (x_offset, y_offset))) self.center = tuple(map(add, self.center, (x_offset, y_offset))) @@ -369,19 +486,37 @@ class Arc(Primitive): class Circle(Primitive): """ - """ + """ def __init__(self, position, diameter, hole_diameter = None, **kwargs): super(Circle, self).__init__(**kwargs) validate_coordinates(position) - self.position = position - self.diameter = diameter + self._position = position + self._diameter = diameter self.hole_diameter = hole_diameter - self._to_convert = ['position', 'diameter', 'hole_diameter'] + self._to_convert = ['position', 'diameter', 'hole_diameter'] @property def flashed(self): return True + @property + def position(self): + return self._position + + @position.setter + def position(self, value): + self._changed() + self._position = value + + @property + def diameter(self): + return self._diameter + + @diameter.setter + def diameter(self, value): + self._changed() + self._diameter = value + @property def radius(self): return self.diameter / 2. @@ -394,12 +529,14 @@ class Circle(Primitive): @property def bounding_box(self): - min_x = self.position[0] - self.radius - max_x = self.position[0] + self.radius - min_y = self.position[1] - self.radius - max_y = self.position[1] + self.radius - return ((min_x, max_x), (min_y, max_y)) - + if self._bounding_box is None: + min_x = self.position[0] - self.radius + max_x = self.position[0] + self.radius + 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 + def offset(self, x_offset=0, y_offset=0): self.position = tuple(map(add, self.position, (x_offset, y_offset))) @@ -420,39 +557,68 @@ class Circle(Primitive): class Ellipse(Primitive): """ """ + def __init__(self, position, width, height, **kwargs): super(Ellipse, self).__init__(**kwargs) validate_coordinates(position) - self.position = position - self.width = width - self.height = height + self._position = position + self._width = width + self._height = height self._to_convert = ['position', 'width', 'height'] - + @property def flashed(self): return True + + @property + def position(self): + return self._position + + @position.setter + def position(self, value): + self._changed() + self._position = value @property - def bounding_box(self): - 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)) + def width(self): + return self._width - def offset(self, x_offset=0, y_offset=0): - self.position = tuple(map(add, self.position, (x_offset, y_offset))) + @width.setter + def width(self, value): + self._changed() + self._width = value + + @property + def height(self): + return self._height + + @height.setter + def height(self, value): + self._changed() + self._height = value @property - def _abs_width(self): + def bounding_box(self): + if self._bounding_box is None: + min_x = self.position[0] - (self.axis_aligned_width / 2.0) + max_x = self.position[0] + (self.axis_aligned_width / 2.0) + min_y = self.position[1] - (self.axis_aligned_height / 2.0) + max_y = self.position[1] + (self.axis_aligned_height / 2.0) + self._bounding_box = ((min_x, max_x), (min_y, max_y)) + return self._bounding_box + + @property + def axis_aligned_width(self): ux = (self.width / 2.) * math.cos(math.radians(self.rotation)) - vx = (self.height / 2.) * math.cos(math.radians(self.rotation) + (math.pi / 2.)) + vx = (self.height / 2.) * \ + math.cos(math.radians(self.rotation) + (math.pi / 2.)) return 2 * math.sqrt((ux * ux) + (vx * vx)) - + @property - def _abs_height(self): + def axis_aligned_height(self): uy = (self.width / 2.) * math.sin(math.radians(self.rotation)) - vy = (self.height / 2.) * math.sin(math.radians(self.rotation) + (math.pi / 2.)) + vy = (self.height / 2.) * \ + math.sin(math.radians(self.rotation) + (math.pi / 2.)) return 2 * math.sqrt((uy * uy) + (vy * vy)) @@ -462,29 +628,49 @@ class Rectangle(Primitive): 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): super(Rectangle, self).__init__(**kwargs) validate_coordinates(position) - self.position = position - self.width = width - self.height = height + self._position = position + self._width = width + 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 def flashed(self): return True - + @property - def lower_left(self): - return (self.position[0] - (self._abs_width / 2.), - self.position[1] - (self._abs_height / 2.)) + def position(self): + return self._position + + @position.setter + def position(self, value): + self._changed() + self._position = value @property - def upper_right(self): - return (self.position[0] + (self._abs_width / 2.), - self.position[1] + (self._abs_height / 2.)) + def width(self): + return self._width + + @width.setter + def width(self, value): + self._changed() + self._width = value + + @property + def height(self): + return self._height + + @height.setter + def height(self, value): + self._changed() + self._height = value @property def hole_radius(self): @@ -493,26 +679,52 @@ class Rectangle(Primitive): return self.hole_diameter / 2. return None + @property + def upper_right(self): + return (self.position[0] + (self._abs_width / 2.), + self.position[1] + (self._abs_height / 2.)) + + @property + def lower_left(self): + return (self.position[0] - (self.axis_aligned_width / 2.), + self.position[1] - (self.axis_aligned_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)) - - def offset(self, x_offset=0, y_offset=0): - self.position = tuple(map(add, self.position, (x_offset, y_offset))) + if self._bounding_box is None: + ll = (self.position[0] - (self.axis_aligned_width / 2.), + self.position[1] - (self.axis_aligned_height / 2.)) + ur = (self.position[0] + (self.axis_aligned_width / 2.), + self.position[1] + (self.axis_aligned_height / 2.)) + self._bounding_box = ((ll[0], ur[0]), (ll[1], ur[1])) + return self._bounding_box @property - def _abs_width(self): - return (math.cos(math.radians(self.rotation)) * self.width + - math.sin(math.radians(self.rotation)) * self.height) - @property + def vertices(self): + if self._vertices is None: + delta_w = self.width / 2. + delta_h = self.height / 2. + ll = ((self.position[0] - delta_w), (self.position[1] - delta_h)) + ul = ((self.position[0] - delta_w), (self.position[1] + delta_h)) + ur = ((self.position[0] + delta_w), (self.position[1] + delta_h)) + lr = ((self.position[0] + delta_w), (self.position[1] - delta_h)) + self._vertices = [((x * self._cos_theta - y * self._sin_theta), + (x * self._sin_theta + y * self._cos_theta)) + for x, y in [ll, ul, ur, lr]] + return self._vertices + + @property + def axis_aligned_width(self): + return (self._cos_theta * self.width + self._sin_theta * self.height) + + @property def _abs_height(self): return (math.cos(math.radians(self.rotation)) * self.height + math.sin(math.radians(self.rotation)) * self.width) - + + 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?""" @@ -524,18 +736,19 @@ class Rectangle(Primitive): 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): """ """ + def __init__(self, position, width, height, **kwargs): super(Diamond, self).__init__(**kwargs) validate_coordinates(position) - self.position = position - self.width = width - self.height = height + self._position = position + self._width = width + self._height = height self._to_convert = ['position', 'width', 'height'] @property @@ -543,47 +756,77 @@ class Diamond(Primitive): return True @property - def lower_left(self): - return (self.position[0] - (self._abs_width / 2.), - self.position[1] - (self._abs_height / 2.)) + def position(self): + return self._position + + @position.setter + def position(self, value): + self._changed() + self._position = value @property - def upper_right(self): - return (self.position[0] + (self._abs_width / 2.), - self.position[1] + (self._abs_height / 2.)) + def width(self): + return self._width + + @width.setter + def width(self, value): + self._changed() + self._width = value + + @property + def height(self): + return self._height + + @height.setter + def height(self, value): + self._changed() + self._height = value @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)) + if self._bounding_box is None: + ll = (self.position[0] - (self.axis_aligned_width / 2.), + self.position[1] - (self.axis_aligned_height / 2.)) + ur = (self.position[0] + (self.axis_aligned_width / 2.), + self.position[1] + (self.axis_aligned_height / 2.)) + self._bounding_box = ((ll[0], ur[0]), (ll[1], ur[1])) + 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): + if self._vertices is None: + delta_w = self.width / 2. + delta_h = self.height / 2. + top = (self.position[0], (self.position[1] + delta_h)) + right = ((self.position[0] + delta_w), self.position[1]) + bottom = (self.position[0], (self.position[1] - delta_h)) + left = ((self.position[0] - delta_w), self.position[1]) + self._vertices = [(((x * self._cos_theta) - (y * self._sin_theta)), + ((x * self._sin_theta) + (y * self._cos_theta))) + for x, y in [top, right, bottom, left]] + return self._vertices @property - def _abs_width(self): - return (math.cos(math.radians(self.rotation)) * self.width + - math.sin(math.radians(self.rotation)) * self.height) + def axis_aligned_width(self): + return (self._cos_theta * self.width + self._sin_theta * self.height) + @property - def _abs_height(self): - return (math.cos(math.radians(self.rotation)) * self.height + - math.sin(math.radians(self.rotation)) * self.width) + def axis_aligned_height(self): + return (self._cos_theta * self.height + self._sin_theta * self.width) class ChamferRectangle(Primitive): """ """ + def __init__(self, position, width, height, chamfer, corners, **kwargs): super(ChamferRectangle, self).__init__(**kwargs) validate_coordinates(position) - self.position = position - self.width = width - self.height = height - self.chamfer = chamfer - self.corners = corners + self._position = position + self._width = width + self._height = height + self._chamfer = chamfer + self._corners = corners self._to_convert = ['position', 'width', 'height', 'chamfer'] @property @@ -591,46 +834,88 @@ class ChamferRectangle(Primitive): return True @property - def lower_left(self): - return (self.position[0] - (self._abs_width / 2.), - self.position[1] - (self._abs_height / 2.)) + def position(self): + return self._position + + @position.setter + def position(self, value): + self._changed() + self._position = value @property - def upper_right(self): - return (self.position[0] + (self._abs_width / 2.), - self.position[1] + (self._abs_height / 2.)) + def width(self): + return self._width + + @width.setter + def width(self, value): + self._changed() + self._width = value + + @property + def height(self): + return self._height + + @height.setter + def height(self, value): + self._changed() + self._height = value + + @property + def chamfer(self): + return self._chamfer + + @chamfer.setter + def chamfer(self, value): + self._changed() + self._chamfer = value + + @property + def corners(self): + return self._corners + + @corners.setter + def corners(self, value): + self._changed() + self._corners = value @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)) + if self._bounding_box is None: + ll = (self.position[0] - (self.axis_aligned_width / 2.), + self.position[1] - (self.axis_aligned_height / 2.)) + ur = (self.position[0] + (self.axis_aligned_width / 2.), + self.position[1] + (self.axis_aligned_height / 2.)) + self._bounding_box = ((ll[0], ur[0]), (ll[1], ur[1])) + 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): + # TODO + return self._vertices @property - def _abs_width(self): - return (math.cos(math.radians(self.rotation)) * self.width + - math.sin(math.radians(self.rotation)) * self.height) + def axis_aligned_width(self): + return (self._cos_theta * self.width + + self._sin_theta * self.height) + @property - def _abs_height(self): - return (math.cos(math.radians(self.rotation)) * self.height + - math.sin(math.radians(self.rotation)) * self.width) + def axis_aligned_height(self): + return (self._cos_theta * self.height + + self._sin_theta * self.width) + class RoundRectangle(Primitive): """ """ + def __init__(self, position, width, height, radius, corners, **kwargs): super(RoundRectangle, self).__init__(**kwargs) validate_coordinates(position) - self.position = position - self.width = width - self.height = height - self.radius = radius - self.corners = corners + self._position = position + self._width = width + self._height = height + self._radius = radius + self._corners = corners self._to_convert = ['position', 'width', 'height', 'radius'] @property @@ -638,67 +923,126 @@ class RoundRectangle(Primitive): return True @property - def lower_left(self): - return (self.position[0] - (self._abs_width / 2.), - self.position[1] - (self._abs_height / 2.)) + def position(self): + return self._position + + @position.setter + def position(self, value): + self._changed() + self._position = value @property - def upper_right(self): - return (self.position[0] + (self._abs_width / 2.), - self.position[1] + (self._abs_height / 2.)) + def width(self): + return self._width + + @width.setter + def width(self, value): + self._changed() + self._width = value @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)) + def height(self): + return self._height - def offset(self, x_offset=0, y_offset=0): - self.position = tuple(map(add, self.position, (x_offset, y_offset))) + @height.setter + def height(self, value): + self._changed() + self._height = value @property - def _abs_width(self): - return (math.cos(math.radians(self.rotation)) * self.width + - math.sin(math.radians(self.rotation)) * self.height) + def radius(self): + return self._radius + + @radius.setter + def radius(self, value): + self._changed() + self._radius = value + @property - def _abs_height(self): - return (math.cos(math.radians(self.rotation)) * self.height + - math.sin(math.radians(self.rotation)) * self.width) + def corners(self): + return self._corners + + @corners.setter + def corners(self, value): + self._changed() + self._corners = value + + @property + def bounding_box(self): + if self._bounding_box is None: + ll = (self.position[0] - (self.axis_aligned_width / 2.), + self.position[1] - (self.axis_aligned_height / 2.)) + ur = (self.position[0] + (self.axis_aligned_width / 2.), + self.position[1] + (self.axis_aligned_height / 2.)) + self._bounding_box = ((ll[0], ur[0]), (ll[1], ur[1])) + return self._bounding_box + + @property + def axis_aligned_width(self): + return (self._cos_theta * self.width + + self._sin_theta * self.height) + + @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): super(Obround, self).__init__(**kwargs) validate_coordinates(position) - self.position = position - self.width = width - self.height = height + self._position = position + self._width = width + self._height = height self.hole_diameter = hole_diameter self._to_convert = ['position', 'width', 'height', 'hole_diameter'] @property def flashed(self): - return True + return True @property - def lower_left(self): - return (self.position[0] - (self._abs_width / 2.), - self.position[1] - (self._abs_height / 2.)) + def position(self): + return self._position + + @position.setter + def position(self, value): + self._changed() + self._position = value @property + def width(self): + return self._width + + @width.setter + def width(self, value): + self._changed() + self._width = value + + @property def upper_right(self): return (self.position[0] + (self._abs_width / 2.), self.position[1] + (self._abs_height / 2.)) - + + @property + def height(self): + return self._height + + @height.setter + 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""" if self.hole_diameter != None: return self.hole_diameter / 2. - return None + + return None @property def orientation(self): @@ -706,52 +1050,55 @@ class Obround(Primitive): @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)) + if self._bounding_box is None: + ll = (self.position[0] - (self.axis_aligned_width / 2.), + self.position[1] - (self.axis_aligned_height / 2.)) + ur = (self.position[0] + (self.axis_aligned_width / 2.), + self.position[1] + (self.axis_aligned_height / 2.)) + self._bounding_box = ((ll[0], ur[0]), (ll[1], ur[1])) + return self._bounding_box @property def subshapes(self): if self.orientation == 'vertical': circle1 = Circle((self.position[0], self.position[1] + - (self.height-self.width) / 2.), self.width) + (self.height - self.width) / 2.), self.width) circle2 = Circle((self.position[0], self.position[1] - - (self.height-self.width) / 2.), self.width) + (self.height - self.width) / 2.), self.width) rect = Rectangle(self.position, self.width, - (self.height - self.width)) + (self.height - self.width)) else: - circle1 = Circle((self.position[0] - (self.height - self.width) / 2., + circle1 = Circle((self.position[0] + - (self.height - self.width) / 2., self.position[1]), self.height) - circle2 = Circle((self.position[0] + (self.height - self.width) / 2., + circle2 = Circle((self.position[0] + + (self.height - self.width) / 2., self.position[1]), self.height) rect = Rectangle(self.position, (self.width - self.height), - self.height) + self.height) return {'circle1': circle1, 'circle2': circle2, 'rectangle': rect} - def offset(self, x_offset=0, y_offset=0): - self.position = tuple(map(add, self.position, (x_offset, y_offset))) - @property - def _abs_width(self): - return (math.cos(math.radians(self.rotation)) * self.width + - math.sin(math.radians(self.rotation)) * self.height) + def axis_aligned_width(self): + return (self._cos_theta * self.width + + self._sin_theta * self.height) + @property - def _abs_height(self): - return (math.cos(math.radians(self.rotation)) * self.height + - math.sin(math.radians(self.rotation)) * self.width) + def axis_aligned_height(self): + return (self._cos_theta * self.height + + self._sin_theta * self.width) + 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.radius = radius + self._position = position + self.sides = sides + self._radius = radius self.hole_diameter = hole_diameter self._to_convert = ['position', 'radius', 'hole_diameter'] @@ -767,16 +1114,36 @@ class Polygon(Primitive): def hole_radius(self): if self.hole_diameter != None: return self.hole_diameter / 2. - return None + return None @property - def bounding_box(self): - min_x = self.position[0] - self.radius - max_x = self.position[0] + self.radius - min_y = self.position[1] - self.radius - max_y = self.position[1] + self.radius - return ((min_x, max_x), (min_y, max_y)) + def position(self): + return self._position + + @position.setter + def position(self, value): + self._changed() + self._position = value + @property + def radius(self): + return self._radius + + @radius.setter + def radius(self, value): + self._changed() + self._radius = value + + @property + def bounding_box(self): + if self._bounding_box is None: + min_x = self.position[0] - self.radius + max_x = self.position[0] + self.radius + 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 + def offset(self, x_offset=0, y_offset=0): self.position = tuple(map(add, self.position, (x_offset, y_offset))) @@ -823,7 +1190,7 @@ class AMGroup(Primitive): for p in prim: self.primitives.append(p) elif prim: - self.primitives.append(prim) + self.primitives.append(prim) self._position = None self._to_convert = ['_position', 'primitives'] self.stmt = stmt @@ -851,6 +1218,7 @@ class AMGroup(Primitive): @property def bounding_box(self): + # TODO Make this cached like other items xlims, ylims = zip(*[p.bounding_box for p in self.primitives]) minx, maxx = zip(*xlims) miny, maxy = zip(*ylims) @@ -936,16 +1304,23 @@ class Outline(Primitive): def offset(self, x_offset=0, y_offset=0): for p in self.primitives: p.offset(x_offset, y_offset) + + @property + def vertices(self): + if self._vertices is None: + theta = math.radians(360/self.sides) + vertices = [(self.position[0] + (math.cos(theta * side) * self.radius), + self.position[1] + (math.sin(theta * side) * self.radius)) + for side in range(self.sides)] + 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 @property def width(self): bounding_box = self.bounding_box() return bounding_box[0][1] - bounding_box[0][0] - - @property - def width(self): - bounding_box = self.bounding_box() - return bounding_box[1][1] - bounding_box[1][0] def equivalent(self, other, offset): ''' @@ -965,6 +1340,7 @@ class Outline(Primitive): class Region(Primitive): """ """ + def __init__(self, primitives, **kwargs): super(Region, self).__init__(**kwargs) self.primitives = primitives @@ -975,17 +1351,20 @@ class Region(Primitive): return False @property - def bounding_box(self): - xlims, ylims = zip(*[p.bounding_box_no_aperture for p in self.primitives]) - minx, maxx = zip(*xlims) - miny, maxy = zip(*ylims) - min_x = min(minx) - max_x = max(maxx) - min_y = min(miny) - max_y = max(maxy) - return ((min_x, max_x), (min_y, max_y)) + 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) + miny, maxy = zip(*ylims) + min_x = min(minx) + max_x = max(maxx) + min_y = min(miny) + max_y = max(maxy) + 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() for p in self.primitives: p.offset(x_offset, y_offset) @@ -993,6 +1372,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) validate_coordinates(position) @@ -1000,6 +1380,8 @@ class RoundButterfly(Primitive): self.diameter = diameter self._to_convert = ['position', 'diameter'] + # TODO This does not reset bounding box correctly + @property def flashed(self): return True @@ -1010,19 +1392,19 @@ class RoundButterfly(Primitive): @property def bounding_box(self): - min_x = self.position[0] - self.radius - max_x = self.position[0] + self.radius - min_y = self.position[1] - self.radius - max_y = self.position[1] + self.radius - return ((min_x, max_x), (min_y, max_y)) - - def offset(self, x_offset=0, y_offset=0): - self.position = tuple(map(add, self.position, (x_offset, y_offset))) + if self._bounding_box is None: + min_x = self.position[0] - self.radius + max_x = self.position[0] + self.radius + 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 class SquareButterfly(Primitive): """ A square with two diagonally-opposite quadrants removed """ + def __init__(self, position, side, **kwargs): super(SquareButterfly, self).__init__(**kwargs) validate_coordinates(position) @@ -1030,34 +1412,39 @@ class SquareButterfly(Primitive): self.side = side self._to_convert = ['position', 'side'] + # TODO This does not reset bounding box correctly + @property def flashed(self): - return True + return True @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)) - - def offset(self, x_offset=0, y_offset=0): - self.position = tuple(map(add, self.position, (x_offset, y_offset))) + if self._bounding_box is None: + 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.) + self._bounding_box = ((min_x, max_x), (min_y, max_y)) + return self._bounding_box class Donut(Primitive): """ A Shape with an identical concentric shape removed from its center """ - def __init__(self, position, shape, inner_diameter, outer_diameter, **kwargs): + + def __init__(self, position, shape, inner_diameter, + outer_diameter, **kwargs): super(Donut, self).__init__(**kwargs) validate_coordinates(position) self.position = position if shape not in ('round', 'square', 'hexagon', 'octagon'): - raise ValueError('Valid shapes are round, square, hexagon or octagon') + raise ValueError( + 'Valid shapes are round, square, hexagon or octagon') self.shape = shape if inner_diameter >= outer_diameter: - raise ValueError('Outer diameter must be larger than inner diameter.') + raise ValueError( + 'Outer diameter must be larger than inner diameter.') self.inner_diameter = inner_diameter self.outer_diameter = outer_diameter if self.shape in ('round', 'square', 'octagon'): @@ -1067,8 +1454,11 @@ class Donut(Primitive): # Hexagon self.width = 0.5 * math.sqrt(3.) * 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 def flashed(self): return True @@ -1081,29 +1471,30 @@ 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): - 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)) - - def offset(self, x_offset=0, y_offset=0): - self.position = tuple(map(add, self.position, (x_offset, y_offset))) + if self._bounding_box is None: + ll = (self.position[0] - (self.width / 2.), + self.position[1] - (self.height / 2.)) + ur = (self.position[0] + (self.width / 2.), + self.position[1] + (self.height / 2.)) + self._bounding_box = ((ll[0], ur[0]), (ll[1], ur[1])) + return self._bounding_box class SquareRoundDonut(Primitive): """ A Square with a circular cutout in the center """ + def __init__(self, position, inner_diameter, outer_diameter, **kwargs): super(SquareRoundDonut, self).__init__(**kwargs) validate_coordinates(position) self.position = position if inner_diameter >= outer_diameter: - raise ValueError('Outer diameter must be larger than inner diameter.') + raise ValueError( + 'Outer diameter must be larger than inner diameter.') self.inner_diameter = inner_diameter self.outer_diameter = outer_diameter self._to_convert = ['position', 'inner_diameter', 'outer_diameter'] @@ -1112,40 +1503,47 @@ class SquareRoundDonut(Primitive): def flashed(self): return True - @property - def lower_left(self): - return tuple([c - self.outer_diameter / 2. for c in self.position]) - - @property - def upper_right(self): - return tuple([c + self.outer_diameter / 2. for c in self.position]) - @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)) - - def offset(self, x_offset=0, y_offset=0): - self.position = tuple(map(add, self.position, (x_offset, y_offset))) + if self._bounding_box is None: + ll = tuple([c - self.outer_diameter / 2. for c in self.position]) + ur = tuple([c + self.outer_diameter / 2. for c in self.position]) + self._bounding_box = ((ll[0], ur[0]), (ll[1], ur[1])) + return self._bounding_box class Drill(Primitive): """ A drill hole - """ + """ def __init__(self, position, diameter, hit, **kwargs): super(Drill, self).__init__('dark', **kwargs) validate_coordinates(position) - self.position = position - self.diameter = diameter + self._position = position + self._diameter = diameter self.hit = hit self._to_convert = ['position', 'diameter', 'hit'] @property def flashed(self): - return False + return False + + @property + def position(self): + return self._position + + @position.setter + def position(self, value): + self._changed() + self._position = value + + @property + def diameter(self): + return self._diameter + + @diameter.setter + def diameter(self, value): + self._changed() + self._diameter = value @property def radius(self): @@ -1153,13 +1551,16 @@ class Drill(Primitive): @property def bounding_box(self): - min_x = self.position[0] - self.radius - max_x = self.position[0] + self.radius - min_y = self.position[1] - self.radius - max_y = self.position[1] + self.radius - return ((min_x, max_x), (min_y, max_y)) - + if self._bounding_box is None: + min_x = self.position[0] - self.radius + max_x = self.position[0] + self.radius + 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 + 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): @@ -1179,6 +1580,8 @@ class Slot(Primitive): self.hit = hit self._to_convert = ['start', 'end', 'diameter', 'hit'] + # TODO this needs to use cached bounding box + @property def flashed(self): return False @@ -1199,15 +1602,15 @@ 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 """ + def __init__(self, position, net_name, layer, **kwargs): super(TestRecord, self).__init__(**kwargs) validate_coordinates(position) self.position = position self.net_name = net_name self.layer = layer - -- cgit From 8d5e782ccf220d77f0aad5a4e5605dc5cbe0f410 Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Sat, 6 Aug 2016 09:51:58 +0800 Subject: Fix multiple problems with the merge. There are still errors, but I will intentionally leave them because future merges might resolve them --- gerber/primitives.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) (limited to 'gerber/primitives.py') diff --git a/gerber/primitives.py b/gerber/primitives.py index 98b3e1c..d78c6d9 100644 --- a/gerber/primitives.py +++ b/gerber/primitives.py @@ -16,12 +16,14 @@ # limitations under the License. +from itertools import combinations 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 @@ -721,7 +723,8 @@ class Rectangle(Primitive): def _abs_height(self): return (math.cos(math.radians(self.rotation)) * self.height + math.sin(math.radians(self.rotation)) * self.width) - + + @property def axis_aligned_height(self): return (self._cos_theta * self.height + self._sin_theta * self.width) -- cgit From 5af19af190c1fb0f0c5be029d46d63e657dde4d9 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Thu, 21 Jan 2016 03:57:44 -0500 Subject: Commit partial merge so I can work on the plane --- gerber/primitives.py | 172 ++++++++++++++++++++++++++++----------------------- 1 file changed, 94 insertions(+), 78 deletions(-) (limited to 'gerber/primitives.py') diff --git a/gerber/primitives.py b/gerber/primitives.py index d78c6d9..a291c26 100644 --- a/gerber/primitives.py +++ b/gerber/primitives.py @@ -16,11 +16,11 @@ # limitations under the License. -from itertools import combinations + import math from operator import add - -from .utils import validate_coordinates, inch, metric, convex_hull, rotate_point, nearly_equal +from itertools import combinations +from .utils import validate_coordinates, inch, metric, convex_hull, rotate_point, nearly_equal @@ -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.net_name = net_name self._to_convert = list() - self.id = id + self.id = id self._memoized = list() self._units = units self._rotation = rotation @@ -60,18 +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): @@ -81,7 +84,7 @@ class Primitive(object): @property def rotation(self): return self._rotation - + @rotation.setter def rotation(self, value): self._changed() @@ -172,8 +175,8 @@ class Primitive(object): except: if value is not None: setattr(self, attr, metric(value)) - - def offset(self, x_offset=0, y_offset=0): + + def offset(self, x_offset=0, y_offset=0): """ Move the primitive by the specified x and y offset amount. values are specified in the primitive's native units @@ -183,10 +186,7 @@ class Primitive(object): self.position = tuple([coord + offset for coord, offset in zip(self.position, (x_offset, y_offset))]) - - def __eq__(self, other): - return self.__dict__ == other.__dict__ - + def to_statement(self): pass @@ -201,9 +201,8 @@ class Primitive(object): self._bounding_box = None self._vertices = None self._segments = None - for attr in self._memoized: - setattr(self, attr, None) - + for attr in self._memoized: + setattr(self, attr, None) class Line(Primitive): """ @@ -238,7 +237,6 @@ class Line(Primitive): self._changed() self._end = value - @property def angle(self): delta_x, delta_y = tuple( @@ -246,7 +244,7 @@ class Line(Primitive): angle = math.atan2(delta_y, delta_x) return angle - @property + @property def bounding_box(self): if self._bounding_box is None: if isinstance(self.aperture, Circle): @@ -261,7 +259,7 @@ class Line(Primitive): max_y = max(self.start[1], self.end[1]) + height_2 self._bounding_box = ((min_x, max_x), (min_y, max_y)) return self._bounding_box - + @property def bounding_box_no_aperture(self): '''Gets the bounding box without the aperture''' @@ -293,13 +291,13 @@ 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): + + def offset(self, x_offset=0, y_offset=0): + 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))]) - self._changed() def equivalent(self, other, offset): @@ -308,12 +306,14 @@ class Line(Primitive): equiv_start = tuple(map(add, other.start, 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): super(Arc, self).__init__(**kwargs) self._start = start @@ -436,7 +436,7 @@ 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): @@ -488,12 +488,13 @@ 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'] @@ -537,7 +538,7 @@ 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))) @@ -553,7 +554,7 @@ class Circle(Primitive): 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): @@ -575,7 +576,7 @@ class Ellipse(Primitive): @property def position(self): return self._position - + @position.setter def position(self, value): self._changed() @@ -625,18 +626,19 @@ 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 @@ -656,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): @@ -685,7 +687,7 @@ class Rectangle(Primitive): def upper_right(self): return (self.position[0] + (self._abs_width / 2.), self.position[1] + (self._abs_height / 2.)) - + @property def lower_left(self): return (self.position[0] - (self.axis_aligned_width / 2.), @@ -765,7 +767,7 @@ class Diamond(Primitive): @position.setter def position(self, value): self._changed() - self._position = value + self._position = value @property def width(self): @@ -776,7 +778,7 @@ class Diamond(Primitive): self._changed() self._width = value - @property + @property def height(self): return self._height @@ -950,7 +952,7 @@ class RoundRectangle(Primitive): @height.setter def height(self, value): self._changed() - self._height = value + self._height = value @property def radius(self): @@ -985,21 +987,22 @@ 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'] @@ -1014,7 +1017,7 @@ class Obround(Primitive): @position.setter def position(self, value): self._changed() - self._position = value + self._position = value @property def width(self): @@ -1030,7 +1033,7 @@ class Obround(Primitive): return (self.position[0] + (self._abs_width / 2.), self.position[1] + (self._abs_height / 2.)) - @property + @property def height(self): return self._height @@ -1093,7 +1096,7 @@ class Obround(Primitive): class Polygon(Primitive): - """ + """ Polygon flash defined by a set number of sides. """ def __init__(self, position, sides, radius, hole_diameter, **kwargs): @@ -1126,7 +1129,7 @@ class Polygon(Primitive): @position.setter def position(self, value): self._changed() - self._position = value + self._position = value @property def radius(self): @@ -1162,6 +1165,18 @@ class Polygon(Primitive): return points + @property + def vertices(self): + if self._vertices is None: + theta = math.radians(360/self.sides) + vertices = [(self.position[0] + (math.cos(theta * side) * self.radius), + self.position[1] + (math.sin(theta * side) * self.radius)) + for side in range(self.sides)] + 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 + def equivalent(self, other, offset): """ Is this the outline the same as the other, ignoring the position offset? @@ -1170,7 +1185,7 @@ class Polygon(Primitive): # 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) @@ -1178,7 +1193,7 @@ class Polygon(Primitive): class AMGroup(Primitive): """ - """ + """ def __init__(self, amprimitives, stmt = None, **kwargs): """ @@ -1281,6 +1296,7 @@ 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 @@ -1295,16 +1311,19 @@ class Outline(Primitive): @property def bounding_box(self): - xlims, ylims = zip(*[p.bounding_box for p in self.primitives]) - minx, maxx = zip(*xlims) - miny, maxy = zip(*ylims) - min_x = min(minx) - max_x = max(maxx) - min_y = min(miny) - max_y = max(maxy) - return ((min_x, max_x), (min_y, max_y)) + if self._bounding_box is None: + xlims, ylims = zip(*[p.bounding_box for p in self.primitives]) + minx, maxx = zip(*xlims) + miny, maxy = zip(*ylims) + min_x = min(minx) + max_x = max(maxx) + min_y = min(miny) + max_y = max(maxy) + 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() for p in self.primitives: p.offset(x_offset, y_offset) @@ -1416,11 +1435,11 @@ class SquareButterfly(Primitive): self._to_convert = ['position', 'side'] # TODO This does not reset bounding box correctly - + @property def flashed(self): return True - + @property def bounding_box(self): if self._bounding_box is None: @@ -1456,9 +1475,10 @@ 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'] + self._to_convert = ['position', 'width', + 'height', 'inner_diameter', 'outer_diameter'] # TODO This does not reset bounding box correctly @@ -1474,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): @@ -1526,11 +1546,13 @@ class Drill(Primitive): 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 + @property def position(self): return self._position @@ -1588,19 +1610,13 @@ class Slot(Primitive): @property def flashed(self): return False - - @property - def radius(self): - return self.diameter / 2. - - @property + def bounding_box(self): - radius = self.radius - min_x = min(self.start[0], self.end[0]) - radius - max_x = max(self.start[0], self.end[0]) + radius - min_y = min(self.start[1], self.end[1]) - radius - max_y = max(self.start[1], self.end[1]) + radius - return ((min_x, max_x), (min_y, max_y)) + if self._bounding_box is None: + ll = tuple([c - self.outer_diameter / 2. for c in self.position]) + ur = tuple([c + self.outer_diameter / 2. for c in self.position]) + self._bounding_box = ((ll[0], ur[0]), (ll[1], ur[1])) + return self._bounding_box def offset(self, x_offset=0, y_offset=0): self.start = tuple(map(add, self.start, (x_offset, y_offset))) -- cgit