This fixes region rendering with programatically generated primitives
such that clear level polarity works in an intuitive way. This is useful
for e.g. cutouts in regions. Before, the renderer would set level
polarity twice, both when starting the region and then again once for
each region primitive (line or arc). The problem was that the primitives
in a region with "clear" polarity would when constructed with unset
polarity default to "dark". Thus the renderer would emit something like
LPC (clear polarity) -> G36 (start region) -> LPD (dark polarity) ->
{lines...} instead of LPC -> G36 -> {lines...}.
After this commit, Line and Arc will retain None as level polarity when
created with unset level polarity, and region rendering will override
None with the region's polarity. Outside regions, the old dark default
remains unchanged.
Note on verification: Somehow, gEDA gerbv would still render the broken
regions the way one would have intended, but other viewers (KiCAD
gerbview, the online EasyEDA one and whatever JLC uses to make their
silkscreens) would not.
1697 lines
56 KiB
Python
1697 lines
56 KiB
Python
#! /usr/bin/env python
|
|
# -*- coding: utf-8 -*-
|
|
|
|
# copyright 2016 Hamilton Kibbe <ham@hamiltonkib.be>
|
|
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# 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
|
|
from itertools import combinations
|
|
from .utils import validate_coordinates, inch, metric, convex_hull
|
|
from .utils import rotate_point, nearly_equal
|
|
|
|
|
|
|
|
|
|
class Primitive(object):
|
|
""" Base class for all Cam file primitives
|
|
|
|
Parameters
|
|
---------
|
|
level_polarity : string
|
|
Polarity of the parameter. May be 'dark' or 'clear'. Dark indicates
|
|
a "positive" primitive, i.e. indicating where coppper should remain,
|
|
and clear indicates a negative primitive, such as where copper should
|
|
be removed. clear primitives are often used to create cutouts in region
|
|
pours.
|
|
|
|
rotation : float
|
|
Rotation of a primitive about its origin in degrees. Positive rotation
|
|
is counter-clockwise as viewed from the board top.
|
|
|
|
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, net_name=None):
|
|
self.level_polarity = level_polarity
|
|
self.net_name = net_name
|
|
self._to_convert = list()
|
|
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):
|
|
'''Is this a flashed primitive'''
|
|
raise NotImplementedError('Is flashed must be '
|
|
'implemented in subclass')
|
|
|
|
def __eq__(self, other):
|
|
return self.__dict__ == other.__dict__
|
|
|
|
@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 axis-aligned bounding box
|
|
|
|
will be helpful for sweep & prune during DRC clearance checks.
|
|
|
|
Return ((min x, max x), (min y, max y))
|
|
"""
|
|
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):
|
|
""" 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]:
|
|
if hasattr(value, 'to_inch'):
|
|
value.to_inch()
|
|
else:
|
|
try:
|
|
if len(value) > 1:
|
|
if hasattr(value[0], 'to_inch'):
|
|
for v in value:
|
|
v.to_inch()
|
|
elif isinstance(value[0], tuple):
|
|
setattr(self, attr,
|
|
[tuple(map(inch, point))
|
|
for point in value])
|
|
else:
|
|
setattr(self, attr, tuple(map(inch, value)))
|
|
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]:
|
|
if hasattr(value, 'to_metric'):
|
|
value.to_metric()
|
|
else:
|
|
try:
|
|
if len(value) > 1:
|
|
if hasattr(value[0], 'to_metric'):
|
|
for v in value:
|
|
v.to_metric()
|
|
elif isinstance(value[0], tuple):
|
|
setattr(self, attr,
|
|
[tuple(map(metric, point))
|
|
for point in value])
|
|
else:
|
|
setattr(self, attr, tuple(map(metric, value)))
|
|
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.
|
|
|
|
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 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, level_polarity=None, **kwargs):
|
|
super(Line, self).__init__(**kwargs)
|
|
self.level_polarity = level_polarity
|
|
self._start = start
|
|
self._end = end
|
|
self.aperture = aperture
|
|
self._to_convert = ['start', 'end', '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 angle(self):
|
|
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 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):
|
|
'''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):
|
|
if self._vertices is None:
|
|
start = self.start
|
|
end = self.end
|
|
if isinstance(self.aperture, Rectangle):
|
|
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))
|
|
elif isinstance(self.aperture, Polygon):
|
|
points = [map(add, point, vertex)
|
|
for vertex in self.aperture.vertices
|
|
for point in (start, end)]
|
|
self._vertices = convex_hull(points)
|
|
return self._vertices
|
|
|
|
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))])
|
|
|
|
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)
|
|
|
|
def __str__(self):
|
|
return "<Line {} to {}>".format(self.start, self.end)
|
|
|
|
def __repr__(self):
|
|
return str(self)
|
|
|
|
class Arc(Primitive):
|
|
"""
|
|
"""
|
|
|
|
def __init__(self, start, end, center, direction, aperture, quadrant_mode,
|
|
level_polarity=None, **kwargs):
|
|
super(Arc, self).__init__(**kwargs)
|
|
self.level_polarity = level_polarity
|
|
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
|
|
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 = tuple([start - center for start, center
|
|
in zip(self.start, self.center)])
|
|
return math.sqrt(dy ** 2 + dx ** 2)
|
|
|
|
@property
|
|
def start_angle(self):
|
|
dx, dy = tuple([start - center for start, center
|
|
in zip(self.start, self.center)])
|
|
return math.atan2(dy, dx)
|
|
|
|
@property
|
|
def end_angle(self):
|
|
dx, dy = tuple([end - center for end, center
|
|
in zip(self.end, self.center)])
|
|
return math.atan2(dy, dx)
|
|
|
|
@property
|
|
def sweep_angle(self):
|
|
two_pi = 2 * math.pi
|
|
theta0 = (self.start_angle + two_pi) % two_pi
|
|
theta1 = (self.end_angle + two_pi) % two_pi
|
|
if self.direction == 'counterclockwise':
|
|
return abs(theta1 - theta0)
|
|
else:
|
|
theta0 += two_pi
|
|
return abs(theta0 - theta1) % two_pi
|
|
|
|
@property
|
|
def bounding_box(self):
|
|
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.quadrant_mode == 'multi-quadrant':
|
|
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)))
|
|
or ((theta1 > math.pi / 2.) and (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))
|
|
or ((theta1 > math.pi) and (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)
|
|
or ((theta1 > math.pi * 1.5) and (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))
|
|
or ((theta0 > math.pi / 2.) and (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))
|
|
or ((theta0 > math.pi) and (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))
|
|
or ((theta0 > math.pi * 1.5) and (theta0 <= theta1))):
|
|
points.append((self.center[0], self.center[1] - self.radius))
|
|
x, y = zip(*points)
|
|
if hasattr(self.aperture, 'radius'):
|
|
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
|
|
else:
|
|
min_x = min(x) - self.aperture.width
|
|
max_x = max(x) + self.aperture.width
|
|
min_y = min(y) - self.aperture.height
|
|
max_y = max(y) + self.aperture.height
|
|
|
|
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 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.quadrant_mode == 'multi-quadrant':
|
|
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)))
|
|
or ((theta1 > math.pi / 2.) and (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))
|
|
or ((theta1 > math.pi) and (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)
|
|
or ((theta1 > math.pi * 1.5) and (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))
|
|
or ((theta0 > math.pi / 2.) and (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))
|
|
or ((theta0 > math.pi) and (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))
|
|
or ((theta0 > math.pi * 1.5) and (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._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)))
|
|
|
|
|
|
class Circle(Primitive):
|
|
"""
|
|
"""
|
|
|
|
def __init__(self, position, diameter, hole_diameter=None,
|
|
hole_width=0, hole_height=0, **kwargs):
|
|
super(Circle, self).__init__(**kwargs)
|
|
validate_coordinates(position)
|
|
self._position = position
|
|
self._diameter = diameter
|
|
self.hole_diameter = hole_diameter
|
|
self.hole_width = hole_width
|
|
self.hole_height = hole_height
|
|
self._to_convert = ['position', 'diameter', 'hole_diameter', 'hole_width', 'hole_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 diameter(self):
|
|
return self._diameter
|
|
|
|
@diameter.setter
|
|
def diameter(self, value):
|
|
self._changed()
|
|
self._diameter = value
|
|
|
|
@property
|
|
def radius(self):
|
|
return self.diameter / 2.
|
|
|
|
@property
|
|
def hole_radius(self):
|
|
if self.hole_diameter != None:
|
|
return self.hole_diameter / 2.
|
|
return None
|
|
|
|
@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)))
|
|
|
|
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)
|
|
|
|
|
|
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._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 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):
|
|
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.))
|
|
return 2 * math.sqrt((ux * ux) + (vx * vx))
|
|
|
|
@property
|
|
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.))
|
|
return 2 * math.sqrt((uy * uy) + (vy * vy))
|
|
|
|
|
|
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,
|
|
hole_width=0, hole_height=0, **kwargs):
|
|
super(Rectangle, self).__init__(**kwargs)
|
|
validate_coordinates(position)
|
|
self._position = position
|
|
self._width = width
|
|
self._height = height
|
|
self.hole_diameter = hole_diameter
|
|
self.hole_width = hole_width
|
|
self.hole_height = hole_height
|
|
self._to_convert = ['position', 'width', 'height', 'hole_diameter',
|
|
'hole_width', 'hole_height']
|
|
# TODO These are probably wrong when rotated
|
|
self._lower_left = None
|
|
self._upper_right = None
|
|
|
|
@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 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):
|
|
"""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 upper_right(self):
|
|
return (self.position[0] + (self.axis_aligned_width / 2.),
|
|
self.position[1] + (self.axis_aligned_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):
|
|
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 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 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)
|
|
|
|
def __str__(self):
|
|
return "<Rectangle W {} H {} R {}>".format(self.width, self.height, self.rotation * 180/math.pi)
|
|
|
|
def __repr__(self):
|
|
return self.__str__()
|
|
|
|
|
|
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._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 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):
|
|
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 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 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 ChamferRectangle(Primitive):
|
|
"""
|
|
"""
|
|
def __init__(self, position, width, height, chamfer, corners=None, **kwargs):
|
|
super(ChamferRectangle, self).__init__(**kwargs)
|
|
validate_coordinates(position)
|
|
self._position = position
|
|
self._width = width
|
|
self._height = height
|
|
self._chamfer = chamfer
|
|
self._corners = corners if corners is not None else [True] * 4
|
|
self._to_convert = ['position', 'width', 'height', 'chamfer']
|
|
|
|
@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 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):
|
|
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 vertices(self):
|
|
if self._vertices is None:
|
|
vertices = []
|
|
delta_w = self.width / 2.
|
|
delta_h = self.height / 2.
|
|
# order is UR, UL, LL, LR
|
|
rect_corners = [
|
|
((self.position[0] + delta_w), (self.position[1] + delta_h)),
|
|
((self.position[0] - delta_w), (self.position[1] + delta_h)),
|
|
((self.position[0] - delta_w), (self.position[1] - delta_h)),
|
|
((self.position[0] + delta_w), (self.position[1] - delta_h))
|
|
]
|
|
for idx, params in enumerate(zip(rect_corners, self.corners)):
|
|
corner, chamfered = params
|
|
x, y = corner
|
|
if chamfered:
|
|
if idx == 0:
|
|
vertices.append((x - self.chamfer, y))
|
|
vertices.append((x, y - self.chamfer))
|
|
elif idx == 1:
|
|
vertices.append((x + self.chamfer, y))
|
|
vertices.append((x, y - self.chamfer))
|
|
elif idx == 2:
|
|
vertices.append((x + self.chamfer, y))
|
|
vertices.append((x, y + self.chamfer))
|
|
elif idx == 3:
|
|
vertices.append((x - self.chamfer, y))
|
|
vertices.append((x, y + self.chamfer))
|
|
else:
|
|
vertices.append(corner)
|
|
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 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 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._to_convert = ['position', 'width', 'height', 'radius']
|
|
|
|
@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 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 radius(self):
|
|
return self._radius
|
|
|
|
@radius.setter
|
|
def radius(self, value):
|
|
self._changed()
|
|
self._radius = value
|
|
|
|
@property
|
|
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,
|
|
hole_width=0,hole_height=0, **kwargs):
|
|
super(Obround, self).__init__(**kwargs)
|
|
validate_coordinates(position)
|
|
self._position = position
|
|
self._width = width
|
|
self._height = height
|
|
self.hole_diameter = hole_diameter
|
|
self.hole_width = hole_width
|
|
self.hole_height = hole_height
|
|
self._to_convert = ['position', 'width', 'height', 'hole_diameter',
|
|
'hole_width', 'hole_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 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):
|
|
"""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):
|
|
return 'vertical' if self.height > self.width else 'horizontal'
|
|
|
|
@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 subshapes(self):
|
|
if self.orientation == 'vertical':
|
|
circle1 = Circle((self.position[0], self.position[1] +
|
|
(self.height - self.width) / 2.), self.width)
|
|
circle2 = Circle((self.position[0], self.position[1] -
|
|
(self.height - self.width) / 2.), self.width)
|
|
rect = Rectangle(self.position, self.width,
|
|
(self.height - self.width))
|
|
else:
|
|
circle1 = Circle((self.position[0]
|
|
- (self.height - self.width) / 2.,
|
|
self.position[1]), self.height)
|
|
circle2 = Circle((self.position[0]
|
|
+ (self.height - self.width) / 2.,
|
|
self.position[1]), self.height)
|
|
rect = Rectangle(self.position, (self.width - self.height),
|
|
self.height)
|
|
return {'circle1': circle1, 'circle2': circle2, 'rectangle': rect}
|
|
|
|
@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 Polygon(Primitive):
|
|
"""
|
|
Polygon flash defined by a set number of sides.
|
|
"""
|
|
def __init__(self, position, sides, radius, hole_diameter=0,
|
|
hole_width=0, hole_height=0, **kwargs):
|
|
super(Polygon, self).__init__(**kwargs)
|
|
validate_coordinates(position)
|
|
self._position = position
|
|
self.sides = sides
|
|
self._radius = radius
|
|
self.hole_diameter = hole_diameter
|
|
self.hole_width = hole_width
|
|
self.hole_height = hole_height
|
|
self._to_convert = ['position', 'radius', 'hole_diameter',
|
|
'hole_width', 'hole_height']
|
|
|
|
@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
|
|
|
|
@property
|
|
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)))
|
|
|
|
@property
|
|
def vertices(self):
|
|
|
|
offset = self.rotation
|
|
delta_angle = 360.0 / self.sides
|
|
|
|
points = []
|
|
for i in range(self.sides):
|
|
points.append(
|
|
rotate_point((self.position[0] + self.radius, self.position[1]), offset + delta_angle * 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):
|
|
"""
|
|
"""
|
|
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)
|
|
if isinstance(prim, list):
|
|
for p in prim:
|
|
self.primitives.append(p)
|
|
elif prim:
|
|
self.primitives.append(prim)
|
|
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()
|
|
|
|
|
|
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
|
|
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
|
|
|
|
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
|
|
|
|
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
|
|
def flashed(self):
|
|
return True
|
|
|
|
@property
|
|
def bounding_box(self):
|
|
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)
|
|
|
|
@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]
|
|
|
|
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):
|
|
"""
|
|
"""
|
|
|
|
def __init__(self, primitives, **kwargs):
|
|
super(Region, self).__init__(**kwargs)
|
|
self.primitives = primitives
|
|
self._to_convert = ['primitives']
|
|
|
|
@property
|
|
def flashed(self):
|
|
return False
|
|
|
|
@property
|
|
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)
|
|
|
|
|
|
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)
|
|
self.position = position
|
|
self.diameter = diameter
|
|
self._to_convert = ['position', 'diameter']
|
|
|
|
# TODO This does not reset bounding box correctly
|
|
|
|
@property
|
|
def flashed(self):
|
|
return True
|
|
|
|
@property
|
|
def radius(self):
|
|
return self.diameter / 2.
|
|
|
|
@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
|
|
|
|
|
|
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)
|
|
self.position = position
|
|
self.side = side
|
|
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:
|
|
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):
|
|
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')
|
|
self.shape = shape
|
|
if inner_diameter >= outer_diameter:
|
|
raise ValueError(
|
|
'Outer diameter must be larger than inner diameter.')
|
|
self.inner_diameter = inner_diameter
|
|
self.outer_diameter = outer_diameter
|
|
if self.shape in ('round', 'square', 'octagon'):
|
|
self.width = outer_diameter
|
|
self.height = outer_diameter
|
|
else:
|
|
# 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
|
|
|
|
@property
|
|
def lower_left(self):
|
|
return (self.position[0] - (self.width / 2.),
|
|
self.position[1] - (self.height / 2.))
|
|
|
|
@property
|
|
def upper_right(self):
|
|
return (self.position[0] + (self.width / 2.),
|
|
self.position[1] + (self.height / 2.))
|
|
|
|
@property
|
|
def bounding_box(self):
|
|
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.')
|
|
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 bounding_box(self):
|
|
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, **kwargs):
|
|
super(Drill, self).__init__('dark', **kwargs)
|
|
validate_coordinates(position)
|
|
self._position = position
|
|
self._diameter = diameter
|
|
self._to_convert = ['position', 'diameter']
|
|
|
|
@property
|
|
def flashed(self):
|
|
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):
|
|
return self.diameter / 2.
|
|
|
|
@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._changed()
|
|
self.position = tuple(map(add, self.position, (x_offset, y_offset)))
|
|
|
|
def __str__(self):
|
|
return '<Drill %f %s (%f, %f)>' % (self.diameter, self.units, self.position[0], self.position[1])
|
|
|
|
|
|
class Slot(Primitive):
|
|
""" A drilled slot
|
|
"""
|
|
def __init__(self, start, end, diameter, **kwargs):
|
|
super(Slot, self).__init__('dark', **kwargs)
|
|
validate_coordinates(start)
|
|
validate_coordinates(end)
|
|
self.start = start
|
|
self.end = end
|
|
self.diameter = diameter
|
|
self._to_convert = ['start', 'end', 'diameter']
|
|
|
|
|
|
@property
|
|
def flashed(self):
|
|
return False
|
|
|
|
@property
|
|
def bounding_box(self):
|
|
if self._bounding_box is None:
|
|
radius = self.diameter / 2.
|
|
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
|
|
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.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
|
|
self._to_convert = ['position']
|