summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--gerber/primitives.py232
-rw-r--r--gerber/rs274x.py115
-rw-r--r--gerber/utils.py7
-rw-r--r--requirements.txt2
4 files changed, 223 insertions, 133 deletions
diff --git a/gerber/primitives.py b/gerber/primitives.py
index bd93e04..f583ca9 100644
--- a/gerber/primitives.py
+++ b/gerber/primitives.py
@@ -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,77 +410,98 @@ 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.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:
+ 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):
+ 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):
+ 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):
+ 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:
+ 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):
+ 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):
+ 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):
+ 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
- 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.direction == 'counterclockwise':
- # Passes through 0 degrees
- if theta0 > theta1:
- points.append((self.center[0] + self.radius, self.center[1]))
- # Passes through 90 degrees
- if theta0 <= math.pi / 2. and (theta1 >= math.pi / 2. or theta1 < theta0):
- points.append((self.center[0], self.center[1] + self.radius))
- # Passes through 180 degrees
- if theta0 <= math.pi and (theta1 >= math.pi or theta1 < theta0):
- points.append((self.center[0] - self.radius, self.center[1]))
- # Passes through 270 degrees
- if theta0 <= math.pi * 1.5 and (theta1 >= math.pi * 1.5 or theta1 < theta0):
- points.append((self.center[0], self.center[1] - self.radius ))
- else:
- # Passes through 0 degrees
- if theta1 > theta0:
- points.append((self.center[0] + self.radius, self.center[1]))
- # Passes through 90 degrees
- if theta1 <= math.pi / 2. and (theta0 >= math.pi / 2. or theta0 < theta1):
- points.append((self.center[0], self.center[1] + self.radius))
- # Passes through 180 degrees
- if theta1 <= math.pi and (theta0 >= math.pi or theta0 < theta1):
- points.append((self.center[0] - self.radius, self.center[1]))
- # Passes through 270 degrees
- if theta1 <= math.pi * 1.5 and (theta0 >= math.pi * 1.5 or theta0 < theta1):
- points.append((self.center[0], self.center[1] - self.radius ))
x, y = zip(*points)
min_x = min(x)
@@ -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
diff --git a/gerber/rs274x.py b/gerber/rs274x.py
index ff8addd..5191fb7 100644
--- a/gerber/rs274x.py
+++ b/gerber/rs274x.py
@@ -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
-
- aperture = Rectangle(position=None, width=width, height=height, hole_diameter=hole_diameter, units=self.settings.units)
+ elif len(modifiers[0]) == 4:
+ rectangular_hole = modifiers[0][2:4]
+
+ 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
-
- aperture = Obround(position=None, width=width, height=height, hole_diameter=hole_diameter, units=self.settings.units)
+ elif len(modifiers[0]) == 4:
+ rectangular_hole = modifiers[0][2:4]
+
+ 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:
diff --git a/gerber/utils.py b/gerber/utils.py
index c62ad2a..06adfd7 100644
--- a/gerber/utils.py
+++ b/gerber/utils.py
@@ -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]
diff --git a/requirements.txt b/requirements.txt
index 014e92b..e049232 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,3 +1,3 @@
## The following requirements were added by pip --freeze:
cairocffi==0.6
-pyhull==1.5.6
+scipy