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 From 724c2b3bced319ed0b50c4302fed9b0e1aa9ce9c Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Sat, 5 Nov 2016 20:56:47 -0400 Subject: Finish Merge, most tests passing --- gerber/primitives.py | 366 +++++++++++++++++++++++++-------------------------- 1 file changed, 183 insertions(+), 183 deletions(-) (limited to 'gerber/primitives.py') diff --git a/gerber/primitives.py b/gerber/primitives.py index a291c26..a66400a 100644 --- a/gerber/primitives.py +++ b/gerber/primitives.py @@ -16,14 +16,14 @@ # limitations under the License. - + import math from operator import add from itertools import combinations -from .utils import validate_coordinates, inch, metric, convex_hull, rotate_point, nearly_equal +from .utils import validate_coordinates, inch, metric, convex_hull, rotate_point, nearly_equal + - class Primitive(object): """ Base class for all Cam file primitives @@ -50,9 +50,9 @@ class Primitive(object): def __init__(self, level_polarity='dark', rotation=0, units=None, net_name=None): self.level_polarity = level_polarity - self.net_name = net_name - self._to_convert = list() - self.id = id + self.net_name = net_name + self._to_convert = list() + self.id = id self._memoized = list() self._units = units self._rotation = rotation @@ -60,21 +60,21 @@ class Primitive(object): self._sin_theta = math.sin(math.radians(rotation)) self._bounding_box = None self._vertices = None - self._segments = None - + self._segments = None + @property def flashed(self): '''Is this a flashed primitive''' - + raise NotImplementedError('Is flashed must be ' - 'implemented in subclass') + 'implemented in subclass') def __eq__(self, other): return self.__dict__ == other.__dict__ - + @property def units(self): - return self._units + return self._units @units.setter def units(self, value): @@ -84,7 +84,7 @@ class Primitive(object): @property def rotation(self): return self._rotation - + @rotation.setter def rotation(self, value): self._changed() @@ -103,7 +103,7 @@ class Primitive(object): self._segments = [segment for segment in combinations(self.vertices, 2)] return self._segments - + @property def bounding_box(self): """ Calculate axis-aligned bounding box @@ -114,14 +114,14 @@ class Primitive(object): """ raise NotImplementedError('Bounding box calculation must be ' 'implemented in subclass') - + @property def bounding_box_no_aperture(self): """ Calculate bouxing box without considering the aperture - + for most objects, this is the same as the bounding_box, but is different for Lines and Arcs (which are not flashed) - + Return ((min x, max x), (min y, max y)) """ return self.bounding_box @@ -175,7 +175,7 @@ class Primitive(object): except: if value is not None: setattr(self, attr, metric(value)) - + def offset(self, x_offset=0, y_offset=0): """ Move the primitive by the specified x and y offset amount. @@ -186,7 +186,7 @@ class Primitive(object): self.position = tuple([coord + offset for coord, offset in zip(self.position, (x_offset, y_offset))]) - + def to_statement(self): pass @@ -201,7 +201,7 @@ class Primitive(object): self._bounding_box = None self._vertices = None self._segments = None - for attr in self._memoized: + for attr in self._memoized: setattr(self, attr, None) class Line(Primitive): @@ -214,8 +214,8 @@ class Line(Primitive): self._end = end self.aperture = aperture self._to_convert = ['start', 'end', 'aperture'] - - @property + + @property def flashed(self): return False @@ -244,8 +244,8 @@ class Line(Primitive): angle = math.atan2(delta_y, delta_x) return angle - @property - def bounding_box(self): + @property + def bounding_box(self): if self._bounding_box is None: if isinstance(self.aperture, Circle): width_2 = self.aperture.radius @@ -267,7 +267,7 @@ class Line(Primitive): max_x = max(self.start[0], self.end[0]) min_y = min(self.start[1], self.end[1]) max_y = max(self.start[1], self.end[1]) - return ((min_x, max_x), (min_y, max_y)) + return ((min_x, max_x), (min_y, max_y)) @property def vertices(self): @@ -291,30 +291,30 @@ class Line(Primitive): # The line is defined by the convex hull of the points self._vertices = convex_hull((start_ll, start_lr, start_ul, start_ur, end_ll, end_lr, end_ul, end_ur)) return self._vertices - + def offset(self, x_offset=0, y_offset=0): - self._changed() + self._changed() self.start = tuple([coord + offset for coord, offset in zip(self.start, (x_offset, y_offset))]) self.end = tuple([coord + offset for coord, offset in zip(self.end, (x_offset, y_offset))]) - + def equivalent(self, other, offset): - + if not isinstance(other, Line): return False - + equiv_start = tuple(map(add, other.start, offset)) - equiv_end = tuple(map(add, other.end, offset)) - + equiv_end = tuple(map(add, other.end, offset)) + return nearly_equal(self.start, equiv_start) and nearly_equal(self.end, equiv_end) class Arc(Primitive): """ """ - - def __init__(self, start, end, center, direction, aperture, quadrant_mode, **kwargs): + + def __init__(self, start, end, center, direction, aperture, quadrant_mode, **kwargs): super(Arc, self).__init__(**kwargs) self._start = start self._end = end @@ -324,10 +324,10 @@ class Arc(Primitive): self._quadrant_mode = quadrant_mode self._to_convert = ['start', 'end', 'center', 'aperture'] - @property + @property def flashed(self): return False - + @property def start(self): return self._start @@ -354,11 +354,11 @@ class Arc(Primitive): def center(self, value): self._changed() self._center = value - + @property def quadrant_mode(self): return self._quadrant_mode - + @quadrant_mode.setter def quadrant_mode(self, quadrant_mode): self._changed() @@ -436,8 +436,8 @@ class Arc(Primitive): min_y = min(y) - self.aperture.radius max_y = max(y) + self.aperture.radius self._bounding_box = ((min_x, max_x), (min_y, max_y)) - return self._bounding_box - + return self._bounding_box + @property def bounding_box_no_aperture(self): '''Gets the bounding box without considering the aperture''' @@ -472,12 +472,12 @@ class Arc(Primitive): if theta1 <= math.pi * 1.5 and (theta0 >= math.pi * 1.5 or theta0 < theta1): points.append((self.center[0], self.center[1] - self.radius )) x, y = zip(*points) - + min_x = min(x) max_x = max(x) min_y = min(y) max_y = max(y) - return ((min_x, max_x), (min_y, max_y)) + return ((min_x, max_x), (min_y, max_y)) def offset(self, x_offset=0, y_offset=0): self._changed() @@ -489,19 +489,19 @@ class Arc(Primitive): class Circle(Primitive): """ """ - - def __init__(self, position, diameter, hole_diameter = None, **kwargs): + + def __init__(self, position, diameter, hole_diameter = None, **kwargs): super(Circle, self).__init__(**kwargs) validate_coordinates(position) self._position = position - self._diameter = diameter + self._diameter = diameter self.hole_diameter = hole_diameter - self._to_convert = ['position', 'diameter', 'hole_diameter'] + self._to_convert = ['position', 'diameter', 'hole_diameter'] - @property + @property def flashed(self): return True - + @property def position(self): return self._position @@ -523,7 +523,7 @@ class Circle(Primitive): @property def radius(self): return self.diameter / 2. - + @property def hole_radius(self): if self.hole_diameter != None: @@ -538,23 +538,23 @@ class Circle(Primitive): min_y = self.position[1] - self.radius max_y = self.position[1] + self.radius self._bounding_box = ((min_x, max_x), (min_y, max_y)) - return self._bounding_box - + return self._bounding_box + def offset(self, x_offset=0, y_offset=0): self.position = tuple(map(add, self.position, (x_offset, y_offset))) - + def equivalent(self, other, offset): '''Is this the same as the other circle, ignoring the offiset?''' if not isinstance(other, Circle): return False - + if self.diameter != other.diameter or self.hole_diameter != other.hole_diameter: return False - + equiv_position = tuple(map(add, other.position, offset)) - return nearly_equal(self.position, equiv_position) + return nearly_equal(self.position, equiv_position) class Ellipse(Primitive): @@ -568,19 +568,19 @@ class Ellipse(Primitive): self._width = width self._height = height self._to_convert = ['position', 'width', 'height'] - - @property + + @property def flashed(self): return True - + @property def position(self): return self._position - + @position.setter def position(self, value): self._changed() - self._position = value + self._position = value @property def width(self): @@ -626,29 +626,29 @@ class Ellipse(Primitive): class Rectangle(Primitive): - """ + """ When rotated, the rotation is about the center point. - + Only aperture macro generated Rectangle objects can be rotated. If you aren't in a AMGroup, then you don't need to worry about rotation """ - - def __init__(self, position, width, height, hole_diameter=0, **kwargs): + + def __init__(self, position, width, height, hole_diameter=0, **kwargs): super(Rectangle, self).__init__(**kwargs) validate_coordinates(position) self._position = position self._width = width - self._height = height + self._height = height self.hole_diameter = hole_diameter self._to_convert = ['position', 'width', 'height', 'hole_diameter'] # TODO These are probably wrong when rotated self._lower_left = None self._upper_right = None - - @property + + @property def flashed(self): return True - + @property def position(self): return self._position @@ -658,14 +658,14 @@ class Rectangle(Primitive): self._changed() self._position = value - @property + @property def width(self): return self._width @width.setter def width(self, value): self._changed() - self._width = value + self._width = value @property def height(self): @@ -675,7 +675,7 @@ class Rectangle(Primitive): def height(self, value): self._changed() self._height = value - + @property def hole_radius(self): """The radius of the hole. If there is no hole, returns None""" @@ -683,12 +683,12 @@ class Rectangle(Primitive): return self.hole_diameter / 2. return None - @property + @property def upper_right(self): return (self.position[0] + (self._abs_width / 2.), self.position[1] + (self._abs_height / 2.)) - @property + @property def lower_left(self): return (self.position[0] - (self.axis_aligned_width / 2.), self.position[1] - (self.axis_aligned_height / 2.)) @@ -721,27 +721,27 @@ class Rectangle(Primitive): def axis_aligned_width(self): return (self._cos_theta * self.width + self._sin_theta * self.height) - @property + @property def _abs_height(self): return (math.cos(math.radians(self.rotation)) * self.height + math.sin(math.radians(self.rotation)) * self.width) - @property + @property def axis_aligned_height(self): return (self._cos_theta * self.height + self._sin_theta * self.width) - + def equivalent(self, other, offset): """Is this the same as the other rect, ignoring the offset?""" if not isinstance(other, Rectangle): return False - + if self.width != other.width or self.height != other.height or self.rotation != other.rotation or self.hole_diameter != other.hole_diameter: return False - + equiv_position = tuple(map(add, other.position, offset)) - return nearly_equal(self.position, equiv_position) + return nearly_equal(self.position, equiv_position) class Diamond(Primitive): @@ -755,8 +755,8 @@ class Diamond(Primitive): self._width = width self._height = height self._to_convert = ['position', 'width', 'height'] - - @property + + @property def flashed(self): return True @@ -767,7 +767,7 @@ class Diamond(Primitive): @position.setter def position(self, value): self._changed() - self._position = value + self._position = value @property def width(self): @@ -778,7 +778,7 @@ class Diamond(Primitive): self._changed() self._width = value - @property + @property def height(self): return self._height @@ -833,8 +833,8 @@ class ChamferRectangle(Primitive): self._chamfer = chamfer self._corners = corners self._to_convert = ['position', 'width', 'height', 'chamfer'] - - @property + + @property def flashed(self): return True @@ -922,8 +922,8 @@ class RoundRectangle(Primitive): self._radius = radius self._corners = corners self._to_convert = ['position', 'width', 'height', 'radius'] - - @property + + @property def flashed(self): return True @@ -952,7 +952,7 @@ class RoundRectangle(Primitive): @height.setter def height(self, value): self._changed() - self._height = value + self._height = value @property def radius(self): @@ -987,28 +987,28 @@ class RoundRectangle(Primitive): return (self._cos_theta * self.width + self._sin_theta * self.height) - @property + @property def axis_aligned_height(self): return (self._cos_theta * self.height + self._sin_theta * self.width) class Obround(Primitive): - """ """ - - def __init__(self, position, width, height, hole_diameter=0, **kwargs): + """ + + def __init__(self, position, width, height, hole_diameter=0, **kwargs): super(Obround, self).__init__(**kwargs) validate_coordinates(position) self._position = position self._width = width - self._height = height + self._height = height self.hole_diameter = hole_diameter self._to_convert = ['position', 'width', 'height', 'hole_diameter'] - - @property + + @property def flashed(self): - return True + return True @property def position(self): @@ -1017,7 +1017,7 @@ class Obround(Primitive): @position.setter def position(self, value): self._changed() - self._position = value + self._position = value @property def width(self): @@ -1028,7 +1028,7 @@ class Obround(Primitive): self._changed() self._width = value - @property + @property def upper_right(self): return (self.position[0] + (self._abs_width / 2.), self.position[1] + (self._abs_height / 2.)) @@ -1047,8 +1047,8 @@ class Obround(Primitive): """The radius of the hole. If there is no hole, returns None""" if self.hole_diameter != None: return self.hole_diameter / 2. - - return None + + return None @property def orientation(self): @@ -1096,31 +1096,31 @@ class Obround(Primitive): class Polygon(Primitive): - """ + """ Polygon flash defined by a set number of sides. - """ - def __init__(self, position, sides, radius, hole_diameter, **kwargs): + """ + def __init__(self, position, sides, radius, hole_diameter, **kwargs): super(Polygon, self).__init__(**kwargs) validate_coordinates(position) self._position = position - self.sides = sides + self.sides = sides self._radius = radius self.hole_diameter = hole_diameter self._to_convert = ['position', 'radius', 'hole_diameter'] - - @property + + @property def flashed(self): return True - + @property def diameter(self): return self.radius * 2 - + @property def hole_radius(self): if self.hole_diameter != None: return self.hole_diameter / 2. - return None + return None @property def position(self): @@ -1129,7 +1129,7 @@ class Polygon(Primitive): @position.setter def position(self, value): self._changed() - self._position = value + self._position = value @property def radius(self): @@ -1149,22 +1149,22 @@ class Polygon(Primitive): max_y = self.position[1] + self.radius self._bounding_box = ((min_x, max_x), (min_y, max_y)) return self._bounding_box - + def offset(self, x_offset=0, y_offset=0): self.position = tuple(map(add, self.position, (x_offset, y_offset))) - + @property def vertices(self): - + offset = self.rotation da = 360.0 / self.sides - + points = [] for i in xrange(self.sides): points.append(rotate_point((self.position[0] + self.radius, self.position[1]), offset + da * i, self.position)) - + return points - + @property def vertices(self): if self._vertices is None: @@ -1175,17 +1175,17 @@ class Polygon(Primitive): self._vertices = [(((x * self._cos_theta) - (y * self._sin_theta)), ((x * self._sin_theta) + (y * self._cos_theta))) for x, y in vertices] - return self._vertices + return self._vertices def equivalent(self, other, offset): """ Is this the outline the same as the other, ignoring the position offset? """ - + # Quick check if it even makes sense to compare them if type(self) != type(other) or self.sides != other.sides or self.radius != other.radius: return False - + equiv_pos = tuple(map(add, other.position, offset)) return nearly_equal(self.position, equiv_pos) @@ -1193,14 +1193,14 @@ class Polygon(Primitive): class AMGroup(Primitive): """ - """ + """ def __init__(self, amprimitives, stmt = None, **kwargs): """ - + stmt : The original statment that generated this, since it is really hard to re-generate from primitives """ super(AMGroup, self).__init__(**kwargs) - + self.primitives = [] for amprim in amprimitives: prim = amprim.to_primitive(self.units) @@ -1212,11 +1212,11 @@ class AMGroup(Primitive): self._position = None self._to_convert = ['_position', 'primitives'] self.stmt = stmt - + def to_inch(self): if self.units == 'metric': super(AMGroup, self).to_inch() - + # If we also have a stmt, convert that too if self.stmt: self.stmt.to_inch() @@ -1225,15 +1225,15 @@ class AMGroup(Primitive): def to_metric(self): if self.units == 'inch': super(AMGroup, self).to_metric() - + # If we also have a stmt, convert that too if self.stmt: self.stmt.to_metric() - + @property def flashed(self): return True - + @property def bounding_box(self): # TODO Make this cached like other items @@ -1245,49 +1245,49 @@ class AMGroup(Primitive): min_y = min(miny) max_y = max(maxy) return ((min_x, max_x), (min_y, max_y)) - + @property def position(self): return self._position - + def offset(self, x_offset=0, y_offset=0): self._position = tuple(map(add, self._position, (x_offset, y_offset))) - + for primitive in self.primitives: primitive.offset(x_offset, y_offset) - + @position.setter def position(self, new_pos): ''' Sets the position of the AMGroup. This offset all of the objects by the specified distance. ''' - + if self._position: dx = new_pos[0] - self._position[0] dy = new_pos[1] - self._position[1] else: dx = new_pos[0] dy = new_pos[1] - + for primitive in self.primitives: primitive.offset(dx, dy) - + self._position = new_pos - + def equivalent(self, other, offset): ''' Is this the macro group the same as the other, ignoring the position offset? ''' - + if len(self.primitives) != len(other.primitives): return False - + # We know they have the same number of primitives, so now check them all for i in range(0, len(self.primitives)): if not self.primitives[i].equivalent(other.primitives[i], offset): return False - + # If we didn't find any differences, then they are the same return True @@ -1296,16 +1296,16 @@ class Outline(Primitive): Outlines only exist as the rendering for a apeture macro outline. They don't exist outside of AMGroup objects """ - + def __init__(self, primitives, **kwargs): super(Outline, self).__init__(**kwargs) self.primitives = primitives self._to_convert = ['primitives'] - + if self.primitives[0].start != self.primitives[-1].end: raise ValueError('Outline must be closed') - - @property + + @property def flashed(self): return True @@ -1326,7 +1326,7 @@ class Outline(Primitive): self._changed() for p in self.primitives: p.offset(x_offset, y_offset) - + @property def vertices(self): if self._vertices is None: @@ -1337,7 +1337,7 @@ class Outline(Primitive): self._vertices = [(((x * self._cos_theta) - (y * self._sin_theta)), ((x * self._sin_theta) + (y * self._cos_theta))) for x, y in vertices] - return self._vertices + return self._vertices @property def width(self): @@ -1348,15 +1348,15 @@ class Outline(Primitive): ''' Is this the outline the same as the other, ignoring the position offset? ''' - + # Quick check if it even makes sense to compare them if type(self) != type(other) or len(self.primitives) != len(other.primitives): return False - + for i in range(0, len(self.primitives)): if not self.primitives[i].equivalent(other.primitives[i], offset): return False - + return True class Region(Primitive): @@ -1367,13 +1367,13 @@ class Region(Primitive): super(Region, self).__init__(**kwargs) self.primitives = primitives self._to_convert = ['primitives'] - - @property + + @property def flashed(self): return False @property - def bounding_box(self): + def bounding_box(self): if self._bounding_box is None: xlims, ylims = zip(*[p.bounding_box_no_aperture for p in self.primitives]) minx, maxx = zip(*xlims) @@ -1383,7 +1383,7 @@ class Region(Primitive): min_y = min(miny) max_y = max(maxy) self._bounding_box = ((min_x, max_x), (min_y, max_y)) - return self._bounding_box + return self._bounding_box def offset(self, x_offset=0, y_offset=0): self._changed() @@ -1401,10 +1401,10 @@ class RoundButterfly(Primitive): self.position = position self.diameter = diameter self._to_convert = ['position', 'diameter'] - + # TODO This does not reset bounding box correctly - - @property + + @property def flashed(self): return True @@ -1433,13 +1433,13 @@ class SquareButterfly(Primitive): self.position = position self.side = side self._to_convert = ['position', 'side'] - + # TODO This does not reset bounding box correctly - - @property + + @property def flashed(self): - return True - + return True + @property def bounding_box(self): if self._bounding_box is None: @@ -1475,14 +1475,14 @@ class Donut(Primitive): else: # Hexagon self.width = 0.5 * math.sqrt(3.) * outer_diameter - self.height = outer_diameter - + self.height = outer_diameter + self._to_convert = ['position', 'width', 'height', 'inner_diameter', 'outer_diameter'] - + # TODO This does not reset bounding box correctly - - @property + + @property def flashed(self): return True @@ -1494,7 +1494,7 @@ class Donut(Primitive): @property def upper_right(self): return (self.position[0] + (self.width / 2.), - self.position[1] + (self.height / 2.) + self.position[1] + (self.height / 2.)) @property def bounding_box(self): @@ -1521,11 +1521,11 @@ class SquareRoundDonut(Primitive): self.inner_diameter = inner_diameter self.outer_diameter = outer_diameter self._to_convert = ['position', 'inner_diameter', 'outer_diameter'] - - @property + + @property def flashed(self): return True - + @property def bounding_box(self): if self._bounding_box is None: @@ -1537,7 +1537,7 @@ class SquareRoundDonut(Primitive): class Drill(Primitive): """ A drill hole - """ + """ def __init__(self, position, diameter, hit, **kwargs): super(Drill, self).__init__('dark', **kwargs) validate_coordinates(position) @@ -1545,14 +1545,14 @@ class Drill(Primitive): self._diameter = diameter self.hit = hit self._to_convert = ['position', 'diameter', 'hit'] - + # TODO Ths won't handle the hit updates correctly - - @property + + @property def flashed(self): - return False + return False - @property + @property def position(self): return self._position @@ -1583,15 +1583,15 @@ class Drill(Primitive): max_y = self.position[1] + self.radius self._bounding_box = ((min_x, max_x), (min_y, max_y)) return self._bounding_box - + def offset(self, x_offset=0, y_offset=0): self._changed() self.position = tuple(map(add, self.position, (x_offset, y_offset))) - + def __str__(self): return '' % (self.diameter, self.position[0], self.position[1], self.hit) - - + + class Slot(Primitive): """ A drilled slot """ @@ -1604,13 +1604,13 @@ class Slot(Primitive): self.diameter = diameter self.hit = hit self._to_convert = ['start', 'end', 'diameter', 'hit'] - + # TODO this needs to use cached bounding box - - @property + + @property def flashed(self): return False - + def bounding_box(self): if self._bounding_box is None: ll = tuple([c - self.outer_diameter / 2. for c in self.position]) @@ -1621,7 +1621,7 @@ class Slot(Primitive): def offset(self, x_offset=0, y_offset=0): self.start = tuple(map(add, self.start, (x_offset, y_offset))) self.end = tuple(map(add, self.end, (x_offset, y_offset))) - + class TestRecord(Primitive): """ Netlist Test record -- cgit