gerbonara/gerber/primitives.py
jaseg a7a5981e0e Make primitives with unset level polarity inherit from region
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.
2019-02-03 14:37:26 +09:00

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']