summaryrefslogtreecommitdiff
path: root/gerber
diff options
context:
space:
mode:
authorHamilton Kibbe <hamilton.kibbe@gmail.com>2016-01-21 03:57:44 -0500
committerGarret Fick <garret@ficksworkshop.com>2016-08-06 09:40:40 +0800
commit8cd842a41a55ab3d8f558a2e3e198beba7da58a1 (patch)
tree895d818072e043ac5275ed6cdc1d724ea5cd489a /gerber
parent965d3ce23b92f8aff1063debd6d3364de15791fe (diff)
downloadgerbonara-8cd842a41a55ab3d8f558a2e3e198beba7da58a1.tar.gz
gerbonara-8cd842a41a55ab3d8f558a2e3e198beba7da58a1.tar.bz2
gerbonara-8cd842a41a55ab3d8f558a2e3e198beba7da58a1.zip
Manually mere rendering changes
Diffstat (limited to 'gerber')
-rw-r--r--gerber/am_eval.py19
-rw-r--r--gerber/am_read.py7
-rw-r--r--gerber/am_statements.py113
-rw-r--r--gerber/cam.py18
-rw-r--r--gerber/common.py3
-rwxr-xr-xgerber/excellon.py94
-rw-r--r--gerber/excellon_statements.py6
-rw-r--r--gerber/gerber_statements.py47
-rw-r--r--gerber/layers.py7
-rw-r--r--gerber/operations.py5
-rw-r--r--gerber/pcb.py15
-rw-r--r--gerber/primitives.py1159
-rw-r--r--gerber/render/cairo_backend.py395
-rw-r--r--gerber/render/render.py8
-rw-r--r--gerber/render/theme.py4
-rw-r--r--gerber/rs274x.py55
-rw-r--r--gerber/tests/test_am_statements.py65
-rw-r--r--gerber/tests/test_cam.py27
-rw-r--r--gerber/tests/test_common.py8
-rw-r--r--gerber/tests/test_excellon.py8
-rw-r--r--gerber/tests/test_excellon_statements.py173
-rw-r--r--gerber/tests/test_gerber_statements.py145
-rw-r--r--gerber/tests/test_ipc356.py29
-rw-r--r--gerber/tests/test_layers.py2
-rw-r--r--gerber/tests/test_primitives.py416
-rw-r--r--gerber/tests/test_rs274x.py9
-rw-r--r--gerber/tests/test_utils.py25
-rw-r--r--gerber/tests/tests.py3
-rw-r--r--gerber/utils.py41
29 files changed, 1865 insertions, 1041 deletions
diff --git a/gerber/am_eval.py b/gerber/am_eval.py
index 29b380d..3a7e1ed 100644
--- a/gerber/am_eval.py
+++ b/gerber/am_eval.py
@@ -18,15 +18,16 @@
""" This module provides RS-274-X AM macro evaluation.
"""
+
class OpCode:
- PUSH = 1
- LOAD = 2
+ PUSH = 1
+ LOAD = 2
STORE = 3
- ADD = 4
- SUB = 5
- MUL = 6
- DIV = 7
- PRIM = 8
+ ADD = 4
+ SUB = 5
+ MUL = 6
+ DIV = 7
+ PRIM = 8
@staticmethod
def str(opcode):
@@ -49,16 +50,18 @@ class OpCode:
else:
return "UNKNOWN"
+
def eval_macro(instructions, parameters={}):
if not isinstance(parameters, type({})):
p = {}
for i, val in enumerate(parameters):
- p[i+1] = val
+ p[i + 1] = val
parameters = p
stack = []
+
def pop():
return stack.pop()
diff --git a/gerber/am_read.py b/gerber/am_read.py
index 65d08a6..4aff00b 100644
--- a/gerber/am_read.py
+++ b/gerber/am_read.py
@@ -26,7 +26,8 @@ import string
class Token:
ADD = "+"
SUB = "-"
- MULT = ("x", "X") # compatibility as many gerber writes do use non compliant X
+ # compatibility as many gerber writes do use non compliant X
+ MULT = ("x", "X")
DIV = "/"
OPERATORS = (ADD, SUB, MULT[0], MULT[1], DIV)
LEFT_PARENS = "("
@@ -62,6 +63,7 @@ def is_op(token):
class Scanner:
+
def __init__(self, s):
self.buff = s
self.n = 0
@@ -111,7 +113,8 @@ class Scanner:
def print_instructions(instructions):
for opcode, argument in instructions:
- print("%s %s" % (OpCode.str(opcode), str(argument) if argument is not None else ""))
+ print("%s %s" % (OpCode.str(opcode),
+ str(argument) if argument is not None else ""))
def read_macro(macro):
diff --git a/gerber/am_statements.py b/gerber/am_statements.py
index ed9f71e..248542d 100644
--- a/gerber/am_statements.py
+++ b/gerber/am_statements.py
@@ -16,14 +16,14 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+from math import asin
import math
-from .utils import validate_coordinates, inch, metric, rotate_point
+
from .primitives import Circle, Line, Outline, Polygon, Rectangle
-from math import asin
+from .utils import validate_coordinates, inch, metric, rotate_point
# TODO: Add support for aperture macro variables
-
__all__ = ['AMPrimitive', 'AMCommentPrimitive', 'AMCirclePrimitive',
'AMVectorLinePrimitive', 'AMOutlinePrimitive', 'AMPolygonPrimitive',
'AMMoirePrimitive', 'AMThermalPrimitive', 'AMCenterLinePrimitive',
@@ -54,12 +54,14 @@ class AMPrimitive(object):
------
TypeError, ValueError
"""
+
def __init__(self, code, exposure=None):
VALID_CODES = (0, 1, 2, 4, 5, 6, 7, 20, 21, 22, 9999)
if not isinstance(code, int):
raise TypeError('Aperture Macro Primitive code must be an integer')
elif code not in VALID_CODES:
- raise ValueError('Invalid Code. Valid codes are %s.' % ', '.join(map(str, VALID_CODES)))
+ raise ValueError('Invalid Code. Valid codes are %s.' %
+ ', '.join(map(str, VALID_CODES)))
if exposure is not None and exposure.lower() not in ('on', 'off'):
raise ValueError('Exposure must be either on or off')
self.code = code
@@ -71,21 +73,21 @@ class AMPrimitive(object):
def to_metric(self):
raise NotImplementedError('Subclass must implement `to-metric`')
- def to_primitive(self, units):
- """
- Convert to a primitive, as defines the primitives module (for drawing)
- """
- raise NotImplementedError('Subclass must implement `to-primitive`')
-
@property
def _level_polarity(self):
if self.exposure == 'off':
return 'clear'
return 'dark'
+ def to_primitive(self, units):
+ """ Return a Primitive instance based on the specified macro params.
+ """
+ print('Rendering {}s is not supported yet.'.format(str(self.__class__)))
+
def __eq__(self, other):
return self.__dict__ == other.__dict__
+
class AMCommentPrimitive(AMPrimitive):
""" Aperture Macro Comment primitive. Code 0
@@ -207,11 +209,11 @@ class AMCirclePrimitive(AMPrimitive):
self.position = tuple([metric(x) for x in self.position])
def to_gerber(self, settings=None):
- data = dict(code = self.code,
- exposure = '1' if self.exposure == 'on' else 0,
- diameter = self.diameter,
- x = self.position[0],
- y = self.position[1])
+ data = dict(code=self.code,
+ exposure='1' if self.exposure == 'on' else 0,
+ diameter=self.diameter,
+ x=self.position[0],
+ y=self.position[1])
return '{code},{exposure},{diameter},{x},{y}*'.format(**data)
def to_primitive(self, units):
@@ -294,21 +296,26 @@ class AMVectorLinePrimitive(AMPrimitive):
self.start = tuple([metric(x) for x in self.start])
self.end = tuple([metric(x) for x in self.end])
-
def to_gerber(self, settings=None):
fmtstr = '{code},{exp},{width},{startx},{starty},{endx},{endy},{rotation}*'
- data = dict(code = self.code,
- exp = 1 if self.exposure == 'on' else 0,
- width = self.width,
- startx = self.start[0],
- starty = self.start[1],
- endx = self.end[0],
- endy = self.end[1],
- rotation = self.rotation)
+ data = dict(code=self.code,
+ exp=1 if self.exposure == 'on' else 0,
+ width=self.width,
+ startx=self.start[0],
+ starty=self.start[1],
+ endx=self.end[0],
+ endy=self.end[1],
+ rotation=self.rotation)
return fmtstr.format(**data)
def to_primitive(self, units):
+ """
+ Convert this to a primitive. We use the Outline to represent this (instead of Line)
+ because the behaviour of the end caps is different for aperture macros compared to Lines
+ when rotated.
+ """
+ # Use a line to generate our vertices easily
line = Line(self.start, self.end, Rectangle(None, self.width, self.width))
vertices = line.vertices
@@ -385,7 +392,8 @@ class AMOutlinePrimitive(AMPrimitive):
start_point = (float(modifiers[3]), float(modifiers[4]))
points = []
for i in range(n):
- points.append((float(modifiers[5 + i*2]), float(modifiers[5 + i*2 + 1])))
+ points.append((float(modifiers[5 + i * 2]),
+ float(modifiers[5 + i * 2 + 1])))
rotation = float(modifiers[-1])
return cls(code, exposure, start_point, points, rotation)
@@ -425,6 +433,10 @@ class AMOutlinePrimitive(AMPrimitive):
return "{code},{exposure},{n_points},{start_point},{points},\n{rotation}*".format(**data)
def to_primitive(self, units):
+ """
+ Convert this to a drawable primitive. This uses the Outline instead of Line
+ primitive to handle differences in end caps when rotated.
+ """
lines = []
prev_point = rotate_point(self.start_point, self.rotation)
@@ -500,7 +512,6 @@ class AMPolygonPrimitive(AMPrimitive):
rotation = float(modifiers[6])
return cls(code, exposure, vertices, position, diameter, rotation)
-
def __init__(self, code, exposure, vertices, position, diameter, rotation):
""" Initialize AMPolygonPrimitive
"""
@@ -529,7 +540,7 @@ class AMPolygonPrimitive(AMPrimitive):
exposure="1" if self.exposure == "on" else "0",
vertices=self.vertices,
position="%.4g,%.4g" % self.position,
- diameter = '%.4g' % self.diameter,
+ diameter='%.4g' % self.diameter,
rotation=str(self.rotation)
)
fmt = "{code},{exposure},{vertices},{position},{diameter},{rotation}*"
@@ -633,17 +644,16 @@ class AMMoirePrimitive(AMPrimitive):
self.crosshair_thickness = metric(self.crosshair_thickness)
self.crosshair_length = metric(self.crosshair_length)
-
def to_gerber(self, settings=None):
data = dict(
code=self.code,
position="%.4g,%.4g" % self.position,
- diameter = self.diameter,
- ring_thickness = self.ring_thickness,
- gap = self.gap,
- max_rings = self.max_rings,
- crosshair_thickness = self.crosshair_thickness,
- crosshair_length = self.crosshair_length,
+ diameter=self.diameter,
+ ring_thickness=self.ring_thickness,
+ gap=self.gap,
+ max_rings=self.max_rings,
+ crosshair_thickness=self.crosshair_thickness,
+ crosshair_length=self.crosshair_length,
rotation=self.rotation
)
fmt = "{code},{position},{diameter},{ring_thickness},{gap},{max_rings},{crosshair_thickness},{crosshair_length},{rotation}*"
@@ -698,7 +708,7 @@ class AMThermalPrimitive(AMPrimitive):
code = int(modifiers[0])
position = (float(modifiers[1]), float(modifiers[2]))
outer_diameter = float(modifiers[3])
- inner_diameter= float(modifiers[4])
+ inner_diameter = float(modifiers[4])
gap = float(modifiers[5])
rotation = float(modifiers[6])
return cls(code, position, outer_diameter, inner_diameter, gap, rotation)
@@ -720,7 +730,6 @@ class AMThermalPrimitive(AMPrimitive):
self.inner_diameter = inch(self.inner_diameter)
self.gap = inch(self.gap)
-
def to_metric(self):
self.position = tuple([metric(x) for x in self.position])
self.outer_diameter = metric(self.outer_diameter)
@@ -873,14 +882,14 @@ class AMCenterLinePrimitive(AMPrimitive):
exposure = 'on' if float(modifiers[1]) == 1 else 'off'
width = float(modifiers[2])
height = float(modifiers[3])
- center= (float(modifiers[4]), float(modifiers[5]))
+ center = (float(modifiers[4]), float(modifiers[5]))
rotation = float(modifiers[6])
return cls(code, exposure, width, height, center, rotation)
def __init__(self, code, exposure, width, height, center, rotation):
if code != 21:
raise ValueError('CenterLinePrimitive code is 21')
- super (AMCenterLinePrimitive, self).__init__(code, exposure)
+ super(AMCenterLinePrimitive, self).__init__(code, exposure)
self.width = width
self.height = height
validate_coordinates(center)
@@ -900,9 +909,9 @@ class AMCenterLinePrimitive(AMPrimitive):
def to_gerber(self, settings=None):
data = dict(
code=self.code,
- exposure = '1' if self.exposure == 'on' else '0',
- width = self.width,
- height = self.height,
+ exposure='1' if self.exposure == 'on' else '0',
+ width=self.width,
+ height=self.height,
center="%.4g,%.4g" % self.center,
rotation=self.rotation
)
@@ -986,7 +995,7 @@ class AMLowerLeftLinePrimitive(AMPrimitive):
def __init__(self, code, exposure, width, height, lower_left, rotation):
if code != 22:
raise ValueError('LowerLeftLinePrimitive code is 22')
- super (AMLowerLeftLinePrimitive, self).__init__(code, exposure)
+ super(AMLowerLeftLinePrimitive, self).__init__(code, exposure)
self.width = width
self.height = height
validate_coordinates(lower_left)
@@ -1003,23 +1012,31 @@ class AMLowerLeftLinePrimitive(AMPrimitive):
self.width = metric(self.width)
self.height = metric(self.height)
+ def to_primitive(self, units):
+ # TODO I think I have merged this wrong
+ # Offset the primitive from macro position
+ position = tuple([a + b for a , b in zip (position, self.lower_left)])
+ position = tuple([pos + offset for pos, offset in
+ zip(position, (self.width/2, self.height/2))])
+ # Return a renderable primitive
+ return Rectangle(self.position, self.width, self.height,
+ level_polarity=self._level_polarity, units=units)
+
def to_gerber(self, settings=None):
data = dict(
code=self.code,
- exposure = '1' if self.exposure == 'on' else '0',
- width = self.width,
- height = self.height,
+ exposure='1' if self.exposure == 'on' else '0',
+ width=self.width,
+ height=self.height,
lower_left="%.4g,%.4g" % self.lower_left,
rotation=self.rotation
)
fmt = "{code},{exposure},{width},{height},{lower_left},{rotation}*"
return fmt.format(**data)
- def to_primitive(self, units):
- raise NotImplementedError()
-
class AMUnsupportPrimitive(AMPrimitive):
+
@classmethod
def from_gerber(cls, primitive):
return cls(primitive)
diff --git a/gerber/cam.py b/gerber/cam.py
index f64aa34..0e19b05 100644
--- a/gerber/cam.py
+++ b/gerber/cam.py
@@ -22,6 +22,7 @@ CAM File
This module provides common base classes for Excellon/Gerber CNC files
"""
+
class FileSettings(object):
""" CAM File Settings
@@ -52,6 +53,7 @@ class FileSettings(object):
specify both. `zero_suppression` will take on the opposite value of `zeros`
and vice versa
"""
+
def __init__(self, notation='absolute', units='inch',
zero_suppression=None, format=(2, 5), zeros=None,
angle_units='degrees'):
@@ -248,6 +250,12 @@ class CamFile(object):
"""
pass
+ def to_inch(self):
+ pass
+
+ def to_metric(self):
+ pass
+
def render(self, ctx, invert=False, filename=None):
""" Generate image of layer.
@@ -262,15 +270,11 @@ class CamFile(object):
ctx.set_bounds(self.bounding_box)
ctx._paint_background()
-
- if invert:
- ctx.invert = True
- ctx._clear_mask()
+ ctx.invert = invert
+ ctx._new_render_layer()
for p in self.primitives:
ctx.render(p)
- if invert:
- ctx.invert = False
- ctx._render_mask()
+ ctx._flatten()
if filename is not None:
ctx.dump(filename)
diff --git a/gerber/common.py b/gerber/common.py
index 04b6423..cf137dd 100644
--- a/gerber/common.py
+++ b/gerber/common.py
@@ -22,7 +22,6 @@ from .exceptions import ParseError
from .utils import detect_file_format
-
def read(filename):
""" Read a gerber or excellon file and return a representative object.
@@ -73,5 +72,3 @@ def loads(data):
return excellon.loads(data)
else:
raise TypeError('Unable to detect file format')
-
-
diff --git a/gerber/excellon.py b/gerber/excellon.py
index a0bad4f..a5da42a 100755
--- a/gerber/excellon.py
+++ b/gerber/excellon.py
@@ -81,7 +81,7 @@ def loads(data, settings = None, tools = None):
return ExcellonParser(settings, tools).parse_raw(data)
-class DrillHit(object):
+class DrillHit(object):
"""Drill feature that is a single drill hole.
Attributes
@@ -92,6 +92,7 @@ class DrillHit(object):
Center position of the drill.
"""
+
def __init__(self, tool, position):
self.tool = tool
self.position = position
@@ -184,6 +185,7 @@ class ExcellonFile(CamFile):
either 'inch' or 'metric'.
"""
+
def __init__(self, statements, tools, hits, settings, filename=None):
super(ExcellonFile, self).__init__(statements=statements,
settings=settings,
@@ -193,7 +195,9 @@ class ExcellonFile(CamFile):
@property
def primitives(self):
-
+ """
+ Gets the primitives. Note that unlike Gerber, this generates new objects
+ """
primitives = []
for hit in self.hits:
if isinstance(hit, DrillHit):
@@ -203,8 +207,7 @@ class ExcellonFile(CamFile):
else:
raise ValueError('Unknown hit type')
- return primitives
-
+ return primitives
@property
def bounds(self):
@@ -237,7 +240,8 @@ class ExcellonFile(CamFile):
rprt += ' Code Size Hits Path Length\n'
rprt += ' --------------------------------------\n'
for tool in iter(self.tools.values()):
- rprt += toolfmt.format(tool.number, tool.diameter, tool.hit_count, self.path_length(tool.number))
+ rprt += toolfmt.format(tool.number, tool.diameter,
+ tool.hit_count, self.path_length(tool.number))
if filename is not None:
with open(filename, 'w') as f:
f.write(rprt)
@@ -245,13 +249,21 @@ class ExcellonFile(CamFile):
def write(self, filename=None):
filename = filename if filename is not None else self.filename
- with open(filename, 'w') as f:
- self.writes(f)
-
- def writes(self, f):
- # Copy the header verbatim
- for statement in self.statements:
- f.write(statement.to_excellon(self.settings) + '\n')
+ with open(filename, 'w') as f:
+ for statement in self.statements:
+ if not isinstance(statement, ToolSelectionStmt):
+ f.write(statement.to_excellon(self.settings) + '\n')
+ else:
+ break
+
+ # Write out coordinates for drill hits by tool
+ for tool in iter(self.tools.values()):
+ f.write(ToolSelectionStmt(tool.number).to_excellon(self.settings) + '\n')
+ for hit in self.hits:
+ if hit.tool.number == tool.number:
+ f.write(CoordinateStmt(
+ *hit.position).to_excellon(self.settings) + '\n')
+ f.write(EndOfProgramStmt().to_excellon() + '\n')
def to_inch(self):
"""
@@ -265,9 +277,8 @@ class ExcellonFile(CamFile):
tool.to_inch()
for primitive in self.primitives:
primitive.to_inch()
- for hit in self.hits:
- hit.to_inch()
-
+ for hit in self.hits:
+ hit.to_inch()
def to_metric(self):
""" Convert units to metric
@@ -288,8 +299,8 @@ class ExcellonFile(CamFile):
statement.offset(x_offset, y_offset)
for primitive in self.primitives:
primitive.offset(x_offset, y_offset)
- for hit in self. hits:
- hit.offset(x_offset, y_offset)
+ for hit in self. hits:
+ hit.offset(x_offset, y_offset)
def path_length(self, tool_number=None):
""" Return the path length for a given tool
@@ -299,9 +310,11 @@ class ExcellonFile(CamFile):
for hit in self.hits:
tool = hit.tool
num = tool.number
- positions[num] = (0, 0) if positions.get(num) is None else positions[num]
+ positions[num] = (0, 0) if positions.get(
+ num) is None else positions[num]
lengths[num] = 0.0 if lengths.get(num) is None else lengths[num]
- lengths[num] = lengths[num] + math.hypot(*tuple(map(operator.sub, positions[num], hit.position)))
+ lengths[num] = lengths[
+ num] + math.hypot(*tuple(map(operator.sub, positions[num], hit.position)))
positions[num] = hit.position
if tool_number is None:
@@ -310,13 +323,13 @@ class ExcellonFile(CamFile):
return lengths.get(tool_number)
def hit_count(self, tool_number=None):
- counts = {}
- for tool in iter(self.tools.values()):
- counts[tool.number] = tool.hit_count
- if tool_number is None:
- return counts
- else:
- return counts.get(tool_number)
+ counts = {}
+ for tool in iter(self.tools.values()):
+ counts[tool.number] = tool.hit_count
+ if tool_number is None:
+ return counts
+ else:
+ return counts.get(tool_number)
def update_tool(self, tool_number, **kwargs):
""" Change parameters of a tool
@@ -340,7 +353,6 @@ class ExcellonFile(CamFile):
hit.tool = newtool
-
class ExcellonParser(object):
""" Excellon File Parser
@@ -348,8 +360,8 @@ class ExcellonParser(object):
----------
settings : FileSettings or dict-like
Excellon file settings to use when interpreting the excellon file.
- """
- def __init__(self, settings=None, ext_tools=None):
+ """
+ def __init__(self, settings=None, ext_tools=None):
self.notation = 'absolute'
self.units = 'inch'
self.zeros = 'leading'
@@ -371,7 +383,6 @@ class ExcellonParser(object):
self.notation = settings.notation
self.format = settings.format
-
@property
def coordinates(self):
return [(stmt.x, stmt.y) for stmt in self.statements if isinstance(stmt, CoordinateStmt)]
@@ -421,7 +432,8 @@ class ExcellonParser(object):
# get format from altium comment
if "FILE_FORMAT" in comment_stmt.comment:
- detected_format = tuple([int(x) for x in comment_stmt.comment.split('=')[1].split(":")])
+ detected_format = tuple(
+ [int(x) for x in comment_stmt.comment.split('=')[1].split(":")])
if detected_format:
self.format = detected_format
@@ -553,7 +565,7 @@ class ExcellonParser(object):
self.format = stmt.format
self.statements.append(stmt)
- elif line[:3] == 'M71' or line [:3] == 'M72':
+ elif line[:3] == 'M71' or line[:3] == 'M72':
stmt = MeasuringModeStmt.from_excellon(line)
self.units = stmt.units
self.statements.append(stmt)
@@ -603,20 +615,22 @@ class ExcellonParser(object):
self.statements.append(stmt)
# T0 is used as END marker, just ignore
- if stmt.tool != 0:
+ if stmt.tool != 0:
tool = self._get_tool(stmt.tool)
if not tool:
- # FIXME: for weird files with no tools defined, original calc from gerbv
+ # FIXME: for weird files with no tools defined, original calc from gerbv
if self._settings().units == "inch":
- diameter = (16 + 8 * stmt.tool) / 1000.0;
+ diameter = (16 + 8 * stmt.tool) / 1000.0
else:
- diameter = metric((16 + 8 * stmt.tool) / 1000.0);
+ diameter = metric((16 + 8 * stmt.tool) / 1000.0)
- tool = ExcellonTool(self._settings(), number=stmt.tool, diameter=diameter)
+ tool = ExcellonTool(
+ self._settings(), number=stmt.tool, diameter=diameter)
self.tools[tool.number] = tool
- # FIXME: need to add this tool definition inside header to make sure it is properly written
+ # FIXME: need to add this tool definition inside header to
+ # make sure it is properly written
for i, s in enumerate(self.statements):
if isinstance(s, ToolSelectionStmt) or isinstance(s, ExcellonTool):
self.statements.insert(i, tool)
@@ -787,7 +801,7 @@ def detect_excellon_format(data=None, filename=None):
and 'FILE_FORMAT' in stmt.comment]
detected_format = (tuple([int(val) for val in
- format_comment[0].split('=')[1].split(':')])
+ format_comment[0].split('=')[1].split(':')])
if len(format_comment) == 1 else None)
detected_zeros = zero_statements[0] if len(zero_statements) == 1 else None
@@ -852,6 +866,6 @@ def _layer_size_score(size, hole_count, hole_area):
hole_percentage = hole_area / board_area
hole_score = (hole_percentage - 0.25) ** 2
- size_score = (board_area - 8) **2
+ size_score = (board_area - 8) ** 2
return hole_score * size_score
\ No newline at end of file
diff --git a/gerber/excellon_statements.py b/gerber/excellon_statements.py
index 7153c82..ac9c528 100644
--- a/gerber/excellon_statements.py
+++ b/gerber/excellon_statements.py
@@ -56,6 +56,7 @@ class ExcellonStatement(object):
def to_excellon(self, settings=None):
raise NotImplementedError('to_excellon must be implemented in a '
'subclass')
+
def to_inch(self):
self.units = 'inch'
@@ -68,6 +69,7 @@ class ExcellonStatement(object):
def __eq__(self, other):
return self.__dict__ == other.__dict__
+
class ExcellonTool(ExcellonStatement):
""" Excellon Tool class
@@ -239,7 +241,6 @@ class ExcellonTool(ExcellonStatement):
if self.diameter is not None:
self.diameter = inch(self.diameter)
-
def to_metric(self):
if self.settings.units != 'metric':
self.settings.units = 'metric'
@@ -648,6 +649,7 @@ class EndOfProgramStmt(ExcellonStatement):
if self.y is not None:
self.y += y_offset
+
class UnitStmt(ExcellonStatement):
@classmethod
@@ -689,6 +691,7 @@ class UnitStmt(ExcellonStatement):
def to_metric(self):
self.units = 'metric'
+
class IncrementalModeStmt(ExcellonStatement):
@classmethod
@@ -784,6 +787,7 @@ class MeasuringModeStmt(ExcellonStatement):
def to_metric(self):
self.units = 'metric'
+
class RouteModeStmt(ExcellonStatement):
def __init__(self, **kwargs):
diff --git a/gerber/gerber_statements.py b/gerber/gerber_statements.py
index fba2a3c..08dbd82 100644
--- a/gerber/gerber_statements.py
+++ b/gerber/gerber_statements.py
@@ -44,6 +44,7 @@ class Statement(object):
type : string
String identifying the statement type.
"""
+
def __init__(self, stype, units='inch'):
self.type = stype
self.units = units
@@ -85,6 +86,7 @@ class ParamStmt(Statement):
param : string
Parameter type code
"""
+
def __init__(self, param):
Statement.__init__(self, "PARAM")
self.param = param
@@ -163,8 +165,6 @@ class FSParamStmt(ParamStmt):
return '%FS{0}{1}X{2}Y{3}*%'.format(zero_suppression, notation, fmt, fmt)
-
-
def __str__(self):
return ('<Format Spec: %d:%d %s zero suppression %s notation>' %
(self.format[0], self.format[1], self.zero_suppression, self.notation))
@@ -343,13 +343,15 @@ class ADParamStmt(ParamStmt):
def to_inch(self):
if self.units == 'metric':
- self.units = 'inch'
- self.modifiers = [tuple([inch(x) for x in modifier]) for modifier in self.modifiers]
+ self.units = 'inch'
+ self.modifiers = [tuple([inch(x) for x in modifier])
+ for modifier in self.modifiers]
def to_metric(self):
if self.units == 'inch':
- self.units = 'metric'
- self.modifiers = [tuple([metric(x) for x in modifier]) for modifier in self.modifiers]
+ self.units = 'metric'
+ self.modifiers = [tuple([metric(x) for x in modifier])
+ for modifier in self.modifiers]
def to_gerber(self, settings=None):
if any(self.modifiers):
@@ -426,10 +428,11 @@ class AMParamStmt(ParamStmt):
self.primitives.append(AMOutlinePrimitive.from_gerber(primitive))
elif primitive[0] == '5':
self.primitives.append(AMPolygonPrimitive.from_gerber(primitive))
- elif primitive[0] =='6':
+ elif primitive[0] == '6':
self.primitives.append(AMMoirePrimitive.from_gerber(primitive))
elif primitive[0] == '7':
- self.primitives.append(AMThermalPrimitive.from_gerber(primitive))
+ self.primitives.append(
+ AMThermalPrimitive.from_gerber(primitive))
else:
self.primitives.append(AMUnsupportPrimitive.from_gerber(primitive))
@@ -878,13 +881,17 @@ class CoordStmt(Statement):
op = stmt_dict.get('op')
if x is not None:
- x = parse_gerber_value(stmt_dict.get('x'), settings.format, settings.zero_suppression)
+ x = parse_gerber_value(stmt_dict.get('x'), settings.format,
+ settings.zero_suppression)
if y is not None:
- y = parse_gerber_value(stmt_dict.get('y'), settings.format, settings.zero_suppression)
+ y = parse_gerber_value(stmt_dict.get('y'), settings.format,
+ settings.zero_suppression)
if i is not None:
- i = parse_gerber_value(stmt_dict.get('i'), settings.format, settings.zero_suppression)
+ i = parse_gerber_value(stmt_dict.get('i'), settings.format,
+ settings.zero_suppression)
if j is not None:
- j = parse_gerber_value(stmt_dict.get('j'), settings.format, settings.zero_suppression)
+ j = parse_gerber_value(stmt_dict.get('j'), settings.format,
+ settings.zero_suppression)
return cls(function, x, y, i, j, op, settings)
@classmethod
@@ -958,13 +965,17 @@ class CoordStmt(Statement):
if self.function:
ret += self.function
if self.x is not None:
- ret += 'X{0}'.format(write_gerber_value(self.x, settings.format, settings.zero_suppression))
+ ret += 'X{0}'.format(write_gerber_value(self.x, settings.format,
+ settings.zero_suppression))
if self.y is not None:
- ret += 'Y{0}'.format(write_gerber_value(self.y, settings.format, settings.zero_suppression))
+ ret += 'Y{0}'.format(write_gerber_value(self.y, settings.format,
+ settings.zero_suppression))
if self.i is not None:
- ret += 'I{0}'.format(write_gerber_value(self.i, settings.format, settings.zero_suppression))
+ ret += 'I{0}'.format(write_gerber_value(self.i, settings.format,
+ settings.zero_suppression))
if self.j is not None:
- ret += 'J{0}'.format(write_gerber_value(self.j, settings.format, settings.zero_suppression))
+ ret += 'J{0}'.format(write_gerber_value(self.j, settings.format,
+ settings.zero_suppression))
if self.op:
ret += self.op
return ret + '*'
@@ -1046,6 +1057,7 @@ class CoordStmt(Statement):
class ApertureStmt(Statement):
""" Aperture Statement
"""
+
def __init__(self, d, deprecated=None):
Statement.__init__(self, "APERTURE")
self.d = int(d)
@@ -1079,6 +1091,7 @@ class CommentStmt(Statement):
class EofStmt(Statement):
""" EOF Statement
"""
+
def __init__(self):
Statement.__init__(self, "EOF")
@@ -1149,6 +1162,7 @@ class RegionModeStmt(Statement):
class UnknownStmt(Statement):
""" Unknown Statement
"""
+
def __init__(self, line):
Statement.__init__(self, "UNKNOWN")
self.line = line
@@ -1158,4 +1172,3 @@ class UnknownStmt(Statement):
def __str__(self):
return '<Unknown Statement: \'%s\'>' % self.line
-
diff --git a/gerber/layers.py b/gerber/layers.py
index 2b73893..29e452b 100644
--- a/gerber/layers.py
+++ b/gerber/layers.py
@@ -95,7 +95,8 @@ def sort_layers(layers):
'bottompaste', 'drill', ]
output = []
drill_layers = [layer for layer in layers if layer.layer_class == 'drill']
- internal_layers = list(sorted([layer for layer in layers if layer.layer_class == 'internal']))
+ internal_layers = list(sorted([layer for layer in layers
+ if layer.layer_class == 'internal']))
for layer_class in layer_order:
if layer_class == 'internal':
@@ -151,6 +152,8 @@ class PCBLayer(object):
else:
return None
+ def __repr__(self):
+ return '<PCBLayer: {}>'.format(self.layer_class)
class DrillLayer(PCBLayer):
@classmethod
@@ -163,6 +166,7 @@ class DrillLayer(PCBLayer):
class InternalLayer(PCBLayer):
+
@classmethod
def from_gerber(cls, camfile):
filename = camfile.filename
@@ -208,6 +212,7 @@ class InternalLayer(PCBLayer):
class LayerSet(object):
+
def __init__(self, name, layers, **kwargs):
super(LayerSet, self).__init__(**kwargs)
self.name = name
diff --git a/gerber/operations.py b/gerber/operations.py
index 4eb10e5..d06876e 100644
--- a/gerber/operations.py
+++ b/gerber/operations.py
@@ -22,6 +22,7 @@ CAM File Operations
"""
import copy
+
def to_inch(cam_file):
""" Convert Gerber or Excellon file units to imperial
@@ -39,6 +40,7 @@ def to_inch(cam_file):
cam_file.to_inch()
return cam_file
+
def to_metric(cam_file):
""" Convert Gerber or Excellon file units to metric
@@ -56,6 +58,7 @@ def to_metric(cam_file):
cam_file.to_metric()
return cam_file
+
def offset(cam_file, x_offset, y_offset):
""" Offset a Cam file by a specified amount in the X and Y directions.
@@ -79,6 +82,7 @@ def offset(cam_file, x_offset, y_offset):
cam_file.offset(x_offset, y_offset)
return cam_file
+
def scale(cam_file, x_scale, y_scale):
""" Scale a Cam file by a specified amount in the X and Y directions.
@@ -101,6 +105,7 @@ def scale(cam_file, x_scale, y_scale):
# TODO
pass
+
def rotate(cam_file, angle):
""" Rotate a Cam file a specified amount about the origin.
diff --git a/gerber/pcb.py b/gerber/pcb.py
index 0518dd4..92a1f28 100644
--- a/gerber/pcb.py
+++ b/gerber/pcb.py
@@ -63,13 +63,15 @@ class PCB(object):
@property
def top_layers(self):
- board_layers = [l for l in reversed(self.layers) if l.layer_class in ('topsilk', 'topmask', 'top')]
+ board_layers = [l for l in reversed(self.layers) if l.layer_class in
+ ('topsilk', 'topmask', 'top')]
drill_layers = [l for l in self.drill_layers if 'top' in l.layers]
return board_layers + drill_layers
@property
def bottom_layers(self):
- board_layers = [l for l in self.layers if l.layer_class in ('bottomsilk', 'bottommask', 'bottom')]
+ board_layers = [l for l in self.layers if l.layer_class in
+ ('bottomsilk', 'bottommask', 'bottom')]
drill_layers = [l for l in self.drill_layers if 'bottom' in l.layers]
return board_layers + drill_layers
@@ -78,10 +80,16 @@ class PCB(object):
return [l for l in self.layers if l.layer_class == 'drill']
@property
+ def copper_layers(self):
+ return [layer for layer in self.layers if layer.layer_class in
+ ('top', 'bottom', 'internal')]
+
+ @property
def layer_count(self):
""" Number of *COPPER* layers
"""
- return len([l for l in self.layers if l.layer_class in ('top', 'bottom', 'internal')])
+ return len([l for l in self.layers if l.layer_class in
+ ('top', 'bottom', 'internal')])
@property
def board_bounds(self):
@@ -91,4 +99,3 @@ class PCB(object):
for layer in self.layers:
if layer.layer_class == 'top':
return layer.bounds
-
diff --git a/gerber/primitives.py b/gerber/primitives.py
index f259eff..98b3e1c 100644
--- a/gerber/primitives.py
+++ b/gerber/primitives.py
@@ -1,7 +1,7 @@
#! /usr/bin/env python
# -*- coding: utf-8 -*-
-# copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be>
+# 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.
@@ -14,10 +14,13 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
-import math
-from operator import add, sub
-from .utils import validate_coordinates, inch, metric, rotate_point, nearly_equal
+
+import math
+from operator import add
+from itertools import combinations
+
+from .utils import validate_coordinates, inch, metric, convex_hull, rotate_point, nearly_equal
class Primitive(object):
@@ -35,14 +38,27 @@ class Primitive(object):
rotation : float
Rotation of a primitive about its origin in degrees. Positive rotation
is counter-clockwise as viewed from the board top.
+
+ units : string
+ Units in which primitive was defined. 'inch' or 'metric'
+
+ net_name : string
+ Name of the electrical net the primitive belongs to
"""
- def __init__(self, level_polarity='dark', rotation=0, units=None, id=None, statement_id=None):
+
+ def __init__(self, level_polarity='dark', rotation=0, units=None, net_name=None):
self.level_polarity = level_polarity
- self.rotation = rotation
- self.units = units
- self._to_convert = list()
+ self.net_name = net_name
+ self._to_convert = list()
self.id = id
- self.statement_id = statement_id
+ self._memoized = list()
+ self._units = units
+ self._rotation = rotation
+ self._cos_theta = math.cos(math.radians(rotation))
+ self._sin_theta = math.sin(math.radians(rotation))
+ self._bounding_box = None
+ self._vertices = None
+ self._segments = None
@property
def flashed(self):
@@ -52,8 +68,40 @@ class Primitive(object):
'implemented in subclass')
@property
+ def units(self):
+ return self._units
+
+ @units.setter
+ def units(self, value):
+ self._changed()
+ self._units = value
+
+ @property
+ def rotation(self):
+ return self._rotation
+
+ @rotation.setter
+ def rotation(self, value):
+ self._changed()
+ self._rotation = value
+ self._cos_theta = math.cos(math.radians(value))
+ self._sin_theta = math.sin(math.radians(value))
+
+ @property
+ def vertices(self):
+ return None
+
+ @property
+ def segments(self):
+ if self._segments is None:
+ if self.vertices is not None and len(self.vertices):
+ self._segments = [segment for segment in
+ combinations(self.vertices, 2)]
+ return self._segments
+
+ @property
def bounding_box(self):
- """ Calculate bounding box
+ """ Calculate axis-aligned bounding box
will be helpful for sweep & prune during DRC clearance checks.
@@ -74,9 +122,12 @@ class Primitive(object):
return self.bounding_box
def to_inch(self):
+ """ Convert primitive units to inches.
+ """
if self.units == 'metric':
self.units = 'inch'
- for attr, value in [(attr, getattr(self, attr)) for attr in self._to_convert]:
+ for attr, value in [(attr, getattr(self, attr))
+ for attr in self._to_convert]:
if hasattr(value, 'to_inch'):
value.to_inch()
else:
@@ -86,18 +137,22 @@ class Primitive(object):
for v in value:
v.to_inch()
elif isinstance(value[0], tuple):
- setattr(self, attr, [tuple(map(inch, point)) for point in value])
+ setattr(self, attr,
+ [tuple(map(inch, point))
+ for point in value])
else:
setattr(self, attr, tuple(map(inch, value)))
except:
if value is not None:
setattr(self, attr, inch(value))
-
def to_metric(self):
+ """ Convert primitive units to metric.
+ """
if self.units == 'inch':
self.units = 'metric'
- for attr, value in [(attr, getattr(self, attr)) for attr in self._to_convert]:
+ for attr, value in [(attr, getattr(self, attr))
+ for attr in self._to_convert]:
if hasattr(value, 'to_metric'):
value.to_metric()
else:
@@ -107,15 +162,25 @@ class Primitive(object):
for v in value:
v.to_metric()
elif isinstance(value[0], tuple):
- setattr(self, attr, [tuple(map(metric, point)) for point in value])
+ setattr(self, attr,
+ [tuple(map(metric, point))
+ for point in value])
else:
setattr(self, attr, tuple(map(metric, value)))
except:
if value is not None:
setattr(self, attr, metric(value))
- def offset(self, x_offset=0, y_offset=0):
- raise NotImplementedError('The offset member must be implemented')
+ def offset(self, x_offset=0, y_offset=0):
+ """ Move the primitive by the specified x and y offset amount.
+
+ values are specified in the primitive's native units
+ """
+ if hasattr(self, 'position'):
+ self._changed()
+ self.position = tuple([coord + offset for coord, offset
+ in zip(self.position,
+ (x_offset, y_offset))])
def __eq__(self, other):
return self.__dict__ == other.__dict__
@@ -123,14 +188,29 @@ class Primitive(object):
def to_statement(self):
pass
+ def _changed(self):
+ """ Clear memoized properties.
+
+ Forces a recalculation next time any memoized propery is queried.
+ This must be called from a subclass every time a parameter that affects
+ a memoized property is changed. The easiest way to do this is to call
+ _changed() from property.setter methods.
+ """
+ self._bounding_box = None
+ self._vertices = None
+ self._segments = None
+ for attr in self._memoized:
+ setattr(self, attr, None)
+
class Line(Primitive):
"""
"""
+
def __init__(self, start, end, aperture, **kwargs):
super(Line, self).__init__(**kwargs)
- self.start = start
- self.end = end
+ self._start = start
+ self._end = end
self.aperture = aperture
self._to_convert = ['start', 'end', 'aperture']
@@ -139,24 +219,46 @@ class Line(Primitive):
return False
@property
+ def start(self):
+ return self._start
+
+ @start.setter
+ def start(self, value):
+ self._changed()
+ self._start = value
+
+ @property
+ def end(self):
+ return self._end
+
+ @end.setter
+ def end(self, value):
+ self._changed()
+ self._end = value
+
+
+ @property
def angle(self):
- delta_x, delta_y = tuple(map(sub, self.end, self.start))
+ delta_x, delta_y = tuple(
+ [end - start for end, start in zip(self.end, self.start)])
angle = math.atan2(delta_y, delta_x)
return angle
@property
- def bounding_box(self):
- if isinstance(self.aperture, Circle):
- width_2 = self.aperture.radius
- height_2 = width_2
- else:
- width_2 = self.aperture.width / 2.
- height_2 = self.aperture.height / 2.
- min_x = min(self.start[0], self.end[0]) - width_2
- max_x = max(self.start[0], self.end[0]) + width_2
- min_y = min(self.start[1], self.end[1]) - height_2
- max_y = max(self.start[1], self.end[1]) + height_2
- return ((min_x, max_x), (min_y, max_y))
+ def bounding_box(self):
+ if self._bounding_box is None:
+ if isinstance(self.aperture, Circle):
+ width_2 = self.aperture.radius
+ height_2 = width_2
+ else:
+ width_2 = self.aperture.width / 2.
+ height_2 = self.aperture.height / 2.
+ min_x = min(self.start[0], self.end[0]) - width_2
+ max_x = max(self.start[0], self.end[0]) + width_2
+ min_y = min(self.start[1], self.end[1]) - height_2
+ max_y = max(self.start[1], self.end[1]) + height_2
+ self._bounding_box = ((min_x, max_x), (min_y, max_y))
+ return self._bounding_box
@property
def bounding_box_no_aperture(self):
@@ -165,59 +267,37 @@ class Line(Primitive):
max_x = max(self.start[0], self.end[0])
min_y = min(self.start[1], self.end[1])
max_y = max(self.start[1], self.end[1])
- return ((min_x, max_x), (min_y, max_y))
+ return ((min_x, max_x), (min_y, max_y))
@property
def vertices(self):
- if not isinstance(self.aperture, Rectangle):
- return None
- else:
- start = self.start
- end = self.end
- width = self.aperture.width
- height = self.aperture.height
-
- # Find all the corners of the start and end position
- start_ll = (start[0] - (width / 2.),
- start[1] - (height / 2.))
- start_lr = (start[0] + (width / 2.),
- start[1] - (height / 2.))
- start_ul = (start[0] - (width / 2.),
- start[1] + (height / 2.))
- start_ur = (start[0] + (width / 2.),
- start[1] + (height / 2.))
- end_ll = (end[0] - (width / 2.),
- end[1] - (height / 2.))
- end_lr = (end[0] + (width / 2.),
- end[1] - (height / 2.))
- end_ul = (end[0] - (width / 2.),
- end[1] + (height / 2.))
- end_ur = (end[0] + (width / 2.),
- end[1] + (height / 2.))
-
- if end[0] == start[0] and end[1] == start[1]:
- return (start_ll, start_lr, start_ur, start_ul)
- elif end[0] == start[0] and end[1] > start[1]:
- return (start_ll, start_lr, end_ur, end_ul)
- elif end[0] > start[0] and end[1] > start[1]:
- return (start_ll, start_lr, end_lr, end_ur, end_ul, start_ul)
- elif end[0] > start[0] and end[1] == start[1]:
- return (start_ll, end_lr, end_ur, start_ul)
- elif end[0] > start[0] and end[1] < start[1]:
- return (start_ll, end_ll, end_lr, end_ur, start_ur, start_ul)
- elif end[0] == start[0] and end[1] < start[1]:
- return (end_ll, end_lr, start_ur, start_ul)
- elif end[0] < start[0] and end[1] < start[1]:
- return (end_ll, end_lr, start_lr, start_ur, start_ul, end_ul)
- elif end[0] < start[0] and end[1] == start[1]:
- return (end_ll, start_lr, start_ur, end_ul)
- elif end[0] < start[0] and end[1] > start[1]:
- return (start_ll, start_lr, start_ur, end_ur, end_ul, end_ll)
-
-
- def offset(self, x_offset=0, y_offset=0):
- self.start = tuple(map(add, self.start, (x_offset, y_offset)))
- self.end = tuple(map(add, self.end, (x_offset, y_offset)))
+ if self._vertices is None:
+ if isinstance(self.aperture, Rectangle):
+ start = self.start
+ end = self.end
+ width = self.aperture.width
+ height = self.aperture.height
+
+ # Find all the corners of the start and end position
+ start_ll = (start[0] - (width / 2.), start[1] - (height / 2.))
+ start_lr = (start[0] + (width / 2.), start[1] - (height / 2.))
+ start_ul = (start[0] - (width / 2.), start[1] + (height / 2.))
+ start_ur = (start[0] + (width / 2.), start[1] + (height / 2.))
+ end_ll = (end[0] - (width / 2.), end[1] - (height / 2.))
+ end_lr = (end[0] + (width / 2.), end[1] - (height / 2.))
+ end_ul = (end[0] - (width / 2.), end[1] + (height / 2.))
+ end_ur = (end[0] + (width / 2.), end[1] + (height / 2.))
+
+ # The line is defined by the convex hull of the points
+ self._vertices = convex_hull((start_ll, start_lr, start_ul, start_ur, end_ll, end_lr, end_ul, end_ur))
+ return self._vertices
+
+ def offset(self, x_offset=0, y_offset=0):
+ self.start = tuple([coord + offset for coord, offset
+ in zip(self.start, (x_offset, y_offset))])
+ self.end = tuple([coord + offset for coord, offset
+ in zip(self.end, (x_offset, y_offset))])
+ self._changed()
def equivalent(self, other, offset):
@@ -225,21 +305,21 @@ class Line(Primitive):
return False
equiv_start = tuple(map(add, other.start, offset))
- equiv_end = tuple(map(add, other.end, offset))
+ equiv_end = tuple(map(add, other.end, offset))
return nearly_equal(self.start, equiv_start) and nearly_equal(self.end, equiv_end)
class Arc(Primitive):
"""
- """
- def __init__(self, start, end, center, direction, aperture, quadrant_mode, **kwargs):
+ """
+ def __init__(self, start, end, center, direction, aperture, quadrant_mode, **kwargs):
super(Arc, self).__init__(**kwargs)
- self.start = start
- self.end = end
- self.center = center
+ self._start = start
+ self._end = end
+ self._center = center
self.direction = direction
self.aperture = aperture
- self.quadrant_mode = quadrant_mode
+ self._quadrant_mode = quadrant_mode
self._to_convert = ['start', 'end', 'center', 'aperture']
@property
@@ -247,18 +327,57 @@ class Arc(Primitive):
return False
@property
+ def start(self):
+ return self._start
+
+ @start.setter
+ def start(self, value):
+ self._changed()
+ self._start = value
+
+ @property
+ def end(self):
+ return self._end
+
+ @end.setter
+ def end(self, value):
+ self._changed()
+ self._end = value
+
+ @property
+ def center(self):
+ return self._center
+
+ @center.setter
+ def center(self, value):
+ self._changed()
+ self._center = value
+
+ @property
+ def quadrant_mode(self):
+ return self._quadrant_mode
+
+ @quadrant_mode.setter
+ def quadrant_mode(self, quadrant_mode):
+ self._changed()
+ self._quadrant_mode = quadrant_mode
+
+ @property
def radius(self):
- dy, dx = map(sub, self.start, self.center)
- return math.sqrt(dy**2 + dx**2)
+ dy, dx = tuple([start - center for start, center
+ in zip(self.start, self.center)])
+ return math.sqrt(dy ** 2 + dx ** 2)
@property
def start_angle(self):
- dy, dx = map(sub, self.start, self.center)
+ dy, dx = tuple([start - center for start, center
+ in zip(self.start, self.center)])
return math.atan2(dx, dy)
@property
def end_angle(self):
- dy, dx = map(sub, self.end, self.center)
+ dy, dx = tuple([end - center for end, center
+ in zip(self.end, self.center)])
return math.atan2(dx, dy)
@property
@@ -274,51 +393,48 @@ class Arc(Primitive):
@property
def bounding_box(self):
- two_pi = 2 * math.pi
- theta0 = (self.start_angle + two_pi) % two_pi
- theta1 = (self.end_angle + two_pi) % two_pi
- points = [self.start, self.end]
- if self.direction == 'counterclockwise':
- # Passes through 0 degrees
- if theta0 > theta1:
- points.append((self.center[0] + self.radius, self.center[1]))
- # Passes through 90 degrees
- if theta0 <= math.pi / 2. and (theta1 >= math.pi / 2. or theta1 < theta0):
- points.append((self.center[0], self.center[1] + self.radius))
- # Passes through 180 degrees
- if theta0 <= math.pi and (theta1 >= math.pi or theta1 < theta0):
- points.append((self.center[0] - self.radius, self.center[1]))
- # Passes through 270 degrees
- if theta0 <= math.pi * 1.5 and (theta1 >= math.pi * 1.5 or theta1 < theta0):
- points.append((self.center[0], self.center[1] - self.radius ))
- else:
- # Passes through 0 degrees
- if theta1 > theta0:
- points.append((self.center[0] + self.radius, self.center[1]))
- # Passes through 90 degrees
- if theta1 <= math.pi / 2. and (theta0 >= math.pi / 2. or theta0 < theta1):
- points.append((self.center[0], self.center[1] + self.radius))
- # Passes through 180 degrees
- if theta1 <= math.pi and (theta0 >= math.pi or theta0 < theta1):
- points.append((self.center[0] - self.radius, self.center[1]))
- # Passes through 270 degrees
- if theta1 <= math.pi * 1.5 and (theta0 >= math.pi * 1.5 or theta0 < theta1):
- points.append((self.center[0], self.center[1] - self.radius ))
- x, y = zip(*points)
-
- if isinstance(self.aperture, Circle):
- radius = self.aperture.radius
- else:
- # TODO this is actually not valid, but files contain it
- width = self.aperture.width
- height = self.aperture.height
- radius = max(width, height)
-
- min_x = min(x) - radius
- max_x = max(x) + radius
- min_y = min(y) - radius
- max_y = max(y) + radius
- return ((min_x, max_x), (min_y, max_y))
+ if self._bounding_box is None:
+ two_pi = 2 * math.pi
+ theta0 = (self.start_angle + two_pi) % two_pi
+ theta1 = (self.end_angle + two_pi) % two_pi
+ points = [self.start, self.end]
+ if self.direction == 'counterclockwise':
+ # Passes through 0 degrees
+ if theta0 > theta1:
+ points.append((self.center[0] + self.radius, self.center[1]))
+ # Passes through 90 degrees
+ if theta0 <= math.pi / \
+ 2. and (theta1 >= math.pi / 2. or theta1 < theta0):
+ points.append((self.center[0], self.center[1] + self.radius))
+ # Passes through 180 degrees
+ if theta0 <= math.pi and (theta1 >= math.pi or theta1 < theta0):
+ points.append((self.center[0] - self.radius, self.center[1]))
+ # Passes through 270 degrees
+ if theta0 <= math.pi * \
+ 1.5 and (theta1 >= math.pi * 1.5 or theta1 < theta0):
+ points.append((self.center[0], self.center[1] - self.radius))
+ else:
+ # Passes through 0 degrees
+ if theta1 > theta0:
+ points.append((self.center[0] + self.radius, self.center[1]))
+ # Passes through 90 degrees
+ if theta1 <= math.pi / \
+ 2. and (theta0 >= math.pi / 2. or theta0 < theta1):
+ points.append((self.center[0], self.center[1] + self.radius))
+ # Passes through 180 degrees
+ if theta1 <= math.pi and (theta0 >= math.pi or theta0 < theta1):
+ points.append((self.center[0] - self.radius, self.center[1]))
+ # Passes through 270 degrees
+ if theta1 <= math.pi * \
+ 1.5 and (theta0 >= math.pi * 1.5 or theta0 < theta1):
+ points.append((self.center[0], self.center[1] - self.radius))
+ x, y = zip(*points)
+ min_x = min(x) - self.aperture.radius
+ max_x = max(x) + self.aperture.radius
+ min_y = min(y) - self.aperture.radius
+ max_y = max(y) + self.aperture.radius
+ self._bounding_box = ((min_x, max_x), (min_y, max_y))
+ return self._bounding_box
@property
def bounding_box_no_aperture(self):
@@ -341,7 +457,7 @@ class Arc(Primitive):
if theta0 <= math.pi * 1.5 and (theta1 >= math.pi * 1.5 or theta1 < theta0):
points.append((self.center[0], self.center[1] - self.radius ))
else:
- # Passes through 0 degrees
+ # Passes through 0 degrees
if theta1 > theta0:
points.append((self.center[0] + self.radius, self.center[1]))
# Passes through 90 degrees
@@ -359,9 +475,10 @@ class Arc(Primitive):
max_x = max(x)
min_y = min(y)
max_y = max(y)
- return ((min_x, max_x), (min_y, max_y))
+ return ((min_x, max_x), (min_y, max_y))
def offset(self, x_offset=0, y_offset=0):
+ self._changed()
self.start = tuple(map(add, self.start, (x_offset, y_offset)))
self.end = tuple(map(add, self.end, (x_offset, y_offset)))
self.center = tuple(map(add, self.center, (x_offset, y_offset)))
@@ -369,20 +486,38 @@ class Arc(Primitive):
class Circle(Primitive):
"""
- """
+ """
def __init__(self, position, diameter, hole_diameter = None, **kwargs):
super(Circle, self).__init__(**kwargs)
validate_coordinates(position)
- self.position = position
- self.diameter = diameter
+ self._position = position
+ self._diameter = diameter
self.hole_diameter = hole_diameter
- self._to_convert = ['position', 'diameter', 'hole_diameter']
+ self._to_convert = ['position', 'diameter', 'hole_diameter']
@property
def flashed(self):
return True
@property
+ def position(self):
+ return self._position
+
+ @position.setter
+ def position(self, value):
+ self._changed()
+ self._position = value
+
+ @property
+ def diameter(self):
+ return self._diameter
+
+ @diameter.setter
+ def diameter(self, value):
+ self._changed()
+ self._diameter = value
+
+ @property
def radius(self):
return self.diameter / 2.
@@ -394,12 +529,14 @@ class Circle(Primitive):
@property
def bounding_box(self):
- min_x = self.position[0] - self.radius
- max_x = self.position[0] + self.radius
- min_y = self.position[1] - self.radius
- max_y = self.position[1] + self.radius
- return ((min_x, max_x), (min_y, max_y))
-
+ if self._bounding_box is None:
+ min_x = self.position[0] - self.radius
+ max_x = self.position[0] + self.radius
+ min_y = self.position[1] - self.radius
+ max_y = self.position[1] + self.radius
+ self._bounding_box = ((min_x, max_x), (min_y, max_y))
+ return self._bounding_box
+
def offset(self, x_offset=0, y_offset=0):
self.position = tuple(map(add, self.position, (x_offset, y_offset)))
@@ -420,39 +557,68 @@ class Circle(Primitive):
class Ellipse(Primitive):
"""
"""
+
def __init__(self, position, width, height, **kwargs):
super(Ellipse, self).__init__(**kwargs)
validate_coordinates(position)
- self.position = position
- self.width = width
- self.height = height
+ self._position = position
+ self._width = width
+ self._height = height
self._to_convert = ['position', 'width', 'height']
-
+
@property
def flashed(self):
return True
+
+ @property
+ def position(self):
+ return self._position
+
+ @position.setter
+ def position(self, value):
+ self._changed()
+ self._position = value
@property
- def bounding_box(self):
- min_x = self.position[0] - (self._abs_width / 2.0)
- max_x = self.position[0] + (self._abs_width / 2.0)
- min_y = self.position[1] - (self._abs_height / 2.0)
- max_y = self.position[1] + (self._abs_height / 2.0)
- return ((min_x, max_x), (min_y, max_y))
+ def width(self):
+ return self._width
- def offset(self, x_offset=0, y_offset=0):
- self.position = tuple(map(add, self.position, (x_offset, y_offset)))
+ @width.setter
+ def width(self, value):
+ self._changed()
+ self._width = value
+
+ @property
+ def height(self):
+ return self._height
+
+ @height.setter
+ def height(self, value):
+ self._changed()
+ self._height = value
@property
- def _abs_width(self):
+ def bounding_box(self):
+ if self._bounding_box is None:
+ min_x = self.position[0] - (self.axis_aligned_width / 2.0)
+ max_x = self.position[0] + (self.axis_aligned_width / 2.0)
+ min_y = self.position[1] - (self.axis_aligned_height / 2.0)
+ max_y = self.position[1] + (self.axis_aligned_height / 2.0)
+ self._bounding_box = ((min_x, max_x), (min_y, max_y))
+ return self._bounding_box
+
+ @property
+ def axis_aligned_width(self):
ux = (self.width / 2.) * math.cos(math.radians(self.rotation))
- vx = (self.height / 2.) * math.cos(math.radians(self.rotation) + (math.pi / 2.))
+ vx = (self.height / 2.) * \
+ math.cos(math.radians(self.rotation) + (math.pi / 2.))
return 2 * math.sqrt((ux * ux) + (vx * vx))
-
+
@property
- def _abs_height(self):
+ def axis_aligned_height(self):
uy = (self.width / 2.) * math.sin(math.radians(self.rotation))
- vy = (self.height / 2.) * math.sin(math.radians(self.rotation) + (math.pi / 2.))
+ vy = (self.height / 2.) * \
+ math.sin(math.radians(self.rotation) + (math.pi / 2.))
return 2 * math.sqrt((uy * uy) + (vy * vy))
@@ -462,29 +628,49 @@ class Rectangle(Primitive):
Only aperture macro generated Rectangle objects can be rotated. If you aren't in a AMGroup,
then you don't need to worry about rotation
- """
+ """
def __init__(self, position, width, height, hole_diameter=0, **kwargs):
super(Rectangle, self).__init__(**kwargs)
validate_coordinates(position)
- self.position = position
- self.width = width
- self.height = height
+ self._position = position
+ self._width = width
+ self._height = height
self.hole_diameter = hole_diameter
self._to_convert = ['position', 'width', 'height', 'hole_diameter']
+ # TODO These are probably wrong when rotated
+ self._lower_left = None
+ self._upper_right = None
@property
def flashed(self):
return True
-
+
@property
- def lower_left(self):
- return (self.position[0] - (self._abs_width / 2.),
- self.position[1] - (self._abs_height / 2.))
+ def position(self):
+ return self._position
+
+ @position.setter
+ def position(self, value):
+ self._changed()
+ self._position = value
@property
- def upper_right(self):
- return (self.position[0] + (self._abs_width / 2.),
- self.position[1] + (self._abs_height / 2.))
+ def width(self):
+ return self._width
+
+ @width.setter
+ def width(self, value):
+ self._changed()
+ self._width = value
+
+ @property
+ def height(self):
+ return self._height
+
+ @height.setter
+ def height(self, value):
+ self._changed()
+ self._height = value
@property
def hole_radius(self):
@@ -493,26 +679,52 @@ class Rectangle(Primitive):
return self.hole_diameter / 2.
return None
+ @property
+ def upper_right(self):
+ return (self.position[0] + (self._abs_width / 2.),
+ self.position[1] + (self._abs_height / 2.))
+
+ @property
+ def lower_left(self):
+ return (self.position[0] - (self.axis_aligned_width / 2.),
+ self.position[1] - (self.axis_aligned_height / 2.))
+
@property
def bounding_box(self):
- min_x = self.lower_left[0]
- max_x = self.upper_right[0]
- min_y = self.lower_left[1]
- max_y = self.upper_right[1]
- return ((min_x, max_x), (min_y, max_y))
-
- def offset(self, x_offset=0, y_offset=0):
- self.position = tuple(map(add, self.position, (x_offset, y_offset)))
+ if self._bounding_box is None:
+ ll = (self.position[0] - (self.axis_aligned_width / 2.),
+ self.position[1] - (self.axis_aligned_height / 2.))
+ ur = (self.position[0] + (self.axis_aligned_width / 2.),
+ self.position[1] + (self.axis_aligned_height / 2.))
+ self._bounding_box = ((ll[0], ur[0]), (ll[1], ur[1]))
+ return self._bounding_box
@property
- def _abs_width(self):
- return (math.cos(math.radians(self.rotation)) * self.width +
- math.sin(math.radians(self.rotation)) * self.height)
- @property
+ def vertices(self):
+ if self._vertices is None:
+ delta_w = self.width / 2.
+ delta_h = self.height / 2.
+ ll = ((self.position[0] - delta_w), (self.position[1] - delta_h))
+ ul = ((self.position[0] - delta_w), (self.position[1] + delta_h))
+ ur = ((self.position[0] + delta_w), (self.position[1] + delta_h))
+ lr = ((self.position[0] + delta_w), (self.position[1] - delta_h))
+ self._vertices = [((x * self._cos_theta - y * self._sin_theta),
+ (x * self._sin_theta + y * self._cos_theta))
+ for x, y in [ll, ul, ur, lr]]
+ return self._vertices
+
+ @property
+ def axis_aligned_width(self):
+ return (self._cos_theta * self.width + self._sin_theta * self.height)
+
+ @property
def _abs_height(self):
return (math.cos(math.radians(self.rotation)) * self.height +
math.sin(math.radians(self.rotation)) * self.width)
-
+
+ def axis_aligned_height(self):
+ return (self._cos_theta * self.height + self._sin_theta * self.width)
+
def equivalent(self, other, offset):
"""Is this the same as the other rect, ignoring the offset?"""
@@ -524,18 +736,19 @@ class Rectangle(Primitive):
equiv_position = tuple(map(add, other.position, offset))
- return nearly_equal(self.position, equiv_position)
+ return nearly_equal(self.position, equiv_position)
class Diamond(Primitive):
"""
"""
+
def __init__(self, position, width, height, **kwargs):
super(Diamond, self).__init__(**kwargs)
validate_coordinates(position)
- self.position = position
- self.width = width
- self.height = height
+ self._position = position
+ self._width = width
+ self._height = height
self._to_convert = ['position', 'width', 'height']
@property
@@ -543,47 +756,77 @@ class Diamond(Primitive):
return True
@property
- def lower_left(self):
- return (self.position[0] - (self._abs_width / 2.),
- self.position[1] - (self._abs_height / 2.))
+ def position(self):
+ return self._position
+
+ @position.setter
+ def position(self, value):
+ self._changed()
+ self._position = value
@property
- def upper_right(self):
- return (self.position[0] + (self._abs_width / 2.),
- self.position[1] + (self._abs_height / 2.))
+ def width(self):
+ return self._width
+
+ @width.setter
+ def width(self, value):
+ self._changed()
+ self._width = value
+
+ @property
+ def height(self):
+ return self._height
+
+ @height.setter
+ def height(self, value):
+ self._changed()
+ self._height = value
@property
def bounding_box(self):
- min_x = self.lower_left[0]
- max_x = self.upper_right[0]
- min_y = self.lower_left[1]
- max_y = self.upper_right[1]
- return ((min_x, max_x), (min_y, max_y))
+ if self._bounding_box is None:
+ ll = (self.position[0] - (self.axis_aligned_width / 2.),
+ self.position[1] - (self.axis_aligned_height / 2.))
+ ur = (self.position[0] + (self.axis_aligned_width / 2.),
+ self.position[1] + (self.axis_aligned_height / 2.))
+ self._bounding_box = ((ll[0], ur[0]), (ll[1], ur[1]))
+ return self._bounding_box
- def offset(self, x_offset=0, y_offset=0):
- self.position = tuple(map(add, self.position, (x_offset, y_offset)))
+ @property
+ def vertices(self):
+ if self._vertices is None:
+ delta_w = self.width / 2.
+ delta_h = self.height / 2.
+ top = (self.position[0], (self.position[1] + delta_h))
+ right = ((self.position[0] + delta_w), self.position[1])
+ bottom = (self.position[0], (self.position[1] - delta_h))
+ left = ((self.position[0] - delta_w), self.position[1])
+ self._vertices = [(((x * self._cos_theta) - (y * self._sin_theta)),
+ ((x * self._sin_theta) + (y * self._cos_theta)))
+ for x, y in [top, right, bottom, left]]
+ return self._vertices
@property
- def _abs_width(self):
- return (math.cos(math.radians(self.rotation)) * self.width +
- math.sin(math.radians(self.rotation)) * self.height)
+ def axis_aligned_width(self):
+ return (self._cos_theta * self.width + self._sin_theta * self.height)
+
@property
- def _abs_height(self):
- return (math.cos(math.radians(self.rotation)) * self.height +
- math.sin(math.radians(self.rotation)) * self.width)
+ def axis_aligned_height(self):
+ return (self._cos_theta * self.height + self._sin_theta * self.width)
class ChamferRectangle(Primitive):
"""
"""
+
def __init__(self, position, width, height, chamfer, corners, **kwargs):
super(ChamferRectangle, self).__init__(**kwargs)
validate_coordinates(position)
- self.position = position
- self.width = width
- self.height = height
- self.chamfer = chamfer
- self.corners = corners
+ self._position = position
+ self._width = width
+ self._height = height
+ self._chamfer = chamfer
+ self._corners = corners
self._to_convert = ['position', 'width', 'height', 'chamfer']
@property
@@ -591,46 +834,88 @@ class ChamferRectangle(Primitive):
return True
@property
- def lower_left(self):
- return (self.position[0] - (self._abs_width / 2.),
- self.position[1] - (self._abs_height / 2.))
+ def position(self):
+ return self._position
+
+ @position.setter
+ def position(self, value):
+ self._changed()
+ self._position = value
@property
- def upper_right(self):
- return (self.position[0] + (self._abs_width / 2.),
- self.position[1] + (self._abs_height / 2.))
+ def width(self):
+ return self._width
+
+ @width.setter
+ def width(self, value):
+ self._changed()
+ self._width = value
+
+ @property
+ def height(self):
+ return self._height
+
+ @height.setter
+ def height(self, value):
+ self._changed()
+ self._height = value
+
+ @property
+ def chamfer(self):
+ return self._chamfer
+
+ @chamfer.setter
+ def chamfer(self, value):
+ self._changed()
+ self._chamfer = value
+
+ @property
+ def corners(self):
+ return self._corners
+
+ @corners.setter
+ def corners(self, value):
+ self._changed()
+ self._corners = value
@property
def bounding_box(self):
- min_x = self.lower_left[0]
- max_x = self.upper_right[0]
- min_y = self.lower_left[1]
- max_y = self.upper_right[1]
- return ((min_x, max_x), (min_y, max_y))
+ if self._bounding_box is None:
+ ll = (self.position[0] - (self.axis_aligned_width / 2.),
+ self.position[1] - (self.axis_aligned_height / 2.))
+ ur = (self.position[0] + (self.axis_aligned_width / 2.),
+ self.position[1] + (self.axis_aligned_height / 2.))
+ self._bounding_box = ((ll[0], ur[0]), (ll[1], ur[1]))
+ return self._bounding_box
- def offset(self, x_offset=0, y_offset=0):
- self.position = tuple(map(add, self.position, (x_offset, y_offset)))
+ @property
+ def vertices(self):
+ # TODO
+ return self._vertices
@property
- def _abs_width(self):
- return (math.cos(math.radians(self.rotation)) * self.width +
- math.sin(math.radians(self.rotation)) * self.height)
+ def axis_aligned_width(self):
+ return (self._cos_theta * self.width +
+ self._sin_theta * self.height)
+
@property
- def _abs_height(self):
- return (math.cos(math.radians(self.rotation)) * self.height +
- math.sin(math.radians(self.rotation)) * self.width)
+ def axis_aligned_height(self):
+ return (self._cos_theta * self.height +
+ self._sin_theta * self.width)
+
class RoundRectangle(Primitive):
"""
"""
+
def __init__(self, position, width, height, radius, corners, **kwargs):
super(RoundRectangle, self).__init__(**kwargs)
validate_coordinates(position)
- self.position = position
- self.width = width
- self.height = height
- self.radius = radius
- self.corners = corners
+ self._position = position
+ self._width = width
+ self._height = height
+ self._radius = radius
+ self._corners = corners
self._to_convert = ['position', 'width', 'height', 'radius']
@property
@@ -638,67 +923,126 @@ class RoundRectangle(Primitive):
return True
@property
- def lower_left(self):
- return (self.position[0] - (self._abs_width / 2.),
- self.position[1] - (self._abs_height / 2.))
+ def position(self):
+ return self._position
+
+ @position.setter
+ def position(self, value):
+ self._changed()
+ self._position = value
@property
- def upper_right(self):
- return (self.position[0] + (self._abs_width / 2.),
- self.position[1] + (self._abs_height / 2.))
+ def width(self):
+ return self._width
+
+ @width.setter
+ def width(self, value):
+ self._changed()
+ self._width = value
@property
- def bounding_box(self):
- min_x = self.lower_left[0]
- max_x = self.upper_right[0]
- min_y = self.lower_left[1]
- max_y = self.upper_right[1]
- return ((min_x, max_x), (min_y, max_y))
+ def height(self):
+ return self._height
- def offset(self, x_offset=0, y_offset=0):
- self.position = tuple(map(add, self.position, (x_offset, y_offset)))
+ @height.setter
+ def height(self, value):
+ self._changed()
+ self._height = value
@property
- def _abs_width(self):
- return (math.cos(math.radians(self.rotation)) * self.width +
- math.sin(math.radians(self.rotation)) * self.height)
+ def radius(self):
+ return self._radius
+
+ @radius.setter
+ def radius(self, value):
+ self._changed()
+ self._radius = value
+
@property
- def _abs_height(self):
- return (math.cos(math.radians(self.rotation)) * self.height +
- math.sin(math.radians(self.rotation)) * self.width)
+ def corners(self):
+ return self._corners
+
+ @corners.setter
+ def corners(self, value):
+ self._changed()
+ self._corners = value
+
+ @property
+ def bounding_box(self):
+ if self._bounding_box is None:
+ ll = (self.position[0] - (self.axis_aligned_width / 2.),
+ self.position[1] - (self.axis_aligned_height / 2.))
+ ur = (self.position[0] + (self.axis_aligned_width / 2.),
+ self.position[1] + (self.axis_aligned_height / 2.))
+ self._bounding_box = ((ll[0], ur[0]), (ll[1], ur[1]))
+ return self._bounding_box
+
+ @property
+ def axis_aligned_width(self):
+ return (self._cos_theta * self.width +
+ self._sin_theta * self.height)
+
+ @property
+ def axis_aligned_height(self):
+ return (self._cos_theta * self.height +
+ self._sin_theta * self.width)
+
class Obround(Primitive):
"""
- """
+ """
def __init__(self, position, width, height, hole_diameter=0, **kwargs):
super(Obround, self).__init__(**kwargs)
validate_coordinates(position)
- self.position = position
- self.width = width
- self.height = height
+ self._position = position
+ self._width = width
+ self._height = height
self.hole_diameter = hole_diameter
self._to_convert = ['position', 'width', 'height', 'hole_diameter']
@property
def flashed(self):
- return True
+ return True
@property
- def lower_left(self):
- return (self.position[0] - (self._abs_width / 2.),
- self.position[1] - (self._abs_height / 2.))
+ def position(self):
+ return self._position
+
+ @position.setter
+ def position(self, value):
+ self._changed()
+ self._position = value
@property
+ def width(self):
+ return self._width
+
+ @width.setter
+ def width(self, value):
+ self._changed()
+ self._width = value
+
+ @property
def upper_right(self):
return (self.position[0] + (self._abs_width / 2.),
self.position[1] + (self._abs_height / 2.))
-
+
+ @property
+ def height(self):
+ return self._height
+
+ @height.setter
+ def height(self, value):
+ self._changed()
+ self._height = value
+
@property
def hole_radius(self):
"""The radius of the hole. If there is no hole, returns None"""
if self.hole_diameter != None:
return self.hole_diameter / 2.
- return None
+
+ return None
@property
def orientation(self):
@@ -706,52 +1050,55 @@ class Obround(Primitive):
@property
def bounding_box(self):
- min_x = self.lower_left[0]
- max_x = self.upper_right[0]
- min_y = self.lower_left[1]
- max_y = self.upper_right[1]
- return ((min_x, max_x), (min_y, max_y))
+ if self._bounding_box is None:
+ ll = (self.position[0] - (self.axis_aligned_width / 2.),
+ self.position[1] - (self.axis_aligned_height / 2.))
+ ur = (self.position[0] + (self.axis_aligned_width / 2.),
+ self.position[1] + (self.axis_aligned_height / 2.))
+ self._bounding_box = ((ll[0], ur[0]), (ll[1], ur[1]))
+ return self._bounding_box
@property
def subshapes(self):
if self.orientation == 'vertical':
circle1 = Circle((self.position[0], self.position[1] +
- (self.height-self.width) / 2.), self.width)
+ (self.height - self.width) / 2.), self.width)
circle2 = Circle((self.position[0], self.position[1] -
- (self.height-self.width) / 2.), self.width)
+ (self.height - self.width) / 2.), self.width)
rect = Rectangle(self.position, self.width,
- (self.height - self.width))
+ (self.height - self.width))
else:
- circle1 = Circle((self.position[0] - (self.height - self.width) / 2.,
+ circle1 = Circle((self.position[0]
+ - (self.height - self.width) / 2.,
self.position[1]), self.height)
- circle2 = Circle((self.position[0] + (self.height - self.width) / 2.,
+ circle2 = Circle((self.position[0]
+ + (self.height - self.width) / 2.,
self.position[1]), self.height)
rect = Rectangle(self.position, (self.width - self.height),
- self.height)
+ self.height)
return {'circle1': circle1, 'circle2': circle2, 'rectangle': rect}
- def offset(self, x_offset=0, y_offset=0):
- self.position = tuple(map(add, self.position, (x_offset, y_offset)))
-
@property
- def _abs_width(self):
- return (math.cos(math.radians(self.rotation)) * self.width +
- math.sin(math.radians(self.rotation)) * self.height)
+ def axis_aligned_width(self):
+ return (self._cos_theta * self.width +
+ self._sin_theta * self.height)
+
@property
- def _abs_height(self):
- return (math.cos(math.radians(self.rotation)) * self.height +
- math.sin(math.radians(self.rotation)) * self.width)
+ def axis_aligned_height(self):
+ return (self._cos_theta * self.height +
+ self._sin_theta * self.width)
+
class Polygon(Primitive):
"""
Polygon flash defined by a set number of sides.
- """
- def __init__(self, position, sides, radius, hole_diameter, **kwargs):
+ """
+ def __init__(self, position, sides, radius, hole_diameter, **kwargs):
super(Polygon, self).__init__(**kwargs)
validate_coordinates(position)
- self.position = position
- self.sides = sides
- self.radius = radius
+ self._position = position
+ self.sides = sides
+ self._radius = radius
self.hole_diameter = hole_diameter
self._to_convert = ['position', 'radius', 'hole_diameter']
@@ -767,16 +1114,36 @@ class Polygon(Primitive):
def hole_radius(self):
if self.hole_diameter != None:
return self.hole_diameter / 2.
- return None
+ return None
@property
- def bounding_box(self):
- min_x = self.position[0] - self.radius
- max_x = self.position[0] + self.radius
- min_y = self.position[1] - self.radius
- max_y = self.position[1] + self.radius
- return ((min_x, max_x), (min_y, max_y))
+ def position(self):
+ return self._position
+
+ @position.setter
+ def position(self, value):
+ self._changed()
+ self._position = value
+ @property
+ def radius(self):
+ return self._radius
+
+ @radius.setter
+ def radius(self, value):
+ self._changed()
+ self._radius = value
+
+ @property
+ def bounding_box(self):
+ if self._bounding_box is None:
+ min_x = self.position[0] - self.radius
+ max_x = self.position[0] + self.radius
+ min_y = self.position[1] - self.radius
+ max_y = self.position[1] + self.radius
+ self._bounding_box = ((min_x, max_x), (min_y, max_y))
+ return self._bounding_box
+
def offset(self, x_offset=0, y_offset=0):
self.position = tuple(map(add, self.position, (x_offset, y_offset)))
@@ -823,7 +1190,7 @@ class AMGroup(Primitive):
for p in prim:
self.primitives.append(p)
elif prim:
- self.primitives.append(prim)
+ self.primitives.append(prim)
self._position = None
self._to_convert = ['_position', 'primitives']
self.stmt = stmt
@@ -851,6 +1218,7 @@ class AMGroup(Primitive):
@property
def bounding_box(self):
+ # TODO Make this cached like other items
xlims, ylims = zip(*[p.bounding_box for p in self.primitives])
minx, maxx = zip(*xlims)
miny, maxy = zip(*ylims)
@@ -936,16 +1304,23 @@ class Outline(Primitive):
def offset(self, x_offset=0, y_offset=0):
for p in self.primitives:
p.offset(x_offset, y_offset)
+
+ @property
+ def vertices(self):
+ if self._vertices is None:
+ theta = math.radians(360/self.sides)
+ vertices = [(self.position[0] + (math.cos(theta * side) * self.radius),
+ self.position[1] + (math.sin(theta * side) * self.radius))
+ for side in range(self.sides)]
+ self._vertices = [(((x * self._cos_theta) - (y * self._sin_theta)),
+ ((x * self._sin_theta) + (y * self._cos_theta)))
+ for x, y in vertices]
+ return self._vertices
@property
def width(self):
bounding_box = self.bounding_box()
return bounding_box[0][1] - bounding_box[0][0]
-
- @property
- def width(self):
- bounding_box = self.bounding_box()
- return bounding_box[1][1] - bounding_box[1][0]
def equivalent(self, other, offset):
'''
@@ -965,6 +1340,7 @@ class Outline(Primitive):
class Region(Primitive):
"""
"""
+
def __init__(self, primitives, **kwargs):
super(Region, self).__init__(**kwargs)
self.primitives = primitives
@@ -975,17 +1351,20 @@ class Region(Primitive):
return False
@property
- def bounding_box(self):
- xlims, ylims = zip(*[p.bounding_box_no_aperture for p in self.primitives])
- minx, maxx = zip(*xlims)
- miny, maxy = zip(*ylims)
- min_x = min(minx)
- max_x = max(maxx)
- min_y = min(miny)
- max_y = max(maxy)
- return ((min_x, max_x), (min_y, max_y))
+ def bounding_box(self):
+ if self._bounding_box is None:
+ xlims, ylims = zip(*[p.bounding_box_no_aperture for p in self.primitives])
+ minx, maxx = zip(*xlims)
+ miny, maxy = zip(*ylims)
+ min_x = min(minx)
+ max_x = max(maxx)
+ min_y = min(miny)
+ max_y = max(maxy)
+ self._bounding_box = ((min_x, max_x), (min_y, max_y))
+ return self._bounding_box
def offset(self, x_offset=0, y_offset=0):
+ self._changed()
for p in self.primitives:
p.offset(x_offset, y_offset)
@@ -993,6 +1372,7 @@ class Region(Primitive):
class RoundButterfly(Primitive):
""" A circle with two diagonally-opposite quadrants removed
"""
+
def __init__(self, position, diameter, **kwargs):
super(RoundButterfly, self).__init__(**kwargs)
validate_coordinates(position)
@@ -1000,6 +1380,8 @@ class RoundButterfly(Primitive):
self.diameter = diameter
self._to_convert = ['position', 'diameter']
+ # TODO This does not reset bounding box correctly
+
@property
def flashed(self):
return True
@@ -1010,19 +1392,19 @@ class RoundButterfly(Primitive):
@property
def bounding_box(self):
- min_x = self.position[0] - self.radius
- max_x = self.position[0] + self.radius
- min_y = self.position[1] - self.radius
- max_y = self.position[1] + self.radius
- return ((min_x, max_x), (min_y, max_y))
-
- def offset(self, x_offset=0, y_offset=0):
- self.position = tuple(map(add, self.position, (x_offset, y_offset)))
+ if self._bounding_box is None:
+ min_x = self.position[0] - self.radius
+ max_x = self.position[0] + self.radius
+ min_y = self.position[1] - self.radius
+ max_y = self.position[1] + self.radius
+ self._bounding_box = ((min_x, max_x), (min_y, max_y))
+ return self._bounding_box
class SquareButterfly(Primitive):
""" A square with two diagonally-opposite quadrants removed
"""
+
def __init__(self, position, side, **kwargs):
super(SquareButterfly, self).__init__(**kwargs)
validate_coordinates(position)
@@ -1030,34 +1412,39 @@ class SquareButterfly(Primitive):
self.side = side
self._to_convert = ['position', 'side']
+ # TODO This does not reset bounding box correctly
+
@property
def flashed(self):
- return True
+ return True
@property
def bounding_box(self):
- min_x = self.position[0] - (self.side / 2.)
- max_x = self.position[0] + (self.side / 2.)
- min_y = self.position[1] - (self.side / 2.)
- max_y = self.position[1] + (self.side / 2.)
- return ((min_x, max_x), (min_y, max_y))
-
- def offset(self, x_offset=0, y_offset=0):
- self.position = tuple(map(add, self.position, (x_offset, y_offset)))
+ if self._bounding_box is None:
+ min_x = self.position[0] - (self.side / 2.)
+ max_x = self.position[0] + (self.side / 2.)
+ min_y = self.position[1] - (self.side / 2.)
+ max_y = self.position[1] + (self.side / 2.)
+ self._bounding_box = ((min_x, max_x), (min_y, max_y))
+ return self._bounding_box
class Donut(Primitive):
""" A Shape with an identical concentric shape removed from its center
"""
- def __init__(self, position, shape, inner_diameter, outer_diameter, **kwargs):
+
+ def __init__(self, position, shape, inner_diameter,
+ outer_diameter, **kwargs):
super(Donut, self).__init__(**kwargs)
validate_coordinates(position)
self.position = position
if shape not in ('round', 'square', 'hexagon', 'octagon'):
- raise ValueError('Valid shapes are round, square, hexagon or octagon')
+ raise ValueError(
+ 'Valid shapes are round, square, hexagon or octagon')
self.shape = shape
if inner_diameter >= outer_diameter:
- raise ValueError('Outer diameter must be larger than inner diameter.')
+ raise ValueError(
+ 'Outer diameter must be larger than inner diameter.')
self.inner_diameter = inner_diameter
self.outer_diameter = outer_diameter
if self.shape in ('round', 'square', 'octagon'):
@@ -1067,8 +1454,11 @@ class Donut(Primitive):
# Hexagon
self.width = 0.5 * math.sqrt(3.) * outer_diameter
self.height = outer_diameter
+
self._to_convert = ['position', 'width', 'height', 'inner_diameter', 'outer_diameter']
+ # TODO This does not reset bounding box correctly
+
@property
def flashed(self):
return True
@@ -1081,29 +1471,30 @@ class Donut(Primitive):
@property
def upper_right(self):
return (self.position[0] + (self.width / 2.),
- self.position[1] + (self.height / 2.))
+ self.position[1] + (self.height / 2.))
@property
def bounding_box(self):
- min_x = self.lower_left[0]
- max_x = self.upper_right[0]
- min_y = self.lower_left[1]
- max_y = self.upper_right[1]
- return ((min_x, max_x), (min_y, max_y))
-
- def offset(self, x_offset=0, y_offset=0):
- self.position = tuple(map(add, self.position, (x_offset, y_offset)))
+ if self._bounding_box is None:
+ ll = (self.position[0] - (self.width / 2.),
+ self.position[1] - (self.height / 2.))
+ ur = (self.position[0] + (self.width / 2.),
+ self.position[1] + (self.height / 2.))
+ self._bounding_box = ((ll[0], ur[0]), (ll[1], ur[1]))
+ return self._bounding_box
class SquareRoundDonut(Primitive):
""" A Square with a circular cutout in the center
"""
+
def __init__(self, position, inner_diameter, outer_diameter, **kwargs):
super(SquareRoundDonut, self).__init__(**kwargs)
validate_coordinates(position)
self.position = position
if inner_diameter >= outer_diameter:
- raise ValueError('Outer diameter must be larger than inner diameter.')
+ raise ValueError(
+ 'Outer diameter must be larger than inner diameter.')
self.inner_diameter = inner_diameter
self.outer_diameter = outer_diameter
self._to_convert = ['position', 'inner_diameter', 'outer_diameter']
@@ -1113,39 +1504,46 @@ class SquareRoundDonut(Primitive):
return True
@property
- def lower_left(self):
- return tuple([c - self.outer_diameter / 2. for c in self.position])
-
- @property
- def upper_right(self):
- return tuple([c + self.outer_diameter / 2. for c in self.position])
-
- @property
def bounding_box(self):
- min_x = self.lower_left[0]
- max_x = self.upper_right[0]
- min_y = self.lower_left[1]
- max_y = self.upper_right[1]
- return ((min_x, max_x), (min_y, max_y))
-
- def offset(self, x_offset=0, y_offset=0):
- self.position = tuple(map(add, self.position, (x_offset, y_offset)))
+ if self._bounding_box is None:
+ ll = tuple([c - self.outer_diameter / 2. for c in self.position])
+ ur = tuple([c + self.outer_diameter / 2. for c in self.position])
+ self._bounding_box = ((ll[0], ur[0]), (ll[1], ur[1]))
+ return self._bounding_box
class Drill(Primitive):
""" A drill hole
- """
+ """
def __init__(self, position, diameter, hit, **kwargs):
super(Drill, self).__init__('dark', **kwargs)
validate_coordinates(position)
- self.position = position
- self.diameter = diameter
+ self._position = position
+ self._diameter = diameter
self.hit = hit
self._to_convert = ['position', 'diameter', 'hit']
@property
def flashed(self):
- return False
+ return False
+
+ @property
+ def position(self):
+ return self._position
+
+ @position.setter
+ def position(self, value):
+ self._changed()
+ self._position = value
+
+ @property
+ def diameter(self):
+ return self._diameter
+
+ @diameter.setter
+ def diameter(self, value):
+ self._changed()
+ self._diameter = value
@property
def radius(self):
@@ -1153,13 +1551,16 @@ class Drill(Primitive):
@property
def bounding_box(self):
- min_x = self.position[0] - self.radius
- max_x = self.position[0] + self.radius
- min_y = self.position[1] - self.radius
- max_y = self.position[1] + self.radius
- return ((min_x, max_x), (min_y, max_y))
-
+ if self._bounding_box is None:
+ min_x = self.position[0] - self.radius
+ max_x = self.position[0] + self.radius
+ min_y = self.position[1] - self.radius
+ max_y = self.position[1] + self.radius
+ self._bounding_box = ((min_x, max_x), (min_y, max_y))
+ return self._bounding_box
+
def offset(self, x_offset=0, y_offset=0):
+ self._changed()
self.position = tuple(map(add, self.position, (x_offset, y_offset)))
def __str__(self):
@@ -1179,6 +1580,8 @@ class Slot(Primitive):
self.hit = hit
self._to_convert = ['start', 'end', 'diameter', 'hit']
+ # TODO this needs to use cached bounding box
+
@property
def flashed(self):
return False
@@ -1199,15 +1602,15 @@ class Slot(Primitive):
def offset(self, x_offset=0, y_offset=0):
self.start = tuple(map(add, self.start, (x_offset, y_offset)))
self.end = tuple(map(add, self.end, (x_offset, y_offset)))
-
+
class TestRecord(Primitive):
""" Netlist Test record
"""
+
def __init__(self, position, net_name, layer, **kwargs):
super(TestRecord, self).__init__(**kwargs)
validate_coordinates(position)
self.position = position
self.net_name = net_name
self.layer = layer
-
diff --git a/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py
index 78ccf34..349640a 100644
--- a/gerber/render/cairo_backend.py
+++ b/gerber/render/cairo_backend.py
@@ -12,7 +12,7 @@
# 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
+# See the License for the specific language governing permissions and
# limitations under the License.
try:
@@ -21,7 +21,8 @@ except ImportError:
import cairocffi as cairo
import math
-from operator import mul, div
+from operator import mul, di
+
import tempfile
from ..primitives import *
@@ -36,11 +37,14 @@ except(ImportError):
class GerberCairoContext(GerberContext):
+
def __init__(self, scale=300):
- GerberContext.__init__(self)
+ super(GerberCairoContext, self).__init__()
self.scale = (scale, scale)
self.surface = None
self.ctx = None
+ self.active_layer = None
+ self.output_ctx = None
self.bg = False
self.mask = None
self.mask_ctx = None
@@ -50,37 +54,40 @@ class GerberCairoContext(GerberContext):
@property
def origin_in_pixels(self):
- return tuple(map(mul, self.origin_in_inch, self.scale)) if self.origin_in_inch is not None else (0.0, 0.0)
+ return (self.scale_point(self.origin_in_inch)
+ if self.origin_in_inch is not None else (0.0, 0.0))
@property
def size_in_pixels(self):
- return tuple(map(mul, self.size_in_inch, self.scale)) if self.size_in_inch is not None else (0.0, 0.0)
+ return (self.scale_point(self.size_in_inch)
+ if self.size_in_inch is not None else (0.0, 0.0))
def set_bounds(self, bounds, new_surface=False):
origin_in_inch = (bounds[0][0], bounds[1][0])
- size_in_inch = (abs(bounds[0][1] - bounds[0][0]), abs(bounds[1][1] - bounds[1][0]))
- size_in_pixels = tuple(map(mul, size_in_inch, self.scale))
+ size_in_inch = (abs(bounds[0][1] - bounds[0][0]),
+ abs(bounds[1][1] - bounds[1][0]))
+ size_in_pixels = self.scale_point(size_in_inch)
self.origin_in_inch = origin_in_inch if self.origin_in_inch is None else self.origin_in_inch
self.size_in_inch = size_in_inch if self.size_in_inch is None else self.size_in_inch
if (self.surface is None) or new_surface:
self.surface_buffer = tempfile.NamedTemporaryFile()
- self.surface = cairo.SVGSurface(self.surface_buffer, size_in_pixels[0], size_in_pixels[1])
- self.ctx = cairo.Context(self.surface)
- self.ctx.set_fill_rule(cairo.FILL_RULE_EVEN_ODD)
- self.ctx.scale(1, -1)
- self.ctx.translate(-(origin_in_inch[0] * self.scale[0]), (-origin_in_inch[1]*self.scale[0]) - size_in_pixels[1])
- self.mask = cairo.SVGSurface(None, size_in_pixels[0], size_in_pixels[1])
- self.mask_ctx = cairo.Context(self.mask)
- self.mask_ctx.set_fill_rule(cairo.FILL_RULE_EVEN_ODD)
- self.mask_ctx.scale(1, -1)
- self.mask_ctx.translate(-(origin_in_inch[0] * self.scale[0]), (-origin_in_inch[1]*self.scale[0]) - size_in_pixels[1])
- self._xform_matrix = cairo.Matrix(xx=1.0, yy=-1.0, x0=-self.origin_in_pixels[0], y0=self.size_in_pixels[1] + self.origin_in_pixels[1])
+ self.surface = cairo.SVGSurface(
+ self.surface_buffer, size_in_pixels[0], size_in_pixels[1])
+ self.output_ctx = cairo.Context(self.surface)
+ self.output_ctx.set_fill_rule(cairo.FILL_RULE_EVEN_ODD)
+ self.output_ctx.scale(1, -1)
+ self.output_ctx.translate(-(origin_in_inch[0] * self.scale[0]),
+ (-origin_in_inch[1] * self.scale[0]) - size_in_pixels[1])
+ self._xform_matrix = cairo.Matrix(xx=1.0, yy=-1.0,
+ x0=-self.origin_in_pixels[0],
+ y0=self.size_in_pixels[1] + self.origin_in_pixels[1])
def render_layers(self, layers, filename, theme=THEMES['default']):
""" Render a set of layers
"""
self.set_bounds(layers[0].bounds, True)
self._paint_background(True)
+
for layer in layers:
self._render_layer(layer, theme)
self.dump(filename)
@@ -117,46 +124,46 @@ class GerberCairoContext(GerberContext):
self.color = settings.color
self.alpha = settings.alpha
self.invert = settings.invert
+
+ # Get a new clean layer to render on
+ self._new_render_layer()
if settings.mirror:
raise Warning('mirrored layers aren\'t supported yet...')
- if self.invert:
- self._clear_mask()
for prim in layer.primitives:
self.render(prim)
- if self.invert:
- self._render_mask()
+ # Add layer to image
+ self._flatten()
def _render_line(self, line, color):
- start = map(mul, line.start, self.scale)
- end = map(mul, line.end, self.scale)
+ start = [pos * scale for pos, scale in zip(line.start, self.scale)]
+ end = [pos * scale for pos, scale in zip(line.end, self.scale)]
if not self.invert:
- ctx = self.ctx
- ctx.set_source_rgba(color[0], color[1], color[2], alpha=self.alpha)
- ctx.set_operator(cairo.OPERATOR_OVER if line.level_polarity == "dark" else cairo.OPERATOR_CLEAR)
+ self.ctx.set_source_rgba(color[0], color[1], color[2], alpha=self.alpha)
+ self.ctx.set_operator(cairo.OPERATOR_OVER
+ if line.level_polarity == "dark"
+ else cairo.OPERATOR_CLEAR)
else:
- ctx = self.mask_ctx
- ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0)
- ctx.set_operator(cairo.OPERATOR_CLEAR)
+ self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0)
+ self.ctx.set_operator(cairo.OPERATOR_CLEAR)
if isinstance(line.aperture, Circle):
- width = line.aperture.diameter
- ctx.set_line_width(width * self.scale[0])
- ctx.set_line_cap(cairo.LINE_CAP_ROUND)
-
- ctx.move_to(*start)
- ctx.line_to(*end)
- ctx.stroke()
+ width = line.aperture.diameter
+ self.ctx.set_line_width(width * self.scale[0])
+ self.ctx.set_line_cap(cairo.LINE_CAP_ROUND)
+ self.ctx.move_to(*start)
+ self.ctx.line_to(*end)
+ self.ctx.stroke()
elif isinstance(line.aperture, Rectangle):
- points = [tuple(map(mul, x, self.scale)) for x in line.vertices]
- ctx.set_line_width(0)
- ctx.move_to(*points[0])
+ points = [self.scale_point(x) for x in line.vertices]
+ self.ctx.set_line_width(0)
+ self.ctx.move_to(*points[0])
for point in points[1:]:
- ctx.line_to(*point)
- ctx.fill()
+ self.ctx.line_to(*point)
+ self.ctx.fill()
def _render_arc(self, arc, color):
- center = map(mul, arc.center, self.scale)
- start = map(mul, arc.start, self.scale)
- end = map(mul, arc.end, self.scale)
+ center = self.scale_point(arc.center)
+ start = self.scale_point(arc.start)
+ end = self.scale_point(arc.end)
radius = self.scale[0] * arc.radius
angle1 = arc.start_angle
angle2 = arc.end_angle
@@ -169,141 +176,137 @@ class GerberCairoContext(GerberContext):
width = max(arc.aperture.width, arc.aperture.height, 0.001)
if not self.invert:
- ctx = self.ctx
- ctx.set_source_rgba(color[0], color[1], color[2], alpha=self.alpha)
- ctx.set_operator(cairo.OPERATOR_OVER if arc.level_polarity == "dark" else cairo.OPERATOR_CLEAR)
+ self.ctx.set_source_rgba(color[0], color[1], color[2], alpha=self.alpha)
+ self.ctx.set_operator(cairo.OPERATOR_OVER
+ if arc.level_polarity == "dark"\
+ else cairo.OPERATOR_CLEAR)
else:
- ctx = self.mask_ctx
- ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0)
- ctx.set_operator(cairo.OPERATOR_CLEAR)
+ self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0)
+ self.ctx.set_operator(cairo.OPERATOR_CLEAR)
- ctx.set_line_width(width * self.scale[0])
- ctx.set_line_cap(cairo.LINE_CAP_ROUND)
- ctx.move_to(*start) # You actually have to do this...
+ self.ctx.set_line_width(width * self.scale[0])
+ self.ctx.set_line_cap(cairo.LINE_CAP_ROUND)
+ self.ctx.move_to(*start) # You actually have to do this...
if arc.direction == 'counterclockwise':
- ctx.arc(center[0], center[1], radius, angle1, angle2)
+ self.ctx.arc(center[0], center[1], radius, angle1, angle2)
else:
- ctx.arc_negative(center[0], center[1], radius, angle1, angle2)
- ctx.move_to(*end) # ...lame
- ctx.stroke()
+ self.ctx.arc_negative(center[0], center[1], radius, angle1, angle2)
+ self.ctx.move_to(*end) # ...lame
def _render_region(self, region, color):
if not self.invert:
- ctx = self.ctx
- ctx.set_source_rgba(color[0], color[1], color[2], alpha=self.alpha)
- ctx.set_operator(cairo.OPERATOR_OVER if region.level_polarity == "dark" else cairo.OPERATOR_CLEAR)
+ self.ctx.set_source_rgba(color[0], color[1], color[2], alpha=self.alpha)
+ self.ctx.set_operator(cairo.OPERATOR_OVER
+ if region.level_polarity == "dark"
+ else cairo.OPERATOR_CLEAR)
else:
- ctx = self.mask_ctx
- ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0)
- ctx.set_operator(cairo.OPERATOR_CLEAR)
-
- ctx.set_line_width(0)
- ctx.set_line_cap(cairo.LINE_CAP_ROUND)
- ctx.move_to(*tuple(map(mul, region.primitives[0].start, self.scale)))
- for p in region.primitives:
- if isinstance(p, Line):
- ctx.line_to(*tuple(map(mul, p.end, self.scale)))
+ self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0)
+ self.ctx.set_operator(cairo.OPERATOR_CLEAR)
+
+ self.ctx.set_line_width(0)
+ self.ctx.set_line_cap(cairo.LINE_CAP_ROUND)
+ self.ctx.move_to(*self.scale_point(region.primitives[0].start))
+ for prim in region.primitives:
+ if isinstance(prim, Line):
+ self.ctx.line_to(*self.scale_point(prim.end))
else:
- center = map(mul, p.center, self.scale)
- start = map(mul, p.start, self.scale)
- end = map(mul, p.end, self.scale)
- radius = self.scale[0] * p.radius
- angle1 = p.start_angle
- angle2 = p.end_angle
- if p.direction == 'counterclockwise':
- ctx.arc(center[0], center[1], radius, angle1, angle2)
+ center = self.scale_point(prim.center)
+ radius = self.scale[0] * prim.radius
+ angle1 = prim.start_angle
+ angle2 = prim.end_angle
+ if prim.direction == 'counterclockwise':
+ self.ctx.arc(*center, radius=radius,
+ angle1=angle1, angle2=angle2)
else:
- ctx.arc_negative(center[0], center[1], radius, angle1, angle2)
- ctx.fill()
-
+ self.ctx.arc_negative(*center, radius=radius,
+ angle1=angle1, angle2=angle2)
+ self.ctx.fill()
def _render_circle(self, circle, color):
- center = tuple(map(mul, circle.position, self.scale))
+ center = self.scale_point(circle.position)
if not self.invert:
- ctx = self.ctx
- ctx.set_source_rgba(color[0], color[1], color[2], alpha=self.alpha)
- ctx.set_operator(cairo.OPERATOR_OVER if circle.level_polarity == "dark" else cairo.OPERATOR_CLEAR)
+ self.ctx.set_source_rgba(color[0], color[1], color[2], alpha=self.alpha)
+ self.ctx.set_operator(cairo.OPERATOR_OVER
+ if circle.level_polarity == "dark"
+ else cairo.OPERATOR_CLEAR)
else:
- ctx = self.mask_ctx
- ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0)
- ctx.set_operator(cairo.OPERATOR_CLEAR)
+ self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0)
+ self.ctx.set_operator(cairo.OPERATOR_CLEAR)
if circle.hole_diameter > 0:
- ctx.push_group()
+ self.ctx.push_group()
- ctx.set_line_width(0)
- ctx.arc(center[0], center[1], radius=circle.radius * self.scale[0], angle1=0, angle2=2 * math.pi)
- ctx.fill()
+ self.ctx.set_line_width(0)
+ self.ctx.arc(center[0], center[1], radius=circle.radius * self.scale[0], angle1=0, angle2=2 * math.pi)
+ self.ctx.fill()
if circle.hole_diameter > 0:
# Render the center clear
- ctx.set_source_rgba(color[0], color[1], color[2], self.alpha)
- ctx.set_operator(cairo.OPERATOR_CLEAR)
- ctx.arc(center[0], center[1], radius=circle.hole_radius * self.scale[0], angle1=0, angle2=2 * math.pi)
- ctx.fill()
+ self.ctx.set_source_rgba(color[0], color[1], color[2], self.alpha)
+ self.ctx.set_operator(cairo.OPERATOR_CLEAR)
+ self.ctx.arc(center[0], center[1], radius=circle.hole_radius * self.scale[0], angle1=0, angle2=2 * math.pi)
+ self.ctx.fill()
- ctx.pop_group_to_source()
- ctx.paint_with_alpha(1)
+ self.ctx.pop_group_to_source()
+ self.ctx.paint_with_alpha(1)
def _render_rectangle(self, rectangle, color):
- ll = map(mul, rectangle.lower_left, self.scale)
- width, height = tuple(map(mul, (rectangle.width, rectangle.height), map(abs, self.scale)))
+ lower_left = self.scale_point(rectangle.lower_left)
+ width, height = tuple([abs(coord) for coord in self.scale_point((rectangle.width, rectangle.height))])
if not self.invert:
- ctx = self.ctx
- ctx.set_source_rgba(color[0], color[1], color[2], alpha=self.alpha)
- ctx.set_operator(cairo.OPERATOR_OVER if rectangle.level_polarity == "dark" else cairo.OPERATOR_CLEAR)
+ self.ctx.set_source_rgba(color[0], color[1], color[2], alpha=self.alpha)
+ self.ctx.set_operator(cairo.OPERATOR_OVER
+ if rectangle.level_polarity == "dark"
+ else cairo.OPERATOR_CLEAR)
else:
- ctx = self.mask_ctx
- ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0)
- ctx.set_operator(cairo.OPERATOR_CLEAR)
+ self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0)
+ self.ctx.set_operator(cairo.OPERATOR_CLEAR)
if rectangle.rotation != 0:
- ctx.save()
+ self.ctx.save()
center = map(mul, rectangle.position, self.scale)
matrix = cairo.Matrix()
matrix.translate(center[0], center[1])
# For drawing, we already handles the translation
- ll[0] = ll[0] - center[0]
- ll[1] = ll[1] - center[1]
+ lower_left[0] = lower_left[0] - center[0]
+ lower_left[1] = lower_left[1] - center[1]
matrix.rotate(rectangle.rotation)
- ctx.transform(matrix)
+ self.ctx.transform(matrix)
if rectangle.hole_diameter > 0:
- ctx.push_group()
+ self.ctx.push_group()
- ctx.set_line_width(0)
- ctx.rectangle(ll[0], ll[1], width, height)
- ctx.fill()
+ self.ctx.set_line_width(0)
+ self.ctx.rectangle(lower_left[0], lower_left[1], width, height)
+ self.ctx.fill()
if rectangle.hole_diameter > 0:
# Render the center clear
- ctx.set_source_rgba(color[0], color[1], color[2], self.alpha)
- ctx.set_operator(cairo.OPERATOR_CLEAR)
+ self.ctx.set_source_rgba(color[0], color[1], color[2], self.alpha)
+ self.ctx.set_operator(cairo.OPERATOR_CLEAR)
center = map(mul, rectangle.position, self.scale)
- ctx.arc(center[0], center[1], radius=rectangle.hole_radius * self.scale[0], angle1=0, angle2=2 * math.pi)
- ctx.fill()
+ self.ctx.arc(center[0], center[1], radius=rectangle.hole_radius * self.scale[0], angle1=0, angle2=2 * math.pi)
+ self.ctx.fill()
- ctx.pop_group_to_source()
- ctx.paint_with_alpha(1)
+ self.ctx.pop_group_to_source()
+ self.ctx.paint_with_alpha(1)
if rectangle.rotation != 0:
- ctx.restore()
+ self.ctx.restore()
def _render_obround(self, obround, color):
if not self.invert:
- ctx = self.ctx
- ctx.set_source_rgba(color[0], color[1], color[2], alpha=self.alpha)
- ctx.set_operator(cairo.OPERATOR_OVER if obround.level_polarity == "dark" else cairo.OPERATOR_CLEAR)
+ self.ctx.set_source_rgba(color[0], color[1], color[2], alpha=self.alpha)
+ self.ctx.set_operator(cairo.OPERATOR_OVER if obround.level_polarity == "dark" else cairo.OPERATOR_CLEAR)
else:
- ctx = self.mask_ctx
- ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0)
- ctx.set_operator(cairo.OPERATOR_CLEAR)
+ self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0)
+ self.ctx.set_operator(cairo.OPERATOR_CLEAR)
if obround.hole_diameter > 0:
- ctx.push_group()
+ self.ctx.push_group()
self._render_circle(obround.subshapes['circle1'], color)
self._render_circle(obround.subshapes['circle2'], color)
@@ -311,55 +314,54 @@ class GerberCairoContext(GerberContext):
if obround.hole_diameter > 0:
# Render the center clear
- ctx.set_source_rgba(color[0], color[1], color[2], self.alpha)
- ctx.set_operator(cairo.OPERATOR_CLEAR)
+ self.ctx.set_source_rgba(color[0], color[1], color[2], self.alpha)
+ self.ctx.set_operator(cairo.OPERATOR_CLEAR)
center = map(mul, obround.position, self.scale)
- ctx.arc(center[0], center[1], radius=obround.hole_radius * self.scale[0], angle1=0, angle2=2 * math.pi)
- ctx.fill()
+ self.ctx.arc(center[0], center[1], radius=obround.hole_radius * self.scale[0], angle1=0, angle2=2 * math.pi)
+ self.ctx.fill()
- ctx.pop_group_to_source()
- ctx.paint_with_alpha(1)
+ self.ctx.pop_group_to_source()
+ self.ctx.paint_with_alpha(1)
def _render_polygon(self, polygon, color):
# TODO Ths does not handle rotation of a polygon
if not self.invert:
- ctx = self.ctx
- ctx.set_source_rgba(color[0], color[1], color[2], alpha=self.alpha)
- ctx.set_operator(cairo.OPERATOR_OVER if polygon.level_polarity == "dark" else cairo.OPERATOR_CLEAR)
+ self.ctx.set_source_rgba(color[0], color[1], color[2], alpha=self.alpha)
+ self.ctx.set_operator(cairo.OPERATOR_OVER if polygon.level_polarity == "dark" else cairo.OPERATOR_CLEAR)
else:
- ctx = self.mask_ctx
- ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0)
- ctx.set_operator(cairo.OPERATOR_CLEAR)
+ self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0)
+ self.ctx.set_operator(cairo.OPERATOR_CLEAR)
if polygon.hole_radius > 0:
- ctx.push_group()
+ self.ctx.push_group()
vertices = polygon.vertices
- ctx.set_line_width(0)
- ctx.set_line_cap(cairo.LINE_CAP_ROUND)
+ self.ctx.set_line_width(0)
+ self.ctx.set_line_cap(cairo.LINE_CAP_ROUND)
# Start from before the end so it is easy to iterate and make sure it is closed
- ctx.move_to(*map(mul, vertices[-1], self.scale))
+ self.ctx.move_to(*map(mul, vertices[-1], self.scale))
for v in vertices:
- ctx.line_to(*map(mul, v, self.scale))
+ self.ctx.line_to(*map(mul, v, self.scale))
- ctx.fill()
+ self.ctx.fill()
if polygon.hole_radius > 0:
# Render the center clear
center = tuple(map(mul, polygon.position, self.scale))
- ctx.set_source_rgba(color[0], color[1], color[2], self.alpha)
- ctx.set_operator(cairo.OPERATOR_CLEAR)
- ctx.set_line_width(0)
- ctx.arc(center[0], center[1], polygon.hole_radius * self.scale[0], 0, 2 * math.pi)
- ctx.fill()
+ self.ctx.set_source_rgba(color[0], color[1], color[2], self.alpha)
+ self.ctx.set_operator(cairo.OPERATOR_CLEAR)
+ self.ctx.set_line_width(0)
+ self.ctx.arc(center[0], center[1], polygon.hole_radius * self.scale[0], 0, 2 * math.pi)
+ self.ctx.fill()
- ctx.pop_group_to_source()
- ctx.paint_with_alpha(1)
+ self.ctx.pop_group_to_source()
+ self.ctx.paint_with_alpha(1)
- def _render_drill(self, circle, color):
+ def _render_drill(self, circle, color=None):
+ color = color if color is not None else self.drill_color
self._render_circle(circle, color)
def _render_slot(self, slot, color):
@@ -369,19 +371,17 @@ class GerberCairoContext(GerberContext):
width = slot.diameter
if not self.invert:
- ctx = self.ctx
- ctx.set_source_rgba(color[0], color[1], color[2], alpha=self.alpha)
- ctx.set_operator(cairo.OPERATOR_OVER if slot.level_polarity == "dark" else cairo.OPERATOR_CLEAR)
+ self.ctx.set_source_rgba(color[0], color[1], color[2], alpha=self.alpha)
+ self.ctx.set_operator(cairo.OPERATOR_OVER if slot.level_polarity == "dark" else cairo.OPERATOR_CLEAR)
else:
- ctx = self.mask_ctx
- ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0)
- ctx.set_operator(cairo.OPERATOR_CLEAR)
-
- ctx.set_line_width(width * self.scale[0])
- ctx.set_line_cap(cairo.LINE_CAP_ROUND)
- ctx.move_to(*start)
- ctx.line_to(*end)
- ctx.stroke()
+ self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0)
+ self.ctx.set_operator(cairo.OPERATOR_CLEAR)
+
+ self.ctx.set_line_width(width * self.scale[0])
+ self.ctx.set_line_cap(cairo.LINE_CAP_ROUND)
+ self.ctx.move_to(*start)
+ self.ctx.line_to(*end)
+ self.ctx.stroke()
def _render_amgroup(self, amgroup, color):
self.ctx.push_group()
@@ -391,33 +391,52 @@ class GerberCairoContext(GerberContext):
self.ctx.paint_with_alpha(1)
def _render_test_record(self, primitive, color):
- position = tuple(map(add, primitive.position, self.origin_in_inch))
+ position = [pos + origin for pos, origin in zip(primitive.position, self.origin_in_inch)]
self.ctx.set_operator(cairo.OPERATOR_OVER)
- self.ctx.select_font_face('monospace', cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_BOLD)
+ self.ctx.select_font_face(
+ 'monospace', cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_BOLD)
self.ctx.set_font_size(13)
self._render_circle(Circle(position, 0.015), color)
self.ctx.set_source_rgba(*color, alpha=self.alpha)
- self.ctx.set_operator(cairo.OPERATOR_OVER if primitive.level_polarity == "dark" else cairo.OPERATOR_CLEAR)
- self.ctx.move_to(*[self.scale[0] * (coord + 0.015) for coord in position])
+ self.ctx.set_operator(
+ cairo.OPERATOR_OVER if primitive.level_polarity == 'dark' else cairo.OPERATOR_CLEAR)
+ self.ctx.move_to(*[self.scale[0] * (coord + 0.015)
+ for coord in position])
self.ctx.scale(1, -1)
self.ctx.show_text(primitive.net_name)
- self.ctx.scale(1, -1)
-
- def _clear_mask(self):
- self.mask_ctx.set_operator(cairo.OPERATOR_OVER)
- self.mask_ctx.set_source_rgba(self.background_color[0], self.background_color[1], self.background_color[2], alpha=self.alpha)
- self.mask_ctx.paint()
-
- def _render_mask(self):
- self.ctx.set_operator(cairo.OPERATOR_OVER)
- ptn = cairo.SurfacePattern(self.mask)
+ self.ctx.scale(1, -1)
+
+ def _new_render_layer(self, color=None):
+ size_in_pixels = self.scale_point(self.size_in_inch)
+ layer = cairo.SVGSurface(None, size_in_pixels[0], size_in_pixels[1])
+ ctx = cairo.Context(layer)
+ ctx.set_fill_rule(cairo.FILL_RULE_EVEN_ODD)
+ ctx.scale(1, -1)
+ ctx.translate(-(self.origin_in_inch[0] * self.scale[0]),
+ (-self.origin_in_inch[1] * self.scale[0])
+ - size_in_pixels[1])
+ if self.invert:
+ ctx.set_operator(cairo.OPERATOR_OVER)
+ ctx.set_source_rgba(*self.color, alpha=self.alpha)
+ ctx.paint()
+ self.ctx = ctx
+ self.active_layer = layer
+
+ def _flatten(self):
+ self.output_ctx.set_operator(cairo.OPERATOR_OVER)
+ ptn = cairo.SurfacePattern(self.active_layer)
ptn.set_matrix(self._xform_matrix)
- self.ctx.set_source(ptn)
- self.ctx.paint()
+ self.output_ctx.set_source(ptn)
+ self.output_ctx.paint()
+ self.ctx = None
+ self.active_layer = None
def _paint_background(self, force=False):
- if (not self.bg) or force:
- self.bg = True
- self.ctx.set_source_rgba(self.background_color[0], self.background_color[1], self.background_color[2], alpha=1.0)
- self.ctx.paint()
-
+ if (not self.bg) or force:
+ self.bg = True
+ self.output_ctx.set_operator(cairo.OPERATOR_OVER)
+ self.output_ctx.set_source_rgba(self.background_color[0], self.background_color[1], self.background_color[2], alpha=1.0)
+ self.output_ctx.paint()
+
+ def scale_point(self, point):
+ return tuple([coord * scale for coord, scale in zip(point, self.scale)]) \ No newline at end of file
diff --git a/gerber/render/render.py b/gerber/render/render.py
index f521c44..7bd4c00 100644
--- a/gerber/render/render.py
+++ b/gerber/render/render.py
@@ -57,12 +57,14 @@ class GerberContext(object):
alpha : float
Rendering opacity. Between 0.0 (transparent) and 1.0 (opaque.)
"""
+
def __init__(self, units='inch'):
self._units = units
self._color = (0.7215, 0.451, 0.200)
self._background_color = (0.0, 0.0, 0.0)
self._alpha = 1.0
self._invert = False
+ self.ctx = None
@property
def units(self):
@@ -134,11 +136,10 @@ class GerberContext(object):
def render(self, primitive):
if not primitive:
return
- color = (self.color if primitive.level_polarity == 'dark'
- else self.background_color)
self._pre_render_primitive(primitive)
+ color = self.color
if isinstance(primitive, Line):
self._render_line(primitive, color)
elif isinstance(primitive, Arc):
@@ -180,6 +181,7 @@ class GerberContext(object):
"""
return
+
def _render_line(self, primitive, color):
pass
@@ -215,9 +217,9 @@ class GerberContext(object):
class RenderSettings(object):
+
def __init__(self, color=(0.0, 0.0, 0.0), alpha=1.0, invert=False, mirror=False):
self.color = color
self.alpha = alpha
self.invert = invert
self.mirror = mirror
-
diff --git a/gerber/render/theme.py b/gerber/render/theme.py
index e538df8..6135ccb 100644
--- a/gerber/render/theme.py
+++ b/gerber/render/theme.py
@@ -23,7 +23,7 @@ COLORS = {
'white': (1.0, 1.0, 1.0),
'red': (1.0, 0.0, 0.0),
'green': (0.0, 1.0, 0.0),
- 'blue' : (0.0, 0.0, 1.0),
+ 'blue': (0.0, 0.0, 1.0),
'fr-4': (0.290, 0.345, 0.0),
'green soldermask': (0.0, 0.612, 0.396),
'blue soldermask': (0.059, 0.478, 0.651),
@@ -36,6 +36,7 @@ COLORS = {
class Theme(object):
+
def __init__(self, name=None, **kwargs):
self.name = 'Default' if name is None else name
self.background = kwargs.get('background', RenderSettings(COLORS['black'], alpha=0.0))
@@ -67,4 +68,3 @@ THEMES = {
topmask=RenderSettings(COLORS['blue soldermask'], alpha=0.8, invert=True),
bottommask=RenderSettings(COLORS['blue soldermask'], alpha=0.8, invert=True)),
}
-
diff --git a/gerber/rs274x.py b/gerber/rs274x.py
index f009232..7fec64f 100644
--- a/gerber/rs274x.py
+++ b/gerber/rs274x.py
@@ -200,7 +200,8 @@ class GerberParser(object):
DEPRECATED_FORMAT = re.compile(r'(?P<format>G9[01])\*')
# end deprecated
- PARAMS = (FS, MO, LP, AD_CIRCLE, AD_RECT, AD_OBROUND, AD_POLY, AD_MACRO, AM, AS, IN, IP, IR, MI, OF, SF, LN)
+ PARAMS = (FS, MO, LP, AD_CIRCLE, AD_RECT, AD_OBROUND, AD_POLY,
+ AD_MACRO, AM, AS, IN, IP, IR, MI, OF, SF, LN)
PARAM_STMT = [re.compile(r"%?{0}\*%?".format(p)) for p in PARAMS]
@@ -418,7 +419,8 @@ class GerberParser(object):
# deprecated codes
(deprecated_unit, r) = _match_one(self.DEPRECATED_UNIT, line)
if deprecated_unit:
- stmt = MOParamStmt(param="MO", mo="inch" if "G70" in deprecated_unit["mode"] else "metric")
+ stmt = MOParamStmt(param="MO", mo="inch" if "G70" in
+ deprecated_unit["mode"] else "metric")
self.settings.units = stmt.mode
yield stmt
line = r
@@ -532,7 +534,9 @@ class GerberParser(object):
if self.region_mode == 'on' and stmt.mode == 'off':
# Sometimes we have regions that have no points. Skip those
if self.current_region:
- 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
self.region_mode = stmt.mode
elif stmt.type == 'QuadrantMode':
@@ -562,7 +566,8 @@ class GerberParser(object):
self.interpolation = 'linear'
elif stmt.function in ('G02', 'G2', 'G03', 'G3'):
self.interpolation = 'arc'
- self.direction = ('clockwise' if stmt.function in ('G02', 'G2') else 'counterclockwise')
+ self.direction = ('clockwise' if stmt.function in
+ ('G02', 'G2') else 'counterclockwise')
if stmt.only_function:
# Sometimes we get a coordinate statement
@@ -582,16 +587,30 @@ class GerberParser(object):
if self.interpolation == 'linear':
if self.region_mode == 'off':
- self.primitives.append(Line(start, end, self.apertures[self.aperture], level_polarity=self.level_polarity, units=self.settings.units))
+ self.primitives.append(Line(start, end,
+ self.apertures[self.aperture],
+ level_polarity=self.level_polarity,
+ units=self.settings.units))
else:
# from gerber spec revision J3, Section 4.5, page 55:
# The segments are not graphics objects in themselves; segments are part of region which is the graphics object. The segments have no thickness.
- # The current aperture is associated with the region. This has no graphical effect, but allows all its attributes to be applied to the region.
+
+ # The current aperture is associated with the region.
+ # This has no graphical effect, but allows all its attributes to
+ # be applied to the region.
if self.current_region is None:
- self.current_region = [Line(start, end, self.apertures.get(self.aperture, Circle((0,0), 0)), level_polarity=self.level_polarity, units=self.settings.units),]
- else:
- self.current_region.append(Line(start, end, self.apertures.get(self.aperture, Circle((0,0), 0)), level_polarity=self.level_polarity, units=self.settings.units))
+ self.current_region = [Line(start, end,
+ self.apertures.get(self.aperture,
+ Circle((0, 0), 0)),
+ level_polarity=self.level_polarity,
+ units=self.settings.units), ]
+ else:
+ self.current_region.append(Line(start, end,
+ self.apertures.get(self.aperture,
+ Circle((0, 0), 0)),
+ level_polarity=self.level_polarity,
+ units=self.settings.units))
else:
i = 0 if stmt.i is None else stmt.i
j = 0 if stmt.j is None else stmt.j
@@ -614,17 +633,23 @@ class GerberParser(object):
elif self.op == "D03" or self.op == "D3":
primitive = copy.deepcopy(self.apertures[self.aperture])
- # XXX: temporary fix because there are no primitives for Macros and Polygon
+
+
if primitive is not None:
- # XXX: just to make it easy to spot
- if isinstance(primitive, type([])):
- print(primitive[0].to_gerber())
- else:
+
+ if not isinstance(primitive, AMParamStmt):
primitive.position = (x, y)
primitive.level_polarity = self.level_polarity
primitive.units = self.settings.units
self.primitives.append(primitive)
-
+ else:
+ # Aperture Macro
+ for am_prim in primitive.primitives:
+ renderable = am_prim.to_primitive((x, y),
+ self.level_polarity,
+ self.settings.units)
+ if renderable is not None:
+ self.primitives.append(renderable)
self.x, self.y = x, y
def _find_center(self, start, end, offsets):
diff --git a/gerber/tests/test_am_statements.py b/gerber/tests/test_am_statements.py
index 39324e5..c5ae6ae 100644
--- a/gerber/tests/test_am_statements.py
+++ b/gerber/tests/test_am_statements.py
@@ -7,6 +7,7 @@ from .tests import *
from ..am_statements import *
from ..am_statements import inch, metric
+
def test_AMPrimitive_ctor():
for exposure in ('on', 'off', 'ON', 'OFF'):
for code in (0, 1, 2, 4, 5, 6, 7, 20, 21, 22):
@@ -20,13 +21,13 @@ def test_AMPrimitive_validation():
assert_raises(ValueError, AMPrimitive, 0, 'exposed')
assert_raises(ValueError, AMPrimitive, 3, 'off')
+
def test_AMPrimitive_conversion():
p = AMPrimitive(4, 'on')
assert_raises(NotImplementedError, p.to_inch)
assert_raises(NotImplementedError, p.to_metric)
-
def test_AMCommentPrimitive_ctor():
c = AMCommentPrimitive(0, ' This is a comment *')
assert_equal(c.code, 0)
@@ -47,6 +48,7 @@ def test_AMCommentPrimitive_dump():
c = AMCommentPrimitive(0, 'Rectangle with rounded corners.')
assert_equal(c.to_gerber(), '0 Rectangle with rounded corners. *')
+
def test_AMCommentPrimitive_conversion():
c = AMCommentPrimitive(0, 'Rectangle with rounded corners.')
ci = c
@@ -56,6 +58,7 @@ def test_AMCommentPrimitive_conversion():
assert_equal(c, ci)
assert_equal(c, cm)
+
def test_AMCommentPrimitive_string():
c = AMCommentPrimitive(0, 'Test Comment')
assert_equal(str(c), '<Aperture Macro Comment: Test Comment>')
@@ -83,7 +86,7 @@ def test_AMCirclePrimitive_factory():
assert_equal(c.code, 1)
assert_equal(c.exposure, 'off')
assert_equal(c.diameter, 5)
- assert_equal(c.position, (0,0))
+ assert_equal(c.position, (0, 0))
def test_AMCirclePrimitive_dump():
@@ -92,6 +95,7 @@ def test_AMCirclePrimitive_dump():
c = AMCirclePrimitive(1, 'on', 5, (0, 0))
assert_equal(c.to_gerber(), '1,1,5,0,0*')
+
def test_AMCirclePrimitive_conversion():
c = AMCirclePrimitive(1, 'off', 25.4, (25.4, 0))
c.to_inch()
@@ -103,8 +107,11 @@ def test_AMCirclePrimitive_conversion():
assert_equal(c.diameter, 25.4)
assert_equal(c.position, (25.4, 0))
+
def test_AMVectorLinePrimitive_validation():
- assert_raises(ValueError, AMVectorLinePrimitive, 3, 'on', 0.1, (0,0), (3.3, 5.4), 0)
+ assert_raises(ValueError, AMVectorLinePrimitive,
+ 3, 'on', 0.1, (0, 0), (3.3, 5.4), 0)
+
def test_AMVectorLinePrimitive_factory():
l = AMVectorLinePrimitive.from_gerber('20,1,0.9,0,0.45,12,0.45,0*')
@@ -115,26 +122,32 @@ def test_AMVectorLinePrimitive_factory():
assert_equal(l.end, (12, 0.45))
assert_equal(l.rotation, 0)
+
def test_AMVectorLinePrimitive_dump():
l = AMVectorLinePrimitive.from_gerber('20,1,0.9,0,0.45,12,0.45,0*')
assert_equal(l.to_gerber(), '20,1,0.9,0.0,0.45,12.0,0.45,0.0*')
+
def test_AMVectorLinePrimtive_conversion():
- l = AMVectorLinePrimitive(20, 'on', 25.4, (0,0), (25.4, 25.4), 0)
+ l = AMVectorLinePrimitive(20, 'on', 25.4, (0, 0), (25.4, 25.4), 0)
l.to_inch()
assert_equal(l.width, 1)
assert_equal(l.start, (0, 0))
assert_equal(l.end, (1, 1))
- l = AMVectorLinePrimitive(20, 'on', 1, (0,0), (1, 1), 0)
+ l = AMVectorLinePrimitive(20, 'on', 1, (0, 0), (1, 1), 0)
l.to_metric()
assert_equal(l.width, 25.4)
assert_equal(l.start, (0, 0))
assert_equal(l.end, (25.4, 25.4))
+
def test_AMOutlinePrimitive_validation():
- assert_raises(ValueError, AMOutlinePrimitive, 7, 'on', (0,0), [(3.3, 5.4), (4.0, 5.4), (0, 0)], 0)
- assert_raises(ValueError, AMOutlinePrimitive, 4, 'on', (0,0), [(3.3, 5.4), (4.0, 5.4), (0, 1)], 0)
+ assert_raises(ValueError, AMOutlinePrimitive, 7, 'on',
+ (0, 0), [(3.3, 5.4), (4.0, 5.4), (0, 0)], 0)
+ assert_raises(ValueError, AMOutlinePrimitive, 4, 'on',
+ (0, 0), [(3.3, 5.4), (4.0, 5.4), (0, 1)], 0)
+
def test_AMOutlinePrimitive_factory():
o = AMOutlinePrimitive.from_gerber('4,1,3,0,0,3,3,3,0,0,0,0*')
@@ -144,14 +157,17 @@ def test_AMOutlinePrimitive_factory():
assert_equal(o.points, [(3, 3), (3, 0), (0, 0)])
assert_equal(o.rotation, 0)
+
def test_AMOUtlinePrimitive_dump():
o = AMOutlinePrimitive(4, 'on', (0, 0), [(3, 3), (3, 0), (0, 0)], 0)
# New lines don't matter for Gerber, but we insert them to make it easier to remove
# For test purposes we can ignore them
assert_equal(o.to_gerber().replace('\n', ''), '4,1,3,0,0,3,3,3,0,0,0,0*')
+
def test_AMOutlinePrimitive_conversion():
- o = AMOutlinePrimitive(4, 'on', (0, 0), [(25.4, 25.4), (25.4, 0), (0, 0)], 0)
+ o = AMOutlinePrimitive(
+ 4, 'on', (0, 0), [(25.4, 25.4), (25.4, 0), (0, 0)], 0)
o.to_inch()
assert_equal(o.start_point, (0, 0))
assert_equal(o.points, ((1., 1.), (1., 0.), (0., 0.)))
@@ -167,6 +183,7 @@ def test_AMPolygonPrimitive_validation():
assert_raises(ValueError, AMPolygonPrimitive, 5, 'on', 2, (3.3, 5.4), 3, 0)
assert_raises(ValueError, AMPolygonPrimitive, 5, 'on', 13, (3.3, 5.4), 3, 0)
+
def test_AMPolygonPrimitive_factory():
p = AMPolygonPrimitive.from_gerber('5,1,3,3.3,5.4,3,0')
assert_equal(p.code, 5)
@@ -176,10 +193,12 @@ def test_AMPolygonPrimitive_factory():
assert_equal(p.diameter, 3)
assert_equal(p.rotation, 0)
+
def test_AMPolygonPrimitive_dump():
p = AMPolygonPrimitive(5, 'on', 3, (3.3, 5.4), 3, 0)
assert_equal(p.to_gerber(), '5,1,3,3.3,5.4,3,0*')
+
def test_AMPolygonPrimitive_conversion():
p = AMPolygonPrimitive(5, 'off', 3, (25.4, 0), 25.4, 0)
p.to_inch()
@@ -193,7 +212,9 @@ def test_AMPolygonPrimitive_conversion():
def test_AMMoirePrimitive_validation():
- assert_raises(ValueError, AMMoirePrimitive, 7, (0, 0), 5.1, 0.2, 0.4, 6, 0.1, 6.1, 0)
+ assert_raises(ValueError, AMMoirePrimitive, 7,
+ (0, 0), 5.1, 0.2, 0.4, 6, 0.1, 6.1, 0)
+
def test_AMMoirePrimitive_factory():
m = AMMoirePrimitive.from_gerber('6,0,0,5,0.5,0.5,2,0.1,6,0*')
@@ -207,10 +228,12 @@ def test_AMMoirePrimitive_factory():
assert_equal(m.crosshair_length, 6)
assert_equal(m.rotation, 0)
+
def test_AMMoirePrimitive_dump():
m = AMMoirePrimitive.from_gerber('6,0,0,5,0.5,0.5,2,0.1,6,0*')
assert_equal(m.to_gerber(), '6,0,0,5.0,0.5,0.5,2,0.1,6.0,0.0*')
+
def test_AMMoirePrimitive_conversion():
m = AMMoirePrimitive(6, (25.4, 25.4), 25.4, 25.4, 25.4, 6, 25.4, 25.4, 0)
m.to_inch()
@@ -230,10 +253,12 @@ def test_AMMoirePrimitive_conversion():
assert_equal(m.crosshair_thickness, 25.4)
assert_equal(m.crosshair_length, 25.4)
+
def test_AMThermalPrimitive_validation():
assert_raises(ValueError, AMThermalPrimitive, 8, (0.0, 0.0), 7, 5, 0.2, 0.0)
assert_raises(TypeError, AMThermalPrimitive, 7, (0.0, '0'), 7, 5, 0.2, 0.0)
+
def test_AMThermalPrimitive_factory():
t = AMThermalPrimitive.from_gerber('7,0,0,7,6,0.2,45*')
assert_equal(t.code, 7)
@@ -243,10 +268,12 @@ def test_AMThermalPrimitive_factory():
assert_equal(t.gap, 0.2)
assert_equal(t.rotation, 45)
+
def test_AMThermalPrimitive_dump():
t = AMThermalPrimitive.from_gerber('7,0,0,7,6,0.2,30*')
assert_equal(t.to_gerber(), '7,0,0,7.0,6.0,0.2,30.0*')
+
def test_AMThermalPrimitive_conversion():
t = AMThermalPrimitive(7, (25.4, 25.4), 25.4, 25.4, 25.4, 0.0)
t.to_inch()
@@ -264,7 +291,9 @@ def test_AMThermalPrimitive_conversion():
def test_AMCenterLinePrimitive_validation():
- assert_raises(ValueError, AMCenterLinePrimitive, 22, 1, 0.2, 0.5, (0, 0), 0)
+ assert_raises(ValueError, AMCenterLinePrimitive,
+ 22, 1, 0.2, 0.5, (0, 0), 0)
+
def test_AMCenterLinePrimtive_factory():
l = AMCenterLinePrimitive.from_gerber('21,1,6.8,1.2,3.4,0.6,0*')
@@ -275,10 +304,12 @@ def test_AMCenterLinePrimtive_factory():
assert_equal(l.center, (3.4, 0.6))
assert_equal(l.rotation, 0)
+
def test_AMCenterLinePrimitive_dump():
l = AMCenterLinePrimitive.from_gerber('21,1,6.8,1.2,3.4,0.6,0*')
assert_equal(l.to_gerber(), '21,1,6.8,1.2,3.4,0.6,0.0*')
+
def test_AMCenterLinePrimitive_conversion():
l = AMCenterLinePrimitive(21, 'on', 25.4, 25.4, (25.4, 25.4), 0)
l.to_inch()
@@ -292,8 +323,11 @@ def test_AMCenterLinePrimitive_conversion():
assert_equal(l.height, 25.4)
assert_equal(l.center, (25.4, 25.4))
+
def test_AMLowerLeftLinePrimitive_validation():
- assert_raises(ValueError, AMLowerLeftLinePrimitive, 23, 1, 0.2, 0.5, (0, 0), 0)
+ assert_raises(ValueError, AMLowerLeftLinePrimitive,
+ 23, 1, 0.2, 0.5, (0, 0), 0)
+
def test_AMLowerLeftLinePrimtive_factory():
l = AMLowerLeftLinePrimitive.from_gerber('22,1,6.8,1.2,3.4,0.6,0*')
@@ -304,10 +338,12 @@ def test_AMLowerLeftLinePrimtive_factory():
assert_equal(l.lower_left, (3.4, 0.6))
assert_equal(l.rotation, 0)
+
def test_AMLowerLeftLinePrimitive_dump():
l = AMLowerLeftLinePrimitive.from_gerber('22,1,6.8,1.2,3.4,0.6,0*')
assert_equal(l.to_gerber(), '22,1,6.8,1.2,3.4,0.6,0.0*')
+
def test_AMLowerLeftLinePrimitive_conversion():
l = AMLowerLeftLinePrimitive(22, 'on', 25.4, 25.4, (25.4, 25.4), 0)
l.to_inch()
@@ -321,24 +357,23 @@ def test_AMLowerLeftLinePrimitive_conversion():
assert_equal(l.height, 25.4)
assert_equal(l.lower_left, (25.4, 25.4))
+
def test_AMUnsupportPrimitive():
u = AMUnsupportPrimitive.from_gerber('Test')
assert_equal(u.primitive, 'Test')
u = AMUnsupportPrimitive('Test')
assert_equal(u.to_gerber(), 'Test')
+
def test_AMUnsupportPrimitive_smoketest():
u = AMUnsupportPrimitive.from_gerber('Test')
u.to_inch()
u.to_metric()
-
def test_inch():
assert_equal(inch(25.4), 1)
+
def test_metric():
assert_equal(metric(1), 25.4)
-
-
-
diff --git a/gerber/tests/test_cam.py b/gerber/tests/test_cam.py
index 3ae0a24..24f2b9b 100644
--- a/gerber/tests/test_cam.py
+++ b/gerber/tests/test_cam.py
@@ -54,17 +54,20 @@ def test_filesettings_dict_assign():
assert_equal(fs.zero_suppression, 'leading')
assert_equal(fs.format, (1, 2))
+
def test_camfile_init():
""" Smoke test CamFile test
"""
cf = CamFile()
+
def test_camfile_settings():
""" Test CamFile Default Settings
"""
cf = CamFile()
assert_equal(cf.settings, FileSettings())
+
def test_bounds_override_smoketest():
cf = CamFile()
cf.bounds
@@ -89,7 +92,7 @@ def test_zeros():
assert_equal(fs.zeros, 'trailing')
assert_equal(fs.zero_suppression, 'leading')
- fs.zeros= 'leading'
+ fs.zeros = 'leading'
assert_equal(fs.zeros, 'leading')
assert_equal(fs.zero_suppression, 'trailing')
@@ -113,21 +116,27 @@ def test_zeros():
def test_filesettings_validation():
""" Test FileSettings constructor argument validation
"""
-
# absolute-ish is not a valid notation
- assert_raises(ValueError, FileSettings, 'absolute-ish', 'inch', None, (2, 5), None)
+ assert_raises(ValueError, FileSettings, 'absolute-ish',
+ 'inch', None, (2, 5), None)
# degrees kelvin isn't a valid unit for a CAM file
- assert_raises(ValueError, FileSettings, 'absolute', 'degrees kelvin', None, (2, 5), None)
+ assert_raises(ValueError, FileSettings, 'absolute',
+ 'degrees kelvin', None, (2, 5), None)
- assert_raises(ValueError, FileSettings, 'absolute', 'inch', 'leading', (2, 5), 'leading')
+ assert_raises(ValueError, FileSettings, 'absolute',
+ 'inch', 'leading', (2, 5), 'leading')
# Technnically this should be an error, but Eangle files often do this incorrectly so we
# allow it
- # assert_raises(ValueError, FileSettings, 'absolute', 'inch', 'following', (2, 5), None)
+ #assert_raises(ValueError, FileSettings, 'absolute',
+ # 'inch', 'following', (2, 5), None)
- assert_raises(ValueError, FileSettings, 'absolute', 'inch', None, (2, 5), 'following')
- assert_raises(ValueError, FileSettings, 'absolute', 'inch', None, (2, 5, 6), None)
+ assert_raises(ValueError, FileSettings, 'absolute',
+ 'inch', None, (2, 5), 'following')
+ assert_raises(ValueError, FileSettings, 'absolute',
+ 'inch', None, (2, 5, 6), None)
+
def test_key_validation():
fs = FileSettings()
@@ -138,5 +147,3 @@ def test_key_validation():
assert_raises(ValueError, fs.__setitem__, 'zero_suppression', 'following')
assert_raises(ValueError, fs.__setitem__, 'zeros', 'following')
assert_raises(ValueError, fs.__setitem__, 'format', (2, 5, 6))
-
-
diff --git a/gerber/tests/test_common.py b/gerber/tests/test_common.py
index 5991e5e..357ed18 100644
--- a/gerber/tests/test_common.py
+++ b/gerber/tests/test_common.py
@@ -12,9 +12,10 @@ import os
NCDRILL_FILE = os.path.join(os.path.dirname(__file__),
- 'resources/ncdrill.DRD')
+ 'resources/ncdrill.DRD')
TOP_COPPER_FILE = os.path.join(os.path.dirname(__file__),
- 'resources/top_copper.GTL')
+ 'resources/top_copper.GTL')
+
def test_file_type_detection():
""" Test file type detection
@@ -38,6 +39,3 @@ def test_file_type_validation():
""" Test file format validation
"""
assert_raises(ParseError, read, 'LICENSE')
-
-
-
diff --git a/gerber/tests/test_excellon.py b/gerber/tests/test_excellon.py
index cd94b0f..1402938 100644
--- a/gerber/tests/test_excellon.py
+++ b/gerber/tests/test_excellon.py
@@ -13,6 +13,7 @@ from .tests import *
NCDRILL_FILE = os.path.join(os.path.dirname(__file__),
'resources/ncdrill.DRD')
+
def test_format_detection():
""" Test file type detection
"""
@@ -75,7 +76,8 @@ def test_conversion():
for statement in ncdrill_inch.statements:
statement.to_metric()
- for m_tool, i_tool in zip(iter(ncdrill.tools.values()), iter(ncdrill_inch.tools.values())):
+ for m_tool, i_tool in zip(iter(ncdrill.tools.values()),
+ iter(ncdrill_inch.tools.values())):
assert_equal(i_tool, m_tool)
for m, i in zip(ncdrill.primitives, inch_primitives):
@@ -188,12 +190,10 @@ def test_parse_incremental_position():
p = ExcellonParser(FileSettings(notation='incremental'))
p._parse_line('X01Y01')
p._parse_line('X01Y01')
- assert_equal(p.pos, [2.,2.])
+ assert_equal(p.pos, [2., 2.])
def test_parse_unknown():
p = ExcellonParser(FileSettings())
p._parse_line('Not A Valid Statement')
assert_equal(p.statements[0].stmt, 'Not A Valid Statement')
-
-
diff --git a/gerber/tests/test_excellon_statements.py b/gerber/tests/test_excellon_statements.py
index 2f0ef10..8e6e06e 100644
--- a/gerber/tests/test_excellon_statements.py
+++ b/gerber/tests/test_excellon_statements.py
@@ -7,11 +7,13 @@ from .tests import assert_equal, assert_not_equal, assert_raises
from ..excellon_statements import *
from ..cam import FileSettings
+
def test_excellon_statement_implementation():
stmt = ExcellonStatement()
assert_raises(NotImplementedError, stmt.from_excellon, None)
assert_raises(NotImplementedError, stmt.to_excellon)
+
def test_excellontstmt():
""" Smoke test ExcellonStatement
"""
@@ -20,17 +22,18 @@ def test_excellontstmt():
stmt.to_metric()
stmt.offset()
+
def test_excellontool_factory():
""" Test ExcellonTool factory methods
"""
exc_line = 'T8F01B02S00003H04Z05C0.12500'
settings = FileSettings(format=(2, 5), zero_suppression='trailing',
- units='inch', notation='absolute')
+ units='inch', notation='absolute')
tool = ExcellonTool.from_excellon(exc_line, settings)
assert_equal(tool.number, 8)
assert_equal(tool.diameter, 0.125)
assert_equal(tool.feed_rate, 1)
- assert_equal(tool.retract_rate,2)
+ assert_equal(tool.retract_rate, 2)
assert_equal(tool.rpm, 3)
assert_equal(tool.max_hit_count, 4)
assert_equal(tool.depth_offset, 5)
@@ -41,7 +44,7 @@ def test_excellontool_factory():
assert_equal(tool.number, 8)
assert_equal(tool.diameter, 0.125)
assert_equal(tool.feed_rate, 1)
- assert_equal(tool.retract_rate,2)
+ assert_equal(tool.retract_rate, 2)
assert_equal(tool.rpm, 3)
assert_equal(tool.max_hit_count, 4)
assert_equal(tool.depth_offset, 5)
@@ -55,7 +58,7 @@ def test_excellontool_dump():
'T07F0S0C0.04300', 'T08F0S0C0.12500', 'T09F0S0C0.13000',
'T08B01F02H03S00003C0.12500Z04', 'T01F0S300.999C0.01200']
settings = FileSettings(format=(2, 5), zero_suppression='trailing',
- units='inch', notation='absolute')
+ units='inch', notation='absolute')
for line in exc_lines:
tool = ExcellonTool.from_excellon(line, settings)
assert_equal(tool.to_excellon(), line)
@@ -63,7 +66,7 @@ def test_excellontool_dump():
def test_excellontool_order():
settings = FileSettings(format=(2, 5), zero_suppression='trailing',
- units='inch', notation='absolute')
+ units='inch', notation='absolute')
line = 'T8F00S00C0.12500'
tool1 = ExcellonTool.from_excellon(line, settings)
line = 'T8C0.12500F00S00'
@@ -72,36 +75,48 @@ def test_excellontool_order():
assert_equal(tool1.feed_rate, tool2.feed_rate)
assert_equal(tool1.rpm, tool2.rpm)
+
def test_excellontool_conversion():
- tool = ExcellonTool.from_dict(FileSettings(units='metric'), {'number': 8, 'diameter': 25.4})
+ tool = ExcellonTool.from_dict(FileSettings(units='metric'),
+ {'number': 8, 'diameter': 25.4})
tool.to_inch()
assert_equal(tool.diameter, 1.)
- tool = ExcellonTool.from_dict(FileSettings(units='inch'), {'number': 8, 'diameter': 1.})
+ tool = ExcellonTool.from_dict(FileSettings(units='inch'),
+ {'number': 8, 'diameter': 1.})
tool.to_metric()
assert_equal(tool.diameter, 25.4)
# Shouldn't change units if we're already using target units
- tool = ExcellonTool.from_dict(FileSettings(units='inch'), {'number': 8, 'diameter': 25.4})
+ tool = ExcellonTool.from_dict(FileSettings(units='inch'),
+ {'number': 8, 'diameter': 25.4})
tool.to_inch()
assert_equal(tool.diameter, 25.4)
- tool = ExcellonTool.from_dict(FileSettings(units='metric'), {'number': 8, 'diameter': 1.})
+ tool = ExcellonTool.from_dict(FileSettings(units='metric'),
+ {'number': 8, 'diameter': 1.})
tool.to_metric()
assert_equal(tool.diameter, 1.)
def test_excellontool_repr():
- tool = ExcellonTool.from_dict(FileSettings(), {'number': 8, 'diameter': 0.125})
+ tool = ExcellonTool.from_dict(FileSettings(),
+ {'number': 8, 'diameter': 0.125})
assert_equal(str(tool), '<ExcellonTool 08: 0.125in. dia.>')
- tool = ExcellonTool.from_dict(FileSettings(units='metric'), {'number': 8, 'diameter': 0.125})
+ tool = ExcellonTool.from_dict(FileSettings(units='metric'),
+ {'number': 8, 'diameter': 0.125})
assert_equal(str(tool), '<ExcellonTool 08: 0.125mm dia.>')
+
def test_excellontool_equality():
- t = ExcellonTool.from_dict(FileSettings(), {'number': 8, 'diameter': 0.125})
- t1 = ExcellonTool.from_dict(FileSettings(), {'number': 8, 'diameter': 0.125})
+ t = ExcellonTool.from_dict(
+ FileSettings(), {'number': 8, 'diameter': 0.125})
+ t1 = ExcellonTool.from_dict(
+ FileSettings(), {'number': 8, 'diameter': 0.125})
assert_equal(t, t1)
- t1 = ExcellonTool.from_dict(FileSettings(units='metric'), {'number': 8, 'diameter': 0.125})
+ t1 = ExcellonTool.from_dict(FileSettings(units='metric'),
+ {'number': 8, 'diameter': 0.125})
assert_not_equal(t, t1)
+
def test_toolselection_factory():
""" Test ToolSelectionStmt factory method
"""
@@ -115,6 +130,7 @@ def test_toolselection_factory():
assert_equal(stmt.tool, 42)
assert_equal(stmt.compensation_index, None)
+
def test_toolselection_dump():
""" Test ToolSelectionStmt to_excellon()
"""
@@ -123,6 +139,7 @@ def test_toolselection_dump():
stmt = ToolSelectionStmt.from_excellon(line)
assert_equal(stmt.to_excellon(), line)
+
def test_z_axis_infeed_rate_factory():
""" Test ZAxisInfeedRateStmt factory method
"""
@@ -133,6 +150,7 @@ def test_z_axis_infeed_rate_factory():
stmt = ZAxisInfeedRateStmt.from_excellon('F03')
assert_equal(stmt.rate, 3)
+
def test_z_axis_infeed_rate_dump():
""" Test ZAxisInfeedRateStmt to_excellon()
"""
@@ -145,11 +163,12 @@ def test_z_axis_infeed_rate_dump():
stmt = ZAxisInfeedRateStmt.from_excellon(input_rate)
assert_equal(stmt.to_excellon(), expected_output)
+
def test_coordinatestmt_factory():
""" Test CoordinateStmt factory method
"""
settings = FileSettings(format=(2, 5), zero_suppression='trailing',
- units='inch', notation='absolute')
+ units='inch', notation='absolute')
line = 'X0278207Y0065293'
stmt = CoordinateStmt.from_excellon(line, settings)
@@ -165,7 +184,7 @@ def test_coordinatestmt_factory():
# assert_equal(stmt.y, 0.575)
settings = FileSettings(format=(2, 4), zero_suppression='leading',
- units='inch', notation='absolute')
+ units='inch', notation='absolute')
line = 'X9660Y4639'
stmt = CoordinateStmt.from_excellon(line, settings)
@@ -173,12 +192,12 @@ def test_coordinatestmt_factory():
assert_equal(stmt.y, 0.4639)
assert_equal(stmt.to_excellon(settings), "X9660Y4639")
assert_equal(stmt.units, 'inch')
-
+
settings.units = 'metric'
stmt = CoordinateStmt.from_excellon(line, settings)
assert_equal(stmt.units, 'metric')
-
-
+
+
def test_coordinatestmt_dump():
""" Test CoordinateStmt to_excellon()
"""
@@ -186,102 +205,110 @@ def test_coordinatestmt_dump():
'X251295Y81528', 'X2525Y78', 'X255Y575', 'Y52',
'X2675', 'Y575', 'X2425', 'Y52', 'X23', ]
settings = FileSettings(format=(2, 4), zero_suppression='leading',
- units='inch', notation='absolute')
+ units='inch', notation='absolute')
for line in lines:
stmt = CoordinateStmt.from_excellon(line, settings)
assert_equal(stmt.to_excellon(settings), line)
+
def test_coordinatestmt_conversion():
-
+
settings = FileSettings()
settings.units = 'metric'
stmt = CoordinateStmt.from_excellon('X254Y254', settings)
-
- #No effect
+
+ # No effect
stmt.to_metric()
assert_equal(stmt.x, 25.4)
assert_equal(stmt.y, 25.4)
-
+
stmt.to_inch()
assert_equal(stmt.units, 'inch')
assert_equal(stmt.x, 1.)
assert_equal(stmt.y, 1.)
-
- #No effect
+
+ # No effect
stmt.to_inch()
assert_equal(stmt.x, 1.)
assert_equal(stmt.y, 1.)
-
+
settings.units = 'inch'
stmt = CoordinateStmt.from_excellon('X01Y01', settings)
-
- #No effect
+
+ # No effect
stmt.to_inch()
assert_equal(stmt.x, 1.)
assert_equal(stmt.y, 1.)
-
+
stmt.to_metric()
assert_equal(stmt.units, 'metric')
assert_equal(stmt.x, 25.4)
assert_equal(stmt.y, 25.4)
-
- #No effect
+
+ # No effect
stmt.to_metric()
assert_equal(stmt.x, 25.4)
assert_equal(stmt.y, 25.4)
+
def test_coordinatestmt_offset():
stmt = CoordinateStmt.from_excellon('X01Y01', FileSettings())
stmt.offset()
assert_equal(stmt.x, 1)
assert_equal(stmt.y, 1)
- stmt.offset(1,0)
+ stmt.offset(1, 0)
assert_equal(stmt.x, 2.)
assert_equal(stmt.y, 1.)
- stmt.offset(0,1)
+ stmt.offset(0, 1)
assert_equal(stmt.x, 2.)
assert_equal(stmt.y, 2.)
def test_coordinatestmt_string():
settings = FileSettings(format=(2, 4), zero_suppression='leading',
- units='inch', notation='absolute')
+ units='inch', notation='absolute')
stmt = CoordinateStmt.from_excellon('X9660Y4639', settings)
assert_equal(str(stmt), '<Coordinate Statement: X: 0.966 Y: 0.4639 >')
def test_repeathole_stmt_factory():
- stmt = RepeatHoleStmt.from_excellon('R0004X015Y32', FileSettings(zeros='leading', units='inch'))
+ stmt = RepeatHoleStmt.from_excellon('R0004X015Y32',
+ FileSettings(zeros='leading',
+ units='inch'))
assert_equal(stmt.count, 4)
assert_equal(stmt.xdelta, 1.5)
assert_equal(stmt.ydelta, 32)
assert_equal(stmt.units, 'inch')
-
- stmt = RepeatHoleStmt.from_excellon('R0004X015Y32', FileSettings(zeros='leading', units='metric'))
+
+ stmt = RepeatHoleStmt.from_excellon('R0004X015Y32',
+ FileSettings(zeros='leading',
+ units='metric'))
assert_equal(stmt.units, 'metric')
+
def test_repeatholestmt_dump():
line = 'R4X015Y32'
stmt = RepeatHoleStmt.from_excellon(line, FileSettings())
assert_equal(stmt.to_excellon(FileSettings()), line)
+
def test_repeatholestmt_conversion():
line = 'R4X0254Y254'
settings = FileSettings()
settings.units = 'metric'
stmt = RepeatHoleStmt.from_excellon(line, settings)
-
- #No effect
+
+ # No effect
stmt.to_metric()
assert_equal(stmt.xdelta, 2.54)
assert_equal(stmt.ydelta, 25.4)
-
+
stmt.to_inch()
assert_equal(stmt.units, 'inch')
assert_equal(stmt.xdelta, 0.1)
assert_equal(stmt.ydelta, 1.)
-
- #no effect
+
+ # no effect
stmt.to_inch()
assert_equal(stmt.xdelta, 0.1)
assert_equal(stmt.ydelta, 1.)
@@ -289,26 +316,28 @@ def test_repeatholestmt_conversion():
line = 'R4X01Y1'
settings.units = 'inch'
stmt = RepeatHoleStmt.from_excellon(line, settings)
-
- #no effect
+
+ # no effect
stmt.to_inch()
assert_equal(stmt.xdelta, 1.)
assert_equal(stmt.ydelta, 10.)
-
+
stmt.to_metric()
assert_equal(stmt.units, 'metric')
assert_equal(stmt.xdelta, 25.4)
assert_equal(stmt.ydelta, 254.)
-
- #No effect
+
+ # No effect
stmt.to_metric()
assert_equal(stmt.xdelta, 25.4)
assert_equal(stmt.ydelta, 254.)
+
def test_repeathole_str():
stmt = RepeatHoleStmt.from_excellon('R4X015Y32', FileSettings())
assert_equal(str(stmt), '<Repeat Hole: 4 times, offset X: 1.5 Y: 32>')
+
def test_commentstmt_factory():
""" Test CommentStmt factory method
"""
@@ -333,42 +362,52 @@ def test_commentstmt_dump():
stmt = CommentStmt.from_excellon(line)
assert_equal(stmt.to_excellon(), line)
+
def test_header_begin_stmt():
stmt = HeaderBeginStmt()
assert_equal(stmt.to_excellon(None), 'M48')
+
def test_header_end_stmt():
stmt = HeaderEndStmt()
assert_equal(stmt.to_excellon(None), 'M95')
+
def test_rewindstop_stmt():
stmt = RewindStopStmt()
assert_equal(stmt.to_excellon(None), '%')
+
def test_z_axis_rout_position_stmt():
stmt = ZAxisRoutPositionStmt()
assert_equal(stmt.to_excellon(None), 'M15')
+
def test_retract_with_clamping_stmt():
stmt = RetractWithClampingStmt()
assert_equal(stmt.to_excellon(None), 'M16')
+
def test_retract_without_clamping_stmt():
stmt = RetractWithoutClampingStmt()
assert_equal(stmt.to_excellon(None), 'M17')
+
def test_cutter_compensation_off_stmt():
stmt = CutterCompensationOffStmt()
assert_equal(stmt.to_excellon(None), 'G40')
+
def test_cutter_compensation_left_stmt():
stmt = CutterCompensationLeftStmt()
assert_equal(stmt.to_excellon(None), 'G41')
+
def test_cutter_compensation_right_stmt():
stmt = CutterCompensationRightStmt()
assert_equal(stmt.to_excellon(None), 'G42')
+
def test_endofprogramstmt_factory():
settings = FileSettings(units='inch')
stmt = EndOfProgramStmt.from_excellon('M30X01Y02', settings)
@@ -384,61 +423,65 @@ def test_endofprogramstmt_factory():
assert_equal(stmt.x, None)
assert_equal(stmt.y, 2.)
+
def test_endofprogramStmt_dump():
- lines = ['M30X01Y02',]
+ lines = ['M30X01Y02', ]
for line in lines:
stmt = EndOfProgramStmt.from_excellon(line, FileSettings())
assert_equal(stmt.to_excellon(FileSettings()), line)
+
def test_endofprogramstmt_conversion():
settings = FileSettings()
settings.units = 'metric'
stmt = EndOfProgramStmt.from_excellon('M30X0254Y254', settings)
- #No effect
+ # No effect
stmt.to_metric()
assert_equal(stmt.x, 2.54)
assert_equal(stmt.y, 25.4)
-
+
stmt.to_inch()
assert_equal(stmt.units, 'inch')
assert_equal(stmt.x, 0.1)
assert_equal(stmt.y, 1.0)
-
- #No effect
+
+ # No effect
stmt.to_inch()
assert_equal(stmt.x, 0.1)
assert_equal(stmt.y, 1.0)
settings.units = 'inch'
stmt = EndOfProgramStmt.from_excellon('M30X01Y1', settings)
-
- #No effect
+
+ # No effect
stmt.to_inch()
assert_equal(stmt.x, 1.)
assert_equal(stmt.y, 10.0)
-
+
stmt.to_metric()
assert_equal(stmt.units, 'metric')
assert_equal(stmt.x, 25.4)
assert_equal(stmt.y, 254.)
-
- #No effect
+
+ # No effect
stmt.to_metric()
assert_equal(stmt.x, 25.4)
assert_equal(stmt.y, 254.)
+
def test_endofprogramstmt_offset():
stmt = EndOfProgramStmt(1, 1)
stmt.offset()
assert_equal(stmt.x, 1)
assert_equal(stmt.y, 1)
- stmt.offset(1,0)
+ stmt.offset(1, 0)
assert_equal(stmt.x, 2.)
assert_equal(stmt.y, 1.)
- stmt.offset(0,1)
+ stmt.offset(0, 1)
assert_equal(stmt.x, 2.)
assert_equal(stmt.y, 2.)
+
def test_unitstmt_factory():
""" Test UnitStmt factory method
"""
@@ -471,6 +514,7 @@ def test_unitstmt_dump():
stmt = UnitStmt.from_excellon(line)
assert_equal(stmt.to_excellon(), line)
+
def test_unitstmt_conversion():
stmt = UnitStmt.from_excellon('METRIC,TZ')
stmt.to_inch()
@@ -480,6 +524,7 @@ def test_unitstmt_conversion():
stmt.to_metric()
assert_equal(stmt.units, 'metric')
+
def test_incrementalmode_factory():
""" Test IncrementalModeStmt factory method
"""
@@ -527,6 +572,7 @@ def test_versionstmt_dump():
stmt = VersionStmt.from_excellon(line)
assert_equal(stmt.to_excellon(), line)
+
def test_versionstmt_validation():
""" Test VersionStmt input validation
"""
@@ -608,6 +654,7 @@ def test_measmodestmt_validation():
assert_raises(ValueError, MeasuringModeStmt.from_excellon, 'M70')
assert_raises(ValueError, MeasuringModeStmt, 'millimeters')
+
def test_measmodestmt_conversion():
line = 'M72'
stmt = MeasuringModeStmt.from_excellon(line)
@@ -621,27 +668,33 @@ def test_measmodestmt_conversion():
stmt.to_inch()
assert_equal(stmt.units, 'inch')
+
def test_routemode_stmt():
stmt = RouteModeStmt()
assert_equal(stmt.to_excellon(FileSettings()), 'G00')
+
def test_linearmode_stmt():
stmt = LinearModeStmt()
assert_equal(stmt.to_excellon(FileSettings()), 'G01')
+
def test_drillmode_stmt():
stmt = DrillModeStmt()
assert_equal(stmt.to_excellon(FileSettings()), 'G05')
+
def test_absolutemode_stmt():
stmt = AbsoluteModeStmt()
assert_equal(stmt.to_excellon(FileSettings()), 'G90')
+
def test_unknownstmt():
stmt = UnknownStmt('TEST')
assert_equal(stmt.stmt, 'TEST')
assert_equal(str(stmt), '<Unknown Statement: TEST>')
+
def test_unknownstmt_dump():
stmt = UnknownStmt('TEST')
assert_equal(stmt.to_excellon(FileSettings()), 'TEST')
diff --git a/gerber/tests/test_gerber_statements.py b/gerber/tests/test_gerber_statements.py
index a89a283..2157390 100644
--- a/gerber/tests/test_gerber_statements.py
+++ b/gerber/tests/test_gerber_statements.py
@@ -7,6 +7,7 @@ from .tests import *
from ..gerber_statements import *
from ..cam import FileSettings
+
def test_Statement_smoketest():
stmt = Statement('Test')
assert_equal(stmt.type, 'Test')
@@ -16,7 +17,8 @@ def test_Statement_smoketest():
assert_in('units=inch', str(stmt))
stmt.to_metric()
stmt.offset(1, 1)
- assert_in('type=Test',str(stmt))
+ assert_in('type=Test', str(stmt))
+
def test_FSParamStmt_factory():
""" Test FSParamStruct factory
@@ -35,6 +37,7 @@ def test_FSParamStmt_factory():
assert_equal(fs.notation, 'incremental')
assert_equal(fs.format, (2, 7))
+
def test_FSParamStmt():
""" Test FSParamStmt initialization
"""
@@ -48,6 +51,7 @@ def test_FSParamStmt():
assert_equal(stmt.notation, notation)
assert_equal(stmt.format, fmt)
+
def test_FSParamStmt_dump():
""" Test FSParamStmt to_gerber()
"""
@@ -62,16 +66,20 @@ def test_FSParamStmt_dump():
settings = FileSettings(zero_suppression='leading', notation='absolute')
assert_equal(fs.to_gerber(settings), '%FSLAX25Y25*%')
+
def test_FSParamStmt_string():
""" Test FSParamStmt.__str__()
"""
stmt = {'param': 'FS', 'zero': 'L', 'notation': 'A', 'x': '27'}
fs = FSParamStmt.from_dict(stmt)
- assert_equal(str(fs), '<Format Spec: 2:7 leading zero suppression absolute notation>')
+ assert_equal(str(fs),
+ '<Format Spec: 2:7 leading zero suppression absolute notation>')
stmt = {'param': 'FS', 'zero': 'T', 'notation': 'I', 'x': '25'}
fs = FSParamStmt.from_dict(stmt)
- assert_equal(str(fs), '<Format Spec: 2:5 trailing zero suppression incremental notation>')
+ assert_equal(str(fs),
+ '<Format Spec: 2:5 trailing zero suppression incremental notation>')
+
def test_MOParamStmt_factory():
""" Test MOParamStruct factory
@@ -94,6 +102,7 @@ def test_MOParamStmt_factory():
stmt = {'param': 'MO', 'mo': 'degrees kelvin'}
assert_raises(ValueError, MOParamStmt.from_dict, stmt)
+
def test_MOParamStmt():
""" Test MOParamStmt initialization
"""
@@ -106,6 +115,7 @@ def test_MOParamStmt():
stmt = MOParamStmt(param, mode)
assert_equal(stmt.mode, mode)
+
def test_MOParamStmt_dump():
""" Test MOParamStmt to_gerber()
"""
@@ -117,6 +127,7 @@ def test_MOParamStmt_dump():
mo = MOParamStmt.from_dict(stmt)
assert_equal(mo.to_gerber(), '%MOMM*%')
+
def test_MOParamStmt_conversion():
stmt = {'param': 'MO', 'mo': 'MM'}
mo = MOParamStmt.from_dict(stmt)
@@ -128,6 +139,7 @@ def test_MOParamStmt_conversion():
mo.to_metric()
assert_equal(mo.mode, 'metric')
+
def test_MOParamStmt_string():
""" Test MOParamStmt.__str__()
"""
@@ -139,6 +151,7 @@ def test_MOParamStmt_string():
mo = MOParamStmt.from_dict(stmt)
assert_equal(str(mo), '<Mode: millimeters>')
+
def test_IPParamStmt_factory():
""" Test IPParamStruct factory
"""
@@ -150,6 +163,7 @@ def test_IPParamStmt_factory():
ip = IPParamStmt.from_dict(stmt)
assert_equal(ip.ip, 'negative')
+
def test_IPParamStmt():
""" Test IPParamStmt initialization
"""
@@ -159,6 +173,7 @@ def test_IPParamStmt():
assert_equal(stmt.param, param)
assert_equal(stmt.ip, ip)
+
def test_IPParamStmt_dump():
""" Test IPParamStmt to_gerber()
"""
@@ -170,6 +185,7 @@ def test_IPParamStmt_dump():
ip = IPParamStmt.from_dict(stmt)
assert_equal(ip.to_gerber(), '%IPNEG*%')
+
def test_IPParamStmt_string():
stmt = {'param': 'IP', 'ip': 'POS'}
ip = IPParamStmt.from_dict(stmt)
@@ -179,22 +195,26 @@ def test_IPParamStmt_string():
ip = IPParamStmt.from_dict(stmt)
assert_equal(str(ip), '<Image Polarity: negative>')
+
def test_IRParamStmt_factory():
stmt = {'param': 'IR', 'angle': '45'}
ir = IRParamStmt.from_dict(stmt)
assert_equal(ir.param, 'IR')
assert_equal(ir.angle, 45)
+
def test_IRParamStmt_dump():
stmt = {'param': 'IR', 'angle': '45'}
ir = IRParamStmt.from_dict(stmt)
assert_equal(ir.to_gerber(), '%IR45*%')
+
def test_IRParamStmt_string():
stmt = {'param': 'IR', 'angle': '45'}
ir = IRParamStmt.from_dict(stmt)
assert_equal(str(ir), '<Image Angle: 45>')
+
def test_OFParamStmt_factory():
""" Test OFParamStmt factory
"""
@@ -203,6 +223,7 @@ def test_OFParamStmt_factory():
assert_equal(of.a, 0.1234567)
assert_equal(of.b, 0.1234567)
+
def test_OFParamStmt():
""" Test IPParamStmt initialization
"""
@@ -213,6 +234,7 @@ def test_OFParamStmt():
assert_equal(stmt.a, val)
assert_equal(stmt.b, val)
+
def test_OFParamStmt_dump():
""" Test OFParamStmt to_gerber()
"""
@@ -220,10 +242,11 @@ def test_OFParamStmt_dump():
of = OFParamStmt.from_dict(stmt)
assert_equal(of.to_gerber(), '%OFA0.12345B0.12345*%')
+
def test_OFParamStmt_conversion():
stmt = {'param': 'OF', 'a': '2.54', 'b': '25.4'}
of = OFParamStmt.from_dict(stmt)
- of.units='metric'
+ of.units = 'metric'
# No effect
of.to_metric()
@@ -235,7 +258,7 @@ def test_OFParamStmt_conversion():
assert_equal(of.a, 0.1)
assert_equal(of.b, 1.0)
- #No effect
+ # No effect
of.to_inch()
assert_equal(of.a, 0.1)
assert_equal(of.b, 1.0)
@@ -244,7 +267,7 @@ def test_OFParamStmt_conversion():
of = OFParamStmt.from_dict(stmt)
of.units = 'inch'
- #No effect
+ # No effect
of.to_inch()
assert_equal(of.a, 0.1)
assert_equal(of.b, 1.0)
@@ -254,11 +277,12 @@ def test_OFParamStmt_conversion():
assert_equal(of.a, 2.54)
assert_equal(of.b, 25.4)
- #No effect
+ # No effect
of.to_metric()
assert_equal(of.a, 2.54)
assert_equal(of.b, 25.4)
+
def test_OFParamStmt_offset():
s = OFParamStmt('OF', 0, 0)
s.offset(1, 0)
@@ -268,6 +292,7 @@ def test_OFParamStmt_offset():
assert_equal(s.a, 1.)
assert_equal(s.b, 1.)
+
def test_OFParamStmt_string():
""" Test OFParamStmt __str__
"""
@@ -275,6 +300,7 @@ def test_OFParamStmt_string():
of = OFParamStmt.from_dict(stmt)
assert_equal(str(of), '<Offset: X: 0.123456 Y: 0.123456 >')
+
def test_SFParamStmt_factory():
stmt = {'param': 'SF', 'a': '1.4', 'b': '0.9'}
sf = SFParamStmt.from_dict(stmt)
@@ -282,18 +308,20 @@ def test_SFParamStmt_factory():
assert_equal(sf.a, 1.4)
assert_equal(sf.b, 0.9)
+
def test_SFParamStmt_dump():
stmt = {'param': 'SF', 'a': '1.4', 'b': '0.9'}
sf = SFParamStmt.from_dict(stmt)
assert_equal(sf.to_gerber(), '%SFA1.4B0.9*%')
+
def test_SFParamStmt_conversion():
stmt = {'param': 'OF', 'a': '2.54', 'b': '25.4'}
of = SFParamStmt.from_dict(stmt)
of.units = 'metric'
of.to_metric()
- #No effect
+ # No effect
assert_equal(of.a, 2.54)
assert_equal(of.b, 25.4)
@@ -302,7 +330,7 @@ def test_SFParamStmt_conversion():
assert_equal(of.a, 0.1)
assert_equal(of.b, 1.0)
- #No effect
+ # No effect
of.to_inch()
assert_equal(of.a, 0.1)
assert_equal(of.b, 1.0)
@@ -311,7 +339,7 @@ def test_SFParamStmt_conversion():
of = SFParamStmt.from_dict(stmt)
of.units = 'inch'
- #No effect
+ # No effect
of.to_inch()
assert_equal(of.a, 0.1)
assert_equal(of.b, 1.0)
@@ -321,11 +349,12 @@ def test_SFParamStmt_conversion():
assert_equal(of.a, 2.54)
assert_equal(of.b, 25.4)
- #No effect
+ # No effect
of.to_metric()
assert_equal(of.a, 2.54)
assert_equal(of.b, 25.4)
+
def test_SFParamStmt_offset():
s = SFParamStmt('OF', 0, 0)
s.offset(1, 0)
@@ -335,11 +364,13 @@ def test_SFParamStmt_offset():
assert_equal(s.a, 1.)
assert_equal(s.b, 1.)
+
def test_SFParamStmt_string():
stmt = {'param': 'SF', 'a': '1.4', 'b': '0.9'}
sf = SFParamStmt.from_dict(stmt)
assert_equal(str(sf), '<Scale Factor: X: 1.4 Y: 0.9>')
+
def test_LPParamStmt_factory():
""" Test LPParamStmt factory
"""
@@ -351,6 +382,7 @@ def test_LPParamStmt_factory():
lp = LPParamStmt.from_dict(stmt)
assert_equal(lp.lp, 'dark')
+
def test_LPParamStmt_dump():
""" Test LPParamStmt to_gerber()
"""
@@ -362,6 +394,7 @@ def test_LPParamStmt_dump():
lp = LPParamStmt.from_dict(stmt)
assert_equal(lp.to_gerber(), '%LPD*%')
+
def test_LPParamStmt_string():
""" Test LPParamStmt.__str__()
"""
@@ -373,6 +406,7 @@ def test_LPParamStmt_string():
lp = LPParamStmt.from_dict(stmt)
assert_equal(str(lp), '<Level Polarity: clear>')
+
def test_AMParamStmt_factory():
name = 'DONUTVAR'
macro = (
@@ -387,7 +421,7 @@ def test_AMParamStmt_factory():
7,0,0,7,6,0.2,0*
8,THIS IS AN UNSUPPORTED PRIMITIVE*
''')
- s = AMParamStmt.from_dict({'param': 'AM', 'name': name, 'macro': macro })
+ s = AMParamStmt.from_dict({'param': 'AM', 'name': name, 'macro': macro})
s.build()
assert_equal(len(s.primitives), 10)
assert_true(isinstance(s.primitives[0], AMCommentPrimitive))
@@ -401,15 +435,16 @@ def test_AMParamStmt_factory():
assert_true(isinstance(s.primitives[8], AMThermalPrimitive))
assert_true(isinstance(s.primitives[9], AMUnsupportPrimitive))
+
def testAMParamStmt_conversion():
name = 'POLYGON'
macro = '5,1,8,25.4,25.4,25.4,0*'
- s = AMParamStmt.from_dict({'param': 'AM', 'name': name, 'macro': macro })
+ s = AMParamStmt.from_dict({'param': 'AM', 'name': name, 'macro': macro})
s.build()
s.units = 'metric'
- #No effect
+ # No effect
s.to_metric()
assert_equal(s.primitives[0].position, (25.4, 25.4))
assert_equal(s.primitives[0].diameter, 25.4)
@@ -419,17 +454,17 @@ def testAMParamStmt_conversion():
assert_equal(s.primitives[0].position, (1., 1.))
assert_equal(s.primitives[0].diameter, 1.)
- #No effect
+ # No effect
s.to_inch()
assert_equal(s.primitives[0].position, (1., 1.))
assert_equal(s.primitives[0].diameter, 1.)
macro = '5,1,8,1,1,1,0*'
- s = AMParamStmt.from_dict({'param': 'AM', 'name': name, 'macro': macro })
+ s = AMParamStmt.from_dict({'param': 'AM', 'name': name, 'macro': macro})
s.build()
s.units = 'inch'
- #No effect
+ # No effect
s.to_inch()
assert_equal(s.primitives[0].position, (1., 1.))
assert_equal(s.primitives[0].diameter, 1.)
@@ -439,15 +474,16 @@ def testAMParamStmt_conversion():
assert_equal(s.primitives[0].position, (25.4, 25.4))
assert_equal(s.primitives[0].diameter, 25.4)
- #No effect
+ # No effect
s.to_metric()
assert_equal(s.primitives[0].position, (25.4, 25.4))
assert_equal(s.primitives[0].diameter, 25.4)
+
def test_AMParamStmt_dump():
name = 'POLYGON'
macro = '5,1,8,25.4,25.4,25.4,0.0'
- s = AMParamStmt.from_dict({'param': 'AM', 'name': name, 'macro': macro })
+ s = AMParamStmt.from_dict({'param': 'AM', 'name': name, 'macro': macro})
s.build()
assert_equal(s.to_gerber(), '%AMPOLYGON*5,1,8,25.4,25.4,25.4,0.0*%')
@@ -455,29 +491,34 @@ def test_AMParamStmt_dump():
s.build()
assert_equal(s.to_gerber(), '%AMOC8*5,1,8,0,0,1.08239X$1,22.5*%')
+
def test_AMParamStmt_string():
name = 'POLYGON'
macro = '5,1,8,25.4,25.4,25.4,0*'
- s = AMParamStmt.from_dict({'param': 'AM', 'name': name, 'macro': macro })
+ s = AMParamStmt.from_dict({'param': 'AM', 'name': name, 'macro': macro})
s.build()
assert_equal(str(s), '<Aperture Macro POLYGON: 5,1,8,25.4,25.4,25.4,0*>')
+
def test_ASParamStmt_factory():
stmt = {'param': 'AS', 'mode': 'AXBY'}
s = ASParamStmt.from_dict(stmt)
assert_equal(s.param, 'AS')
assert_equal(s.mode, 'AXBY')
+
def test_ASParamStmt_dump():
stmt = {'param': 'AS', 'mode': 'AXBY'}
s = ASParamStmt.from_dict(stmt)
assert_equal(s.to_gerber(), '%ASAXBY*%')
+
def test_ASParamStmt_string():
stmt = {'param': 'AS', 'mode': 'AXBY'}
s = ASParamStmt.from_dict(stmt)
assert_equal(str(s), '<Axis Select: AXBY>')
+
def test_INParamStmt_factory():
""" Test INParamStmt factory
"""
@@ -485,6 +526,7 @@ def test_INParamStmt_factory():
inp = INParamStmt.from_dict(stmt)
assert_equal(inp.name, 'test')
+
def test_INParamStmt_dump():
""" Test INParamStmt to_gerber()
"""
@@ -492,11 +534,13 @@ def test_INParamStmt_dump():
inp = INParamStmt.from_dict(stmt)
assert_equal(inp.to_gerber(), '%INtest*%')
+
def test_INParamStmt_string():
stmt = {'param': 'IN', 'name': 'test'}
inp = INParamStmt.from_dict(stmt)
assert_equal(str(inp), '<Image Name: test>')
+
def test_LNParamStmt_factory():
""" Test LNParamStmt factory
"""
@@ -504,6 +548,7 @@ def test_LNParamStmt_factory():
lnp = LNParamStmt.from_dict(stmt)
assert_equal(lnp.name, 'test')
+
def test_LNParamStmt_dump():
""" Test LNParamStmt to_gerber()
"""
@@ -511,11 +556,13 @@ def test_LNParamStmt_dump():
lnp = LNParamStmt.from_dict(stmt)
assert_equal(lnp.to_gerber(), '%LNtest*%')
+
def test_LNParamStmt_string():
stmt = {'param': 'LN', 'name': 'test'}
lnp = LNParamStmt.from_dict(stmt)
assert_equal(str(lnp), '<Level Name: test>')
+
def test_comment_stmt():
""" Test comment statement
"""
@@ -523,31 +570,37 @@ def test_comment_stmt():
assert_equal(stmt.type, 'COMMENT')
assert_equal(stmt.comment, 'A comment')
+
def test_comment_stmt_dump():
""" Test CommentStmt to_gerber()
"""
stmt = CommentStmt('A comment')
assert_equal(stmt.to_gerber(), 'G04A comment*')
+
def test_comment_stmt_string():
stmt = CommentStmt('A comment')
assert_equal(str(stmt), '<Comment: A comment>')
+
def test_eofstmt():
""" Test EofStmt
"""
stmt = EofStmt()
assert_equal(stmt.type, 'EOF')
+
def test_eofstmt_dump():
""" Test EofStmt to_gerber()
"""
stmt = EofStmt()
assert_equal(stmt.to_gerber(), 'M02*')
+
def test_eofstmt_string():
assert_equal(str(EofStmt()), '<EOF Statement>')
+
def test_quadmodestmt_factory():
""" Test QuadrantModeStmt.from_gerber()
"""
@@ -560,6 +613,7 @@ def test_quadmodestmt_factory():
stmt = QuadrantModeStmt.from_gerber(line)
assert_equal(stmt.mode, 'multi-quadrant')
+
def test_quadmodestmt_validation():
""" Test QuadrantModeStmt input validation
"""
@@ -567,6 +621,7 @@ def test_quadmodestmt_validation():
assert_raises(ValueError, QuadrantModeStmt.from_gerber, line)
assert_raises(ValueError, QuadrantModeStmt, 'quadrant-ful')
+
def test_quadmodestmt_dump():
""" Test QuadrantModeStmt.to_gerber()
"""
@@ -574,6 +629,7 @@ def test_quadmodestmt_dump():
stmt = QuadrantModeStmt.from_gerber(line)
assert_equal(stmt.to_gerber(), line)
+
def test_regionmodestmt_factory():
""" Test RegionModeStmt.from_gerber()
"""
@@ -586,6 +642,7 @@ def test_regionmodestmt_factory():
stmt = RegionModeStmt.from_gerber(line)
assert_equal(stmt.mode, 'off')
+
def test_regionmodestmt_validation():
""" Test RegionModeStmt input validation
"""
@@ -593,6 +650,7 @@ def test_regionmodestmt_validation():
assert_raises(ValueError, RegionModeStmt.from_gerber, line)
assert_raises(ValueError, RegionModeStmt, 'off-ish')
+
def test_regionmodestmt_dump():
""" Test RegionModeStmt.to_gerber()
"""
@@ -600,6 +658,7 @@ def test_regionmodestmt_dump():
stmt = RegionModeStmt.from_gerber(line)
assert_equal(stmt.to_gerber(), line)
+
def test_unknownstmt():
""" Test UnknownStmt
"""
@@ -608,6 +667,7 @@ def test_unknownstmt():
assert_equal(stmt.type, 'UNKNOWN')
assert_equal(stmt.line, line)
+
def test_unknownstmt_dump():
""" Test UnknownStmt.to_gerber()
"""
@@ -616,15 +676,17 @@ def test_unknownstmt_dump():
stmt = UnknownStmt(line)
assert_equal(stmt.to_gerber(), line)
+
def test_statement_string():
""" Test Statement.__str__()
"""
stmt = Statement('PARAM')
assert_in('type=PARAM', str(stmt))
- stmt.test='PASS'
+ stmt.test = 'PASS'
assert_in('test=PASS', str(stmt))
assert_in('type=PARAM', str(stmt))
+
def test_ADParamStmt_factory():
""" Test ADParamStmt factory
"""
@@ -656,12 +718,14 @@ def test_ADParamStmt_factory():
assert_equal(ad.shape, 'R')
assert_equal(ad.modifiers, [(1.42, 1.24)])
+
def test_ADParamStmt_conversion():
- stmt = {'param': 'AD', 'd': 0, 'shape': 'C', 'modifiers': '25.4X25.4,25.4X25.4'}
+ stmt = {'param': 'AD', 'd': 0, 'shape': 'C',
+ 'modifiers': '25.4X25.4,25.4X25.4'}
ad = ADParamStmt.from_dict(stmt)
ad.units = 'metric'
- #No effect
+ # No effect
ad.to_metric()
assert_equal(ad.modifiers[0], (25.4, 25.4))
assert_equal(ad.modifiers[1], (25.4, 25.4))
@@ -671,7 +735,7 @@ def test_ADParamStmt_conversion():
assert_equal(ad.modifiers[0], (1., 1.))
assert_equal(ad.modifiers[1], (1., 1.))
- #No effect
+ # No effect
ad.to_inch()
assert_equal(ad.modifiers[0], (1., 1.))
assert_equal(ad.modifiers[1], (1., 1.))
@@ -680,7 +744,7 @@ def test_ADParamStmt_conversion():
ad = ADParamStmt.from_dict(stmt)
ad.units = 'inch'
- #No effect
+ # No effect
ad.to_inch()
assert_equal(ad.modifiers[0], (1., 1.))
assert_equal(ad.modifiers[1], (1., 1.))
@@ -689,11 +753,12 @@ def test_ADParamStmt_conversion():
assert_equal(ad.modifiers[0], (25.4, 25.4))
assert_equal(ad.modifiers[1], (25.4, 25.4))
- #No effect
+ # No effect
ad.to_metric()
assert_equal(ad.modifiers[0], (25.4, 25.4))
assert_equal(ad.modifiers[1], (25.4, 25.4))
+
def test_ADParamStmt_dump():
stmt = {'param': 'AD', 'd': 0, 'shape': 'C'}
ad = ADParamStmt.from_dict(stmt)
@@ -702,6 +767,7 @@ def test_ADParamStmt_dump():
ad = ADParamStmt.from_dict(stmt)
assert_equal(ad.to_gerber(), '%ADD0C,1X1,1X1*%')
+
def test_ADPamramStmt_string():
stmt = {'param': 'AD', 'd': 0, 'shape': 'C'}
ad = ADParamStmt.from_dict(stmt)
@@ -719,12 +785,14 @@ def test_ADPamramStmt_string():
ad = ADParamStmt.from_dict(stmt)
assert_equal(str(ad), '<Aperture Definition: 0: test>')
+
def test_MIParamStmt_factory():
stmt = {'param': 'MI', 'a': 1, 'b': 1}
mi = MIParamStmt.from_dict(stmt)
assert_equal(mi.a, 1)
assert_equal(mi.b, 1)
+
def test_MIParamStmt_dump():
stmt = {'param': 'MI', 'a': 1, 'b': 1}
mi = MIParamStmt.from_dict(stmt)
@@ -736,6 +804,7 @@ def test_MIParamStmt_dump():
mi = MIParamStmt.from_dict(stmt)
assert_equal(mi.to_gerber(), '%MIA0B1*%')
+
def test_MIParamStmt_string():
stmt = {'param': 'MI', 'a': 1, 'b': 1}
mi = MIParamStmt.from_dict(stmt)
@@ -749,6 +818,7 @@ def test_MIParamStmt_string():
mi = MIParamStmt.from_dict(stmt)
assert_equal(str(mi), '<Image Mirror: A=1 B=0>')
+
def test_coordstmt_ctor():
cs = CoordStmt('G04', 0.0, 0.1, 0.2, 0.3, 'D01', FileSettings())
assert_equal(cs.function, 'G04')
@@ -758,8 +828,10 @@ def test_coordstmt_ctor():
assert_equal(cs.j, 0.3)
assert_equal(cs.op, 'D01')
+
def test_coordstmt_factory():
- stmt = {'function': 'G04', 'x': '0', 'y': '001', 'i': '002', 'j': '003', 'op': 'D01'}
+ stmt = {'function': 'G04', 'x': '0', 'y': '001',
+ 'i': '002', 'j': '003', 'op': 'D01'}
cs = CoordStmt.from_dict(stmt, FileSettings())
assert_equal(cs.function, 'G04')
assert_equal(cs.x, 0.0)
@@ -768,15 +840,17 @@ def test_coordstmt_factory():
assert_equal(cs.j, 0.3)
assert_equal(cs.op, 'D01')
+
def test_coordstmt_dump():
cs = CoordStmt('G04', 0.0, 0.1, 0.2, 0.3, 'D01', FileSettings())
assert_equal(cs.to_gerber(FileSettings()), 'G04X0Y001I002J003D01*')
+
def test_coordstmt_conversion():
cs = CoordStmt('G71', 25.4, 25.4, 25.4, 25.4, 'D01', FileSettings())
cs.units = 'metric'
- #No effect
+ # No effect
cs.to_metric()
assert_equal(cs.x, 25.4)
assert_equal(cs.y, 25.4)
@@ -792,7 +866,7 @@ def test_coordstmt_conversion():
assert_equal(cs.j, 1.)
assert_equal(cs.function, 'G70')
- #No effect
+ # No effect
cs.to_inch()
assert_equal(cs.x, 1.)
assert_equal(cs.y, 1.)
@@ -803,7 +877,7 @@ def test_coordstmt_conversion():
cs = CoordStmt('G70', 1., 1., 1., 1., 'D01', FileSettings())
cs.units = 'inch'
- #No effect
+ # No effect
cs.to_inch()
assert_equal(cs.x, 1.)
assert_equal(cs.y, 1.)
@@ -818,7 +892,7 @@ def test_coordstmt_conversion():
assert_equal(cs.j, 25.4)
assert_equal(cs.function, 'G71')
- #No effect
+ # No effect
cs.to_metric()
assert_equal(cs.x, 25.4)
assert_equal(cs.y, 25.4)
@@ -826,6 +900,7 @@ def test_coordstmt_conversion():
assert_equal(cs.j, 25.4)
assert_equal(cs.function, 'G71')
+
def test_coordstmt_offset():
c = CoordStmt('G71', 0, 0, 0, 0, 'D01', FileSettings())
c.offset(1, 0)
@@ -839,9 +914,11 @@ def test_coordstmt_offset():
assert_equal(c.i, 1.)
assert_equal(c.j, 1.)
+
def test_coordstmt_string():
cs = CoordStmt('G04', 0, 1, 2, 3, 'D01', FileSettings())
- assert_equal(str(cs), '<Coordinate Statement: Fn: G04 X: 0 Y: 1 I: 2 J: 3 Op: Lights On>')
+ assert_equal(str(cs),
+ '<Coordinate Statement: Fn: G04 X: 0 Y: 1 I: 2 J: 3 Op: Lights On>')
cs = CoordStmt('G04', None, None, None, None, 'D02', FileSettings())
assert_equal(str(cs), '<Coordinate Statement: Fn: G04 Op: Lights Off>')
cs = CoordStmt('G04', None, None, None, None, 'D03', FileSettings())
@@ -849,6 +926,7 @@ def test_coordstmt_string():
cs = CoordStmt('G04', None, None, None, None, 'TEST', FileSettings())
assert_equal(str(cs), '<Coordinate Statement: Fn: G04 Op: TEST>')
+
def test_aperturestmt_ctor():
ast = ApertureStmt(3, False)
assert_equal(ast.d, 3)
@@ -863,11 +941,10 @@ def test_aperturestmt_ctor():
assert_equal(ast.d, 3)
assert_equal(ast.deprecated, False)
+
def test_aperturestmt_dump():
ast = ApertureStmt(3, False)
assert_equal(ast.to_gerber(), 'D3*')
ast = ApertureStmt(3, True)
assert_equal(ast.to_gerber(), 'G54D3*')
assert_equal(str(ast), '<Aperture: 3>')
-
-
diff --git a/gerber/tests/test_ipc356.py b/gerber/tests/test_ipc356.py
index f123a38..45bb01b 100644
--- a/gerber/tests/test_ipc356.py
+++ b/gerber/tests/test_ipc356.py
@@ -2,18 +2,21 @@
# -*- coding: utf-8 -*-
# Author: Hamilton Kibbe <ham@hamiltonkib.be>
-from ..ipc356 import *
+from ..ipc356 import *
from ..cam import FileSettings
from .tests import *
import os
IPC_D_356_FILE = os.path.join(os.path.dirname(__file__),
- 'resources/ipc-d-356.ipc')
+ 'resources/ipc-d-356.ipc')
+
+
def test_read():
ipcfile = read(IPC_D_356_FILE)
assert(isinstance(ipcfile, IPC_D_356))
+
def test_parser():
ipcfile = read(IPC_D_356_FILE)
assert_equal(ipcfile.settings.units, 'inch')
@@ -28,6 +31,7 @@ def test_parser():
assert_equal(set(ipcfile.outlines[0].points),
{(0., 0.), (2.25, 0.), (2.25, 1.5), (0., 1.5), (0.13, 0.024)})
+
def test_comment():
c = IPC356_Comment('Layer Stackup:')
assert_equal(c.comment, 'Layer Stackup:')
@@ -36,6 +40,7 @@ def test_comment():
assert_raises(ValueError, IPC356_Comment.from_line, 'P JOB')
assert_equal(str(c), '<IPC-D-356 Comment: Layer Stackup:>')
+
def test_parameter():
p = IPC356_Parameter('VER', 'IPC-D-356A')
assert_equal(p.parameter, 'VER')
@@ -43,27 +48,32 @@ def test_parameter():
p = IPC356_Parameter.from_line('P VER IPC-D-356A ')
assert_equal(p.parameter, 'VER')
assert_equal(p.value, 'IPC-D-356A')
- assert_raises(ValueError, IPC356_Parameter.from_line, 'C Layer Stackup: ')
+ assert_raises(ValueError, IPC356_Parameter.from_line,
+ 'C Layer Stackup: ')
assert_equal(str(p), '<IPC-D-356 Parameter: VER=IPC-D-356A>')
+
def test_eof():
e = IPC356_EndOfFile()
assert_equal(e.to_netlist(), '999')
assert_equal(str(e), '<IPC-D-356 EOF>')
+
def test_outline():
type = 'BOARD_EDGE'
points = [(0.01, 0.01), (2., 2.), (4., 2.), (4., 6.)]
b = IPC356_Outline(type, points)
assert_equal(b.type, type)
assert_equal(b.points, points)
- b = IPC356_Outline.from_line('389BOARD_EDGE X100Y100 X20000Y20000'
- ' X40000 Y60000', FileSettings(units='inch'))
+ b = IPC356_Outline.from_line('389BOARD_EDGE X100Y100 X20000Y20000 X40000 Y60000',
+ FileSettings(units='inch'))
assert_equal(b.type, 'BOARD_EDGE')
assert_equal(b.points, points)
+
def test_test_record():
- assert_raises(ValueError, IPC356_TestRecord.from_line, 'P JOB', FileSettings())
+ assert_raises(ValueError, IPC356_TestRecord.from_line,
+ 'P JOB', FileSettings())
record_string = '317+5VDC VIA - D0150PA00X 006647Y 012900X0000 S3'
r = IPC356_TestRecord.from_line(record_string, FileSettings(units='inch'))
assert_equal(r.feature_type, 'through-hole')
@@ -81,8 +91,7 @@ def test_test_record():
assert_almost_equal(r.x_coord, 6.647)
assert_almost_equal(r.y_coord, 12.9)
assert_equal(r.rect_x, 0.)
- assert_equal(str(r),
- '<IPC-D-356 +5VDC Test Record: through-hole>')
+ assert_equal(str(r), '<IPC-D-356 +5VDC Test Record: through-hole>')
record_string = '327+3.3VDC R40 -1 PA01X 032100Y 007124X0236Y0315R180 S0'
r = IPC356_TestRecord.from_line(record_string, FileSettings(units='inch'))
@@ -98,13 +107,13 @@ def test_test_record():
assert_almost_equal(r.rect_y, 0.0315)
assert_equal(r.rect_rotation, 180)
assert_equal(r.soldermask_info, 'none')
- r = IPC356_TestRecord.from_line(record_string, FileSettings(units='metric'))
+ r = IPC356_TestRecord.from_line(
+ record_string, FileSettings(units='metric'))
assert_almost_equal(r.x_coord, 32.1)
assert_almost_equal(r.y_coord, 7.124)
assert_almost_equal(r.rect_x, 0.236)
assert_almost_equal(r.rect_y, 0.315)
-
record_string = '317 J4 -M2 D0330PA00X 012447Y 008030X0000 S1'
r = IPC356_TestRecord.from_line(record_string, FileSettings(units='inch'))
assert_equal(r.feature_type, 'through-hole')
diff --git a/gerber/tests/test_layers.py b/gerber/tests/test_layers.py
index c77084d..3f2bcfc 100644
--- a/gerber/tests/test_layers.py
+++ b/gerber/tests/test_layers.py
@@ -15,7 +15,7 @@ def test_guess_layer_class():
test_vectors = [(None, 'unknown'), ('NCDRILL.TXT', 'unknown'),
('example_board.gtl', 'top'),
('exampmle_board.sst', 'topsilk'),
- ('ipc-d-356.ipc', 'ipc_netlist'),]
+ ('ipc-d-356.ipc', 'ipc_netlist'), ]
for hint in hints:
for ext in hint.ext:
diff --git a/gerber/tests/test_primitives.py b/gerber/tests/test_primitives.py
index 61cf22d..261e6ef 100644
--- a/gerber/tests/test_primitives.py
+++ b/gerber/tests/test_primitives.py
@@ -2,18 +2,23 @@
# -*- coding: utf-8 -*-
# Author: Hamilton Kibbe <ham@hamiltonkib.be>
+from operator import add
+
from ..primitives import *
from .tests import *
-from operator import add
def test_primitive_smoketest():
p = Primitive()
+<<<<<<< HEAD
try:
p.bounding_box
assert_false(True, 'should have thrown the exception')
except NotImplementedError:
pass
+=======
+ #assert_raises(NotImplementedError, p.bounding_box)
+>>>>>>> 5476da8... Fix a bunch of rendering bugs.
p.to_metric()
p.to_inch()
try:
@@ -22,6 +27,7 @@ def test_primitive_smoketest():
except NotImplementedError:
pass
+
def test_line_angle():
""" Test Line primitive angle calculation
"""
@@ -32,19 +38,20 @@ def test_line_angle():
((0, 0), (-1, 0), math.radians(180)),
((0, 0), (-1, -1), math.radians(225)),
((0, 0), (0, -1), math.radians(270)),
- ((0, 0), (1, -1), math.radians(315)),]
+ ((0, 0), (1, -1), math.radians(315)), ]
for start, end, expected in cases:
l = Line(start, end, 0)
line_angle = (l.angle + 2 * math.pi) % (2 * math.pi)
assert_almost_equal(line_angle, expected)
+
def test_line_bounds():
""" Test Line primitive bounding box calculation
"""
cases = [((0, 0), (1, 1), ((-1, 2), (-1, 2))),
((-1, -1), (1, 1), ((-2, 2), (-2, 2))),
((1, 1), (-1, -1), ((-2, 2), (-2, 2))),
- ((-1, 1), (1, -1), ((-2, 2), (-2, 2))),]
+ ((-1, 1), (1, -1), ((-2, 2), (-2, 2))), ]
c = Circle((0, 0), 2)
r = Rectangle((0, 0), 2, 2)
@@ -57,11 +64,12 @@ def test_line_bounds():
cases = [((0, 0), (1, 1), ((-1.5, 2.5), (-1, 2))),
((-1, -1), (1, 1), ((-2.5, 2.5), (-2, 2))),
((1, 1), (-1, -1), ((-2.5, 2.5), (-2, 2))),
- ((-1, 1), (1, -1), ((-2.5, 2.5), (-2, 2))),]
+ ((-1, 1), (1, -1), ((-2.5, 2.5), (-2, 2))), ]
for start, end, expected in cases:
l = Line(start, end, r)
assert_equal(l.bounding_box, expected)
+
def test_line_vertices():
c = Circle((0, 0), 2)
l = Line((0, 0), (1, 1), c)
@@ -69,20 +77,25 @@ def test_line_vertices():
# All 4 compass points, all 4 quadrants and the case where start == end
test_cases = [((0, 0), (1, 0), ((-1, -1), (-1, 1), (2, 1), (2, -1))),
- ((0, 0), (1, 1), ((-1, -1), (-1, 1), (0, 2), (2, 2), (2, 0), (1,-1))),
- ((0, 0), (0, 1), ((-1, -1), (-1, 2), (1, 2), (1, -1))),
- ((0, 0), (-1, 1), ((-1, -1), (-2, 0), (-2, 2), (0, 2), (1, 1), (1, -1))),
- ((0, 0), (-1, 0), ((-2, -1), (-2, 1), (1, 1), (1, -1))),
- ((0, 0), (-1, -1), ((-2, -2), (1, -1), (1, 1), (-1, 1), (-2, 0), (0,-2))),
- ((0, 0), (0, -1), ((-1, -2), (-1, 1), (1, 1), (1, -2))),
- ((0, 0), (1, -1), ((-1, -1), (0, -2), (2, -2), (2, 0), (1, 1), (-1, 1))),
- ((0, 0), (0, 0), ((-1, -1), (-1, 1), (1, 1), (1, -1))),]
+ ((0, 0), (1, 1), ((-1, -1), (-1, 1),
+ (0, 2), (2, 2), (2, 0), (1, -1))),
+ ((0, 0), (0, 1), ((-1, -1), (-1, 2), (1, 2), (1, -1))),
+ ((0, 0), (-1, 1), ((-1, -1), (-2, 0),
+ (-2, 2), (0, 2), (1, 1), (1, -1))),
+ ((0, 0), (-1, 0), ((-2, -1), (-2, 1), (1, 1), (1, -1))),
+ ((0, 0), (-1, -1), ((-2, -2), (1, -1),
+ (1, 1), (-1, 1), (-2, 0), (0, -2))),
+ ((0, 0), (0, -1), ((-1, -2), (-1, 1), (1, 1), (1, -2))),
+ ((0, 0), (1, -1), ((-1, -1), (0, -2),
+ (2, -2), (2, 0), (1, 1), (-1, 1))),
+ ((0, 0), (0, 0), ((-1, -1), (-1, 1), (1, 1), (1, -1))), ]
r = Rectangle((0, 0), 2, 2)
for start, end, vertices in test_cases:
l = Line(start, end, r)
assert_equal(set(vertices), set(l.vertices))
+
def test_line_conversion():
c = Circle((0, 0), 25.4, units='metric')
l = Line((2.54, 25.4), (254.0, 2540.0), c, units='metric')
@@ -113,13 +126,12 @@ def test_line_conversion():
assert_equal(l.end, (10.0, 100.0))
assert_equal(l.aperture.diameter, 1.0)
-
l.to_metric()
assert_equal(l.start, (2.54, 25.4))
assert_equal(l.end, (254.0, 2540.0))
assert_equal(l.aperture.diameter, 25.4)
- #No effect
+ # No effect
l.to_metric()
assert_equal(l.start, (2.54, 25.4))
assert_equal(l.end, (254.0, 2540.0))
@@ -141,56 +153,62 @@ def test_line_conversion():
assert_equal(l.aperture.width, 25.4)
assert_equal(l.aperture.height, 254.0)
+
def test_line_offset():
c = Circle((0, 0), 1)
l = Line((0, 0), (1, 1), c)
l.offset(1, 0)
- assert_equal(l.start,(1., 0.))
+ assert_equal(l.start, (1., 0.))
assert_equal(l.end, (2., 1.))
l.offset(0, 1)
- assert_equal(l.start,(1., 1.))
+ assert_equal(l.start, (1., 1.))
assert_equal(l.end, (2., 2.))
+
def test_arc_radius():
""" Test Arc primitive radius calculation
"""
cases = [((-3, 4), (5, 0), (0, 0), 5),
- ((0, 1), (1, 0), (0, 0), 1),]
+ ((0, 1), (1, 0), (0, 0), 1), ]
for start, end, center, radius in cases:
a = Arc(start, end, center, 'clockwise', 0, 'single-quadrant')
assert_equal(a.radius, radius)
+
def test_arc_sweep_angle():
""" Test Arc primitive sweep angle calculation
"""
cases = [((1, 0), (0, 1), (0, 0), 'counterclockwise', math.radians(90)),
((1, 0), (0, 1), (0, 0), 'clockwise', math.radians(270)),
((1, 0), (-1, 0), (0, 0), 'clockwise', math.radians(180)),
- ((1, 0), (-1, 0), (0, 0), 'counterclockwise', math.radians(180)),]
+ ((1, 0), (-1, 0), (0, 0), 'counterclockwise', math.radians(180)), ]
for start, end, center, direction, sweep in cases:
c = Circle((0,0), 1)
a = Arc(start, end, center, direction, c, 'single-quadrant')
assert_equal(a.sweep_angle, sweep)
+
def test_arc_bounds():
""" Test Arc primitive bounding box calculation
"""
- cases = [((1, 0), (0, 1), (0, 0), 'clockwise', ((-1.5, 1.5), (-1.5, 1.5))),
- ((1, 0), (0, 1), (0, 0), 'counterclockwise', ((-0.5, 1.5), (-0.5, 1.5))),
- #TODO: ADD MORE TEST CASES HERE
+ cases = [((1, 0), (0, 1), (0, 0), 'clockwise', ((-1.5, 1.5), (-1.5, 1.5))),
+ ((1, 0), (0, 1), (0, 0), 'counterclockwise',
+ ((-0.5, 1.5), (-0.5, 1.5))),
+ # TODO: ADD MORE TEST CASES HERE
]
for start, end, center, direction, bounds in cases:
c = Circle((0,0), 1)
a = Arc(start, end, center, direction, c, 'single-quadrant')
assert_equal(a.bounding_box, bounds)
+
def test_arc_conversion():
c = Circle((0, 0), 25.4, units='metric')
a = Arc((2.54, 25.4), (254.0, 2540.0), (25400.0, 254000.0),'clockwise', c, 'single-quadrant', units='metric')
- #No effect
+ # No effect
a.to_metric()
assert_equal(a.start, (2.54, 25.4))
assert_equal(a.end, (254.0, 2540.0))
@@ -203,7 +221,7 @@ def test_arc_conversion():
assert_equal(a.center, (1000.0, 10000.0))
assert_equal(a.aperture.diameter, 1.0)
- #no effect
+ # no effect
a.to_inch()
assert_equal(a.start, (0.1, 1.0))
assert_equal(a.end, (10.0, 100.0))
@@ -218,18 +236,20 @@ def test_arc_conversion():
assert_equal(a.center, (25400.0, 254000.0))
assert_equal(a.aperture.diameter, 25.4)
+
def test_arc_offset():
c = Circle((0, 0), 1)
a = Arc((0, 0), (1, 1), (2, 2), 'clockwise', c, 'single-quadrant')
a.offset(1, 0)
- assert_equal(a.start,(1., 0.))
+ assert_equal(a.start, (1., 0.))
assert_equal(a.end, (2., 1.))
assert_equal(a.center, (3., 2.))
a.offset(0, 1)
- assert_equal(a.start,(1., 1.))
+ assert_equal(a.start, (1., 1.))
assert_equal(a.end, (2., 2.))
assert_equal(a.center, (3., 3.))
+
def test_circle_radius():
""" Test Circle primitive radius calculation
"""
@@ -248,12 +268,13 @@ def test_circle_bounds():
c = Circle((1, 1), 2)
assert_equal(c.bounding_box, ((0, 2), (0, 2)))
+
def test_circle_conversion():
"""Circle conversion of units"""
# Circle initially metric, no hole
c = Circle((2.54, 25.4), 254.0, units='metric')
- c.to_metric() #shouldn't do antyhing
+ c.to_metric() # shouldn't do antyhing
assert_equal(c.position, (2.54, 25.4))
assert_equal(c.diameter, 254.)
assert_equal(c.hole_diameter, None)
@@ -263,7 +284,7 @@ def test_circle_conversion():
assert_equal(c.diameter, 10.)
assert_equal(c.hole_diameter, None)
- #no effect
+ # no effect
c.to_inch()
assert_equal(c.position, (0.1, 1.))
assert_equal(c.diameter, 10.)
@@ -290,7 +311,7 @@ def test_circle_conversion():
# Circle initially inch, no hole
c = Circle((0.1, 1.0), 10.0, units='inch')
- #No effect
+ # No effect
c.to_inch()
assert_equal(c.position, (0.1, 1.))
assert_equal(c.diameter, 10.)
@@ -301,7 +322,7 @@ def test_circle_conversion():
assert_equal(c.diameter, 254.)
assert_equal(c.hole_diameter, None)
- #no effect
+ # no effect
c.to_metric()
assert_equal(c.position, (2.54, 25.4))
assert_equal(c.diameter, 254.)
@@ -325,12 +346,14 @@ def test_circle_conversion():
assert_equal(c.diameter, 254.)
assert_equal(c.hole_diameter, 127.)
+
def test_circle_offset():
c = Circle((0, 0), 1)
c.offset(1, 0)
- assert_equal(c.position,(1., 0.))
+ assert_equal(c.position, (1., 0.))
c.offset(0, 1)
- assert_equal(c.position,(1., 1.))
+ assert_equal(c.position, (1., 1.))
+
def test_ellipse_ctor():
""" Test ellipse creation
@@ -340,6 +363,7 @@ def test_ellipse_ctor():
assert_equal(e.width, 3)
assert_equal(e.height, 2)
+
def test_ellipse_bounds():
""" Test ellipse bounding box calculation
"""
@@ -352,10 +376,11 @@ def test_ellipse_bounds():
e = Ellipse((2, 2), 4, 2, rotation=270)
assert_equal(e.bounding_box, ((1, 3), (0, 4)))
+
def test_ellipse_conversion():
e = Ellipse((2.54, 25.4), 254.0, 2540., units='metric')
- #No effect
+ # No effect
e.to_metric()
assert_equal(e.position, (2.54, 25.4))
assert_equal(e.width, 254.)
@@ -366,7 +391,7 @@ def test_ellipse_conversion():
assert_equal(e.width, 10.)
assert_equal(e.height, 100.)
- #No effect
+ # No effect
e.to_inch()
assert_equal(e.position, (0.1, 1.))
assert_equal(e.width, 10.)
@@ -374,7 +399,7 @@ def test_ellipse_conversion():
e = Ellipse((0.1, 1.), 10.0, 100., units='inch')
- #no effect
+ # no effect
e.to_inch()
assert_equal(e.position, (0.1, 1.))
assert_equal(e.width, 10.)
@@ -391,17 +416,19 @@ def test_ellipse_conversion():
assert_equal(e.width, 254.)
assert_equal(e.height, 2540.)
+
def test_ellipse_offset():
e = Ellipse((0, 0), 1, 2)
e.offset(1, 0)
- assert_equal(e.position,(1., 0.))
+ assert_equal(e.position, (1., 0.))
e.offset(0, 1)
- assert_equal(e.position,(1., 1.))
+ assert_equal(e.position, (1., 1.))
+
def test_rectangle_ctor():
""" Test rectangle creation
"""
- test_cases = (((0,0), 1, 1), ((0, 0), 1, 2), ((1,1), 1, 2))
+ test_cases = (((0, 0), 1, 1), ((0, 0), 1, 2), ((1, 1), 1, 2))
for pos, width, height in test_cases:
r = Rectangle(pos, width, height)
assert_equal(r.position, pos)
@@ -417,18 +444,20 @@ def test_rectangle_hole_radius():
r = Rectangle((0,0), 2, 2, 1)
assert_equal(0.5, r.hole_radius)
+
def test_rectangle_bounds():
""" Test rectangle bounding box calculation
"""
- r = Rectangle((0,0), 2, 2)
+ r = Rectangle((0, 0), 2, 2)
xbounds, ybounds = r.bounding_box
assert_array_almost_equal(xbounds, (-1, 1))
assert_array_almost_equal(ybounds, (-1, 1))
- r = Rectangle((0,0), 2, 2, rotation=45)
+ r = Rectangle((0, 0), 2, 2, rotation=45)
xbounds, ybounds = r.bounding_box
assert_array_almost_equal(xbounds, (-math.sqrt(2), math.sqrt(2)))
assert_array_almost_equal(ybounds, (-math.sqrt(2), math.sqrt(2)))
+
def test_rectangle_conversion():
"""Test converting rectangles between units"""
@@ -436,7 +465,7 @@ def test_rectangle_conversion():
r = Rectangle((2.54, 25.4), 254.0, 2540.0, units='metric')
r.to_metric()
- assert_equal(r.position, (2.54,25.4))
+ assert_equal(r.position, (2.54, 25.4))
assert_equal(r.width, 254.0)
assert_equal(r.height, 2540.0)
@@ -479,12 +508,12 @@ def test_rectangle_conversion():
assert_equal(r.height, 100.0)
r.to_metric()
- assert_equal(r.position, (2.54,25.4))
+ assert_equal(r.position, (2.54, 25.4))
assert_equal(r.width, 254.0)
assert_equal(r.height, 2540.0)
r.to_metric()
- assert_equal(r.position, (2.54,25.4))
+ assert_equal(r.position, (2.54, 25.4))
assert_equal(r.width, 254.0)
assert_equal(r.height, 2540.0)
@@ -508,35 +537,39 @@ def test_rectangle_conversion():
assert_equal(r.height, 2540.0)
assert_equal(r.hole_diameter, 127.0)
+
def test_rectangle_offset():
r = Rectangle((0, 0), 1, 2)
r.offset(1, 0)
- assert_equal(r.position,(1., 0.))
+ assert_equal(r.position, (1., 0.))
r.offset(0, 1)
- assert_equal(r.position,(1., 1.))
+ assert_equal(r.position, (1., 1.))
+
def test_diamond_ctor():
""" Test diamond creation
"""
- test_cases = (((0,0), 1, 1), ((0, 0), 1, 2), ((1,1), 1, 2))
+ test_cases = (((0, 0), 1, 1), ((0, 0), 1, 2), ((1, 1), 1, 2))
for pos, width, height in test_cases:
d = Diamond(pos, width, height)
assert_equal(d.position, pos)
assert_equal(d.width, width)
assert_equal(d.height, height)
+
def test_diamond_bounds():
""" Test diamond bounding box calculation
"""
- d = Diamond((0,0), 2, 2)
+ d = Diamond((0, 0), 2, 2)
xbounds, ybounds = d.bounding_box
assert_array_almost_equal(xbounds, (-1, 1))
assert_array_almost_equal(ybounds, (-1, 1))
- d = Diamond((0,0), math.sqrt(2), math.sqrt(2), rotation=45)
+ d = Diamond((0, 0), math.sqrt(2), math.sqrt(2), rotation=45)
xbounds, ybounds = d.bounding_box
assert_array_almost_equal(xbounds, (-1, 1))
assert_array_almost_equal(ybounds, (-1, 1))
+
def test_diamond_conversion():
d = Diamond((2.54, 25.4), 254.0, 2540.0, units='metric')
@@ -572,19 +605,21 @@ def test_diamond_conversion():
assert_equal(d.width, 254.0)
assert_equal(d.height, 2540.0)
+
def test_diamond_offset():
d = Diamond((0, 0), 1, 2)
d.offset(1, 0)
- assert_equal(d.position,(1., 0.))
+ assert_equal(d.position, (1., 0.))
d.offset(0, 1)
- assert_equal(d.position,(1., 1.))
+ assert_equal(d.position, (1., 1.))
+
def test_chamfer_rectangle_ctor():
""" Test chamfer rectangle creation
"""
- test_cases = (((0,0), 1, 1, 0.2, (True, True, False, False)),
+ test_cases = (((0, 0), 1, 1, 0.2, (True, True, False, False)),
((0, 0), 1, 2, 0.3, (True, True, True, True)),
- ((1,1), 1, 2, 0.4, (False, False, False, False)))
+ ((1, 1), 1, 2, 0.4, (False, False, False, False)))
for pos, width, height, chamfer, corners in test_cases:
r = ChamferRectangle(pos, width, height, chamfer, corners)
assert_equal(r.position, pos)
@@ -593,23 +628,27 @@ def test_chamfer_rectangle_ctor():
assert_equal(r.chamfer, chamfer)
assert_array_almost_equal(r.corners, corners)
+
def test_chamfer_rectangle_bounds():
""" Test chamfer rectangle bounding box calculation
"""
- r = ChamferRectangle((0,0), 2, 2, 0.2, (True, True, False, False))
+ r = ChamferRectangle((0, 0), 2, 2, 0.2, (True, True, False, False))
xbounds, ybounds = r.bounding_box
assert_array_almost_equal(xbounds, (-1, 1))
assert_array_almost_equal(ybounds, (-1, 1))
- r = ChamferRectangle((0,0), 2, 2, 0.2, (True, True, False, False), rotation=45)
+ r = ChamferRectangle(
+ (0, 0), 2, 2, 0.2, (True, True, False, False), rotation=45)
xbounds, ybounds = r.bounding_box
assert_array_almost_equal(xbounds, (-math.sqrt(2), math.sqrt(2)))
assert_array_almost_equal(ybounds, (-math.sqrt(2), math.sqrt(2)))
+
def test_chamfer_rectangle_conversion():
- r = ChamferRectangle((2.54, 25.4), 254.0, 2540.0, 0.254, (True, True, False, False), units='metric')
+ r = ChamferRectangle((2.54, 25.4), 254.0, 2540.0, 0.254,
+ (True, True, False, False), units='metric')
r.to_metric()
- assert_equal(r.position, (2.54,25.4))
+ assert_equal(r.position, (2.54, 25.4))
assert_equal(r.width, 254.0)
assert_equal(r.height, 2540.0)
assert_equal(r.chamfer, 0.254)
@@ -626,7 +665,8 @@ def test_chamfer_rectangle_conversion():
assert_equal(r.height, 100.0)
assert_equal(r.chamfer, 0.01)
- r = ChamferRectangle((0.1, 1.0), 10.0, 100.0, 0.01, (True, True, False, False), units='inch')
+ r = ChamferRectangle((0.1, 1.0), 10.0, 100.0, 0.01,
+ (True, True, False, False), units='inch')
r.to_inch()
assert_equal(r.position, (0.1, 1.0))
assert_equal(r.width, 10.0)
@@ -634,30 +674,32 @@ def test_chamfer_rectangle_conversion():
assert_equal(r.chamfer, 0.01)
r.to_metric()
- assert_equal(r.position, (2.54,25.4))
+ assert_equal(r.position, (2.54, 25.4))
assert_equal(r.width, 254.0)
assert_equal(r.height, 2540.0)
assert_equal(r.chamfer, 0.254)
r.to_metric()
- assert_equal(r.position, (2.54,25.4))
+ assert_equal(r.position, (2.54, 25.4))
assert_equal(r.width, 254.0)
assert_equal(r.height, 2540.0)
assert_equal(r.chamfer, 0.254)
+
def test_chamfer_rectangle_offset():
r = ChamferRectangle((0, 0), 1, 2, 0.01, (True, True, False, False))
r.offset(1, 0)
- assert_equal(r.position,(1., 0.))
+ assert_equal(r.position, (1., 0.))
r.offset(0, 1)
- assert_equal(r.position,(1., 1.))
+ assert_equal(r.position, (1., 1.))
+
def test_round_rectangle_ctor():
""" Test round rectangle creation
"""
- test_cases = (((0,0), 1, 1, 0.2, (True, True, False, False)),
+ test_cases = (((0, 0), 1, 1, 0.2, (True, True, False, False)),
((0, 0), 1, 2, 0.3, (True, True, True, True)),
- ((1,1), 1, 2, 0.4, (False, False, False, False)))
+ ((1, 1), 1, 2, 0.4, (False, False, False, False)))
for pos, width, height, radius, corners in test_cases:
r = RoundRectangle(pos, width, height, radius, corners)
assert_equal(r.position, pos)
@@ -666,23 +708,27 @@ def test_round_rectangle_ctor():
assert_equal(r.radius, radius)
assert_array_almost_equal(r.corners, corners)
+
def test_round_rectangle_bounds():
""" Test round rectangle bounding box calculation
"""
- r = RoundRectangle((0,0), 2, 2, 0.2, (True, True, False, False))
+ r = RoundRectangle((0, 0), 2, 2, 0.2, (True, True, False, False))
xbounds, ybounds = r.bounding_box
assert_array_almost_equal(xbounds, (-1, 1))
assert_array_almost_equal(ybounds, (-1, 1))
- r = RoundRectangle((0,0), 2, 2, 0.2, (True, True, False, False), rotation=45)
+ r = RoundRectangle((0, 0), 2, 2, 0.2,
+ (True, True, False, False), rotation=45)
xbounds, ybounds = r.bounding_box
assert_array_almost_equal(xbounds, (-math.sqrt(2), math.sqrt(2)))
assert_array_almost_equal(ybounds, (-math.sqrt(2), math.sqrt(2)))
+
def test_round_rectangle_conversion():
- r = RoundRectangle((2.54, 25.4), 254.0, 2540.0, 0.254, (True, True, False, False), units='metric')
+ r = RoundRectangle((2.54, 25.4), 254.0, 2540.0, 0.254,
+ (True, True, False, False), units='metric')
r.to_metric()
- assert_equal(r.position, (2.54,25.4))
+ assert_equal(r.position, (2.54, 25.4))
assert_equal(r.width, 254.0)
assert_equal(r.height, 2540.0)
assert_equal(r.radius, 0.254)
@@ -699,7 +745,8 @@ def test_round_rectangle_conversion():
assert_equal(r.height, 100.0)
assert_equal(r.radius, 0.01)
- r = RoundRectangle((0.1, 1.0), 10.0, 100.0, 0.01, (True, True, False, False), units='inch')
+ r = RoundRectangle((0.1, 1.0), 10.0, 100.0, 0.01,
+ (True, True, False, False), units='inch')
r.to_inch()
assert_equal(r.position, (0.1, 1.0))
@@ -708,70 +755,76 @@ def test_round_rectangle_conversion():
assert_equal(r.radius, 0.01)
r.to_metric()
- assert_equal(r.position, (2.54,25.4))
+ assert_equal(r.position, (2.54, 25.4))
assert_equal(r.width, 254.0)
assert_equal(r.height, 2540.0)
assert_equal(r.radius, 0.254)
r.to_metric()
- assert_equal(r.position, (2.54,25.4))
+ assert_equal(r.position, (2.54, 25.4))
assert_equal(r.width, 254.0)
assert_equal(r.height, 2540.0)
assert_equal(r.radius, 0.254)
+
def test_round_rectangle_offset():
r = RoundRectangle((0, 0), 1, 2, 0.01, (True, True, False, False))
r.offset(1, 0)
- assert_equal(r.position,(1., 0.))
+ assert_equal(r.position, (1., 0.))
r.offset(0, 1)
- assert_equal(r.position,(1., 1.))
+ assert_equal(r.position, (1., 1.))
+
def test_obround_ctor():
""" Test obround creation
"""
- test_cases = (((0,0), 1, 1),
+ test_cases = (((0, 0), 1, 1),
((0, 0), 1, 2),
- ((1,1), 1, 2))
+ ((1, 1), 1, 2))
for pos, width, height in test_cases:
o = Obround(pos, width, height)
assert_equal(o.position, pos)
assert_equal(o.width, width)
assert_equal(o.height, height)
+
def test_obround_bounds():
""" Test obround bounding box calculation
"""
- o = Obround((2,2),2,4)
+ o = Obround((2, 2), 2, 4)
xbounds, ybounds = o.bounding_box
assert_array_almost_equal(xbounds, (1, 3))
assert_array_almost_equal(ybounds, (0, 4))
- o = Obround((2,2),4,2)
+ o = Obround((2, 2), 4, 2)
xbounds, ybounds = o.bounding_box
assert_array_almost_equal(xbounds, (0, 4))
assert_array_almost_equal(ybounds, (1, 3))
+
def test_obround_orientation():
o = Obround((0, 0), 2, 1)
assert_equal(o.orientation, 'horizontal')
o = Obround((0, 0), 1, 2)
assert_equal(o.orientation, 'vertical')
+
def test_obround_subshapes():
- o = Obround((0,0), 1, 4)
+ o = Obround((0, 0), 1, 4)
ss = o.subshapes
assert_array_almost_equal(ss['rectangle'].position, (0, 0))
assert_array_almost_equal(ss['circle1'].position, (0, 1.5))
assert_array_almost_equal(ss['circle2'].position, (0, -1.5))
- o = Obround((0,0), 4, 1)
+ o = Obround((0, 0), 4, 1)
ss = o.subshapes
assert_array_almost_equal(ss['rectangle'].position, (0, 0))
assert_array_almost_equal(ss['circle1'].position, (1.5, 0))
assert_array_almost_equal(ss['circle2'].position, (-1.5, 0))
+
def test_obround_conversion():
- o = Obround((2.54,25.4), 254.0, 2540.0, units='metric')
+ o = Obround((2.54, 25.4), 254.0, 2540.0, units='metric')
- #No effect
+ # No effect
o.to_metric()
assert_equal(o.position, (2.54, 25.4))
assert_equal(o.width, 254.0)
@@ -782,15 +835,15 @@ def test_obround_conversion():
assert_equal(o.width, 10.0)
assert_equal(o.height, 100.0)
- #No effect
+ # No effect
o.to_inch()
assert_equal(o.position, (0.1, 1.0))
assert_equal(o.width, 10.0)
assert_equal(o.height, 100.0)
- o= Obround((0.1, 1.0), 10.0, 100.0, units='inch')
+ o = Obround((0.1, 1.0), 10.0, 100.0, units='inch')
- #No effect
+ # No effect
o.to_inch()
assert_equal(o.position, (0.1, 1.0))
assert_equal(o.width, 10.0)
@@ -801,25 +854,27 @@ def test_obround_conversion():
assert_equal(o.width, 254.0)
assert_equal(o.height, 2540.0)
- #No effect
+ # No effect
o.to_metric()
assert_equal(o.position, (2.54, 25.4))
assert_equal(o.width, 254.0)
assert_equal(o.height, 2540.0)
+
def test_obround_offset():
o = Obround((0, 0), 1, 2)
o.offset(1, 0)
- assert_equal(o.position,(1., 0.))
+ assert_equal(o.position, (1., 0.))
o.offset(0, 1)
- assert_equal(o.position,(1., 1.))
+ assert_equal(o.position, (1., 1.))
+
def test_polygon_ctor():
""" Test polygon creation
"""
- test_cases = (((0,0), 3, 5, 0),
+ test_cases = (((0, 0), 3, 5, 0),
((0, 0), 5, 6, 0),
- ((1,1), 7, 7, 45))
+ ((1, 1), 7, 7, 45))
for pos, sides, radius, hole_diameter in test_cases:
p = Polygon(pos, sides, radius, hole_diameter)
assert_equal(p.position, pos)
@@ -827,73 +882,80 @@ def test_polygon_ctor():
assert_equal(p.radius, radius)
assert_equal(p.hole_diameter, hole_diameter)
+
def test_polygon_bounds():
""" Test polygon bounding box calculation
"""
- p = Polygon((2,2), 3, 2, 0)
+ p = Polygon((2, 2), 3, 2, 0)
xbounds, ybounds = p.bounding_box
assert_array_almost_equal(xbounds, (0, 4))
assert_array_almost_equal(ybounds, (0, 4))
- p = Polygon((2,2), 3, 4, 0)
+ p = Polygon((2, 2), 3, 4, 0)
xbounds, ybounds = p.bounding_box
assert_array_almost_equal(xbounds, (-2, 6))
assert_array_almost_equal(ybounds, (-2, 6))
+
def test_polygon_conversion():
p = Polygon((2.54, 25.4), 3, 254.0, 0, units='metric')
- #No effect
+ # No effect
p.to_metric()
assert_equal(p.position, (2.54, 25.4))
assert_equal(p.radius, 254.0)
-
+
p.to_inch()
assert_equal(p.position, (0.1, 1.0))
assert_equal(p.radius, 10.0)
-
- #No effect
+
+ # No effect
p.to_inch()
assert_equal(p.position, (0.1, 1.0))
assert_equal(p.radius, 10.0)
p = Polygon((0.1, 1.0), 3, 10.0, 0, units='inch')
-
- #No effect
+
+ # No effect
p.to_inch()
assert_equal(p.position, (0.1, 1.0))
assert_equal(p.radius, 10.0)
-
+
p.to_metric()
assert_equal(p.position, (2.54, 25.4))
assert_equal(p.radius, 254.0)
-
- #No effect
+
+ # No effect
p.to_metric()
assert_equal(p.position, (2.54, 25.4))
assert_equal(p.radius, 254.0)
+
def test_polygon_offset():
p = Polygon((0, 0), 5, 10, 0)
p.offset(1, 0)
- assert_equal(p.position,(1., 0.))
+ assert_equal(p.position, (1., 0.))
p.offset(0, 1)
- assert_equal(p.position,(1., 1.))
+ assert_equal(p.position, (1., 1.))
+
def test_region_ctor():
""" Test Region creation
"""
- apt = Circle((0,0), 0)
- lines = (Line((0,0), (1,0), apt), Line((1,0), (1,1), apt), Line((1,1), (0,1), apt), Line((0,1), (0,0), apt))
- points = ((0, 0), (1,0), (1,1), (0,1))
+ apt = Circle((0, 0), 0)
+ lines = (Line((0, 0), (1, 0), apt), Line((1, 0), (1, 1), apt),
+ Line((1, 1), (0, 1), apt), Line((0, 1), (0, 0), apt))
+ points = ((0, 0), (1, 0), (1, 1), (0, 1))
r = Region(lines)
for i, p in enumerate(lines):
assert_equal(r.primitives[i], p)
+
def test_region_bounds():
""" Test region bounding box calculation
"""
- apt = Circle((0,0), 0)
- lines = (Line((0,0), (1,0), apt), Line((1,0), (1,1), apt), Line((1,1), (0,1), apt), Line((0,1), (0,0), apt))
+ apt = Circle((0, 0), 0)
+ lines = (Line((0, 0), (1, 0), apt), Line((1, 0), (1, 1), apt),
+ Line((1, 1), (0, 1), apt), Line((0, 1), (0, 0), apt))
r = Region(lines)
xbounds, ybounds = r.bounding_box
assert_array_almost_equal(xbounds, (0, 1))
@@ -901,68 +963,76 @@ def test_region_bounds():
def test_region_offset():
- apt = Circle((0,0), 0)
- lines = (Line((0,0), (1,0), apt), Line((1,0), (1,1), apt), Line((1,1), (0,1), apt), Line((0,1), (0,0), apt))
+ apt = Circle((0, 0), 0)
+ lines = (Line((0, 0), (1, 0), apt), Line((1, 0), (1, 1), apt),
+ Line((1, 1), (0, 1), apt), Line((0, 1), (0, 0), apt))
r = Region(lines)
xlim, ylim = r.bounding_box
r.offset(0, 1)
- assert_array_almost_equal((xlim, tuple([y+1 for y in ylim])), r.bounding_box)
+ new_xlim, new_ylim = r.bounding_box
+ assert_array_almost_equal(new_xlim, xlim)
+ assert_array_almost_equal(new_ylim, tuple([y + 1 for y in ylim]))
+
def test_round_butterfly_ctor():
""" Test round butterfly creation
"""
- test_cases = (((0,0), 3), ((0, 0), 5), ((1,1), 7))
+ test_cases = (((0, 0), 3), ((0, 0), 5), ((1, 1), 7))
for pos, diameter in test_cases:
b = RoundButterfly(pos, diameter)
assert_equal(b.position, pos)
assert_equal(b.diameter, diameter)
- assert_equal(b.radius, diameter/2.)
+ assert_equal(b.radius, diameter / 2.)
+
def test_round_butterfly_ctor_validation():
""" Test RoundButterfly argument validation
"""
assert_raises(TypeError, RoundButterfly, 3, 5)
- assert_raises(TypeError, RoundButterfly, (3,4,5), 5)
+ assert_raises(TypeError, RoundButterfly, (3, 4, 5), 5)
+
def test_round_butterfly_conversion():
b = RoundButterfly((2.54, 25.4), 254.0, units='metric')
-
- #No Effect
+
+ # No Effect
b.to_metric()
assert_equal(b.position, (2.54, 25.4))
assert_equal(b.diameter, (254.0))
-
+
b.to_inch()
assert_equal(b.position, (0.1, 1.0))
assert_equal(b.diameter, 10.0)
-
- #No effect
+
+ # No effect
b.to_inch()
assert_equal(b.position, (0.1, 1.0))
assert_equal(b.diameter, 10.0)
b = RoundButterfly((0.1, 1.0), 10.0, units='inch')
-
- #No effect
+
+ # No effect
b.to_inch()
assert_equal(b.position, (0.1, 1.0))
assert_equal(b.diameter, 10.0)
-
+
b.to_metric()
assert_equal(b.position, (2.54, 25.4))
assert_equal(b.diameter, (254.0))
-
- #No Effect
+
+ # No Effect
b.to_metric()
assert_equal(b.position, (2.54, 25.4))
assert_equal(b.diameter, (254.0))
+
def test_round_butterfly_offset():
b = RoundButterfly((0, 0), 1)
b.offset(1, 0)
- assert_equal(b.position,(1., 0.))
+ assert_equal(b.position, (1., 0.))
b.offset(0, 1)
- assert_equal(b.position,(1., 1.))
+ assert_equal(b.position, (1., 1.))
+
def test_round_butterfly_bounds():
""" Test RoundButterfly bounding box calculation
@@ -972,20 +1042,23 @@ def test_round_butterfly_bounds():
assert_array_almost_equal(xbounds, (-1, 1))
assert_array_almost_equal(ybounds, (-1, 1))
+
def test_square_butterfly_ctor():
""" Test SquareButterfly creation
"""
- test_cases = (((0,0), 3), ((0, 0), 5), ((1,1), 7))
+ test_cases = (((0, 0), 3), ((0, 0), 5), ((1, 1), 7))
for pos, side in test_cases:
b = SquareButterfly(pos, side)
assert_equal(b.position, pos)
assert_equal(b.side, side)
+
def test_square_butterfly_ctor_validation():
""" Test SquareButterfly argument validation
"""
assert_raises(TypeError, SquareButterfly, 3, 5)
- assert_raises(TypeError, SquareButterfly, (3,4,5), 5)
+ assert_raises(TypeError, SquareButterfly, (3, 4, 5), 5)
+
def test_square_butterfly_bounds():
""" Test SquareButterfly bounding box calculation
@@ -995,51 +1068,54 @@ def test_square_butterfly_bounds():
assert_array_almost_equal(xbounds, (-1, 1))
assert_array_almost_equal(ybounds, (-1, 1))
+
def test_squarebutterfly_conversion():
b = SquareButterfly((2.54, 25.4), 254.0, units='metric')
-
- #No effect
+
+ # No effect
b.to_metric()
assert_equal(b.position, (2.54, 25.4))
assert_equal(b.side, (254.0))
-
+
b.to_inch()
assert_equal(b.position, (0.1, 1.0))
assert_equal(b.side, 10.0)
-
- #No effect
+
+ # No effect
b.to_inch()
assert_equal(b.position, (0.1, 1.0))
assert_equal(b.side, 10.0)
b = SquareButterfly((0.1, 1.0), 10.0, units='inch')
-
- #No effect
+
+ # No effect
b.to_inch()
assert_equal(b.position, (0.1, 1.0))
assert_equal(b.side, 10.0)
-
+
b.to_metric()
assert_equal(b.position, (2.54, 25.4))
assert_equal(b.side, (254.0))
-
- #No effect
+
+ # No effect
b.to_metric()
assert_equal(b.position, (2.54, 25.4))
assert_equal(b.side, (254.0))
+
def test_square_butterfly_offset():
b = SquareButterfly((0, 0), 1)
b.offset(1, 0)
- assert_equal(b.position,(1., 0.))
+ assert_equal(b.position, (1., 0.))
b.offset(0, 1)
- assert_equal(b.position,(1., 1.))
+ assert_equal(b.position, (1., 1.))
+
def test_donut_ctor():
""" Test Donut primitive creation
"""
- test_cases = (((0,0), 'round', 3, 5), ((0, 0), 'square', 5, 7),
- ((1,1), 'hexagon', 7, 9), ((2, 2), 'octagon', 9, 11))
+ test_cases = (((0, 0), 'round', 3, 5), ((0, 0), 'square', 5, 7),
+ ((1, 1), 'hexagon', 7, 9), ((2, 2), 'octagon', 9, 11))
for pos, shape, in_d, out_d in test_cases:
d = Donut(pos, shape, in_d, out_d)
assert_equal(d.position, pos)
@@ -1047,65 +1123,68 @@ def test_donut_ctor():
assert_equal(d.inner_diameter, in_d)
assert_equal(d.outer_diameter, out_d)
+
def test_donut_ctor_validation():
assert_raises(TypeError, Donut, 3, 'round', 5, 7)
assert_raises(TypeError, Donut, (3, 4, 5), 'round', 5, 7)
assert_raises(ValueError, Donut, (0, 0), 'triangle', 3, 5)
assert_raises(ValueError, Donut, (0, 0), 'round', 5, 3)
+
def test_donut_bounds():
d = Donut((0, 0), 'round', 0.0, 2.0)
- assert_equal(d.lower_left, (-1.0, -1.0))
- assert_equal(d.upper_right, (1.0, 1.0))
xbounds, ybounds = d.bounding_box
assert_equal(xbounds, (-1., 1.))
assert_equal(ybounds, (-1., 1.))
+
def test_donut_conversion():
d = Donut((2.54, 25.4), 'round', 254.0, 2540.0, units='metric')
-
- #No effect
+
+ # No effect
d.to_metric()
assert_equal(d.position, (2.54, 25.4))
assert_equal(d.inner_diameter, 254.0)
assert_equal(d.outer_diameter, 2540.0)
-
+
d.to_inch()
assert_equal(d.position, (0.1, 1.0))
assert_equal(d.inner_diameter, 10.0)
assert_equal(d.outer_diameter, 100.0)
-
- #No effect
+
+ # No effect
d.to_inch()
assert_equal(d.position, (0.1, 1.0))
assert_equal(d.inner_diameter, 10.0)
assert_equal(d.outer_diameter, 100.0)
d = Donut((0.1, 1.0), 'round', 10.0, 100.0, units='inch')
-
- #No effect
+
+ # No effect
d.to_inch()
assert_equal(d.position, (0.1, 1.0))
assert_equal(d.inner_diameter, 10.0)
assert_equal(d.outer_diameter, 100.0)
-
+
d.to_metric()
assert_equal(d.position, (2.54, 25.4))
assert_equal(d.inner_diameter, 254.0)
assert_equal(d.outer_diameter, 2540.0)
-
- #No effect
+
+ # No effect
d.to_metric()
assert_equal(d.position, (2.54, 25.4))
assert_equal(d.inner_diameter, 254.0)
assert_equal(d.outer_diameter, 2540.0)
+
def test_donut_offset():
d = Donut((0, 0), 'round', 1, 10)
d.offset(1, 0)
- assert_equal(d.position,(1., 0.))
+ assert_equal(d.position, (1., 0.))
d.offset(0, 1)
- assert_equal(d.position,(1., 1.))
+ assert_equal(d.position, (1., 1.))
+
def test_drill_ctor():
""" Test drill primitive creation
@@ -1115,7 +1194,8 @@ def test_drill_ctor():
d = Drill(position, diameter, None)
assert_equal(d.position, position)
assert_equal(d.diameter, diameter)
- assert_equal(d.radius, diameter/2.)
+ assert_equal(d.radius, diameter / 2.)
+
def test_drill_ctor_validation():
""" Test drill argument validation
@@ -1133,46 +1213,48 @@ def test_drill_bounds():
assert_array_almost_equal(xbounds, (0, 2))
assert_array_almost_equal(ybounds, (1, 3))
+
def test_drill_conversion():
d = Drill((2.54, 25.4), 254., None, units='metric')
-
- #No effect
+
+ # No effect
d.to_metric()
assert_equal(d.position, (2.54, 25.4))
assert_equal(d.diameter, 254.0)
-
+
d.to_inch()
assert_equal(d.position, (0.1, 1.0))
assert_equal(d.diameter, 10.0)
-
- #No effect
+
+ # No effect
d.to_inch()
assert_equal(d.position, (0.1, 1.0))
assert_equal(d.diameter, 10.0)
-
d = Drill((0.1, 1.0), 10., None, units='inch')
-
- #No effect
+
+ # No effect
d.to_inch()
assert_equal(d.position, (0.1, 1.0))
assert_equal(d.diameter, 10.0)
-
+
d.to_metric()
assert_equal(d.position, (2.54, 25.4))
assert_equal(d.diameter, 254.0)
-
- #No effect
+
+ # No effect
d.to_metric()
assert_equal(d.position, (2.54, 25.4))
assert_equal(d.diameter, 254.0)
+
def test_drill_offset():
d = Drill((0, 0), 1., None)
d.offset(1, 0)
- assert_equal(d.position,(1., 0.))
+ assert_equal(d.position, (1., 0.))
d.offset(0, 1)
- assert_equal(d.position,(1., 1.))
+ assert_equal(d.position, (1., 1.))
+
def test_drill_equality():
d = Drill((2.54, 25.4), 254., None)
diff --git a/gerber/tests/test_rs274x.py b/gerber/tests/test_rs274x.py
index c084e80..d5acfe8 100644
--- a/gerber/tests/test_rs274x.py
+++ b/gerber/tests/test_rs274x.py
@@ -9,31 +9,35 @@ from .tests import *
TOP_COPPER_FILE = os.path.join(os.path.dirname(__file__),
- 'resources/top_copper.GTL')
+ 'resources/top_copper.GTL')
MULTILINE_READ_FILE = os.path.join(os.path.dirname(__file__),
- 'resources/multiline_read.ger')
+ 'resources/multiline_read.ger')
def test_read():
top_copper = read(TOP_COPPER_FILE)
assert(isinstance(top_copper, GerberFile))
+
def test_multiline_read():
multiline = read(MULTILINE_READ_FILE)
assert(isinstance(multiline, GerberFile))
assert_equal(10, len(multiline.statements))
+
def test_comments_parameter():
top_copper = read(TOP_COPPER_FILE)
assert_equal(top_copper.comments[0], 'This is a comment,:')
+
def test_size_parameter():
top_copper = read(TOP_COPPER_FILE)
size = top_copper.size
assert_almost_equal(size[0], 2.256900, 6)
assert_almost_equal(size[1], 1.500000, 6)
+
def test_conversion():
import copy
top_copper = read(TOP_COPPER_FILE)
@@ -50,4 +54,3 @@ def test_conversion():
for i, m in zip(top_copper.primitives, top_copper_inch.primitives):
assert_equal(i, m)
-
diff --git a/gerber/tests/test_utils.py b/gerber/tests/test_utils.py
index fe9b2e6..35f6f47 100644
--- a/gerber/tests/test_utils.py
+++ b/gerber/tests/test_utils.py
@@ -52,7 +52,7 @@ def test_format():
((2, 6), '-1', -0.000001), ((2, 5), '-1', -0.00001),
((2, 4), '-1', -0.0001), ((2, 3), '-1', -0.001),
((2, 2), '-1', -0.01), ((2, 1), '-1', -0.1),
- ((2, 6), '0', 0) ]
+ ((2, 6), '0', 0)]
for fmt, string, value in test_cases:
assert_equal(value, parse_gerber_value(string, fmt, zero_suppression))
assert_equal(string, write_gerber_value(value, fmt, zero_suppression))
@@ -76,7 +76,7 @@ def test_decimal_truncation():
value = 1.123456789
for x in range(10):
result = decimal_string(value, precision=x)
- calculated = '1.' + ''.join(str(y) for y in range(1,x+1))
+ calculated = '1.' + ''.join(str(y) for y in range(1, x + 1))
assert_equal(result, calculated)
@@ -96,25 +96,34 @@ def test_parse_format_validation():
"""
assert_raises(ValueError, parse_gerber_value, '00001111', (7, 5))
assert_raises(ValueError, parse_gerber_value, '00001111', (5, 8))
- assert_raises(ValueError, parse_gerber_value, '00001111', (13,1))
-
+ assert_raises(ValueError, parse_gerber_value, '00001111', (13, 1))
+
+
def test_write_format_validation():
""" Test write_gerber_value() format validation
"""
assert_raises(ValueError, write_gerber_value, 69.0, (7, 5))
assert_raises(ValueError, write_gerber_value, 69.0, (5, 8))
- assert_raises(ValueError, write_gerber_value, 69.0, (13,1))
+ assert_raises(ValueError, write_gerber_value, 69.0, (13, 1))
def test_detect_format_with_short_file():
""" Verify file format detection works with short files
"""
assert_equal('unknown', detect_file_format('gerber/tests/__init__.py'))
-
+
+
def test_validate_coordinates():
assert_raises(TypeError, validate_coordinates, 3)
assert_raises(TypeError, validate_coordinates, 3.1)
assert_raises(TypeError, validate_coordinates, '14')
assert_raises(TypeError, validate_coordinates, (0,))
- assert_raises(TypeError, validate_coordinates, (0,1,2))
- assert_raises(TypeError, validate_coordinates, (0,'string'))
+ assert_raises(TypeError, validate_coordinates, (0, 1, 2))
+ assert_raises(TypeError, validate_coordinates, (0, 'string'))
+
+
+def test_convex_hull():
+ points = [(0, 0), (1, 0), (1, 1), (0.5, 0.5), (0, 1), (0, 0)]
+ expected = [(0, 0), (1, 0), (1, 1), (0, 1), (0, 0)]
+ assert_equal(set(convex_hull(points)), set(expected))
+ \ No newline at end of file
diff --git a/gerber/tests/tests.py b/gerber/tests/tests.py
index 2c75acd..ac08208 100644
--- a/gerber/tests/tests.py
+++ b/gerber/tests/tests.py
@@ -16,7 +16,8 @@ from nose import with_setup
__all__ = ['assert_in', 'assert_not_in', 'assert_equal', 'assert_not_equal',
'assert_almost_equal', 'assert_array_almost_equal', 'assert_true',
- 'assert_false', 'assert_raises', 'raises', 'with_setup' ]
+ 'assert_false', 'assert_raises', 'raises', 'with_setup']
+
def assert_array_almost_equal(arr1, arr2, decimal=6):
assert_equal(len(arr1), len(arr2))
diff --git a/gerber/utils.py b/gerber/utils.py
index b968dc8..ef9c39e 100644
--- a/gerber/utils.py
+++ b/gerber/utils.py
@@ -23,15 +23,15 @@ This module provides utility functions for working with Gerber and Excellon
files.
"""
-# Author: Hamilton Kibbe <ham@hamiltonkib.be>
-# License:
-
import os
from math import radians, sin, cos
from operator import sub
+from copy import deepcopy
+from pyhull.convex_hull import ConvexHull
MILLIMETERS_PER_INCH = 25.4
+
def parse_gerber_value(value, format=(2, 5), zero_suppression='trailing'):
""" Convert gerber/excellon formatted string to floating-point number
@@ -92,7 +92,8 @@ def parse_gerber_value(value, format=(2, 5), zero_suppression='trailing'):
else:
digits = list(value)
- result = float(''.join(digits[:integer_digits] + ['.'] + digits[integer_digits:]))
+ result = float(
+ ''.join(digits[:integer_digits] + ['.'] + digits[integer_digits:]))
return -result if negative else result
@@ -132,7 +133,8 @@ def write_gerber_value(value, format=(2, 5), zero_suppression='trailing'):
if MAX_DIGITS > 13 or integer_digits > 6 or decimal_digits > 7:
raise ValueError('Parser only supports precision up to 6:7 format')
- # Edge case... (per Gerber spec we should return 0 in all cases, see page 77)
+ # Edge case... (per Gerber spec we should return 0 in all cases, see page
+ # 77)
if value == 0:
return '0'
@@ -222,7 +224,7 @@ def detect_file_format(data):
elif '%FS' in line:
return 'rs274x'
elif ((len(line.split()) >= 2) and
- (line.split()[0] == 'P') and (line.split()[1] == 'JOB')):
+ (line.split()[0] == 'P') and (line.split()[1] == 'JOB')):
return 'ipc_d_356'
return 'unknown'
@@ -252,6 +254,7 @@ def metric(value):
"""
return value * MILLIMETERS_PER_INCH
+
def inch(value):
""" Convert millimeter value to inches
@@ -310,6 +313,26 @@ def sq_distance(point1, point2):
def listdir(directory, ignore_hidden=True, ignore_os=True):
+ """ List files in given directory.
+ Differs from os.listdir() in that hidden and OS-generated files are ignored
+ by default.
+
+ Parameters
+ ----------
+ directory : str
+ path to the directory for which to list files.
+
+ ignore_hidden : bool
+ If True, ignore files beginning with a leading '.'
+
+ ignore_os : bool
+ If True, ignore OS-generated files, e.g. Thumbs.db
+
+ Returns
+ -------
+ files : list
+ list of files in specified directory
+ """
os_files = ('.DS_Store', 'Thumbs.db', 'ethumbs.db')
files = os.listdir(directory)
if ignore_hidden:
@@ -317,3 +340,9 @@ def listdir(directory, ignore_hidden=True, ignore_os=True):
if ignore_os:
files = [f for f in files if not f in os_files]
return files
+
+
+def convex_hull(points):
+ vertices = ConvexHull(points).vertices
+ return [points[idx] for idx in
+ set([point for pair in vertices for point in pair])]