Finish adding square hole support, fix some primitive calculations, etc.

This commit is contained in:
Hamilton Kibbe 2016-11-18 08:05:57 -05:00
parent 6b672e98ff
commit a7f1f6ef0f
4 changed files with 228 additions and 138 deletions

View file

@ -64,7 +64,6 @@ class Primitive(object):
@property
def flashed(self):
'''Is this a flashed primitive'''
raise NotImplementedError('Is flashed must be '
'implemented in subclass')
@ -271,9 +270,9 @@ class Line(Primitive):
@property
def vertices(self):
if self._vertices is None:
start = self.start
end = self.end
if isinstance(self.aperture, Rectangle):
start = self.start
end = self.end
width = self.aperture.width
height = self.aperture.height
@ -289,6 +288,11 @@ class Line(Primitive):
# The line is defined by the convex hull of the points
self._vertices = convex_hull((start_ll, start_lr, start_ul, start_ur, end_ll, end_lr, end_ul, end_ur))
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):
@ -309,11 +313,18 @@ class Line(Primitive):
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, **kwargs):
def __init__(self, start, end, center, direction, aperture, quadrant_mode,
**kwargs):
super(Arc, self).__init__(**kwargs)
self._start = start
self._end = end
@ -371,15 +382,15 @@ class Arc(Primitive):
@property
def start_angle(self):
dy, dx = tuple([start - center for start, center
dx, dy = tuple([start - center for start, center
in zip(self.start, self.center)])
return math.atan2(dx, dy)
return math.atan2(dy, dx)
@property
def end_angle(self):
dy, dx = tuple([end - center for end, center
dx, dy = tuple([end - center for end, center
in zip(self.end, self.center)])
return math.atan2(dx, dy)
return math.atan2(dy, dx)
@property
def sweep_angle(self):
@ -399,41 +410,51 @@ class Arc(Primitive):
theta0 = (self.start_angle + two_pi) % two_pi
theta1 = (self.end_angle + two_pi) % two_pi
points = [self.start, self.end]
if self.direction == 'counterclockwise':
# Passes through 0 degrees
if theta0 > theta1:
points.append((self.center[0] + self.radius, self.center[1]))
# Passes through 90 degrees
if theta0 <= math.pi / \
2. and (theta1 >= math.pi / 2. or theta1 < theta0):
points.append((self.center[0], self.center[1] + self.radius))
# Passes through 180 degrees
if theta0 <= math.pi and (theta1 >= math.pi or theta1 < theta0):
points.append((self.center[0] - self.radius, self.center[1]))
# Passes through 270 degrees
if theta0 <= math.pi * \
1.5 and (theta1 >= math.pi * 1.5 or theta1 < theta0):
points.append((self.center[0], self.center[1] - self.radius))
else:
# Passes through 0 degrees
if theta1 > theta0:
points.append((self.center[0] + self.radius, self.center[1]))
# Passes through 90 degrees
if theta1 <= math.pi / \
2. and (theta0 >= math.pi / 2. or theta0 < theta1):
points.append((self.center[0], self.center[1] + self.radius))
# Passes through 180 degrees
if theta1 <= math.pi and (theta0 >= math.pi or theta0 < theta1):
points.append((self.center[0] - self.radius, self.center[1]))
# Passes through 270 degrees
if theta1 <= math.pi * \
1.5 and (theta0 >= math.pi * 1.5 or theta0 < theta1):
points.append((self.center[0], self.center[1] - self.radius))
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) - self.aperture.radius
max_x = max(x) + self.aperture.radius
min_y = min(y) - self.aperture.radius
max_y = max(y) + self.aperture.radius
if 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
@ -444,32 +465,43 @@ class Arc(Primitive):
theta0 = (self.start_angle + two_pi) % two_pi
theta1 = (self.end_angle + two_pi) % two_pi
points = [self.start, self.end]
if self.direction == 'counterclockwise':
# Passes through 0 degrees
if theta0 > theta1:
points.append((self.center[0] + self.radius, self.center[1]))
# Passes through 90 degrees
if theta0 <= math.pi / 2. and (theta1 >= math.pi / 2. or theta1 < theta0):
points.append((self.center[0], self.center[1] + self.radius))
# Passes through 180 degrees
if theta0 <= math.pi and (theta1 >= math.pi or theta1 < theta0):
points.append((self.center[0] - self.radius, self.center[1]))
# Passes through 270 degrees
if theta0 <= math.pi * 1.5 and (theta1 >= math.pi * 1.5 or theta1 < theta0):
points.append((self.center[0], self.center[1] - self.radius ))
else:
# Passes through 0 degrees
if theta1 > theta0:
points.append((self.center[0] + self.radius, self.center[1]))
# Passes through 90 degrees
if theta1 <= math.pi / 2. and (theta0 >= math.pi / 2. or theta0 < theta1):
points.append((self.center[0], self.center[1] + self.radius))
# Passes through 180 degrees
if theta1 <= math.pi and (theta0 >= math.pi or theta0 < theta1):
points.append((self.center[0] - self.radius, self.center[1]))
# Passes through 270 degrees
if theta1 <= math.pi * 1.5 and (theta0 >= math.pi * 1.5 or theta0 < theta1):
points.append((self.center[0], self.center[1] - self.radius ))
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)
@ -489,13 +521,16 @@ class Circle(Primitive):
"""
"""
def __init__(self, position, diameter, hole_diameter = None, **kwargs):
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._to_convert = ['position', '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):
@ -631,14 +666,18 @@ class Rectangle(Primitive):
then you don't need to worry about rotation
"""
def __init__(self, position, width, height, hole_diameter=0, **kwargs):
def __init__(self, position, width, height, hole_diameter=0,
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._to_convert = ['position', 'width', 'height', '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
@ -736,6 +775,12 @@ class Rectangle(Primitive):
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):
"""
@ -898,7 +943,8 @@ class ChamferRectangle(Primitive):
((self.position[0] - delta_w), (self.position[1] - delta_h)),
((self.position[0] + delta_w), (self.position[1] - delta_h))
]
for idx, corner, chamfered in enumerate((rect_corners, self.corners)):
for idx, params in enumerate(zip(rect_corners, self.corners)):
corner, chamfered = params
x, y = corner
if chamfered:
if idx == 0:
@ -1019,14 +1065,18 @@ class Obround(Primitive):
"""
"""
def __init__(self, position, width, height, hole_diameter=0, **kwargs):
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._to_convert = ['position', 'width', 'height', '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):
@ -1116,14 +1166,18 @@ class Polygon(Primitive):
"""
Polygon flash defined by a set number of sides.
"""
def __init__(self, position, sides, radius, hole_diameter, **kwargs):
def __init__(self, position, sides, radius, hole_diameter=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._to_convert = ['position', 'radius', '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):
@ -1174,25 +1228,14 @@ class Polygon(Primitive):
def vertices(self):
offset = self.rotation
da = 360.0 / self.sides
delta_angle = 360.0 / self.sides
points = []
for i in xrange(self.sides):
points.append(rotate_point((self.position[0] + self.radius, self.position[1]), offset + da * i, self.position))
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
@property
def vertices(self):
if self._vertices is None:
theta = math.radians(360/self.sides)
vertices = [(self.position[0] + (math.cos(theta * side) * self.radius),
self.position[1] + (math.sin(theta * side) * self.radius))
for side in range(self.sides)]
self._vertices = [(((x * self._cos_theta) - (y * self._sin_theta)),
((x * self._sin_theta) + (y * self._cos_theta)))
for x, y in vertices]
return self._vertices
def equivalent(self, other, offset):
"""
@ -1555,15 +1598,12 @@ class SquareRoundDonut(Primitive):
class Drill(Primitive):
""" A drill hole
"""
def __init__(self, position, diameter, hit, **kwargs):
def __init__(self, position, diameter, **kwargs):
super(Drill, self).__init__('dark', **kwargs)
validate_coordinates(position)
self._position = position
self._diameter = diameter
self.hit = hit
self._to_convert = ['position', 'diameter', 'hit']
# TODO Ths won't handle the hit updates correctly
self._to_convert = ['position', 'diameter']
@property
def flashed(self):
@ -1606,23 +1646,21 @@ class Drill(Primitive):
self.position = tuple(map(add, self.position, (x_offset, y_offset)))
def __str__(self):
return '<Drill %f (%f, %f) [%s]>' % (self.diameter, self.position[0], self.position[1], self.hit)
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, hit, **kwargs):
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.hit = hit
self._to_convert = ['start', 'end', 'diameter', 'hit']
self._to_convert = ['start', 'end', 'diameter']
# TODO this needs to use cached bounding box
@property
def flashed(self):
@ -1630,8 +1668,8 @@ class Slot(Primitive):
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])
ll = tuple([c - self.diameter / 2. for c in self.position])
ur = tuple([c + self.diameter / 2. for c in self.position])
self._bounding_box = ((ll[0], ur[0]), (ll[1], ur[1]))
return self._bounding_box

View file

@ -514,32 +514,51 @@ class GerberParser(object):
if shape == 'C':
diameter = modifiers[0][0]
if len(modifiers[0]) >= 2:
hole_diameter = 0
rectangular_hole = (0, 0)
if len(modifiers[0]) == 2:
hole_diameter = modifiers[0][1]
else:
hole_diameter = None
elif len(modifiers[0]) == 3:
rectangular_hole = modifiers[0][1:3]
aperture = Circle(position=None, diameter=diameter,
hole_diameter=hole_diameter,
hole_width=rectangular_hole[0],
hole_height=rectangular_hole[1],
units=self.settings.units)
aperture = Circle(position=None, diameter=diameter, hole_diameter=hole_diameter, units=self.settings.units)
elif shape == 'R':
width = modifiers[0][0]
height = modifiers[0][1]
if len(modifiers[0]) >= 3:
hole_diameter = 0
rectangular_hole = (0, 0)
if len(modifiers[0]) == 3:
hole_diameter = modifiers[0][2]
else:
hole_diameter = None
elif len(modifiers[0]) == 4:
rectangular_hole = modifiers[0][2:4]
aperture = Rectangle(position=None, width=width, height=height, hole_diameter=hole_diameter, units=self.settings.units)
aperture = Rectangle(position=None, width=width, height=height,
hole_diameter=hole_diameter,
hole_width=rectangular_hole[0],
hole_height=rectangular_hole[1],
units=self.settings.units)
elif shape == 'O':
width = modifiers[0][0]
height = modifiers[0][1]
if len(modifiers[0]) >= 3:
hole_diameter = 0
rectangular_hole = (0, 0)
if len(modifiers[0]) == 3:
hole_diameter = modifiers[0][2]
else:
hole_diameter = None
elif len(modifiers[0]) == 4:
rectangular_hole = modifiers[0][2:4]
aperture = Obround(position=None, width=width, height=height, hole_diameter=hole_diameter, units=self.settings.units)
aperture = Obround(position=None, width=width, height=height,
hole_diameter=hole_diameter,
hole_width=rectangular_hole[0],
hole_height=rectangular_hole[1],
units=self.settings.units)
elif shape == 'P':
outer_diameter = modifiers[0][0]
number_vertices = int(modifiers[0][1])
@ -548,11 +567,19 @@ class GerberParser(object):
else:
rotation = 0
if len(modifiers[0]) > 3:
hole_diameter = 0
rectangular_hole = (0, 0)
if len(modifiers[0]) == 4:
hole_diameter = modifiers[0][3]
else:
hole_diameter = None
aperture = Polygon(position=None, sides=number_vertices, radius=outer_diameter/2.0, hole_diameter=hole_diameter, rotation=rotation)
elif len(modifiers[0]) >= 5:
rectangular_hole = modifiers[0][3:5]
aperture = Polygon(position=None, sides=number_vertices,
radius=outer_diameter/2.0,
hole_diameter=hole_diameter,
hole_width=rectangular_hole[0],
hole_height=rectangular_hole[1],
rotation=rotation)
else:
aperture = self.macros[shape].build(modifiers)
@ -663,13 +690,18 @@ class GerberParser(object):
quadrant_mode=self.quadrant_mode,
level_polarity=self.level_polarity,
units=self.settings.units))
# Gerbv seems to reset interpolation mode in regions..
# TODO: Make sure this is right.
self.interpolation = 'linear'
elif self.op == "D02" or self.op == "D2":
if self.region_mode == "on":
# D02 in the middle of a region finishes that region and starts a new one
if self.current_region and len(self.current_region) > 1:
self.primitives.append(Region(self.current_region, level_polarity=self.level_polarity, units=self.settings.units))
self.primitives.append(Region(self.current_region,
level_polarity=self.level_polarity,
units=self.settings.units))
self.current_region = None
elif self.op == "D03" or self.op == "D3":
@ -694,29 +726,53 @@ class GerberParser(object):
def _find_center(self, start, end, offsets):
"""
In single quadrant mode, the offsets are always positive, which means there are 4 possible centers.
The correct center is the only one that results in an arc with sweep angle of less than or equal to 90 degrees
In single quadrant mode, the offsets are always positive, which means
there are 4 possible centers. The correct center is the only one that
results in an arc with sweep angle of less than or equal to 90 degrees
in the specified direction
"""
two_pi = 2 * math.pi
if self.quadrant_mode == 'single-quadrant':
# The Gerber spec says single quadrant only has one possible center,
# and you can detect it based on the angle. But for real files, this
# seems to work better - there is usually only one option that makes
# sense for the center (since the distance should be the same
# from start and end). We select the center with the least error in
# radius from all the options with a valid sweep angle.
# The Gerber spec says single quadrant only has one possible center, and you can detect
# based on the angle. But for real files, this seems to work better - there is usually
# only one option that makes sense for the center (since the distance should be the same
# from start and end). Find the center that makes the most sense
sqdist_diff_min = sys.maxint
center = None
for factors in [(1, 1), (1, -1), (-1, 1), (-1, -1)]:
test_center = (start[0] + offsets[0] * factors[0], start[1] + offsets[1] * factors[1])
test_center = (start[0] + offsets[0] * factors[0],
start[1] + offsets[1] * factors[1])
# Find angle from center to start and end points
start_angle = math.atan2(*reversed([_start - _center for _start, _center in zip(start, test_center)]))
end_angle = math.atan2(*reversed([_end - _center for _end, _center in zip(end, test_center)]))
# Clamp angles to 0, 2pi
theta0 = (start_angle + two_pi) % two_pi
theta1 = (end_angle + two_pi) % two_pi
# Determine sweep angle in the current arc direction
if self.direction == 'counterclockwise':
sweep_angle = abs(theta1 - theta0)
else:
theta0 += two_pi
sweep_angle = abs(theta0 - theta1) % two_pi
# Calculate the radius error
sqdist_start = sq_distance(start, test_center)
sqdist_end = sq_distance(end, test_center)
if abs(sqdist_start - sqdist_end) < sqdist_diff_min:
# Take the option with the lowest radius error from the set of
# options with a valid sweep angle
if ((abs(sqdist_start - sqdist_end) < sqdist_diff_min)
and (sweep_angle >= 0)
and (sweep_angle <= math.pi / 2.0)):
center = test_center
sqdist_diff_min = abs(sqdist_start - sqdist_end)
return center
else:
return (start[0] + offsets[0], start[1] + offsets[1])
@ -724,7 +780,6 @@ class GerberParser(object):
def _evaluate_aperture(self, stmt):
self.aperture = stmt.d
def _match_one(expr, data):
match = expr.match(data)
if match is None:

View file

@ -25,9 +25,7 @@ files.
import os
from math import radians, sin, cos
from operator import sub
from copy import deepcopy
from pyhull.convex_hull import ConvexHull
from scipy.spatial import ConvexHull
MILLIMETERS_PER_INCH = 25.4
@ -344,5 +342,4 @@ def listdir(directory, ignore_hidden=True, ignore_os=True):
def convex_hull(points):
vertices = ConvexHull(points).vertices
return [points[idx] for idx in
set([point for pair in vertices for point in pair])]
return [points[idx] for idx in vertices]

View file

@ -1,3 +1,3 @@
## The following requirements were added by pip --freeze:
cairocffi==0.6
pyhull==1.5.6
scipy