Merge pull request #58 from garretfick/merge-curtacircuitos
Epic merge from @garretfick Thanks a lot @garretfick and @hamiltonkibbe.
6
.gitignore
vendored
|
|
@ -34,7 +34,10 @@ nosetests.xml
|
|||
.mr.developer.cfg
|
||||
.project
|
||||
.pydevproject
|
||||
.idea/workspace.xml
|
||||
.idea/misc.xml
|
||||
.idea
|
||||
.settings
|
||||
|
||||
# Komodo Files
|
||||
*.komodoproject
|
||||
|
|
@ -42,3 +45,6 @@ nosetests.xml
|
|||
# OS Files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Virtual environment
|
||||
venv
|
||||
|
|
|
|||
15
README.md
|
|
@ -42,3 +42,18 @@ $ python setup.py install
|
|||
Documentation:
|
||||
--------------
|
||||
[PCB Tools Documentation](http://pcb-tools.readthedocs.org/en/latest/)
|
||||
|
||||
|
||||
Development and Testing:
|
||||
------------------------
|
||||
|
||||
Dependencies for developing and testing pcb-tools are listed in test-requirements.txt. Use of a virtual environment is strongly recommended.
|
||||
|
||||
$ virtualenv venv
|
||||
$ source venv/bin/activate
|
||||
(venv)$ pip install -r test-requirements.txt
|
||||
(venv)$ pip install -e .
|
||||
|
||||
We use nose to run pcb-tools's suite of unittests and doctests.
|
||||
|
||||
(venv)$ nosetests
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ from gerber.render import GerberCairoContext, theme
|
|||
|
||||
GERBER_FOLDER = os.path.abspath(os.path.join(os.path.dirname(__file__), 'gerbers'))
|
||||
|
||||
|
||||
# Create a new drawing context
|
||||
ctx = GerberCairoContext()
|
||||
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 83 KiB |
|
|
@ -16,9 +16,15 @@
|
|||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from .utils import validate_coordinates, inch, metric
|
||||
from .primitives import *
|
||||
from math import asin
|
||||
import math
|
||||
|
||||
from .primitives import *
|
||||
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',
|
||||
|
|
@ -68,7 +74,13 @@ class AMPrimitive(object):
|
|||
def to_metric(self):
|
||||
raise NotImplementedError('Subclass must implement `to-metric`')
|
||||
|
||||
def to_primitive(self, position, level_polarity, units):
|
||||
@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__)))
|
||||
|
|
@ -126,6 +138,12 @@ class AMCommentPrimitive(AMPrimitive):
|
|||
def to_gerber(self, settings=None):
|
||||
return '0 %s *' % self.comment
|
||||
|
||||
def to_primitive(self, units):
|
||||
"""
|
||||
Returns None - has not primitive representation
|
||||
"""
|
||||
return None
|
||||
|
||||
def __str__(self):
|
||||
return '<Aperture Macro Comment: %s>' % self.comment
|
||||
|
||||
|
|
@ -171,6 +189,10 @@ class AMCirclePrimitive(AMPrimitive):
|
|||
position = (float(modifiers[3]), float(modifiers[4]))
|
||||
return cls(code, exposure, diameter, position)
|
||||
|
||||
@classmethod
|
||||
def from_primitive(cls, primitive):
|
||||
return cls(1, 'on', primitive.diameter, primitive.position)
|
||||
|
||||
def __init__(self, code, exposure, diameter, position):
|
||||
validate_coordinates(position)
|
||||
if code != 1:
|
||||
|
|
@ -187,13 +209,6 @@ class AMCirclePrimitive(AMPrimitive):
|
|||
self.diameter = metric(self.diameter)
|
||||
self.position = tuple([metric(x) for x in self.position])
|
||||
|
||||
def to_primitive(self, position, level_polarity, units):
|
||||
# Offset the primitive from macro position
|
||||
position = tuple([a + b for a , b in zip (position, self.position)])
|
||||
# Return a renderable primitive
|
||||
return Circle(position, self.diameter, level_polarity=level_polarity,
|
||||
units=units)
|
||||
|
||||
def to_gerber(self, settings=None):
|
||||
data = dict(code=self.code,
|
||||
exposure='1' if self.exposure == 'on' else 0,
|
||||
|
|
@ -202,6 +217,9 @@ class AMCirclePrimitive(AMPrimitive):
|
|||
y=self.position[1])
|
||||
return '{code},{exposure},{diameter},{x},{y}*'.format(**data)
|
||||
|
||||
def to_primitive(self, units):
|
||||
return Circle((self.position), self.diameter, units=units, level_polarity=self._level_polarity)
|
||||
|
||||
|
||||
class AMVectorLinePrimitive(AMPrimitive):
|
||||
""" Aperture Macro Vector Line primitive. Code 2 or 20.
|
||||
|
|
@ -242,6 +260,11 @@ class AMVectorLinePrimitive(AMPrimitive):
|
|||
------
|
||||
ValueError, TypeError
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def from_primitive(cls, primitive):
|
||||
return cls(2, 'on', primitive.aperture.width, primitive.start, primitive.end, 0)
|
||||
|
||||
@classmethod
|
||||
def from_gerber(cls, primitive):
|
||||
modifiers = primitive.strip(' *').split(',')
|
||||
|
|
@ -274,14 +297,6 @@ 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_primitive(self, position, level_polarity, units):
|
||||
# Offset the primitive from macro position
|
||||
start = tuple([a + b for a , b in zip (position, self.start)])
|
||||
end = tuple([a + b for a , b in zip (position, self.end)])
|
||||
# Return a renderable primitive
|
||||
ap = Rectangle((0, 0), self.width, self.width)
|
||||
return Line(start, end, ap, level_polarity=level_polarity, units=units)
|
||||
|
||||
def to_gerber(self, settings=None):
|
||||
fmtstr = '{code},{exp},{width},{startx},{starty},{endx},{endy},{rotation}*'
|
||||
data = dict(code=self.code,
|
||||
|
|
@ -294,6 +309,28 @@ class AMVectorLinePrimitive(AMPrimitive):
|
|||
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
|
||||
|
||||
aperture = Circle((0, 0), 0)
|
||||
|
||||
lines = []
|
||||
prev_point = rotate_point(vertices[-1], self.rotation, (0, 0))
|
||||
for point in vertices:
|
||||
cur_point = rotate_point(point, self.rotation, (0, 0))
|
||||
|
||||
lines.append(Line(prev_point, cur_point, aperture))
|
||||
|
||||
return Outline(lines, units=units, level_polarity=self._level_polarity)
|
||||
|
||||
|
||||
class AMOutlinePrimitive(AMPrimitive):
|
||||
""" Aperture Macro Outline primitive. Code 4.
|
||||
|
|
@ -333,6 +370,19 @@ class AMOutlinePrimitive(AMPrimitive):
|
|||
------
|
||||
ValueError, TypeError
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def from_primitive(cls, primitive):
|
||||
|
||||
start_point = (round(primitive.primitives[0].start[0], 6), round(primitive.primitives[0].start[1], 6))
|
||||
points = []
|
||||
for prim in primitive.primitives:
|
||||
points.append((round(prim.end[0], 6), round(prim.end[1], 6)))
|
||||
|
||||
rotation = 0.0
|
||||
|
||||
return cls(4, 'on', start_point, points, rotation)
|
||||
|
||||
@classmethod
|
||||
def from_gerber(cls, primitive):
|
||||
modifiers = primitive.strip(' *').split(",")
|
||||
|
|
@ -376,12 +426,32 @@ class AMOutlinePrimitive(AMPrimitive):
|
|||
code=self.code,
|
||||
exposure="1" if self.exposure == "on" else "0",
|
||||
n_points=len(self.points),
|
||||
start_point="%.4g,%.4g" % self.start_point,
|
||||
points=",".join(["%.4g,%.4g" % point for point in self.points]),
|
||||
start_point="%.6g,%.6g" % self.start_point,
|
||||
points=",\n".join(["%.6g,%.6g" % point for point in self.points]),
|
||||
rotation=str(self.rotation)
|
||||
)
|
||||
return "{code},{exposure},{n_points},{start_point},{points},{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)
|
||||
for point in self.points:
|
||||
cur_point = rotate_point(point, self.rotation)
|
||||
|
||||
lines.append(Line(prev_point, cur_point, Circle((0,0), 0)))
|
||||
|
||||
prev_point = cur_point
|
||||
|
||||
if lines[0].start != lines[-1].end:
|
||||
raise ValueError('Outline must be closed')
|
||||
|
||||
return Outline(lines, units=units, level_polarity=self._level_polarity)
|
||||
|
||||
|
||||
class AMPolygonPrimitive(AMPrimitive):
|
||||
""" Aperture Macro Polygon primitive. Code 5.
|
||||
|
|
@ -422,6 +492,11 @@ class AMPolygonPrimitive(AMPrimitive):
|
|||
------
|
||||
ValueError, TypeError
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def from_primitive(cls, primitive):
|
||||
return cls(5, 'on', primitive.sides, primitive.position, primitive.diameter, primitive.rotation)
|
||||
|
||||
@classmethod
|
||||
def from_gerber(cls, primitive):
|
||||
modifiers = primitive.strip(' *').split(",")
|
||||
|
|
@ -459,14 +534,6 @@ class AMPolygonPrimitive(AMPrimitive):
|
|||
self.position = tuple([metric(x) for x in self.position])
|
||||
self.diameter = metric(self.diameter)
|
||||
|
||||
def to_primitive(self, position, level_polarity, units):
|
||||
# Offset the primitive from macro position
|
||||
position = tuple([a + b for a , b in zip (position, self.position)])
|
||||
# Return a renderable primitive
|
||||
return Polygon(position, self.vertices, self.diameter/2.,
|
||||
rotation=self.rotation, level_polarity=level_polarity,
|
||||
units=units)
|
||||
|
||||
def to_gerber(self, settings=None):
|
||||
data = dict(
|
||||
code=self.code,
|
||||
|
|
@ -479,6 +546,9 @@ class AMPolygonPrimitive(AMPrimitive):
|
|||
fmt = "{code},{exposure},{vertices},{position},{diameter},{rotation}*"
|
||||
return fmt.format(**data)
|
||||
|
||||
def to_primitive(self, units):
|
||||
return Polygon(self.position, self.vertices, self.diameter / 2.0, 0, rotation=math.radians(self.rotation), units=units, level_polarity=self._level_polarity)
|
||||
|
||||
|
||||
class AMMoirePrimitive(AMPrimitive):
|
||||
""" Aperture Macro Moire primitive. Code 6.
|
||||
|
|
@ -574,6 +644,7 @@ 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,
|
||||
|
|
@ -589,6 +660,10 @@ class AMMoirePrimitive(AMPrimitive):
|
|||
fmt = "{code},{position},{diameter},{ring_thickness},{gap},{max_rings},{crosshair_thickness},{crosshair_length},{rotation}*"
|
||||
return fmt.format(**data)
|
||||
|
||||
def to_primitive(self, units):
|
||||
#raise NotImplementedError()
|
||||
return None
|
||||
|
||||
|
||||
class AMThermalPrimitive(AMPrimitive):
|
||||
""" Aperture Macro Thermal primitive. Code 7.
|
||||
|
|
@ -637,9 +712,10 @@ class AMThermalPrimitive(AMPrimitive):
|
|||
outer_diameter = float(modifiers[3])
|
||||
inner_diameter = float(modifiers[4])
|
||||
gap = float(modifiers[5])
|
||||
return cls(code, position, outer_diameter, inner_diameter, gap)
|
||||
rotation = float(modifiers[6])
|
||||
return cls(code, position, outer_diameter, inner_diameter, gap, rotation)
|
||||
|
||||
def __init__(self, code, position, outer_diameter, inner_diameter, gap):
|
||||
def __init__(self, code, position, outer_diameter, inner_diameter, gap, rotation):
|
||||
if code != 7:
|
||||
raise ValueError('ThermalPrimitive code is 7')
|
||||
super(AMThermalPrimitive, self).__init__(code, 'on')
|
||||
|
|
@ -648,6 +724,7 @@ class AMThermalPrimitive(AMPrimitive):
|
|||
self.outer_diameter = outer_diameter
|
||||
self.inner_diameter = inner_diameter
|
||||
self.gap = gap
|
||||
self.rotation = rotation
|
||||
|
||||
def to_inch(self):
|
||||
self.position = tuple([inch(x) for x in self.position])
|
||||
|
|
@ -668,10 +745,90 @@ class AMThermalPrimitive(AMPrimitive):
|
|||
outer_diameter=self.outer_diameter,
|
||||
inner_diameter=self.inner_diameter,
|
||||
gap=self.gap,
|
||||
rotation=self.rotation
|
||||
)
|
||||
fmt = "{code},{position},{outer_diameter},{inner_diameter},{gap}*"
|
||||
fmt = "{code},{position},{outer_diameter},{inner_diameter},{gap},{rotation}*"
|
||||
return fmt.format(**data)
|
||||
|
||||
def _approximate_arc_cw(self, start_angle, end_angle, radius, center):
|
||||
"""
|
||||
Get an arc as a series of points
|
||||
|
||||
Parameters
|
||||
----------
|
||||
start_angle : The start angle in radians
|
||||
end_angle : The end angle in radians
|
||||
radius`: Radius of the arc
|
||||
center : The center point of the arc (x, y) tuple
|
||||
|
||||
Returns
|
||||
-------
|
||||
array of point tuples
|
||||
"""
|
||||
|
||||
# The total sweep
|
||||
sweep_angle = end_angle - start_angle
|
||||
num_steps = 10
|
||||
|
||||
angle_step = sweep_angle / num_steps
|
||||
|
||||
radius = radius
|
||||
center = center
|
||||
|
||||
points = []
|
||||
|
||||
for i in range(num_steps + 1):
|
||||
current_angle = start_angle + (angle_step * i)
|
||||
|
||||
nextx = (center[0] + math.cos(current_angle) * radius)
|
||||
nexty = (center[1] + math.sin(current_angle) * radius)
|
||||
|
||||
points.append((nextx, nexty))
|
||||
|
||||
return points
|
||||
|
||||
def to_primitive(self, units):
|
||||
|
||||
# We start with calculating the top right section, then duplicate it
|
||||
|
||||
inner_radius = self.inner_diameter / 2.0
|
||||
outer_radius = self.outer_diameter / 2.0
|
||||
|
||||
# Calculate the start angle relative to the horizontal axis
|
||||
inner_offset_angle = asin(self.gap / 2.0 / inner_radius)
|
||||
outer_offset_angle = asin(self.gap / 2.0 / outer_radius)
|
||||
|
||||
rotation_rad = math.radians(self.rotation)
|
||||
inner_start_angle = inner_offset_angle + rotation_rad
|
||||
inner_end_angle = math.pi / 2 - inner_offset_angle + rotation_rad
|
||||
|
||||
outer_start_angle = outer_offset_angle + rotation_rad
|
||||
outer_end_angle = math.pi / 2 - outer_offset_angle + rotation_rad
|
||||
|
||||
outlines = []
|
||||
aperture = Circle((0, 0), 0)
|
||||
|
||||
points = (self._approximate_arc_cw(inner_start_angle, inner_end_angle, inner_radius, self.position)
|
||||
+ list(reversed(self._approximate_arc_cw(outer_start_angle, outer_end_angle, outer_radius, self.position))))
|
||||
# Add in the last point since outlines should be closed
|
||||
points.append(points[0])
|
||||
|
||||
# There are four outlines at rotated sections
|
||||
for rotation in [0, 90.0, 180.0, 270.0]:
|
||||
|
||||
lines = []
|
||||
prev_point = rotate_point(points[0], rotation, self.position)
|
||||
for point in points[1:]:
|
||||
cur_point = rotate_point(point, rotation, self.position)
|
||||
|
||||
lines.append(Line(prev_point, cur_point, aperture))
|
||||
|
||||
prev_point = cur_point
|
||||
|
||||
outlines.append(Outline(lines, units=units, level_polarity=self._level_polarity))
|
||||
|
||||
return outlines
|
||||
|
||||
|
||||
class AMCenterLinePrimitive(AMPrimitive):
|
||||
""" Aperture Macro Center Line primitive. Code 21.
|
||||
|
|
@ -712,6 +869,14 @@ class AMCenterLinePrimitive(AMPrimitive):
|
|||
ValueError, TypeError
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def from_primitive(cls, primitive):
|
||||
width = primitive.width
|
||||
height = primitive.height
|
||||
center = primitive.position
|
||||
rotation = math.degrees(primitive.rotation)
|
||||
return cls(21, 'on', width, height, center, rotation)
|
||||
|
||||
@classmethod
|
||||
def from_gerber(cls, primitive):
|
||||
modifiers = primitive.strip(' *').split(",")
|
||||
|
|
@ -743,25 +908,42 @@ class AMCenterLinePrimitive(AMPrimitive):
|
|||
self.width = metric(self.width)
|
||||
self.height = metric(self.height)
|
||||
|
||||
def to_primitive(self, position, level_polarity, units):
|
||||
# Offset the primitive from macro position
|
||||
position = tuple([a + b for a , b in zip (position, self.center)])
|
||||
# Return a renderable primitive
|
||||
return Rectangle(position, self.width, self.height,
|
||||
level_polarity=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,
|
||||
center="%.4g,%.4g" % self.center,
|
||||
rotation=self.rotation
|
||||
)
|
||||
fmt = "{code},{exposure},{width},{height},{center},{rotation}*"
|
||||
return fmt.format(**data)
|
||||
|
||||
def to_primitive(self, units):
|
||||
|
||||
x = self.center[0]
|
||||
y = self.center[1]
|
||||
half_width = self.width / 2.0
|
||||
half_height = self.height / 2.0
|
||||
|
||||
points = []
|
||||
points.append((x - half_width, y + half_height))
|
||||
points.append((x - half_width, y - half_height))
|
||||
points.append((x + half_width, y - half_height))
|
||||
points.append((x + half_width, y + half_height))
|
||||
|
||||
aperture = Circle((0, 0), 0)
|
||||
|
||||
lines = []
|
||||
prev_point = rotate_point(points[3], self.rotation, self.center)
|
||||
for point in points:
|
||||
cur_point = rotate_point(point, self.rotation, self.center)
|
||||
|
||||
lines.append(Line(prev_point, cur_point, aperture))
|
||||
|
||||
return Outline(lines, units=units, level_polarity=self._level_polarity)
|
||||
|
||||
|
||||
class AMLowerLeftLinePrimitive(AMPrimitive):
|
||||
""" Aperture Macro Lower Left Line primitive. Code 22.
|
||||
|
|
@ -815,7 +997,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)
|
||||
|
|
@ -832,21 +1014,12 @@ class AMLowerLeftLinePrimitive(AMPrimitive):
|
|||
self.width = metric(self.width)
|
||||
self.height = metric(self.height)
|
||||
|
||||
def to_primitive(self, position, level_polarity, units):
|
||||
# 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(position, self.width, self.height,
|
||||
level_polarity=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
|
||||
)
|
||||
|
|
@ -855,7 +1028,6 @@ class AMLowerLeftLinePrimitive(AMPrimitive):
|
|||
|
||||
|
||||
class AMUnsupportPrimitive(AMPrimitive):
|
||||
|
||||
@classmethod
|
||||
def from_gerber(cls, primitive):
|
||||
return cls(primitive)
|
||||
|
|
|
|||
|
|
@ -74,9 +74,10 @@ class FileSettings(object):
|
|||
|
||||
elif zero_suppression is not None:
|
||||
if zero_suppression not in ['leading', 'trailing']:
|
||||
raise ValueError('Zero suppression must be either leading or \
|
||||
trailling')
|
||||
self.zero_suppression = zero_suppression
|
||||
# This is a common problem in Eagle files, so just suppress it
|
||||
self.zero_suppression = 'leading'
|
||||
else:
|
||||
self.zero_suppression = zero_suppression
|
||||
|
||||
elif zeros is not None:
|
||||
if zeros not in ['leading', 'trailing']:
|
||||
|
|
@ -168,6 +169,10 @@ class FileSettings(object):
|
|||
self.format == other.format and
|
||||
self.angle_units == other.angle_units)
|
||||
|
||||
def __str__(self):
|
||||
return ('<Settings: %s %s %s %s %s>' %
|
||||
(self.units, self.notation, self.zero_suppression, self.format, self.angle_units))
|
||||
|
||||
|
||||
class CamFile(object):
|
||||
""" Base class for Gerber/Excellon files.
|
||||
|
|
@ -265,13 +270,13 @@ class CamFile(object):
|
|||
if ctx is None:
|
||||
from .render import GerberCairoContext
|
||||
ctx = GerberCairoContext()
|
||||
ctx.set_bounds(self.bounds)
|
||||
ctx.set_bounds(self.bounding_box)
|
||||
ctx._paint_background()
|
||||
ctx.invert = invert
|
||||
ctx._new_render_layer()
|
||||
for p in self.primitives:
|
||||
ctx.render(p)
|
||||
ctx._paint()
|
||||
ctx._flatten()
|
||||
|
||||
if filename is not None:
|
||||
ctx.dump(filename)
|
||||
|
|
|
|||
|
|
@ -62,12 +62,10 @@ def loads(data, filename=None):
|
|||
|
||||
fmt = detect_file_format(data)
|
||||
if fmt == 'rs274x':
|
||||
return rs274x.loads(data, filename)
|
||||
return rs274x.loads(data, filename=filename)
|
||||
elif fmt == 'excellon':
|
||||
return excellon.loads(data, filename)
|
||||
return excellon.loads(data, filename=filename)
|
||||
elif fmt == 'ipc_d_356':
|
||||
return ipc356.loads(data, filename)
|
||||
return ipc356.loads(data, filename=filename)
|
||||
else:
|
||||
raise ParseError('Unable to detect file format')
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -26,15 +26,18 @@ This module provides Excellon file classes and parsing utilities
|
|||
import math
|
||||
import operator
|
||||
|
||||
from .cam import CamFile, FileSettings
|
||||
from .excellon_statements import *
|
||||
from .excellon_tool import ExcellonToolDefinitionParser
|
||||
from .primitives import Drill, Slot
|
||||
from .utils import inch, metric
|
||||
|
||||
|
||||
try:
|
||||
from cStringIO import StringIO
|
||||
except ImportError:
|
||||
except(ImportError):
|
||||
from io import StringIO
|
||||
|
||||
from .excellon_statements import *
|
||||
from .cam import CamFile, FileSettings
|
||||
from .primitives import Drill
|
||||
from .utils import inch, metric
|
||||
|
||||
|
||||
def read(filename):
|
||||
|
|
@ -56,8 +59,7 @@ def read(filename):
|
|||
settings = FileSettings(**detect_excellon_format(data))
|
||||
return ExcellonParser(settings).parse(filename)
|
||||
|
||||
|
||||
def loads(data, filename=None):
|
||||
def loads(data, filename=None, settings=None, tools=None):
|
||||
""" Read data from string and return an ExcellonFile
|
||||
Parameters
|
||||
----------
|
||||
|
|
@ -67,6 +69,9 @@ def loads(data, filename=None):
|
|||
filename : string, optional
|
||||
string containing the filename of the data source
|
||||
|
||||
tools: dict (optional)
|
||||
externally defined tools
|
||||
|
||||
Returns
|
||||
-------
|
||||
file : :class:`gerber.excellon.ExcellonFile`
|
||||
|
|
@ -74,12 +79,22 @@ def loads(data, filename=None):
|
|||
|
||||
"""
|
||||
# File object should use settings from source file by default.
|
||||
settings = FileSettings(**detect_excellon_format(data))
|
||||
return ExcellonParser(settings).parse_raw(data, filename)
|
||||
if not settings:
|
||||
settings = FileSettings(**detect_excellon_format(data))
|
||||
return ExcellonParser(settings, tools).parse_raw(data, filename)
|
||||
|
||||
|
||||
class DrillHit(object):
|
||||
"""Drill feature that is a single drill hole.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
tool : ExcellonTool
|
||||
Tool to drill the hole. Defines the size of the hole that is generated.
|
||||
position : tuple(float, float)
|
||||
Center position of the drill.
|
||||
|
||||
"""
|
||||
def __init__(self, tool, position):
|
||||
self.tool = tool
|
||||
self.position = position
|
||||
|
|
@ -94,6 +109,64 @@ class DrillHit(object):
|
|||
self.tool.to_metric()
|
||||
self.position = tuple(map(metric, self.position))
|
||||
|
||||
@property
|
||||
def bounding_box(self):
|
||||
position = self.position
|
||||
radius = self.tool.diameter / 2.
|
||||
|
||||
min_x = position[0] - radius
|
||||
max_x = position[0] + radius
|
||||
min_y = position[1] - radius
|
||||
max_y = position[1] + radius
|
||||
return ((min_x, max_x), (min_y, max_y))
|
||||
|
||||
def offset(self, x_offset, y_offset):
|
||||
self.position = tuple(map(operator.add, self.position, (x_offset, y_offset)))
|
||||
|
||||
def __str__(self):
|
||||
return 'Hit (%f, %f) {%s}' % (self.position[0], self.position[1], self.tool)
|
||||
|
||||
class DrillSlot(object):
|
||||
"""
|
||||
A slot is created between two points. The way the slot is created depends on the statement used to create it
|
||||
"""
|
||||
|
||||
TYPE_ROUT = 1
|
||||
TYPE_G85 = 2
|
||||
|
||||
def __init__(self, tool, start, end, slot_type):
|
||||
self.tool = tool
|
||||
self.start = start
|
||||
self.end = end
|
||||
self.slot_type = slot_type
|
||||
|
||||
def to_inch(self):
|
||||
if self.tool.units == 'metric':
|
||||
self.tool.to_inch()
|
||||
self.start = tuple(map(inch, self.start))
|
||||
self.end = tuple(map(inch, self.end))
|
||||
|
||||
def to_metric(self):
|
||||
if self.tool.units == 'inch':
|
||||
self.tool.to_metric()
|
||||
self.start = tuple(map(metric, self.start))
|
||||
self.end = tuple(map(metric, self.end))
|
||||
|
||||
@property
|
||||
def bounding_box(self):
|
||||
start = self.start
|
||||
end = self.end
|
||||
radius = self.tool.diameter / 2.
|
||||
min_x = min(start[0], end[0]) - radius
|
||||
max_x = max(start[0], end[0]) + radius
|
||||
min_y = min(start[1], end[1]) - radius
|
||||
max_y = max(start[1], end[1]) + radius
|
||||
return ((min_x, max_x), (min_y, max_y))
|
||||
|
||||
def offset(self, x_offset, y_offset):
|
||||
self.start = tuple(map(operator.add, self.start, (x_offset, y_offset)))
|
||||
self.end = tuple(map(operator.add, self.end, (x_offset, y_offset)))
|
||||
|
||||
|
||||
class ExcellonFile(CamFile):
|
||||
""" A class representing a single excellon file
|
||||
|
|
@ -132,19 +205,30 @@ class ExcellonFile(CamFile):
|
|||
|
||||
@property
|
||||
def primitives(self):
|
||||
return [Drill(hit.position, hit.tool.diameter, units=self.settings.units) for hit in self.hits]
|
||||
"""
|
||||
Gets the primitives. Note that unlike Gerber, this generates new objects
|
||||
"""
|
||||
primitives = []
|
||||
for hit in self.hits:
|
||||
if isinstance(hit, DrillHit):
|
||||
primitives.append(Drill(hit.position, hit.tool.diameter, hit, units=self.settings.units))
|
||||
elif isinstance(hit, DrillSlot):
|
||||
primitives.append(Slot(hit.start, hit.end, hit.tool.diameter, hit, units=self.settings.units))
|
||||
else:
|
||||
raise ValueError('Unknown hit type')
|
||||
|
||||
return primitives
|
||||
|
||||
@property
|
||||
def bounds(self):
|
||||
xmin = ymin = 100000000000
|
||||
xmax = ymax = -100000000000
|
||||
for hit in self.hits:
|
||||
radius = hit.tool.diameter / 2.
|
||||
x, y = hit.position
|
||||
xmin = min(x - radius, xmin)
|
||||
xmax = max(x + radius, xmax)
|
||||
ymin = min(y - radius, ymin)
|
||||
ymax = max(y + radius, ymax)
|
||||
bbox = hit.bounding_box
|
||||
xmin = min(bbox[0][0], xmin)
|
||||
xmax = max(bbox[0][1], xmax)
|
||||
ymin = min(bbox[1][0], ymin)
|
||||
ymax = max(bbox[1][1], ymax)
|
||||
return ((xmin, xmax), (ymin, ymax))
|
||||
|
||||
def report(self, filename=None):
|
||||
|
|
@ -206,7 +290,7 @@ class ExcellonFile(CamFile):
|
|||
for primitive in self.primitives:
|
||||
primitive.to_inch()
|
||||
for hit in self.hits:
|
||||
hit.position = tuple(map(inch, hit, position))
|
||||
hit.to_inch()
|
||||
|
||||
def to_metric(self):
|
||||
""" Convert units to metric
|
||||
|
|
@ -220,7 +304,7 @@ class ExcellonFile(CamFile):
|
|||
for primitive in self.primitives:
|
||||
primitive.to_metric()
|
||||
for hit in self.hits:
|
||||
hit.position = tuple(map(metric, hit.position))
|
||||
hit.to_metric()
|
||||
|
||||
def offset(self, x_offset=0, y_offset=0):
|
||||
for statement in self.statements:
|
||||
|
|
@ -228,8 +312,7 @@ class ExcellonFile(CamFile):
|
|||
for primitive in self.primitives:
|
||||
primitive.offset(x_offset, y_offset)
|
||||
for hit in self. hits:
|
||||
hit.position = tuple(map(operator.add, hit.position,
|
||||
(x_offset, y_offset)))
|
||||
hit.offset(x_offset, y_offset)
|
||||
|
||||
def path_length(self, tool_number=None):
|
||||
""" Return the path length for a given tool
|
||||
|
|
@ -239,8 +322,8 @@ 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)))
|
||||
|
|
@ -290,8 +373,7 @@ class ExcellonParser(object):
|
|||
settings : FileSettings or dict-like
|
||||
Excellon file settings to use when interpreting the excellon file.
|
||||
"""
|
||||
|
||||
def __init__(self, settings=None):
|
||||
def __init__(self, settings=None, ext_tools=None):
|
||||
self.notation = 'absolute'
|
||||
self.units = 'inch'
|
||||
self.zeros = 'leading'
|
||||
|
|
@ -299,9 +381,14 @@ class ExcellonParser(object):
|
|||
self.state = 'INIT'
|
||||
self.statements = []
|
||||
self.tools = {}
|
||||
self.ext_tools = ext_tools or {}
|
||||
self.comment_tools = {}
|
||||
self.hits = []
|
||||
self.active_tool = None
|
||||
self.pos = [0., 0.]
|
||||
self.drill_down = False
|
||||
# Default for plated is None, which means we don't know
|
||||
self.plated = ExcellonTool.PLATED_UNKNOWN
|
||||
if settings is not None:
|
||||
self.units = settings.units
|
||||
self.zeros = settings.zeros
|
||||
|
|
@ -362,6 +449,24 @@ class ExcellonParser(object):
|
|||
if detected_format:
|
||||
self.format = detected_format
|
||||
|
||||
if "TYPE=PLATED" in comment_stmt.comment:
|
||||
self.plated = ExcellonTool.PLATED_YES
|
||||
|
||||
if "TYPE=NON_PLATED" in comment_stmt.comment:
|
||||
self.plated = ExcellonTool.PLATED_NO
|
||||
|
||||
if "HEADER:" in comment_stmt.comment:
|
||||
self.state = "HEADER"
|
||||
|
||||
if " Holesize " in comment_stmt.comment:
|
||||
self.state = "HEADER"
|
||||
|
||||
# Parse this as a hole definition
|
||||
tools = ExcellonToolDefinitionParser(self._settings()).parse_raw(comment_stmt.comment)
|
||||
if len(tools) == 1:
|
||||
tool = tools[tools.keys()[0]]
|
||||
self._add_comment_tool(tool)
|
||||
|
||||
elif line[:3] == 'M48':
|
||||
self.statements.append(HeaderBeginStmt())
|
||||
self.state = 'HEADER'
|
||||
|
|
@ -373,6 +478,16 @@ class ExcellonParser(object):
|
|||
elif self.state == 'INIT':
|
||||
self.state = 'HEADER'
|
||||
|
||||
elif line[:3] == 'M00' and self.state == 'DRILL':
|
||||
if self.active_tool:
|
||||
cur_tool_number = self.active_tool.number
|
||||
next_tool = self._get_tool(cur_tool_number + 1)
|
||||
|
||||
self.statements.append(NextToolSelectionStmt(self.active_tool, next_tool))
|
||||
self.active_tool = next_tool
|
||||
else:
|
||||
raise Exception('Invalid state exception')
|
||||
|
||||
elif line[:3] == 'M95':
|
||||
self.statements.append(HeaderEndStmt())
|
||||
if self.state == 'HEADER':
|
||||
|
|
@ -380,12 +495,15 @@ class ExcellonParser(object):
|
|||
|
||||
elif line[:3] == 'M15':
|
||||
self.statements.append(ZAxisRoutPositionStmt())
|
||||
self.drill_down = True
|
||||
|
||||
elif line[:3] == 'M16':
|
||||
self.statements.append(RetractWithClampingStmt())
|
||||
self.drill_down = False
|
||||
|
||||
elif line[:3] == 'M17':
|
||||
self.statements.append(RetractWithoutClampingStmt())
|
||||
self.drill_down = False
|
||||
|
||||
elif line[:3] == 'M30':
|
||||
stmt = EndOfProgramStmt.from_excellon(line, self._settings())
|
||||
|
|
@ -419,6 +537,9 @@ class ExcellonParser(object):
|
|||
stmt = CoordinateStmt.from_excellon(line[3:], self._settings())
|
||||
stmt.mode = self.state
|
||||
|
||||
# The start position is where we were before the rout command
|
||||
start = (self.pos[0], self.pos[1])
|
||||
|
||||
x = stmt.x
|
||||
y = stmt.y
|
||||
self.statements.append(stmt)
|
||||
|
|
@ -433,14 +554,27 @@ class ExcellonParser(object):
|
|||
if y is not None:
|
||||
self.pos[1] += y
|
||||
|
||||
# Our ending position
|
||||
end = (self.pos[0], self.pos[1])
|
||||
|
||||
if self.drill_down:
|
||||
if not self.active_tool:
|
||||
self.active_tool = self._get_tool(1)
|
||||
|
||||
self.hits.append(DrillSlot(self.active_tool, start, end, DrillSlot.TYPE_ROUT))
|
||||
self.active_tool._hit()
|
||||
|
||||
elif line[:3] == 'G05':
|
||||
self.statements.append(DrillModeStmt())
|
||||
self.drill_down = False
|
||||
self.state = 'DRILL'
|
||||
|
||||
elif 'INCH' in line or 'METRIC' in line:
|
||||
stmt = UnitStmt.from_excellon(line)
|
||||
self.units = stmt.units
|
||||
self.zeros = stmt.zeros
|
||||
if stmt.format:
|
||||
self.format = stmt.format
|
||||
self.statements.append(stmt)
|
||||
|
||||
elif line[:3] == 'M71' or line[:3] == 'M72':
|
||||
|
|
@ -460,6 +594,7 @@ class ExcellonParser(object):
|
|||
elif line[:4] == 'FMAT':
|
||||
stmt = FormatStmt.from_excellon(line)
|
||||
self.statements.append(stmt)
|
||||
self.format = stmt.format_tuple
|
||||
|
||||
elif line[:3] == 'G40':
|
||||
self.statements.append(CutterCompensationOffStmt())
|
||||
|
|
@ -479,9 +614,13 @@ class ExcellonParser(object):
|
|||
self.statements.append(infeed_rate_stmt)
|
||||
|
||||
elif line[0] == 'T' and self.state == 'HEADER':
|
||||
tool = ExcellonTool.from_excellon(line, self._settings())
|
||||
self.tools[tool.number] = tool
|
||||
self.statements.append(tool)
|
||||
if not ',OFF' in line and not ',ON' in line:
|
||||
tool = ExcellonTool.from_excellon(line, self._settings(), None, self.plated)
|
||||
self._merge_properties(tool)
|
||||
self.tools[tool.number] = tool
|
||||
self.statements.append(tool)
|
||||
else:
|
||||
self.statements.append(UnknownStmt.from_excellon(line))
|
||||
|
||||
elif line[0] == 'T' and self.state != 'HEADER':
|
||||
stmt = ToolSelectionStmt.from_excellon(line)
|
||||
|
|
@ -489,9 +628,10 @@ class ExcellonParser(object):
|
|||
|
||||
# T0 is used as END marker, just ignore
|
||||
if stmt.tool != 0:
|
||||
# FIXME: for weird files with no tools defined, original calc
|
||||
# from gerbv
|
||||
if stmt.tool not in self.tools:
|
||||
tool = self._get_tool(stmt.tool)
|
||||
|
||||
if not tool:
|
||||
# FIXME: for weird files with no tools defined, original calc from gerb
|
||||
if self._settings().units == "inch":
|
||||
diameter = (16 + 8 * stmt.tool) / 1000.0
|
||||
else:
|
||||
|
|
@ -508,7 +648,7 @@ class ExcellonParser(object):
|
|||
self.statements.insert(i, tool)
|
||||
break
|
||||
|
||||
self.active_tool = self.tools[stmt.tool]
|
||||
self.active_tool = tool
|
||||
|
||||
elif line[0] == 'R' and self.state != 'HEADER':
|
||||
stmt = RepeatHoleStmt.from_excellon(line, self._settings())
|
||||
|
|
@ -520,23 +660,67 @@ class ExcellonParser(object):
|
|||
self.active_tool._hit()
|
||||
|
||||
elif line[0] in ['X', 'Y']:
|
||||
stmt = CoordinateStmt.from_excellon(line, self._settings())
|
||||
x = stmt.x
|
||||
y = stmt.y
|
||||
self.statements.append(stmt)
|
||||
if self.notation == 'absolute':
|
||||
if x is not None:
|
||||
self.pos[0] = x
|
||||
if y is not None:
|
||||
self.pos[1] = y
|
||||
if 'G85' in line:
|
||||
stmt = SlotStmt.from_excellon(line, self._settings())
|
||||
|
||||
# I don't know if this is actually correct, but it makes sense that this is where the tool would end
|
||||
x = stmt.x_end
|
||||
y = stmt.y_end
|
||||
|
||||
self.statements.append(stmt)
|
||||
|
||||
if self.notation == 'absolute':
|
||||
if x is not None:
|
||||
self.pos[0] = x
|
||||
if y is not None:
|
||||
self.pos[1] = y
|
||||
else:
|
||||
if x is not None:
|
||||
self.pos[0] += x
|
||||
if y is not None:
|
||||
self.pos[1] += y
|
||||
|
||||
if self.state == 'DRILL' or self.state == 'HEADER':
|
||||
if not self.active_tool:
|
||||
self.active_tool = self._get_tool(1)
|
||||
|
||||
self.hits.append(DrillSlot(self.active_tool, (stmt.x_start, stmt.y_start), (stmt.x_end, stmt.y_end), DrillSlot.TYPE_G85))
|
||||
self.active_tool._hit()
|
||||
else:
|
||||
if x is not None:
|
||||
self.pos[0] += x
|
||||
if y is not None:
|
||||
self.pos[1] += y
|
||||
if self.state == 'DRILL':
|
||||
self.hits.append(DrillHit(self.active_tool, tuple(self.pos)))
|
||||
self.active_tool._hit()
|
||||
stmt = CoordinateStmt.from_excellon(line, self._settings())
|
||||
|
||||
# We need this in case we are in rout mode
|
||||
start = (self.pos[0], self.pos[1])
|
||||
|
||||
x = stmt.x
|
||||
y = stmt.y
|
||||
self.statements.append(stmt)
|
||||
if self.notation == 'absolute':
|
||||
if x is not None:
|
||||
self.pos[0] = x
|
||||
if y is not None:
|
||||
self.pos[1] = y
|
||||
else:
|
||||
if x is not None:
|
||||
self.pos[0] += x
|
||||
if y is not None:
|
||||
self.pos[1] += y
|
||||
|
||||
if self.state == 'LINEAR' and self.drill_down:
|
||||
if not self.active_tool:
|
||||
self.active_tool = self._get_tool(1)
|
||||
|
||||
self.hits.append(DrillSlot(self.active_tool, start, tuple(self.pos), DrillSlot.TYPE_ROUT))
|
||||
|
||||
elif self.state == 'DRILL' or self.state == 'HEADER':
|
||||
# Yes, drills in the header doesn't follow the specification, but it there are many
|
||||
# files like this
|
||||
if not self.active_tool:
|
||||
self.active_tool = self._get_tool(1)
|
||||
|
||||
self.hits.append(DrillHit(self.active_tool, tuple(self.pos)))
|
||||
self.active_tool._hit()
|
||||
|
||||
else:
|
||||
self.statements.append(UnknownStmt.from_excellon(line))
|
||||
|
||||
|
|
@ -544,6 +728,49 @@ class ExcellonParser(object):
|
|||
return FileSettings(units=self.units, format=self.format,
|
||||
zeros=self.zeros, notation=self.notation)
|
||||
|
||||
def _add_comment_tool(self, tool):
|
||||
"""
|
||||
Add a tool that was defined in the comments to this file.
|
||||
|
||||
If we have already found this tool, then we will merge this comment tool definition into
|
||||
the information for the tool
|
||||
"""
|
||||
|
||||
existing = self.tools.get(tool.number)
|
||||
if existing and existing.plated == None:
|
||||
existing.plated = tool.plated
|
||||
|
||||
self.comment_tools[tool.number] = tool
|
||||
|
||||
def _merge_properties(self, tool):
|
||||
"""
|
||||
When we have externally defined tools, merge the properties of that tool into this one
|
||||
|
||||
For now, this is only plated
|
||||
"""
|
||||
|
||||
if tool.plated == ExcellonTool.PLATED_UNKNOWN:
|
||||
ext_tool = self.ext_tools.get(tool.number)
|
||||
|
||||
if ext_tool:
|
||||
tool.plated = ext_tool.plated
|
||||
|
||||
def _get_tool(self, toolid):
|
||||
|
||||
tool = self.tools.get(toolid)
|
||||
if not tool:
|
||||
tool = self.comment_tools.get(toolid)
|
||||
if tool:
|
||||
tool.settings = self._settings()
|
||||
self.tools[toolid] = tool
|
||||
|
||||
if not tool:
|
||||
tool = self.ext_tools.get(toolid)
|
||||
if tool:
|
||||
tool.settings = self._settings()
|
||||
self.tools[toolid] = tool
|
||||
|
||||
return tool
|
||||
|
||||
def detect_excellon_format(data=None, filename=None):
|
||||
""" Detect excellon file decimal format and zero-suppression settings.
|
||||
|
|
@ -646,6 +873,9 @@ def _layer_size_score(size, hole_count, hole_area):
|
|||
Lower is better.
|
||||
"""
|
||||
board_area = size[0] * size[1]
|
||||
if board_area == 0:
|
||||
return 0
|
||||
|
||||
hole_percentage = hole_area / board_area
|
||||
hole_score = (hole_percentage - 0.25) ** 2
|
||||
size_score = (board_area - 8) ** 2
|
||||
|
|
|
|||
25
gerber/excellon_report/excellon_drr.py
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2015 Garret Fick <garret@ficksworkshop.com>
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""
|
||||
Excellon DRR File module
|
||||
====================
|
||||
**Excellon file classes**
|
||||
|
||||
Extra parsers for allegro misc files that can be useful when the Excellon file doesn't contain parameter information
|
||||
"""
|
||||
|
||||
105
gerber/excellon_settings.py
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from argparse import PARSER
|
||||
|
||||
# Copyright 2015 Garret Fick <garret@ficksworkshop.com>
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""
|
||||
Excellon Settings Definition File module
|
||||
====================
|
||||
**Excellon file classes**
|
||||
|
||||
This module provides Excellon file classes and parsing utilities
|
||||
"""
|
||||
|
||||
import re
|
||||
try:
|
||||
from cStringIO import StringIO
|
||||
except(ImportError):
|
||||
from io import StringIO
|
||||
|
||||
from .cam import FileSettings
|
||||
|
||||
def loads(data):
|
||||
""" Read settings file information and return an FileSettings
|
||||
Parameters
|
||||
----------
|
||||
data : string
|
||||
string containing Excellon settings file contents
|
||||
|
||||
Returns
|
||||
-------
|
||||
file settings: FileSettings
|
||||
|
||||
"""
|
||||
|
||||
return ExcellonSettingsParser().parse_raw(data)
|
||||
|
||||
def map_coordinates(value):
|
||||
if value == 'ABSOLUTE':
|
||||
return 'absolute'
|
||||
return 'relative'
|
||||
|
||||
def map_units(value):
|
||||
if value == 'ENGLISH':
|
||||
return 'inch'
|
||||
return 'metric'
|
||||
|
||||
def map_boolean(value):
|
||||
return value == 'YES'
|
||||
|
||||
SETTINGS_KEYS = {
|
||||
'INTEGER-PLACES': (int, 'format-int'),
|
||||
'DECIMAL-PLACES': (int, 'format-dec'),
|
||||
'COORDINATES': (map_coordinates, 'notation'),
|
||||
'OUTPUT-UNITS': (map_units, 'units'),
|
||||
}
|
||||
|
||||
class ExcellonSettingsParser(object):
|
||||
"""Excellon Settings PARSER
|
||||
|
||||
Parameters
|
||||
----------
|
||||
None
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.values = {}
|
||||
self.settings = None
|
||||
|
||||
def parse_raw(self, data):
|
||||
for line in StringIO(data):
|
||||
self._parse(line.strip())
|
||||
|
||||
# Create the FileSettings object
|
||||
self.settings = FileSettings(
|
||||
notation=self.values['notation'],
|
||||
units=self.values['units'],
|
||||
format=(self.values['format-int'], self.values['format-dec'])
|
||||
)
|
||||
|
||||
return self.settings
|
||||
|
||||
def _parse(self, line):
|
||||
|
||||
line_items = line.split()
|
||||
if len(line_items) == 2:
|
||||
|
||||
item_type_info = SETTINGS_KEYS.get(line_items[0])
|
||||
if item_type_info:
|
||||
# Convert the value to the expected type
|
||||
item_value = item_type_info[0](line_items[1])
|
||||
|
||||
self.values[item_type_info[1]] = item_value
|
||||
|
|
@ -36,7 +36,8 @@ __all__ = ['ExcellonTool', 'ToolSelectionStmt', 'CoordinateStmt',
|
|||
'ExcellonStatement', 'ZAxisRoutPositionStmt',
|
||||
'RetractWithClampingStmt', 'RetractWithoutClampingStmt',
|
||||
'CutterCompensationOffStmt', 'CutterCompensationLeftStmt',
|
||||
'CutterCompensationRightStmt', 'ZAxisInfeedRateStmt']
|
||||
'CutterCompensationRightStmt', 'ZAxisInfeedRateStmt',
|
||||
'NextToolSelectionStmt', 'SlotStmt']
|
||||
|
||||
|
||||
class ExcellonStatement(object):
|
||||
|
|
@ -112,9 +113,29 @@ class ExcellonTool(ExcellonStatement):
|
|||
hit_count : integer
|
||||
Number of tool hits in excellon file.
|
||||
"""
|
||||
|
||||
PLATED_UNKNOWN = None
|
||||
PLATED_YES = 'plated'
|
||||
PLATED_NO = 'nonplated'
|
||||
PLATED_OPTIONAL = 'optional'
|
||||
|
||||
@classmethod
|
||||
def from_tool(cls, tool):
|
||||
args = {}
|
||||
|
||||
args['depth_offset'] = tool.depth_offset
|
||||
args['diameter'] = tool.diameter
|
||||
args['feed_rate'] = tool.feed_rate
|
||||
args['max_hit_count'] = tool.max_hit_count
|
||||
args['number'] = tool.number
|
||||
args['plated'] = tool.plated
|
||||
args['retract_rate'] = tool.retract_rate
|
||||
args['rpm'] = tool.rpm
|
||||
|
||||
return cls(None, **args)
|
||||
|
||||
@classmethod
|
||||
def from_excellon(cls, line, settings, id=None):
|
||||
def from_excellon(cls, line, settings, id=None, plated=None):
|
||||
""" Create a Tool from an excellon file tool definition line.
|
||||
|
||||
Parameters
|
||||
|
|
@ -151,6 +172,10 @@ class ExcellonTool(ExcellonStatement):
|
|||
args['number'] = int(val)
|
||||
elif cmd == 'Z':
|
||||
args['depth_offset'] = parse_gerber_value(val, nformat, zero_suppression)
|
||||
|
||||
if plated != ExcellonTool.PLATED_UNKNOWN:
|
||||
# Sometimees we can can parse the
|
||||
args['plated'] = plated
|
||||
return cls(settings, **args)
|
||||
|
||||
@classmethod
|
||||
|
|
@ -183,11 +208,15 @@ class ExcellonTool(ExcellonStatement):
|
|||
self.diameter = kwargs.get('diameter')
|
||||
self.max_hit_count = kwargs.get('max_hit_count')
|
||||
self.depth_offset = kwargs.get('depth_offset')
|
||||
self.plated = kwargs.get('plated')
|
||||
|
||||
self.hit_count = 0
|
||||
|
||||
def to_excellon(self, settings=None):
|
||||
fmt = self.settings.format
|
||||
zs = self.settings.zero_suppression
|
||||
if self.settings and not settings:
|
||||
settings = self.settings
|
||||
fmt = settings.format
|
||||
zs = settings.zero_suppression
|
||||
stmt = 'T%02d' % self.number
|
||||
if self.retract_rate is not None:
|
||||
stmt += 'B%s' % write_gerber_value(self.retract_rate, fmt, zs)
|
||||
|
|
@ -220,6 +249,23 @@ class ExcellonTool(ExcellonStatement):
|
|||
|
||||
def _hit(self):
|
||||
self.hit_count += 1
|
||||
|
||||
def equivalent(self, other):
|
||||
"""
|
||||
Is the other tool equal to this, ignoring the tool number, and other file specified properties
|
||||
"""
|
||||
|
||||
if type(self) != type(other):
|
||||
return False
|
||||
|
||||
return (self.diameter == other.diameter
|
||||
and self.feed_rate == other.feed_rate
|
||||
and self.retract_rate == other.retract_rate
|
||||
and self.rpm == other.rpm
|
||||
and self.depth_offset == other.depth_offset
|
||||
and self.max_hit_count == other.max_hit_count
|
||||
and self.plated == other.plated
|
||||
and self.settings.units == other.settings.units)
|
||||
|
||||
def __repr__(self):
|
||||
unit = 'in.' if self.settings.units == 'inch' else 'mm'
|
||||
|
|
@ -268,7 +314,28 @@ class ToolSelectionStmt(ExcellonStatement):
|
|||
if self.compensation_index is not None:
|
||||
stmt += '%02d' % self.compensation_index
|
||||
return stmt
|
||||
|
||||
|
||||
class NextToolSelectionStmt(ExcellonStatement):
|
||||
|
||||
# TODO the statement exists outside of the context of the file,
|
||||
# so it is imposible to know that it is really the next tool
|
||||
|
||||
def __init__(self, cur_tool, next_tool, **kwargs):
|
||||
"""
|
||||
Select the next tool in the wheel.
|
||||
Parameters
|
||||
----------
|
||||
cur_tool : the tool that is currently selected
|
||||
next_tool : the that that is now selected
|
||||
"""
|
||||
super(NextToolSelectionStmt, self).__init__(**kwargs)
|
||||
|
||||
self.cur_tool = cur_tool
|
||||
self.next_tool = next_tool
|
||||
|
||||
def to_excellon(self, settings=None):
|
||||
stmt = 'M00'
|
||||
return stmt
|
||||
|
||||
class ZAxisInfeedRateStmt(ExcellonStatement):
|
||||
|
||||
|
|
@ -300,6 +367,14 @@ class ZAxisInfeedRateStmt(ExcellonStatement):
|
|||
|
||||
class CoordinateStmt(ExcellonStatement):
|
||||
|
||||
@classmethod
|
||||
def from_point(cls, point, mode=None):
|
||||
|
||||
stmt = cls(point[0], point[1])
|
||||
if mode:
|
||||
stmt.mode = mode
|
||||
return stmt
|
||||
|
||||
@classmethod
|
||||
def from_excellon(cls, line, settings, **kwargs):
|
||||
x_coord = None
|
||||
|
|
@ -576,19 +651,35 @@ class EndOfProgramStmt(ExcellonStatement):
|
|||
|
||||
|
||||
class UnitStmt(ExcellonStatement):
|
||||
|
||||
@classmethod
|
||||
def from_settings(cls, settings):
|
||||
"""Create the unit statement from the FileSettings"""
|
||||
|
||||
return cls(settings.units, settings.zeros)
|
||||
|
||||
@classmethod
|
||||
def from_excellon(cls, line, **kwargs):
|
||||
units = 'inch' if 'INCH' in line else 'metric'
|
||||
zeros = 'leading' if 'LZ' in line else 'trailing'
|
||||
return cls(units, zeros, **kwargs)
|
||||
if '0000.00' in line:
|
||||
format = (4, 2)
|
||||
elif '000.000' in line:
|
||||
format = (3, 3)
|
||||
elif '00.0000' in line:
|
||||
format = (2, 4)
|
||||
else:
|
||||
format = None
|
||||
return cls(units, zeros, format, **kwargs)
|
||||
|
||||
def __init__(self, units='inch', zeros='leading', **kwargs):
|
||||
def __init__(self, units='inch', zeros='leading', format=None, **kwargs):
|
||||
super(UnitStmt, self).__init__(**kwargs)
|
||||
self.units = units.lower()
|
||||
self.zeros = zeros
|
||||
self.format = format
|
||||
|
||||
def to_excellon(self, settings=None):
|
||||
# TODO This won't export the invalid format statement if it exists
|
||||
stmt = '%s,%s' % ('INCH' if self.units == 'inch' else 'METRIC',
|
||||
'LZ' if self.zeros == 'leading'
|
||||
else 'TZ')
|
||||
|
|
@ -651,6 +742,10 @@ class FormatStmt(ExcellonStatement):
|
|||
|
||||
def to_excellon(self, settings=None):
|
||||
return 'FMAT,%d' % self.format
|
||||
|
||||
@property
|
||||
def format_tuple(self):
|
||||
return (self.format, 6 - self.format)
|
||||
|
||||
|
||||
class LinkToolStmt(ExcellonStatement):
|
||||
|
|
@ -746,6 +841,133 @@ class UnknownStmt(ExcellonStatement):
|
|||
return "<Unknown Statement: %s>" % self.stmt
|
||||
|
||||
|
||||
class SlotStmt(ExcellonStatement):
|
||||
"""
|
||||
G85 statement. Defines a slot created by multiple drills between two specified points.
|
||||
|
||||
Format is two coordinates, split by G85in the middle, for example, XnY0nG85XnYn
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def from_points(cls, start, end):
|
||||
|
||||
return cls(start[0], start[1], end[0], end[1])
|
||||
|
||||
@classmethod
|
||||
def from_excellon(cls, line, settings, **kwargs):
|
||||
# Split the line based on the G85 separator
|
||||
sub_coords = line.split('G85')
|
||||
(x_start_coord, y_start_coord) = SlotStmt.parse_sub_coords(sub_coords[0], settings)
|
||||
(x_end_coord, y_end_coord) = SlotStmt.parse_sub_coords(sub_coords[1], settings)
|
||||
|
||||
# Some files seem to specify only one of the coordinates
|
||||
if x_end_coord == None:
|
||||
x_end_coord = x_start_coord
|
||||
if y_end_coord == None:
|
||||
y_end_coord = y_start_coord
|
||||
|
||||
c = cls(x_start_coord, y_start_coord, x_end_coord, y_end_coord, **kwargs)
|
||||
c.units = settings.units
|
||||
return c
|
||||
|
||||
@staticmethod
|
||||
def parse_sub_coords(line, settings):
|
||||
|
||||
x_coord = None
|
||||
y_coord = None
|
||||
|
||||
if line[0] == 'X':
|
||||
splitline = line.strip('X').split('Y')
|
||||
x_coord = parse_gerber_value(splitline[0], settings.format,
|
||||
settings.zero_suppression)
|
||||
if len(splitline) == 2:
|
||||
y_coord = parse_gerber_value(splitline[1], settings.format,
|
||||
settings.zero_suppression)
|
||||
else:
|
||||
y_coord = parse_gerber_value(line.strip(' Y'), settings.format,
|
||||
settings.zero_suppression)
|
||||
|
||||
return (x_coord, y_coord)
|
||||
|
||||
|
||||
def __init__(self, x_start=None, y_start=None, x_end=None, y_end=None, **kwargs):
|
||||
super(SlotStmt, self).__init__(**kwargs)
|
||||
self.x_start = x_start
|
||||
self.y_start = y_start
|
||||
self.x_end = x_end
|
||||
self.y_end = y_end
|
||||
self.mode = None
|
||||
|
||||
def to_excellon(self, settings):
|
||||
stmt = ''
|
||||
|
||||
if self.x_start is not None:
|
||||
stmt += 'X%s' % write_gerber_value(self.x_start, settings.format,
|
||||
settings.zero_suppression)
|
||||
if self.y_start is not None:
|
||||
stmt += 'Y%s' % write_gerber_value(self.y_start, settings.format,
|
||||
settings.zero_suppression)
|
||||
|
||||
stmt += 'G85'
|
||||
|
||||
if self.x_end is not None:
|
||||
stmt += 'X%s' % write_gerber_value(self.x_end, settings.format,
|
||||
settings.zero_suppression)
|
||||
if self.y_end is not None:
|
||||
stmt += 'Y%s' % write_gerber_value(self.y_end, settings.format,
|
||||
settings.zero_suppression)
|
||||
|
||||
return stmt
|
||||
|
||||
def to_inch(self):
|
||||
if self.units == 'metric':
|
||||
self.units = 'inch'
|
||||
if self.x_start is not None:
|
||||
self.x_start = inch(self.x_start)
|
||||
if self.y_start is not None:
|
||||
self.y_start = inch(self.y_start)
|
||||
if self.x_end is not None:
|
||||
self.x_end = inch(self.x_end)
|
||||
if self.y_end is not None:
|
||||
self.y_end = inch(self.y_end)
|
||||
|
||||
def to_metric(self):
|
||||
if self.units == 'inch':
|
||||
self.units = 'metric'
|
||||
if self.x_start is not None:
|
||||
self.x_start = metric(self.x_start)
|
||||
if self.y_start is not None:
|
||||
self.y_start = metric(self.y_start)
|
||||
if self.x_end is not None:
|
||||
self.x_end = metric(self.x_end)
|
||||
if self.y_end is not None:
|
||||
self.y_end = metric(self.y_end)
|
||||
|
||||
def offset(self, x_offset=0, y_offset=0):
|
||||
if self.x_start is not None:
|
||||
self.x_start += x_offset
|
||||
if self.y_start is not None:
|
||||
self.y_start += y_offset
|
||||
if self.x_end is not None:
|
||||
self.x_end += x_offset
|
||||
if self.y_end is not None:
|
||||
self.y_end += y_offset
|
||||
|
||||
def __str__(self):
|
||||
start_str = ''
|
||||
if self.x_start is not None:
|
||||
start_str += 'X: %g ' % self.x_start
|
||||
if self.y_start is not None:
|
||||
start_str += 'Y: %g ' % self.y_start
|
||||
|
||||
end_str = ''
|
||||
if self.x_end is not None:
|
||||
end_str += 'X: %g ' % self.x_end
|
||||
if self.y_end is not None:
|
||||
end_str += 'Y: %g ' % self.y_end
|
||||
|
||||
return '<Slot Statement: %s to %s>' % (start_str, end_str)
|
||||
|
||||
def pairwise(iterator):
|
||||
""" Iterate over list taking two elements at a time.
|
||||
|
||||
|
|
|
|||
186
gerber/excellon_tool.py
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2015 Garret Fick <garret@ficksworkshop.com>
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""
|
||||
Excellon Tool Definition File module
|
||||
====================
|
||||
**Excellon file classes**
|
||||
|
||||
This module provides Excellon file classes and parsing utilities
|
||||
"""
|
||||
|
||||
import re
|
||||
try:
|
||||
from cStringIO import StringIO
|
||||
except(ImportError):
|
||||
from io import StringIO
|
||||
|
||||
from .excellon_statements import ExcellonTool
|
||||
|
||||
def loads(data, settings=None):
|
||||
""" Read tool file information and return a map of tools
|
||||
Parameters
|
||||
----------
|
||||
data : string
|
||||
string containing Excellon Tool Definition file contents
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict tool name: ExcellonTool
|
||||
|
||||
"""
|
||||
return ExcellonToolDefinitionParser(settings).parse_raw(data)
|
||||
|
||||
class ExcellonToolDefinitionParser(object):
|
||||
""" Excellon File Parser
|
||||
|
||||
Parameters
|
||||
----------
|
||||
None
|
||||
"""
|
||||
|
||||
allegro_tool = re.compile(r'(?P<size>[0-9/.]+)\s+(?P<plated>P|N)\s+T(?P<toolid>[0-9]{2})\s+(?P<xtol>[0-9/.]+)\s+(?P<ytol>[0-9/.]+)')
|
||||
allegro_comment_mils = re.compile('Holesize (?P<toolid>[0-9]{1,2})\. = (?P<size>[0-9/.]+) Tolerance = \+(?P<xtol>[0-9/.]+)/-(?P<ytol>[0-9/.]+) (?P<plated>(PLATED)|(NON_PLATED)|(OPTIONAL)) MILS Quantity = [0-9]+')
|
||||
allegro2_comment_mils = re.compile('T(?P<toolid>[0-9]{1,2}) Holesize (?P<toolid2>[0-9]{1,2})\. = (?P<size>[0-9/.]+) Tolerance = \+(?P<xtol>[0-9/.]+)/-(?P<ytol>[0-9/.]+) (?P<plated>(PLATED)|(NON_PLATED)|(OPTIONAL)) MILS Quantity = [0-9]+')
|
||||
allegro_comment_mm = re.compile('Holesize (?P<toolid>[0-9]{1,2})\. = (?P<size>[0-9/.]+) Tolerance = \+(?P<xtol>[0-9/.]+)/-(?P<ytol>[0-9/.]+) (?P<plated>(PLATED)|(NON_PLATED)|(OPTIONAL)) MM Quantity = [0-9]+')
|
||||
allegro2_comment_mm = re.compile('T(?P<toolid>[0-9]{1,2}) Holesize (?P<toolid2>[0-9]{1,2})\. = (?P<size>[0-9/.]+) Tolerance = \+(?P<xtol>[0-9/.]+)/-(?P<ytol>[0-9/.]+) (?P<plated>(PLATED)|(NON_PLATED)|(OPTIONAL)) MM Quantity = [0-9]+')
|
||||
|
||||
matchers = [
|
||||
(allegro_tool, 'mils'),
|
||||
(allegro_comment_mils, 'mils'),
|
||||
(allegro2_comment_mils, 'mils'),
|
||||
(allegro_comment_mm, 'mm'),
|
||||
(allegro2_comment_mm, 'mm'),
|
||||
]
|
||||
|
||||
def __init__(self, settings=None):
|
||||
self.tools = {}
|
||||
self.settings = settings
|
||||
|
||||
def parse_raw(self, data):
|
||||
for line in StringIO(data):
|
||||
self._parse(line.strip())
|
||||
|
||||
return self.tools
|
||||
|
||||
def _parse(self, line):
|
||||
|
||||
for matcher in ExcellonToolDefinitionParser.matchers:
|
||||
m = matcher[0].match(line)
|
||||
if m:
|
||||
unit = matcher[1]
|
||||
|
||||
size = float(m.group('size'))
|
||||
platedstr = m.group('plated')
|
||||
toolid = int(m.group('toolid'))
|
||||
xtol = float(m.group('xtol'))
|
||||
ytol = float(m.group('ytol'))
|
||||
|
||||
size = self._convert_length(size, unit)
|
||||
xtol = self._convert_length(xtol, unit)
|
||||
ytol = self._convert_length(ytol, unit)
|
||||
|
||||
if platedstr == 'PLATED':
|
||||
plated = ExcellonTool.PLATED_YES
|
||||
elif platedstr == 'NON_PLATED':
|
||||
plated = ExcellonTool.PLATED_NO
|
||||
elif platedstr == 'OPTIONAL':
|
||||
plated = ExcellonTool.PLATED_OPTIONAL
|
||||
else:
|
||||
plated = ExcellonTool.PLATED_UNKNOWN
|
||||
|
||||
tool = ExcellonTool(None, number=toolid, diameter=size, plated=plated)
|
||||
|
||||
self.tools[tool.number] = tool
|
||||
|
||||
break
|
||||
|
||||
def _convert_length(self, value, unit):
|
||||
|
||||
# Convert the value to mm
|
||||
if unit == 'mils':
|
||||
value /= 39.3700787402
|
||||
|
||||
# Now convert to the settings unit
|
||||
if self.settings.units == 'inch':
|
||||
return value / 25.4
|
||||
else:
|
||||
# Already in mm
|
||||
return value
|
||||
|
||||
def loads_rep(data, settings=None):
|
||||
""" Read tool report information generated by PADS and return a map of tools
|
||||
Parameters
|
||||
----------
|
||||
data : string
|
||||
string containing Excellon Report file contents
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict tool name: ExcellonTool
|
||||
|
||||
"""
|
||||
return ExcellonReportParser(settings).parse_raw(data)
|
||||
|
||||
class ExcellonReportParser(object):
|
||||
|
||||
# We sometimes get files with different encoding, so we can't actually
|
||||
# match the text - the best we can do it detect the table header
|
||||
header = re.compile(r'====\s+====\s+====\s+====\s+=====\s+===')
|
||||
|
||||
def __init__(self, settings=None):
|
||||
self.tools = {}
|
||||
self.settings = settings
|
||||
|
||||
self.found_header = False
|
||||
|
||||
def parse_raw(self, data):
|
||||
for line in StringIO(data):
|
||||
self._parse(line.strip())
|
||||
|
||||
return self.tools
|
||||
|
||||
def _parse(self, line):
|
||||
|
||||
# skip empty lines and "comments"
|
||||
if not line.strip():
|
||||
return
|
||||
|
||||
if not self.found_header:
|
||||
# Try to find the heaader, since we need that to be sure we understand the contents correctly.
|
||||
if ExcellonReportParser.header.match(line):
|
||||
self.found_header = True
|
||||
|
||||
elif line[0] != '=':
|
||||
# Already found the header, so we know to to map the contents
|
||||
parts = line.split()
|
||||
if len(parts) == 6:
|
||||
toolid = int(parts[0])
|
||||
size = float(parts[1])
|
||||
if parts[2] == 'x':
|
||||
plated = ExcellonTool.PLATED_YES
|
||||
elif parts[2] == '-':
|
||||
plated = ExcellonTool.PLATED_NO
|
||||
else:
|
||||
plated = ExcellonTool.PLATED_UNKNOWN
|
||||
feedrate = int(parts[3])
|
||||
speed = int(parts[4])
|
||||
qty = int(parts[5])
|
||||
|
||||
tool = ExcellonTool(None, number=toolid, diameter=size, plated=plated, feed_rate=feedrate, rpm=speed)
|
||||
|
||||
self.tools[tool.number] = tool
|
||||
|
|
@ -26,6 +26,7 @@ from .utils import (parse_gerber_value, write_gerber_value, decimal_string,
|
|||
from .am_statements import *
|
||||
from .am_read import read_macro
|
||||
from .am_eval import eval_macro
|
||||
from .primitives import AMGroup
|
||||
|
||||
|
||||
class Statement(object):
|
||||
|
|
@ -95,6 +96,11 @@ class FSParamStmt(ParamStmt):
|
|||
""" FS - Gerber Format Specification Statement
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def from_settings(cls, settings):
|
||||
|
||||
return cls('FS', settings.zero_suppression, settings.notation, settings.format)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, stmt_dict):
|
||||
"""
|
||||
|
|
@ -168,6 +174,10 @@ class MOParamStmt(ParamStmt):
|
|||
""" MO - Gerber Mode (measurement units) Statement.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def from_units(cls, units):
|
||||
return cls(None, units)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, stmt_dict):
|
||||
param = stmt_dict.get('param')
|
||||
|
|
@ -226,6 +236,11 @@ class LPParamStmt(ParamStmt):
|
|||
lp = 'clear' if stmt_dict.get('lp') == 'C' else 'dark'
|
||||
return cls(param, lp)
|
||||
|
||||
@classmethod
|
||||
def from_region(cls, region):
|
||||
#todo what is the first param?
|
||||
return cls(None, region.level_polarity)
|
||||
|
||||
def __init__(self, param, lp):
|
||||
""" Initialize LPParamStmt class
|
||||
|
||||
|
|
@ -258,6 +273,33 @@ class ADParamStmt(ParamStmt):
|
|||
""" AD - Gerber Aperture Definition Statement
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def rect(cls, dcode, width, height):
|
||||
'''Create a rectangular aperture definition statement'''
|
||||
return cls('AD', dcode, 'R', ([width, height],))
|
||||
|
||||
@classmethod
|
||||
def circle(cls, dcode, diameter, hole_diameter):
|
||||
'''Create a circular aperture definition statement'''
|
||||
|
||||
if hole_diameter != None:
|
||||
return cls('AD', dcode, 'C', ([diameter, hole_diameter],))
|
||||
return cls('AD', dcode, 'C', ([diameter],))
|
||||
|
||||
@classmethod
|
||||
def obround(cls, dcode, width, height):
|
||||
'''Create an obround aperture definition statement'''
|
||||
return cls('AD', dcode, 'O', ([width, height],))
|
||||
|
||||
@classmethod
|
||||
def polygon(cls, dcode, diameter, num_vertices, rotation, hole_diameter):
|
||||
'''Create a polygon aperture definition statement'''
|
||||
return cls('AD', dcode, 'P', ([diameter, num_vertices, rotation, hole_diameter],))
|
||||
|
||||
@classmethod
|
||||
def macro(cls, dcode, name):
|
||||
return cls('AD', dcode, name, '')
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, stmt_dict):
|
||||
param = stmt_dict.get('param')
|
||||
|
|
@ -292,7 +334,9 @@ class ADParamStmt(ParamStmt):
|
|||
ParamStmt.__init__(self, param)
|
||||
self.d = d
|
||||
self.shape = shape
|
||||
if modifiers:
|
||||
if isinstance(modifiers, tuple):
|
||||
self.modifiers = modifiers
|
||||
elif modifiers:
|
||||
self.modifiers = [tuple([float(x) for x in m.split("X") if len(x)])
|
||||
for m in modifiers.split(",") if len(m)]
|
||||
else:
|
||||
|
|
@ -393,7 +437,8 @@ class AMParamStmt(ParamStmt):
|
|||
else:
|
||||
self.primitives.append(
|
||||
AMUnsupportPrimitive.from_gerber(primitive))
|
||||
return self
|
||||
|
||||
return AMGroup(self.primitives, stmt=self, units=self.units)
|
||||
|
||||
def to_inch(self):
|
||||
if self.units == 'metric':
|
||||
|
|
@ -820,6 +865,14 @@ class CoordStmt(Statement):
|
|||
""" Coordinate Data Block
|
||||
"""
|
||||
|
||||
OP_DRAW = 'D01'
|
||||
OP_MOVE = 'D02'
|
||||
OP_FLASH = 'D03'
|
||||
|
||||
FUNC_LINEAR = 'G01'
|
||||
FUNC_ARC_CW = 'G02'
|
||||
FUNC_ARC_CCW = 'G03'
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, stmt_dict, settings):
|
||||
function = stmt_dict['function']
|
||||
|
|
@ -843,6 +896,32 @@ class CoordStmt(Statement):
|
|||
settings.zero_suppression)
|
||||
return cls(function, x, y, i, j, op, settings)
|
||||
|
||||
@classmethod
|
||||
def move(cls, func, point):
|
||||
if point:
|
||||
return cls(func, point[0], point[1], None, None, CoordStmt.OP_MOVE, None)
|
||||
# No point specified, so just write the function. This is normally for ending a region (D02*)
|
||||
return cls(func, None, None, None, None, CoordStmt.OP_MOVE, None)
|
||||
|
||||
@classmethod
|
||||
def line(cls, func, point):
|
||||
return cls(func, point[0], point[1], None, None, CoordStmt.OP_DRAW, None)
|
||||
|
||||
@classmethod
|
||||
def mode(cls, func):
|
||||
return cls(func, None, None, None, None, None, None)
|
||||
|
||||
@classmethod
|
||||
def arc(cls, func, point, center):
|
||||
return cls(func, point[0], point[1], center[0], center[1], CoordStmt.OP_DRAW, None)
|
||||
|
||||
@classmethod
|
||||
def flash(cls, point):
|
||||
if point:
|
||||
return cls(None, point[0], point[1], None, None, CoordStmt.OP_FLASH, None)
|
||||
else:
|
||||
return cls(None, None, None, None, None, CoordStmt.OP_FLASH, None)
|
||||
|
||||
def __init__(self, function, x, y, i, j, op, settings):
|
||||
""" Initialize CoordStmt class
|
||||
|
||||
|
|
@ -966,6 +1045,16 @@ class CoordStmt(Statement):
|
|||
|
||||
return '<Coordinate Statement: %s>' % coord_str
|
||||
|
||||
@property
|
||||
def only_function(self):
|
||||
"""
|
||||
Returns if the statement only set the function.
|
||||
"""
|
||||
|
||||
# TODO I would like to refactor this so that the function is handled separately and then
|
||||
# TODO this isn't required
|
||||
return self.function != None and self.op == None and self.x == None and self.y == None and self.i == None and self.j == None
|
||||
|
||||
|
||||
class ApertureStmt(Statement):
|
||||
""" Aperture Statement
|
||||
|
|
@ -1017,6 +1106,14 @@ class EofStmt(Statement):
|
|||
|
||||
class QuadrantModeStmt(Statement):
|
||||
|
||||
@classmethod
|
||||
def single(cls):
|
||||
return cls('single-quadrant')
|
||||
|
||||
@classmethod
|
||||
def multi(cls):
|
||||
return cls('multi-quadrant')
|
||||
|
||||
@classmethod
|
||||
def from_gerber(cls, line):
|
||||
if 'G74' not in line and 'G75' not in line:
|
||||
|
|
@ -1045,6 +1142,14 @@ class RegionModeStmt(Statement):
|
|||
raise ValueError('%s is not a valid region mode statement' % line)
|
||||
return (cls('on') if line[:3] == 'G36' else cls('off'))
|
||||
|
||||
@classmethod
|
||||
def on(cls):
|
||||
return cls('on')
|
||||
|
||||
@classmethod
|
||||
def off(cls):
|
||||
return cls('off')
|
||||
|
||||
def __init__(self, mode):
|
||||
super(RegionModeStmt, self).__init__('RegionMode')
|
||||
mode = mode.lower()
|
||||
|
|
|
|||
|
|
@ -109,12 +109,15 @@ def sort_layers(layers, from_top=True):
|
|||
append_after = ['drill', 'drawing']
|
||||
|
||||
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']))
|
||||
|
||||
for layer_class in layer_order:
|
||||
if layer_class == 'internal':
|
||||
output += internal_layers
|
||||
elif layer_class == 'drill':
|
||||
output += drill_layers
|
||||
else:
|
||||
for layer in layers:
|
||||
if layer.layer_class == layer_class:
|
||||
|
|
|
|||
25
gerber/ncparam/allegro.py
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2015 Garret Fick <garret@ficksworkshop.com>
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""
|
||||
Allegro File module
|
||||
====================
|
||||
**Excellon file classes**
|
||||
|
||||
Extra parsers for allegro misc files that can be useful when the Excellon file doesn't contain parameter information
|
||||
"""
|
||||
|
||||
|
|
@ -19,8 +19,10 @@
|
|||
import math
|
||||
from operator import add
|
||||
from itertools import combinations
|
||||
|
||||
from .utils import validate_coordinates, inch, metric, convex_hull
|
||||
from .utils import rotate_point, nearly_equal
|
||||
|
||||
|
||||
|
||||
|
||||
class Primitive(object):
|
||||
|
|
@ -59,6 +61,13 @@ class Primitive(object):
|
|||
self._vertices = None
|
||||
self._segments = None
|
||||
|
||||
@property
|
||||
def flashed(self):
|
||||
'''Is this a flashed primitive'''
|
||||
|
||||
raise NotImplementedError('Is flashed must be '
|
||||
'implemented in subclass')
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.__dict__ == other.__dict__
|
||||
|
||||
|
|
@ -105,6 +114,17 @@ class Primitive(object):
|
|||
raise NotImplementedError('Bounding box calculation must be '
|
||||
'implemented in subclass')
|
||||
|
||||
@property
|
||||
def bounding_box_no_aperture(self):
|
||||
""" Calculate bouxing box without considering the aperture
|
||||
|
||||
for most objects, this is the same as the bounding_box, but is different for
|
||||
Lines and Arcs (which are not flashed)
|
||||
|
||||
Return ((min x, max x), (min y, max y))
|
||||
"""
|
||||
return self.bounding_box
|
||||
|
||||
def to_inch(self):
|
||||
""" Convert primitive units to inches.
|
||||
"""
|
||||
|
|
@ -166,6 +186,9 @@ class Primitive(object):
|
|||
in zip(self.position,
|
||||
(x_offset, y_offset))])
|
||||
|
||||
def to_statement(self):
|
||||
pass
|
||||
|
||||
def _changed(self):
|
||||
""" Clear memoized properties.
|
||||
|
||||
|
|
@ -180,7 +203,6 @@ class Primitive(object):
|
|||
for attr in self._memoized:
|
||||
setattr(self, attr, None)
|
||||
|
||||
|
||||
class Line(Primitive):
|
||||
"""
|
||||
"""
|
||||
|
|
@ -192,6 +214,10 @@ class Line(Primitive):
|
|||
self.aperture = aperture
|
||||
self._to_convert = ['start', 'end', 'aperture']
|
||||
|
||||
@property
|
||||
def flashed(self):
|
||||
return False
|
||||
|
||||
@property
|
||||
def start(self):
|
||||
return self._start
|
||||
|
|
@ -210,7 +236,6 @@ class Line(Primitive):
|
|||
self._changed()
|
||||
self._end = value
|
||||
|
||||
|
||||
@property
|
||||
def angle(self):
|
||||
delta_x, delta_y = tuple(
|
||||
|
|
@ -234,6 +259,14 @@ class Line(Primitive):
|
|||
self._bounding_box = ((min_x, max_x), (min_y, max_y))
|
||||
return self._bounding_box
|
||||
|
||||
@property
|
||||
def bounding_box_no_aperture(self):
|
||||
'''Gets the bounding box without the aperture'''
|
||||
min_x = min(self.start[0], self.end[0])
|
||||
max_x = max(self.start[0], self.end[0])
|
||||
min_y = min(self.start[1], self.end[1])
|
||||
max_y = max(self.start[1], self.end[1])
|
||||
return ((min_x, max_x), (min_y, max_y))
|
||||
|
||||
@property
|
||||
def vertices(self):
|
||||
|
|
@ -265,20 +298,35 @@ class Line(Primitive):
|
|||
self.end = tuple([coord + offset for coord, offset
|
||||
in zip(self.end, (x_offset, y_offset))])
|
||||
|
||||
def equivalent(self, other, offset):
|
||||
|
||||
if not isinstance(other, Line):
|
||||
return False
|
||||
|
||||
equiv_start = tuple(map(add, other.start, offset))
|
||||
equiv_end = tuple(map(add, other.end, offset))
|
||||
|
||||
|
||||
return nearly_equal(self.start, equiv_start) and nearly_equal(self.end, equiv_end)
|
||||
|
||||
class Arc(Primitive):
|
||||
"""
|
||||
"""
|
||||
|
||||
def __init__(self, start, end, center, direction, aperture, **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.direction = direction
|
||||
self.aperture = aperture
|
||||
self._quadrant_mode = quadrant_mode
|
||||
self._to_convert = ['start', 'end', 'center', 'aperture']
|
||||
|
||||
@property
|
||||
def flashed(self):
|
||||
return False
|
||||
|
||||
@property
|
||||
def start(self):
|
||||
return self._start
|
||||
|
|
@ -306,6 +354,15 @@ class Arc(Primitive):
|
|||
self._changed()
|
||||
self._center = value
|
||||
|
||||
@property
|
||||
def quadrant_mode(self):
|
||||
return self._quadrant_mode
|
||||
|
||||
@quadrant_mode.setter
|
||||
def quadrant_mode(self, quadrant_mode):
|
||||
self._changed()
|
||||
self._quadrant_mode = quadrant_mode
|
||||
|
||||
@property
|
||||
def radius(self):
|
||||
dy, dx = tuple([start - center for start, center
|
||||
|
|
@ -380,6 +437,47 @@ class Arc(Primitive):
|
|||
self._bounding_box = ((min_x, max_x), (min_y, max_y))
|
||||
return self._bounding_box
|
||||
|
||||
@property
|
||||
def bounding_box_no_aperture(self):
|
||||
'''Gets the bounding box without considering the aperture'''
|
||||
two_pi = 2 * math.pi
|
||||
theta0 = (self.start_angle + two_pi) % two_pi
|
||||
theta1 = (self.end_angle + two_pi) % two_pi
|
||||
points = [self.start, self.end]
|
||||
if self.direction == 'counterclockwise':
|
||||
# Passes through 0 degrees
|
||||
if theta0 > theta1:
|
||||
points.append((self.center[0] + self.radius, self.center[1]))
|
||||
# Passes through 90 degrees
|
||||
if theta0 <= math.pi / 2. and (theta1 >= math.pi / 2. or theta1 < theta0):
|
||||
points.append((self.center[0], self.center[1] + self.radius))
|
||||
# Passes through 180 degrees
|
||||
if theta0 <= math.pi and (theta1 >= math.pi or theta1 < theta0):
|
||||
points.append((self.center[0] - self.radius, self.center[1]))
|
||||
# Passes through 270 degrees
|
||||
if theta0 <= math.pi * 1.5 and (theta1 >= math.pi * 1.5 or theta1 < theta0):
|
||||
points.append((self.center[0], self.center[1] - self.radius ))
|
||||
else:
|
||||
# Passes through 0 degrees
|
||||
if theta1 > theta0:
|
||||
points.append((self.center[0] + self.radius, self.center[1]))
|
||||
# Passes through 90 degrees
|
||||
if theta1 <= math.pi / 2. and (theta0 >= math.pi / 2. or theta0 < theta1):
|
||||
points.append((self.center[0], self.center[1] + self.radius))
|
||||
# Passes through 180 degrees
|
||||
if theta1 <= math.pi and (theta0 >= math.pi or theta0 < theta1):
|
||||
points.append((self.center[0] - self.radius, self.center[1]))
|
||||
# Passes through 270 degrees
|
||||
if theta1 <= math.pi * 1.5 and (theta0 >= math.pi * 1.5 or theta0 < theta1):
|
||||
points.append((self.center[0], self.center[1] - self.radius ))
|
||||
x, y = zip(*points)
|
||||
|
||||
min_x = min(x)
|
||||
max_x = max(x)
|
||||
min_y = min(y)
|
||||
max_y = max(y)
|
||||
return ((min_x, max_x), (min_y, max_y))
|
||||
|
||||
def offset(self, x_offset=0, y_offset=0):
|
||||
self._changed()
|
||||
self.start = tuple(map(add, self.start, (x_offset, y_offset)))
|
||||
|
|
@ -391,12 +489,17 @@ class Circle(Primitive):
|
|||
"""
|
||||
"""
|
||||
|
||||
def __init__(self, position, diameter, **kwargs):
|
||||
def __init__(self, position, diameter, hole_diameter = None, **kwargs):
|
||||
super(Circle, self).__init__(**kwargs)
|
||||
validate_coordinates(position)
|
||||
self._position = position
|
||||
self._diameter = diameter
|
||||
self._to_convert = ['position', 'diameter']
|
||||
self.hole_diameter = hole_diameter
|
||||
self._to_convert = ['position', 'diameter', 'hole_diameter']
|
||||
|
||||
@property
|
||||
def flashed(self):
|
||||
return True
|
||||
|
||||
@property
|
||||
def position(self):
|
||||
|
|
@ -420,6 +523,12 @@ class Circle(Primitive):
|
|||
def radius(self):
|
||||
return self.diameter / 2.
|
||||
|
||||
@property
|
||||
def hole_radius(self):
|
||||
if self.hole_diameter != None:
|
||||
return self.hole_diameter / 2.
|
||||
return None
|
||||
|
||||
@property
|
||||
def bounding_box(self):
|
||||
if self._bounding_box is None:
|
||||
|
|
@ -430,11 +539,26 @@ class Circle(Primitive):
|
|||
self._bounding_box = ((min_x, max_x), (min_y, max_y))
|
||||
return self._bounding_box
|
||||
|
||||
def offset(self, x_offset=0, y_offset=0):
|
||||
self.position = tuple(map(add, self.position, (x_offset, y_offset)))
|
||||
|
||||
def equivalent(self, other, offset):
|
||||
'''Is this the same as the other circle, ignoring the offiset?'''
|
||||
|
||||
if not isinstance(other, Circle):
|
||||
return False
|
||||
|
||||
if self.diameter != other.diameter or self.hole_diameter != other.hole_diameter:
|
||||
return False
|
||||
|
||||
equiv_position = tuple(map(add, other.position, offset))
|
||||
|
||||
return nearly_equal(self.position, equiv_position)
|
||||
|
||||
|
||||
class Ellipse(Primitive):
|
||||
"""
|
||||
"""
|
||||
|
||||
def __init__(self, position, width, height, **kwargs):
|
||||
super(Ellipse, self).__init__(**kwargs)
|
||||
validate_coordinates(position)
|
||||
|
|
@ -443,6 +567,10 @@ class Ellipse(Primitive):
|
|||
self._height = height
|
||||
self._to_convert = ['position', 'width', 'height']
|
||||
|
||||
@property
|
||||
def flashed(self):
|
||||
return True
|
||||
|
||||
@property
|
||||
def position(self):
|
||||
return self._position
|
||||
|
|
@ -497,18 +625,28 @@ class Ellipse(Primitive):
|
|||
|
||||
class Rectangle(Primitive):
|
||||
"""
|
||||
When rotated, the rotation is about the center point.
|
||||
|
||||
Only aperture macro generated Rectangle objects can be rotated. If you aren't in a AMGroup,
|
||||
then you don't need to worry about rotation
|
||||
"""
|
||||
|
||||
def __init__(self, position, width, height, **kwargs):
|
||||
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._to_convert = ['position', 'width', '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 position(self):
|
||||
return self._position
|
||||
|
|
@ -536,6 +674,18 @@ class Rectangle(Primitive):
|
|||
self._changed()
|
||||
self._height = value
|
||||
|
||||
@property
|
||||
def hole_radius(self):
|
||||
"""The radius of the hole. If there is no hole, returns None"""
|
||||
if self.hole_diameter != None:
|
||||
return self.hole_diameter / 2.
|
||||
return None
|
||||
|
||||
@property
|
||||
def upper_right(self):
|
||||
return (self.position[0] + (self.axis_aligned_width / 2.),
|
||||
self.position[1] + (self.axis_aligned_height / 2.))
|
||||
|
||||
@property
|
||||
def lower_left(self):
|
||||
return (self.position[0] - (self.axis_aligned_width / 2.),
|
||||
|
|
@ -567,11 +717,24 @@ class Rectangle(Primitive):
|
|||
|
||||
@property
|
||||
def axis_aligned_width(self):
|
||||
return (self._cos_theta * self.width) + (self._sin_theta * self.height)
|
||||
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)
|
||||
return (self._cos_theta * self.height + self._sin_theta * self.width)
|
||||
|
||||
def equivalent(self, other, offset):
|
||||
"""Is this the same as the other rect, ignoring the offset?"""
|
||||
|
||||
if not isinstance(other, Rectangle):
|
||||
return False
|
||||
|
||||
if self.width != other.width or self.height != other.height or self.rotation != other.rotation or self.hole_diameter != other.hole_diameter:
|
||||
return False
|
||||
|
||||
equiv_position = tuple(map(add, other.position, offset))
|
||||
|
||||
return nearly_equal(self.position, equiv_position)
|
||||
|
||||
|
||||
class Diamond(Primitive):
|
||||
|
|
@ -586,6 +749,10 @@ class Diamond(Primitive):
|
|||
self._height = height
|
||||
self._to_convert = ['position', 'width', 'height']
|
||||
|
||||
@property
|
||||
def flashed(self):
|
||||
return True
|
||||
|
||||
@property
|
||||
def position(self):
|
||||
return self._position
|
||||
|
|
@ -639,11 +806,11 @@ class Diamond(Primitive):
|
|||
|
||||
@property
|
||||
def axis_aligned_width(self):
|
||||
return (self._cos_theta * self.width) + (self._sin_theta * self.height)
|
||||
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)
|
||||
return (self._cos_theta * self.height + self._sin_theta * self.width)
|
||||
|
||||
|
||||
class ChamferRectangle(Primitive):
|
||||
|
|
@ -659,6 +826,10 @@ class ChamferRectangle(Primitive):
|
|||
self._corners = corners if corners is not None else [True] * 4
|
||||
self._to_convert = ['position', 'width', 'height', 'chamfer']
|
||||
|
||||
@property
|
||||
def flashed(self):
|
||||
return True
|
||||
|
||||
@property
|
||||
def position(self):
|
||||
return self._position
|
||||
|
|
@ -774,6 +945,10 @@ class RoundRectangle(Primitive):
|
|||
self._corners = corners
|
||||
self._to_convert = ['position', 'width', 'height', 'radius']
|
||||
|
||||
@property
|
||||
def flashed(self):
|
||||
return True
|
||||
|
||||
@property
|
||||
def position(self):
|
||||
return self._position
|
||||
|
|
@ -844,13 +1019,18 @@ class Obround(Primitive):
|
|||
"""
|
||||
"""
|
||||
|
||||
def __init__(self, position, width, height, **kwargs):
|
||||
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._to_convert = ['position', 'width', 'height']
|
||||
self.hole_diameter = hole_diameter
|
||||
self._to_convert = ['position', 'width', 'height', 'hole_diameter']
|
||||
|
||||
@property
|
||||
def flashed(self):
|
||||
return True
|
||||
|
||||
@property
|
||||
def position(self):
|
||||
|
|
@ -879,6 +1059,14 @@ class Obround(Primitive):
|
|||
self._changed()
|
||||
self._height = value
|
||||
|
||||
@property
|
||||
def hole_radius(self):
|
||||
"""The radius of the hole. If there is no hole, returns None"""
|
||||
if self.hole_diameter != None:
|
||||
return self.hole_diameter / 2.
|
||||
|
||||
return None
|
||||
|
||||
@property
|
||||
def orientation(self):
|
||||
return 'vertical' if self.height > self.width else 'horizontal'
|
||||
|
|
@ -926,15 +1114,30 @@ class Obround(Primitive):
|
|||
|
||||
class Polygon(Primitive):
|
||||
"""
|
||||
Polygon flash defined by a set number of sides.
|
||||
"""
|
||||
|
||||
def __init__(self, position, sides, radius, **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._to_convert = ['position', 'radius']
|
||||
self.hole_diameter = hole_diameter
|
||||
self._to_convert = ['position', 'radius', 'hole_diameter']
|
||||
|
||||
@property
|
||||
def flashed(self):
|
||||
return True
|
||||
|
||||
@property
|
||||
def diameter(self):
|
||||
return self.radius * 2
|
||||
|
||||
@property
|
||||
def hole_radius(self):
|
||||
if self.hole_diameter != None:
|
||||
return self.hole_diameter / 2.
|
||||
return None
|
||||
|
||||
@property
|
||||
def position(self):
|
||||
|
|
@ -964,6 +1167,21 @@ class Polygon(Primitive):
|
|||
self._bounding_box = ((min_x, max_x), (min_y, max_y))
|
||||
return self._bounding_box
|
||||
|
||||
def offset(self, x_offset=0, y_offset=0):
|
||||
self.position = tuple(map(add, self.position, (x_offset, y_offset)))
|
||||
|
||||
@property
|
||||
def vertices(self):
|
||||
|
||||
offset = self.rotation
|
||||
da = 360.0 / self.sides
|
||||
|
||||
points = []
|
||||
for i in xrange(self.sides):
|
||||
points.append(rotate_point((self.position[0] + self.radius, self.position[1]), offset + da * i, self.position))
|
||||
|
||||
return points
|
||||
|
||||
@property
|
||||
def vertices(self):
|
||||
if self._vertices is None:
|
||||
|
|
@ -976,6 +1194,187 @@ class Polygon(Primitive):
|
|||
for x, y in vertices]
|
||||
return self._vertices
|
||||
|
||||
def equivalent(self, other, offset):
|
||||
"""
|
||||
Is this the outline the same as the other, ignoring the position offset?
|
||||
"""
|
||||
|
||||
# Quick check if it even makes sense to compare them
|
||||
if type(self) != type(other) or self.sides != other.sides or self.radius != other.radius:
|
||||
return False
|
||||
|
||||
equiv_pos = tuple(map(add, other.position, offset))
|
||||
|
||||
return nearly_equal(self.position, equiv_pos)
|
||||
|
||||
|
||||
class AMGroup(Primitive):
|
||||
"""
|
||||
"""
|
||||
def __init__(self, amprimitives, stmt = None, **kwargs):
|
||||
"""
|
||||
|
||||
stmt : The original statment that generated this, since it is really hard to re-generate from primitives
|
||||
"""
|
||||
super(AMGroup, self).__init__(**kwargs)
|
||||
|
||||
self.primitives = []
|
||||
for amprim in amprimitives:
|
||||
prim = amprim.to_primitive(self.units)
|
||||
if isinstance(prim, list):
|
||||
for p in prim:
|
||||
self.primitives.append(p)
|
||||
elif prim:
|
||||
self.primitives.append(prim)
|
||||
self._position = None
|
||||
self._to_convert = ['_position', 'primitives']
|
||||
self.stmt = stmt
|
||||
|
||||
def to_inch(self):
|
||||
if self.units == 'metric':
|
||||
super(AMGroup, self).to_inch()
|
||||
|
||||
# If we also have a stmt, convert that too
|
||||
if self.stmt:
|
||||
self.stmt.to_inch()
|
||||
|
||||
|
||||
def to_metric(self):
|
||||
if self.units == 'inch':
|
||||
super(AMGroup, self).to_metric()
|
||||
|
||||
# If we also have a stmt, convert that too
|
||||
if self.stmt:
|
||||
self.stmt.to_metric()
|
||||
|
||||
@property
|
||||
def flashed(self):
|
||||
return True
|
||||
|
||||
@property
|
||||
def bounding_box(self):
|
||||
# TODO Make this cached like other items
|
||||
xlims, ylims = zip(*[p.bounding_box for p in self.primitives])
|
||||
minx, maxx = zip(*xlims)
|
||||
miny, maxy = zip(*ylims)
|
||||
min_x = min(minx)
|
||||
max_x = max(maxx)
|
||||
min_y = min(miny)
|
||||
max_y = max(maxy)
|
||||
return ((min_x, max_x), (min_y, max_y))
|
||||
|
||||
@property
|
||||
def position(self):
|
||||
return self._position
|
||||
|
||||
def offset(self, x_offset=0, y_offset=0):
|
||||
self._position = tuple(map(add, self._position, (x_offset, y_offset)))
|
||||
|
||||
for primitive in self.primitives:
|
||||
primitive.offset(x_offset, y_offset)
|
||||
|
||||
@position.setter
|
||||
def position(self, new_pos):
|
||||
'''
|
||||
Sets the position of the AMGroup.
|
||||
This offset all of the objects by the specified distance.
|
||||
'''
|
||||
|
||||
if self._position:
|
||||
dx = new_pos[0] - self._position[0]
|
||||
dy = new_pos[1] - self._position[1]
|
||||
else:
|
||||
dx = new_pos[0]
|
||||
dy = new_pos[1]
|
||||
|
||||
for primitive in self.primitives:
|
||||
primitive.offset(dx, dy)
|
||||
|
||||
self._position = new_pos
|
||||
|
||||
def equivalent(self, other, offset):
|
||||
'''
|
||||
Is this the macro group the same as the other, ignoring the position offset?
|
||||
'''
|
||||
|
||||
if len(self.primitives) != len(other.primitives):
|
||||
return False
|
||||
|
||||
# We know they have the same number of primitives, so now check them all
|
||||
for i in range(0, len(self.primitives)):
|
||||
if not self.primitives[i].equivalent(other.primitives[i], offset):
|
||||
return False
|
||||
|
||||
# If we didn't find any differences, then they are the same
|
||||
return True
|
||||
|
||||
class Outline(Primitive):
|
||||
"""
|
||||
Outlines only exist as the rendering for a apeture macro outline.
|
||||
They don't exist outside of AMGroup objects
|
||||
"""
|
||||
|
||||
def __init__(self, primitives, **kwargs):
|
||||
super(Outline, self).__init__(**kwargs)
|
||||
self.primitives = primitives
|
||||
self._to_convert = ['primitives']
|
||||
|
||||
if self.primitives[0].start != self.primitives[-1].end:
|
||||
raise ValueError('Outline must be closed')
|
||||
|
||||
@property
|
||||
def flashed(self):
|
||||
return True
|
||||
|
||||
@property
|
||||
def bounding_box(self):
|
||||
if self._bounding_box is None:
|
||||
xlims, ylims = zip(*[p.bounding_box for p in self.primitives])
|
||||
minx, maxx = zip(*xlims)
|
||||
miny, maxy = zip(*ylims)
|
||||
min_x = min(minx)
|
||||
max_x = max(maxx)
|
||||
min_y = min(miny)
|
||||
max_y = max(maxy)
|
||||
self._bounding_box = ((min_x, max_x), (min_y, max_y))
|
||||
return self._bounding_box
|
||||
|
||||
def offset(self, x_offset=0, y_offset=0):
|
||||
self._changed()
|
||||
for p in self.primitives:
|
||||
p.offset(x_offset, y_offset)
|
||||
|
||||
@property
|
||||
def vertices(self):
|
||||
if self._vertices is None:
|
||||
theta = math.radians(360/self.sides)
|
||||
vertices = [(self.position[0] + (math.cos(theta * side) * self.radius),
|
||||
self.position[1] + (math.sin(theta * side) * self.radius))
|
||||
for side in range(self.sides)]
|
||||
self._vertices = [(((x * self._cos_theta) - (y * self._sin_theta)),
|
||||
((x * self._sin_theta) + (y * self._cos_theta)))
|
||||
for x, y in vertices]
|
||||
return self._vertices
|
||||
|
||||
@property
|
||||
def width(self):
|
||||
bounding_box = self.bounding_box()
|
||||
return bounding_box[0][1] - bounding_box[0][0]
|
||||
|
||||
def equivalent(self, other, offset):
|
||||
'''
|
||||
Is this the outline the same as the other, ignoring the position offset?
|
||||
'''
|
||||
|
||||
# Quick check if it even makes sense to compare them
|
||||
if type(self) != type(other) or len(self.primitives) != len(other.primitives):
|
||||
return False
|
||||
|
||||
for i in range(0, len(self.primitives)):
|
||||
if not self.primitives[i].equivalent(other.primitives[i], offset):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
class Region(Primitive):
|
||||
"""
|
||||
|
|
@ -986,10 +1385,14 @@ class Region(Primitive):
|
|||
self.primitives = primitives
|
||||
self._to_convert = ['primitives']
|
||||
|
||||
@property
|
||||
def flashed(self):
|
||||
return False
|
||||
|
||||
@property
|
||||
def bounding_box(self):
|
||||
if self._bounding_box is None:
|
||||
xlims, ylims = zip(*[p.bounding_box for p in self.primitives])
|
||||
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)
|
||||
|
|
@ -1016,6 +1419,12 @@ 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
|
||||
|
||||
@property
|
||||
def radius(self):
|
||||
return self.diameter / 2.
|
||||
|
|
@ -1042,6 +1451,12 @@ 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
|
||||
|
||||
@property
|
||||
def bounding_box(self):
|
||||
if self._bounding_box is None:
|
||||
|
|
@ -1078,9 +1493,26 @@ 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
|
||||
|
||||
@property
|
||||
def lower_left(self):
|
||||
return (self.position[0] - (self.width / 2.),
|
||||
self.position[1] - (self.height / 2.))
|
||||
|
||||
@property
|
||||
def upper_right(self):
|
||||
return (self.position[0] + (self.width / 2.),
|
||||
self.position[1] + (self.height / 2.))
|
||||
|
||||
@property
|
||||
def bounding_box(self):
|
||||
if self._bounding_box is None:
|
||||
|
|
@ -1107,6 +1539,10 @@ class SquareRoundDonut(Primitive):
|
|||
self.outer_diameter = outer_diameter
|
||||
self._to_convert = ['position', 'inner_diameter', 'outer_diameter']
|
||||
|
||||
@property
|
||||
def flashed(self):
|
||||
return True
|
||||
|
||||
@property
|
||||
def bounding_box(self):
|
||||
if self._bounding_box is None:
|
||||
|
|
@ -1119,13 +1555,19 @@ class SquareRoundDonut(Primitive):
|
|||
class Drill(Primitive):
|
||||
""" A drill hole
|
||||
"""
|
||||
|
||||
def __init__(self, position, diameter, **kwargs):
|
||||
def __init__(self, position, diameter, hit, **kwargs):
|
||||
super(Drill, self).__init__('dark', **kwargs)
|
||||
validate_coordinates(position)
|
||||
self._position = position
|
||||
self._diameter = diameter
|
||||
self._to_convert = ['position', 'diameter']
|
||||
self.hit = hit
|
||||
self._to_convert = ['position', 'diameter', 'hit']
|
||||
|
||||
# TODO Ths won't handle the hit updates correctly
|
||||
|
||||
@property
|
||||
def flashed(self):
|
||||
return False
|
||||
|
||||
@property
|
||||
def position(self):
|
||||
|
|
@ -1159,6 +1601,44 @@ class Drill(Primitive):
|
|||
self._bounding_box = ((min_x, max_x), (min_y, max_y))
|
||||
return self._bounding_box
|
||||
|
||||
def offset(self, x_offset=0, y_offset=0):
|
||||
self._changed()
|
||||
self.position = tuple(map(add, self.position, (x_offset, y_offset)))
|
||||
|
||||
def __str__(self):
|
||||
return '<Drill %f (%f, %f) [%s]>' % (self.diameter, self.position[0], self.position[1], self.hit)
|
||||
|
||||
|
||||
class Slot(Primitive):
|
||||
""" A drilled slot
|
||||
"""
|
||||
def __init__(self, start, end, diameter, hit, **kwargs):
|
||||
super(Slot, self).__init__('dark', **kwargs)
|
||||
validate_coordinates(start)
|
||||
validate_coordinates(end)
|
||||
self.start = start
|
||||
self.end = end
|
||||
self.diameter = diameter
|
||||
self.hit = hit
|
||||
self._to_convert = ['start', 'end', 'diameter', 'hit']
|
||||
|
||||
# TODO this needs to use cached bounding box
|
||||
|
||||
@property
|
||||
def flashed(self):
|
||||
return False
|
||||
|
||||
def bounding_box(self):
|
||||
if self._bounding_box is None:
|
||||
ll = tuple([c - self.outer_diameter / 2. for c in self.position])
|
||||
ur = tuple([c + self.outer_diameter / 2. for c in self.position])
|
||||
self._bounding_box = ((ll[0], ur[0]), (ll[1], ur[1]))
|
||||
return self._bounding_box
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -12,14 +12,19 @@
|
|||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
try:
|
||||
import cairo
|
||||
except ImportError:
|
||||
import cairocffi as cairo
|
||||
|
||||
import cairocffi as cairo
|
||||
import os
|
||||
from operator import mul
|
||||
import tempfile
|
||||
import copy
|
||||
import os
|
||||
|
||||
from .render import GerberContext, RenderSettings
|
||||
from .theme import THEMES
|
||||
|
|
@ -105,10 +110,13 @@ class GerberCairoContext(GerberContext):
|
|||
verbose=verbose)
|
||||
self.dump(filename, verbose)
|
||||
|
||||
def dump(self, filename, verbose=False):
|
||||
def dump(self, filename=None, verbose=False):
|
||||
""" Save image as `filename`
|
||||
"""
|
||||
is_svg = os.path.splitext(filename.lower())[1] == '.svg'
|
||||
try:
|
||||
is_svg = os.path.splitext(filename.lower())[1] == '.svg'
|
||||
except:
|
||||
is_svg = False
|
||||
if verbose:
|
||||
print('[Render]: Writing image to {}'.format(filename))
|
||||
if is_svg:
|
||||
|
|
@ -119,7 +127,7 @@ class GerberCairoContext(GerberContext):
|
|||
f.write(self.surface_buffer.read())
|
||||
f.flush()
|
||||
else:
|
||||
self.surface.write_to_png(filename)
|
||||
return self.surface.write_to_png(filename)
|
||||
|
||||
def dump_str(self):
|
||||
""" Return a byte-string containing the rendered image.
|
||||
|
|
@ -154,7 +162,7 @@ class GerberCairoContext(GerberContext):
|
|||
for prim in layer.primitives:
|
||||
self.render(prim)
|
||||
# Add layer to image
|
||||
self._paint(settings.color, settings.alpha)
|
||||
self._flatten(settings.color, settings.alpha)
|
||||
|
||||
def _render_line(self, line, color):
|
||||
start = [pos * scale for pos, scale in zip(line.start, self.scale)]
|
||||
|
|
@ -184,10 +192,17 @@ class GerberCairoContext(GerberContext):
|
|||
radius = self.scale[0] * arc.radius
|
||||
angle1 = arc.start_angle
|
||||
angle2 = arc.end_angle
|
||||
width = arc.aperture.diameter if arc.aperture.diameter != 0 else 0.001
|
||||
if angle1 == angle2 and arc.quadrant_mode != 'single-quadrant':
|
||||
# Make the angles slightly different otherwise Cario will draw nothing
|
||||
angle2 -= 0.000000001
|
||||
if isinstance(arc.aperture, Circle):
|
||||
width = arc.aperture.diameter if arc.aperture.diameter != 0 else 0.001
|
||||
else:
|
||||
width = max(arc.aperture.width, arc.aperture.height, 0.001)
|
||||
|
||||
self.ctx.set_operator(cairo.OPERATOR_SOURCE
|
||||
if arc.level_polarity == 'dark' and
|
||||
(not self.invert) else cairo.OPERATOR_CLEAR)
|
||||
(not self.invert) else 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) # You actually have to do this...
|
||||
|
|
@ -225,12 +240,23 @@ class GerberCairoContext(GerberContext):
|
|||
center = self.scale_point(circle.position)
|
||||
self.ctx.set_operator(cairo.OPERATOR_SOURCE
|
||||
if circle.level_polarity == 'dark' and
|
||||
(not self.invert) else cairo.OPERATOR_CLEAR)
|
||||
(not self.invert) else cairo.OPERATOR_CLEAR)
|
||||
self.ctx.set_line_width(0)
|
||||
self.ctx.arc(*center, radius=(circle.radius * self.scale[0]), angle1=0,
|
||||
angle2=(2 * math.pi))
|
||||
self.ctx.fill()
|
||||
|
||||
if circle.hole_diameter > 0:
|
||||
# Render the center clear
|
||||
|
||||
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()
|
||||
|
||||
|
||||
def _render_rectangle(self, rectangle, color):
|
||||
lower_left = self.scale_point(rectangle.lower_left)
|
||||
width, height = tuple([abs(coord) for coord in
|
||||
|
|
@ -239,19 +265,126 @@ class GerberCairoContext(GerberContext):
|
|||
self.ctx.set_operator(cairo.OPERATOR_SOURCE
|
||||
if rectangle.level_polarity == 'dark' and
|
||||
(not self.invert) else cairo.OPERATOR_CLEAR)
|
||||
|
||||
if rectangle.rotation != 0:
|
||||
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
|
||||
lower_left[0] = lower_left[0] - center[0]
|
||||
lower_left[1] = lower_left[1] - center[1]
|
||||
matrix.rotate(rectangle.rotation)
|
||||
self.ctx.transform(matrix)
|
||||
|
||||
if rectangle.hole_diameter > 0:
|
||||
self.ctx.push_group()
|
||||
|
||||
self.ctx.set_line_width(0)
|
||||
self.ctx.rectangle(*lower_left, width=width, height=height)
|
||||
self.ctx.fill()
|
||||
|
||||
if rectangle.hole_diameter > 0:
|
||||
# Render the center clear
|
||||
self.ctx.set_source_rgba(color[0], color[1], color[2], self.alpha)
|
||||
self.ctx.set_operator(cairo.OPERATOR_CLEAR
|
||||
if rectangle.level_polarity == 'dark'
|
||||
and (not self.invert)
|
||||
else cairo.OPERATOR_SOURCE)
|
||||
center = map(mul, rectangle.position, self.scale)
|
||||
self.ctx.arc(center[0], center[1],
|
||||
radius=rectangle.hole_radius * self.scale[0], angle1=0,
|
||||
angle2=2 * math.pi)
|
||||
self.ctx.fill()
|
||||
|
||||
if rectangle.rotation != 0:
|
||||
self.ctx.restore()
|
||||
|
||||
def _render_obround(self, obround, color):
|
||||
if obround.hole_diameter > 0:
|
||||
self.ctx.push_group()
|
||||
|
||||
self._render_circle(obround.subshapes['circle1'], color)
|
||||
self._render_circle(obround.subshapes['circle2'], color)
|
||||
self._render_rectangle(obround.subshapes['rectangle'], color)
|
||||
|
||||
if obround.hole_diameter > 0:
|
||||
# Render the center 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)
|
||||
self.ctx.arc(center[0], center[1],
|
||||
radius=obround.hole_radius * self.scale[0], angle1=0,
|
||||
angle2=2 * math.pi)
|
||||
self.ctx.fill()
|
||||
|
||||
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
|
||||
self.ctx.set_operator(cairo.OPERATOR_SOURCE
|
||||
if polygon.level_polarity == 'dark' and
|
||||
(not self.invert) else cairo.OPERATOR_CLEAR)
|
||||
if polygon.hole_radius > 0:
|
||||
self.ctx.push_group()
|
||||
|
||||
vertices = polygon.vertices
|
||||
|
||||
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
|
||||
self.ctx.move_to(*map(mul, vertices[-1], self.scale))
|
||||
for v in vertices:
|
||||
self.ctx.line_to(*map(mul, v, self.scale))
|
||||
|
||||
self.ctx.fill()
|
||||
|
||||
if polygon.hole_radius > 0:
|
||||
# Render the center clear
|
||||
center = tuple(map(mul, polygon.position, self.scale))
|
||||
self.ctx.set_source_rgba(color[0], color[1], color[2], self.alpha)
|
||||
self.ctx.set_operator(cairo.OPERATOR_CLEAR
|
||||
if polygon.level_polarity == 'dark'
|
||||
and (not self.invert)
|
||||
else cairo.OPERATOR_SOURCE)
|
||||
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()
|
||||
|
||||
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):
|
||||
start = map(mul, slot.start, self.scale)
|
||||
end = map(mul, slot.end, self.scale)
|
||||
|
||||
width = slot.diameter
|
||||
|
||||
self.ctx.set_operator(cairo.OPERATOR_SOURCE
|
||||
if slot.level_polarity == 'dark' and
|
||||
(not self.invert) else 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()
|
||||
for primitive in amgroup.primitives:
|
||||
self.render(primitive)
|
||||
self.ctx.pop_group_to_source()
|
||||
self.ctx.paint_with_alpha(1)
|
||||
|
||||
def _render_test_record(self, primitive, color):
|
||||
position = [pos + origin for pos, origin in
|
||||
zip(primitive.position, self.origin_in_inch)]
|
||||
|
|
@ -285,7 +418,7 @@ class GerberCairoContext(GerberContext):
|
|||
self.active_layer = layer
|
||||
self.active_matrix = matrix
|
||||
|
||||
def _paint(self, color=None, alpha=None):
|
||||
def _flatten(self, color=None, alpha=None):
|
||||
color = color if color is not None else self.color
|
||||
alpha = alpha if alpha is not None else self.alpha
|
||||
ptn = cairo.SurfacePattern(self.active_layer)
|
||||
|
|
|
|||
189
gerber/render/excellon_backend.py
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
|
||||
from .render import GerberContext
|
||||
from ..excellon import DrillSlot
|
||||
from ..excellon_statements import *
|
||||
|
||||
class ExcellonContext(GerberContext):
|
||||
|
||||
MODE_DRILL = 1
|
||||
MODE_SLOT =2
|
||||
|
||||
def __init__(self, settings):
|
||||
GerberContext.__init__(self)
|
||||
|
||||
# Statements that we write
|
||||
self.comments = []
|
||||
self.header = []
|
||||
self.tool_def = []
|
||||
self.body_start = [RewindStopStmt()]
|
||||
self.body = []
|
||||
self.start = [HeaderBeginStmt()]
|
||||
|
||||
# Current tool and position
|
||||
self.handled_tools = set()
|
||||
self.cur_tool = None
|
||||
self.drill_mode = ExcellonContext.MODE_DRILL
|
||||
self.drill_down = False
|
||||
self._pos = (None, None)
|
||||
|
||||
self.settings = settings
|
||||
|
||||
self._start_header()
|
||||
self._start_comments()
|
||||
|
||||
def _start_header(self):
|
||||
"""Create the header from the settings"""
|
||||
|
||||
self.header.append(UnitStmt.from_settings(self.settings))
|
||||
|
||||
if self.settings.notation == 'incremental':
|
||||
raise NotImplementedError('Incremental mode is not implemented')
|
||||
else:
|
||||
self.body.append(AbsoluteModeStmt())
|
||||
|
||||
def _start_comments(self):
|
||||
|
||||
# Write the digits used - this isn't valid Excellon statement, so we write as a comment
|
||||
self.comments.append(CommentStmt('FILE_FORMAT=%d:%d' % (self.settings.format[0], self.settings.format[1])))
|
||||
|
||||
def _get_end(self):
|
||||
"""How we end depends on our mode"""
|
||||
|
||||
end = []
|
||||
|
||||
if self.drill_down:
|
||||
end.append(RetractWithClampingStmt())
|
||||
end.append(RetractWithoutClampingStmt())
|
||||
|
||||
end.append(EndOfProgramStmt())
|
||||
|
||||
return end
|
||||
|
||||
@property
|
||||
def statements(self):
|
||||
return self.start + self.comments + self.header + self.body_start + self.body + self._get_end()
|
||||
|
||||
def set_bounds(self, bounds):
|
||||
pass
|
||||
|
||||
def _paint_background(self):
|
||||
pass
|
||||
|
||||
def _render_line(self, line, color):
|
||||
raise ValueError('Invalid Excellon object')
|
||||
def _render_arc(self, arc, color):
|
||||
raise ValueError('Invalid Excellon object')
|
||||
|
||||
def _render_region(self, region, color):
|
||||
raise ValueError('Invalid Excellon object')
|
||||
|
||||
def _render_level_polarity(self, region):
|
||||
raise ValueError('Invalid Excellon object')
|
||||
|
||||
def _render_circle(self, circle, color):
|
||||
raise ValueError('Invalid Excellon object')
|
||||
|
||||
def _render_rectangle(self, rectangle, color):
|
||||
raise ValueError('Invalid Excellon object')
|
||||
|
||||
def _render_obround(self, obround, color):
|
||||
raise ValueError('Invalid Excellon object')
|
||||
|
||||
def _render_polygon(self, polygon, color):
|
||||
raise ValueError('Invalid Excellon object')
|
||||
|
||||
def _simplify_point(self, point):
|
||||
return (point[0] if point[0] != self._pos[0] else None, point[1] if point[1] != self._pos[1] else None)
|
||||
|
||||
def _render_drill(self, drill, color):
|
||||
|
||||
if self.drill_mode != ExcellonContext.MODE_DRILL:
|
||||
self._start_drill_mode()
|
||||
|
||||
tool = drill.hit.tool
|
||||
if not tool in self.handled_tools:
|
||||
self.handled_tools.add(tool)
|
||||
self.header.append(ExcellonTool.from_tool(tool))
|
||||
|
||||
if tool != self.cur_tool:
|
||||
self.body.append(ToolSelectionStmt(tool.number))
|
||||
self.cur_tool = tool
|
||||
|
||||
point = self._simplify_point(drill.position)
|
||||
self._pos = drill.position
|
||||
self.body.append(CoordinateStmt.from_point(point))
|
||||
|
||||
def _start_drill_mode(self):
|
||||
"""
|
||||
If we are not in drill mode, then end the ROUT so we can do basic drilling
|
||||
"""
|
||||
|
||||
if self.drill_mode == ExcellonContext.MODE_SLOT:
|
||||
|
||||
# Make sure we are retracted before changing modes
|
||||
last_cmd = self.body[-1]
|
||||
if self.drill_down:
|
||||
self.body.append(RetractWithClampingStmt())
|
||||
self.body.append(RetractWithoutClampingStmt())
|
||||
self.drill_down = False
|
||||
|
||||
# Switch to drill mode
|
||||
self.body.append(DrillModeStmt())
|
||||
self.drill_mode = ExcellonContext.MODE_DRILL
|
||||
|
||||
else:
|
||||
raise ValueError('Should be in slot mode')
|
||||
|
||||
def _render_slot(self, slot, color):
|
||||
|
||||
# Set the tool first, before we might go into drill mode
|
||||
tool = slot.hit.tool
|
||||
if not tool in self.handled_tools:
|
||||
self.handled_tools.add(tool)
|
||||
self.header.append(ExcellonTool.from_tool(tool))
|
||||
|
||||
if tool != self.cur_tool:
|
||||
self.body.append(ToolSelectionStmt(tool.number))
|
||||
self.cur_tool = tool
|
||||
|
||||
# Two types of drilling - normal drill and slots
|
||||
if slot.hit.slot_type == DrillSlot.TYPE_ROUT:
|
||||
|
||||
# For ROUT, setting the mode is part of the actual command.
|
||||
|
||||
# Are we in the right position?
|
||||
if slot.start != self._pos:
|
||||
if self.drill_down:
|
||||
# We need to move into the right position, so retract
|
||||
self.body.append(RetractWithClampingStmt())
|
||||
self.drill_down = False
|
||||
|
||||
# Move to the right spot
|
||||
point = self._simplify_point(slot.start)
|
||||
self._pos = slot.start
|
||||
self.body.append(CoordinateStmt.from_point(point, mode="ROUT"))
|
||||
|
||||
# Now we are in the right spot, so drill down
|
||||
if not self.drill_down:
|
||||
self.body.append(ZAxisRoutPositionStmt())
|
||||
self.drill_down = True
|
||||
|
||||
# Do a linear move from our current position to the end position
|
||||
point = self._simplify_point(slot.end)
|
||||
self._pos = slot.end
|
||||
self.body.append(CoordinateStmt.from_point(point, mode="LINEAR"))
|
||||
|
||||
self.drill_mode = ExcellonContext.MODE_SLOT
|
||||
|
||||
else:
|
||||
# This is a G85 slot, so do this in normally drilling mode
|
||||
if self.drill_mode != ExcellonContext.MODE_DRILL:
|
||||
self._start_drill_mode()
|
||||
|
||||
# Slots don't use simplified points
|
||||
self._pos = slot.end
|
||||
self.body.append(SlotStmt.from_points(slot.start, slot.end))
|
||||
|
||||
def _render_inverted_layer(self):
|
||||
pass
|
||||
|
||||
|
|
@ -65,7 +65,7 @@ class GerberContext(object):
|
|||
self._background_color = (0.0, 0.0, 0.0)
|
||||
self._drill_color = (0.0, 0.0, 0.0)
|
||||
self._alpha = 1.0
|
||||
self.invert = False
|
||||
self._invert = False
|
||||
self.ctx = None
|
||||
|
||||
@property
|
||||
|
|
@ -127,7 +127,20 @@ class GerberContext(object):
|
|||
raise ValueError('Alpha must be between 0.0 and 1.0')
|
||||
self._alpha = alpha
|
||||
|
||||
@property
|
||||
def invert(self):
|
||||
return self._invert
|
||||
|
||||
@invert.setter
|
||||
def invert(self, invert):
|
||||
self._invert = invert
|
||||
|
||||
def render(self, primitive):
|
||||
if not primitive:
|
||||
return
|
||||
|
||||
self._pre_render_primitive(primitive)
|
||||
|
||||
color = self.color
|
||||
if isinstance(primitive, Line):
|
||||
self._render_line(primitive, color)
|
||||
|
|
@ -144,11 +157,32 @@ class GerberContext(object):
|
|||
elif isinstance(primitive, Polygon):
|
||||
self._render_polygon(primitive, color)
|
||||
elif isinstance(primitive, Drill):
|
||||
self._render_drill(primitive, color)
|
||||
self._render_drill(primitive, self.color)
|
||||
elif isinstance(primitive, Slot):
|
||||
self._render_slot(primitive, self.color)
|
||||
elif isinstance(primitive, AMGroup):
|
||||
self._render_amgroup(primitive, color)
|
||||
elif isinstance(primitive, Outline):
|
||||
self._render_region(primitive, color)
|
||||
elif isinstance(primitive, TestRecord):
|
||||
self._render_test_record(primitive, color)
|
||||
else:
|
||||
return
|
||||
|
||||
self._post_render_primitive(primitive)
|
||||
|
||||
def _pre_render_primitive(self, primitive):
|
||||
"""
|
||||
Called before rendering a primitive. Use the callback to perform some action before rendering
|
||||
a primitive, for example adding a comment.
|
||||
"""
|
||||
return
|
||||
|
||||
def _post_render_primitive(self, primitive):
|
||||
"""
|
||||
Called after rendering a primitive. Use the callback to perform some action after rendering
|
||||
a primitive
|
||||
"""
|
||||
return
|
||||
|
||||
|
||||
def _render_line(self, primitive, color):
|
||||
pass
|
||||
|
|
@ -174,6 +208,12 @@ class GerberContext(object):
|
|||
def _render_drill(self, primitive, color):
|
||||
pass
|
||||
|
||||
def _render_slot(self, primitive, color):
|
||||
pass
|
||||
|
||||
def _render_amgroup(self, primitive, color):
|
||||
pass
|
||||
|
||||
def _render_test_record(self, primitive, color):
|
||||
pass
|
||||
|
||||
|
|
|
|||
494
gerber/render/rs274x_backend.py
Normal file
|
|
@ -0,0 +1,494 @@
|
|||
"""Renders an in-memory Gerber file to statements which can be written to a string
|
||||
"""
|
||||
from copy import deepcopy
|
||||
|
||||
try:
|
||||
from cStringIO import StringIO
|
||||
except(ImportError):
|
||||
from io import StringIO
|
||||
|
||||
from .render import GerberContext
|
||||
from ..am_statements import *
|
||||
from ..gerber_statements import *
|
||||
from ..primitives import AMGroup, Arc, Circle, Line, Obround, Outline, Polygon, Rectangle
|
||||
|
||||
|
||||
class AMGroupContext(object):
|
||||
'''A special renderer to generate aperature macros from an AMGroup'''
|
||||
|
||||
def __init__(self):
|
||||
self.statements = []
|
||||
|
||||
def render(self, amgroup, name):
|
||||
|
||||
if amgroup.stmt:
|
||||
# We know the statement it was generated from, so use that to create the AMParamStmt
|
||||
# It will give a much better result
|
||||
|
||||
stmt = deepcopy(amgroup.stmt)
|
||||
stmt.name = name
|
||||
|
||||
return stmt
|
||||
|
||||
else:
|
||||
# Clone ourselves, then offset by the psotion so that
|
||||
# our render doesn't have to consider offset. Just makes things simpler
|
||||
nooffset_group = deepcopy(amgroup)
|
||||
nooffset_group.position = (0, 0)
|
||||
|
||||
# Now draw the shapes
|
||||
for primitive in nooffset_group.primitives:
|
||||
if isinstance(primitive, Outline):
|
||||
self._render_outline(primitive)
|
||||
elif isinstance(primitive, Circle):
|
||||
self._render_circle(primitive)
|
||||
elif isinstance(primitive, Rectangle):
|
||||
self._render_rectangle(primitive)
|
||||
elif isinstance(primitive, Line):
|
||||
self._render_line(primitive)
|
||||
elif isinstance(primitive, Polygon):
|
||||
self._render_polygon(primitive)
|
||||
else:
|
||||
raise ValueError('amgroup')
|
||||
|
||||
statement = AMParamStmt('AM', name, self._statements_to_string())
|
||||
return statement
|
||||
|
||||
def _statements_to_string(self):
|
||||
macro = ''
|
||||
|
||||
for statement in self.statements:
|
||||
macro += statement.to_gerber()
|
||||
|
||||
return macro
|
||||
|
||||
def _render_circle(self, circle):
|
||||
self.statements.append(AMCirclePrimitive.from_primitive(circle))
|
||||
|
||||
def _render_rectangle(self, rectangle):
|
||||
self.statements.append(AMCenterLinePrimitive.from_primitive(rectangle))
|
||||
|
||||
def _render_line(self, line):
|
||||
self.statements.append(AMVectorLinePrimitive.from_primitive(line))
|
||||
|
||||
def _render_outline(self, outline):
|
||||
self.statements.append(AMOutlinePrimitive.from_primitive(outline))
|
||||
|
||||
def _render_polygon(self, polygon):
|
||||
self.statements.append(AMPolygonPrimitive.from_primitive(polygon))
|
||||
|
||||
def _render_thermal(self, thermal):
|
||||
pass
|
||||
|
||||
|
||||
class Rs274xContext(GerberContext):
|
||||
|
||||
def __init__(self, settings):
|
||||
GerberContext.__init__(self)
|
||||
self.comments = []
|
||||
self.header = []
|
||||
self.body = []
|
||||
self.end = [EofStmt()]
|
||||
|
||||
# Current values so we know if we have to execute
|
||||
# moves, levey changes before anything else
|
||||
self._level_polarity = None
|
||||
self._pos = (None, None)
|
||||
self._func = None
|
||||
self._quadrant_mode = None
|
||||
self._dcode = None
|
||||
|
||||
# Primarily for testing and comarison to files, should we write
|
||||
# flashes as a single statement or a move plus flash? Set to true
|
||||
# to do in a single statement. Normally this can be false
|
||||
self.condensed_flash = True
|
||||
|
||||
# When closing a region, force a D02 staement to close a region.
|
||||
# This is normally not necessary because regions are closed with a G37
|
||||
# staement, but this will add an extra statement for doubly close
|
||||
# the region
|
||||
self.explicit_region_move_end = False
|
||||
|
||||
self._next_dcode = 10
|
||||
self._rects = {}
|
||||
self._circles = {}
|
||||
self._obrounds = {}
|
||||
self._polygons = {}
|
||||
self._macros = {}
|
||||
|
||||
self._i_none = 0
|
||||
self._j_none = 0
|
||||
|
||||
self.settings = settings
|
||||
|
||||
self._start_header(settings)
|
||||
|
||||
def _start_header(self, settings):
|
||||
self.header.append(FSParamStmt.from_settings(settings))
|
||||
self.header.append(MOParamStmt.from_units(settings.units))
|
||||
|
||||
def _simplify_point(self, point):
|
||||
return (point[0] if point[0] != self._pos[0] else None, point[1] if point[1] != self._pos[1] else None)
|
||||
|
||||
def _simplify_offset(self, point, offset):
|
||||
|
||||
if point[0] != offset[0]:
|
||||
xoffset = point[0] - offset[0]
|
||||
else:
|
||||
xoffset = self._i_none
|
||||
|
||||
if point[1] != offset[1]:
|
||||
yoffset = point[1] - offset[1]
|
||||
else:
|
||||
yoffset = self._j_none
|
||||
|
||||
return (xoffset, yoffset)
|
||||
|
||||
@property
|
||||
def statements(self):
|
||||
return self.comments + self.header + self.body + self.end
|
||||
|
||||
def set_bounds(self, bounds):
|
||||
pass
|
||||
|
||||
def _paint_background(self):
|
||||
pass
|
||||
|
||||
def _select_aperture(self, aperture):
|
||||
|
||||
# Select the right aperture if not already selected
|
||||
if aperture:
|
||||
if isinstance(aperture, Circle):
|
||||
aper = self._get_circle(aperture.diameter, aperture.hole_diameter)
|
||||
elif isinstance(aperture, Rectangle):
|
||||
aper = self._get_rectangle(aperture.width, aperture.height)
|
||||
elif isinstance(aperture, Obround):
|
||||
aper = self._get_obround(aperture.width, aperture.height)
|
||||
elif isinstance(aperture, AMGroup):
|
||||
aper = self._get_amacro(aperture)
|
||||
else:
|
||||
raise NotImplementedError('Line with invalid aperture type')
|
||||
|
||||
if aper.d != self._dcode:
|
||||
self.body.append(ApertureStmt(aper.d))
|
||||
self._dcode = aper.d
|
||||
|
||||
def _pre_render_primitive(self, primitive):
|
||||
|
||||
if hasattr(primitive, 'comment'):
|
||||
self.body.append(CommentStmt(primitive.comment))
|
||||
|
||||
def _render_line(self, line, color):
|
||||
|
||||
self._select_aperture(line.aperture)
|
||||
|
||||
self._render_level_polarity(line)
|
||||
|
||||
# Get the right function
|
||||
if self._func != CoordStmt.FUNC_LINEAR:
|
||||
func = CoordStmt.FUNC_LINEAR
|
||||
else:
|
||||
func = None
|
||||
self._func = CoordStmt.FUNC_LINEAR
|
||||
|
||||
if self._pos != line.start:
|
||||
self.body.append(CoordStmt.move(func, self._simplify_point(line.start)))
|
||||
self._pos = line.start
|
||||
# We already set the function, so the next command doesn't require that
|
||||
func = None
|
||||
|
||||
point = self._simplify_point(line.end)
|
||||
|
||||
# In some files, we see a lot of duplicated ponts, so omit those
|
||||
if point[0] != None or point[1] != None:
|
||||
self.body.append(CoordStmt.line(func, self._simplify_point(line.end)))
|
||||
self._pos = line.end
|
||||
elif func:
|
||||
self.body.append(CoordStmt.mode(func))
|
||||
|
||||
def _render_arc(self, arc, color):
|
||||
|
||||
# Optionally set the quadrant mode if it has changed:
|
||||
if arc.quadrant_mode != self._quadrant_mode:
|
||||
|
||||
if arc.quadrant_mode != 'multi-quadrant':
|
||||
self.body.append(QuadrantModeStmt.single())
|
||||
else:
|
||||
self.body.append(QuadrantModeStmt.multi())
|
||||
|
||||
self._quadrant_mode = arc.quadrant_mode
|
||||
|
||||
# Select the right aperture if not already selected
|
||||
self._select_aperture(arc.aperture)
|
||||
|
||||
self._render_level_polarity(arc)
|
||||
|
||||
# Find the right movement mode. Always set to be sure it is really right
|
||||
dir = arc.direction
|
||||
if dir == 'clockwise':
|
||||
func = CoordStmt.FUNC_ARC_CW
|
||||
self._func = CoordStmt.FUNC_ARC_CW
|
||||
elif dir == 'counterclockwise':
|
||||
func = CoordStmt.FUNC_ARC_CCW
|
||||
self._func = CoordStmt.FUNC_ARC_CCW
|
||||
else:
|
||||
raise ValueError('Invalid circular interpolation mode')
|
||||
|
||||
if self._pos != arc.start:
|
||||
# TODO I'm not sure if this is right
|
||||
self.body.append(CoordStmt.move(CoordStmt.FUNC_LINEAR, self._simplify_point(arc.start)))
|
||||
self._pos = arc.start
|
||||
|
||||
center = self._simplify_offset(arc.center, arc.start)
|
||||
end = self._simplify_point(arc.end)
|
||||
self.body.append(CoordStmt.arc(func, end, center))
|
||||
self._pos = arc.end
|
||||
|
||||
def _render_region(self, region, color):
|
||||
|
||||
self._render_level_polarity(region)
|
||||
|
||||
self.body.append(RegionModeStmt.on())
|
||||
|
||||
for p in region.primitives:
|
||||
|
||||
if isinstance(p, Line):
|
||||
self._render_line(p, color)
|
||||
else:
|
||||
self._render_arc(p, color)
|
||||
|
||||
if self.explicit_region_move_end:
|
||||
self.body.append(CoordStmt.move(None, None))
|
||||
|
||||
self.body.append(RegionModeStmt.off())
|
||||
|
||||
def _render_level_polarity(self, region):
|
||||
if region.level_polarity != self._level_polarity:
|
||||
self._level_polarity = region.level_polarity
|
||||
self.body.append(LPParamStmt.from_region(region))
|
||||
|
||||
def _render_flash(self, primitive, aperture):
|
||||
|
||||
self._render_level_polarity(primitive)
|
||||
|
||||
if aperture.d != self._dcode:
|
||||
self.body.append(ApertureStmt(aperture.d))
|
||||
self._dcode = aperture.d
|
||||
|
||||
if self.condensed_flash:
|
||||
self.body.append(CoordStmt.flash(self._simplify_point(primitive.position)))
|
||||
else:
|
||||
self.body.append(CoordStmt.move(None, self._simplify_point(primitive.position)))
|
||||
self.body.append(CoordStmt.flash(None))
|
||||
|
||||
self._pos = primitive.position
|
||||
|
||||
def _get_circle(self, diameter, hole_diameter, dcode = None):
|
||||
'''Define a circlar aperture'''
|
||||
|
||||
aper = self._circles.get((diameter, hole_diameter), None)
|
||||
|
||||
if not aper:
|
||||
if not dcode:
|
||||
dcode = self._next_dcode
|
||||
self._next_dcode += 1
|
||||
else:
|
||||
self._next_dcode = max(dcode + 1, self._next_dcode)
|
||||
|
||||
aper = ADParamStmt.circle(dcode, diameter, hole_diameter)
|
||||
self._circles[(diameter, hole_diameter)] = aper
|
||||
self.header.append(aper)
|
||||
|
||||
return aper
|
||||
|
||||
def _render_circle(self, circle, color):
|
||||
|
||||
aper = self._get_circle(circle.diameter, circle.hole_diameter)
|
||||
self._render_flash(circle, aper)
|
||||
|
||||
def _get_rectangle(self, width, height, dcode = None):
|
||||
'''Get a rectanglar aperture. If it isn't defined, create it'''
|
||||
|
||||
key = (width, height)
|
||||
aper = self._rects.get(key, None)
|
||||
|
||||
if not aper:
|
||||
if not dcode:
|
||||
dcode = self._next_dcode
|
||||
self._next_dcode += 1
|
||||
else:
|
||||
self._next_dcode = max(dcode + 1, self._next_dcode)
|
||||
|
||||
aper = ADParamStmt.rect(dcode, width, height)
|
||||
self._rects[(width, height)] = aper
|
||||
self.header.append(aper)
|
||||
|
||||
return aper
|
||||
|
||||
def _render_rectangle(self, rectangle, color):
|
||||
|
||||
aper = self._get_rectangle(rectangle.width, rectangle.height)
|
||||
self._render_flash(rectangle, aper)
|
||||
|
||||
def _get_obround(self, width, height, dcode = None):
|
||||
|
||||
key = (width, height)
|
||||
aper = self._obrounds.get(key, None)
|
||||
|
||||
if not aper:
|
||||
if not dcode:
|
||||
dcode = self._next_dcode
|
||||
self._next_dcode += 1
|
||||
else:
|
||||
self._next_dcode = max(dcode + 1, self._next_dcode)
|
||||
|
||||
aper = ADParamStmt.obround(dcode, width, height)
|
||||
self._obrounds[key] = aper
|
||||
self.header.append(aper)
|
||||
|
||||
return aper
|
||||
|
||||
def _render_obround(self, obround, color):
|
||||
|
||||
aper = self._get_obround(obround.width, obround.height)
|
||||
self._render_flash(obround, aper)
|
||||
|
||||
def _render_polygon(self, polygon, color):
|
||||
|
||||
aper = self._get_polygon(polygon.radius, polygon.sides, polygon.rotation, polygon.hole_radius)
|
||||
self._render_flash(polygon, aper)
|
||||
|
||||
def _get_polygon(self, radius, num_vertices, rotation, hole_radius, dcode = None):
|
||||
|
||||
key = (radius, num_vertices, rotation, hole_radius)
|
||||
aper = self._polygons.get(key, None)
|
||||
|
||||
if not aper:
|
||||
if not dcode:
|
||||
dcode = self._next_dcode
|
||||
self._next_dcode += 1
|
||||
else:
|
||||
self._next_dcode = max(dcode + 1, self._next_dcode)
|
||||
|
||||
aper = ADParamStmt.polygon(dcode, radius * 2, num_vertices, rotation, hole_radius * 2)
|
||||
self._polygons[key] = aper
|
||||
self.header.append(aper)
|
||||
|
||||
return aper
|
||||
|
||||
def _render_drill(self, drill, color):
|
||||
raise ValueError('Drills are not valid in RS274X files')
|
||||
|
||||
def _hash_amacro(self, amgroup):
|
||||
'''Calculate a very quick hash code for deciding if we should even check AM groups for comparision'''
|
||||
|
||||
# We always start with an X because this forms part of the name
|
||||
# Basically, in some cases, the name might start with a C, R, etc. That can appear
|
||||
# to conflict with normal aperture definitions. Technically, it shouldn't because normal
|
||||
# aperture definitions should have a comma, but in some cases the commit is omitted
|
||||
hash = 'X'
|
||||
for primitive in amgroup.primitives:
|
||||
|
||||
hash += primitive.__class__.__name__[0]
|
||||
|
||||
bbox = primitive.bounding_box
|
||||
hash += str((bbox[0][1] - bbox[0][0]) * 100000)[0:2]
|
||||
hash += str((bbox[1][1] - bbox[1][0]) * 100000)[0:2]
|
||||
|
||||
if hasattr(primitive, 'primitives'):
|
||||
hash += str(len(primitive.primitives))
|
||||
|
||||
if isinstance(primitive, Rectangle):
|
||||
hash += str(primitive.width * 1000000)[0:2]
|
||||
hash += str(primitive.height * 1000000)[0:2]
|
||||
elif isinstance(primitive, Circle):
|
||||
hash += str(primitive.diameter * 1000000)[0:2]
|
||||
|
||||
if len(hash) > 20:
|
||||
# The hash might actually get quite complex, so stop before
|
||||
# it gets too long
|
||||
break
|
||||
|
||||
return hash
|
||||
|
||||
def _get_amacro(self, amgroup, dcode = None):
|
||||
# Macros are a little special since we don't have a good way to compare them quickly
|
||||
# but in most cases, this should work
|
||||
|
||||
hash = self._hash_amacro(amgroup)
|
||||
macro = None
|
||||
macroinfo = self._macros.get(hash, None)
|
||||
|
||||
if macroinfo:
|
||||
|
||||
# We have a definition, but check that the groups actually are the same
|
||||
for macro in macroinfo:
|
||||
|
||||
# Macros should have positions, right? But if the macro is selected for non-flashes
|
||||
# then it won't have a position. This is of course a bad gerber, but they do exist
|
||||
if amgroup.position:
|
||||
position = amgroup.position
|
||||
else:
|
||||
position = (0, 0)
|
||||
|
||||
offset = (position[0] - macro[1].position[0], position[1] - macro[1].position[1])
|
||||
if amgroup.equivalent(macro[1], offset):
|
||||
break
|
||||
macro = None
|
||||
|
||||
# Did we find one in the group0
|
||||
if not macro:
|
||||
# This is a new macro, so define it
|
||||
if not dcode:
|
||||
dcode = self._next_dcode
|
||||
self._next_dcode += 1
|
||||
else:
|
||||
self._next_dcode = max(dcode + 1, self._next_dcode)
|
||||
|
||||
# Create the statements
|
||||
# TODO
|
||||
amrenderer = AMGroupContext()
|
||||
statement = amrenderer.render(amgroup, hash)
|
||||
|
||||
self.header.append(statement)
|
||||
|
||||
aperdef = ADParamStmt.macro(dcode, hash)
|
||||
self.header.append(aperdef)
|
||||
|
||||
# Store the dcode and the original so we can check if it really is the same
|
||||
# If it didn't have a postition, set it to 0, 0
|
||||
if amgroup.position == None:
|
||||
amgroup.position = (0, 0)
|
||||
macro = (aperdef, amgroup)
|
||||
|
||||
if macroinfo:
|
||||
macroinfo.append(macro)
|
||||
else:
|
||||
self._macros[hash] = [macro]
|
||||
|
||||
return macro[0]
|
||||
|
||||
def _render_amgroup(self, amgroup, color):
|
||||
|
||||
aper = self._get_amacro(amgroup)
|
||||
self._render_flash(amgroup, aper)
|
||||
|
||||
def _render_inverted_layer(self):
|
||||
pass
|
||||
|
||||
def _new_render_layer(self):
|
||||
# TODO Might need to implement this
|
||||
pass
|
||||
|
||||
def _flatten(self):
|
||||
# TODO Might need to implement this
|
||||
pass
|
||||
|
||||
def dump(self):
|
||||
"""Write the rendered file to a StringIO steam"""
|
||||
statements = map(lambda stmt: stmt.to_gerber(self.settings), self.statements)
|
||||
stream = StringIO()
|
||||
for statement in statements:
|
||||
stream.write(statement + '\n')
|
||||
|
||||
return stream
|
||||
190
gerber/rs274x.py
|
|
@ -21,6 +21,7 @@
|
|||
import copy
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
|
||||
try:
|
||||
from cStringIO import StringIO
|
||||
|
|
@ -30,6 +31,7 @@ except(ImportError):
|
|||
from .gerber_statements import *
|
||||
from .primitives import *
|
||||
from .cam import CamFile, FileSettings
|
||||
from .utils import sq_distance
|
||||
|
||||
|
||||
def read(filename):
|
||||
|
|
@ -97,9 +99,11 @@ class GerberFile(CamFile):
|
|||
|
||||
"""
|
||||
|
||||
def __init__(self, statements, settings, primitives, filename=None):
|
||||
def __init__(self, statements, settings, primitives, apertures, filename=None):
|
||||
super(GerberFile, self).__init__(statements, settings, primitives, filename)
|
||||
|
||||
self.apertures = apertures
|
||||
|
||||
@property
|
||||
def comments(self):
|
||||
return [comment.comment for comment in self.statements
|
||||
|
|
@ -114,13 +118,31 @@ class GerberFile(CamFile):
|
|||
def bounds(self):
|
||||
min_x = min_y = 1000000
|
||||
max_x = max_y = -1000000
|
||||
|
||||
for stmt in [stmt for stmt in self.statements if isinstance(stmt, CoordStmt)]:
|
||||
if stmt.x is not None:
|
||||
min_x = min(stmt.x, min_x)
|
||||
max_x = max(stmt.x, max_x)
|
||||
|
||||
if stmt.y is not None:
|
||||
min_y = min(stmt.y, min_y)
|
||||
max_y = max(stmt.y, max_y)
|
||||
|
||||
return ((min_x, max_x), (min_y, max_y))
|
||||
|
||||
@property
|
||||
def bounding_box(self):
|
||||
min_x = min_y = 1000000
|
||||
max_x = max_y = -1000000
|
||||
|
||||
for prim in self.primitives:
|
||||
bounds = prim.bounding_box
|
||||
min_x = min(bounds[0][0], min_x)
|
||||
max_x = max(bounds[0][1], max_x)
|
||||
|
||||
min_y = min(bounds[1][0], min_y)
|
||||
max_y = max(bounds[1][1], max_y)
|
||||
|
||||
return ((min_x, max_x), (min_y, max_y))
|
||||
|
||||
def write(self, filename, settings=None):
|
||||
|
|
@ -162,14 +184,14 @@ class GerberParser(object):
|
|||
STRING = r"[a-zA-Z0-9_+\-/!?<>”’(){}.\|&@# :]+"
|
||||
NAME = r"[a-zA-Z_$\.][a-zA-Z_$\.0-9+\-]+"
|
||||
|
||||
FS = r"(?P<param>FS)(?P<zero>(L|T|D))?(?P<notation>(A|I))X(?P<x>[0-7][0-7])Y(?P<y>[0-7][0-7])"
|
||||
FS = r"(?P<param>FS)(?P<zero>(L|T|D))?(?P<notation>(A|I))[NG0-9]*X(?P<x>[0-7][0-7])Y(?P<y>[0-7][0-7])[DM0-9]*"
|
||||
MO = r"(?P<param>MO)(?P<mo>(MM|IN))"
|
||||
LP = r"(?P<param>LP)(?P<lp>(D|C))"
|
||||
AD_CIRCLE = r"(?P<param>AD)D(?P<d>\d+)(?P<shape>C)[,]?(?P<modifiers>[^,%]*)?"
|
||||
AD_CIRCLE = r"(?P<param>AD)D(?P<d>\d+)(?P<shape>C)[,]?(?P<modifiers>[^,%]*)"
|
||||
AD_RECT = r"(?P<param>AD)D(?P<d>\d+)(?P<shape>R)[,](?P<modifiers>[^,%]*)"
|
||||
AD_OBROUND = r"(?P<param>AD)D(?P<d>\d+)(?P<shape>O)[,](?P<modifiers>[^,%]*)"
|
||||
AD_POLY = r"(?P<param>AD)D(?P<d>\d+)(?P<shape>P)[,](?P<modifiers>[^,%]*)"
|
||||
AD_MACRO = r"(?P<param>AD)D(?P<d>\d+)(?P<shape>{name})[,]?(?P<modifiers>[^,%]*)?".format(name=NAME)
|
||||
AD_MACRO = r"(?P<param>AD)D(?P<d>\d+)(?P<shape>{name})[,]?(?P<modifiers>[^,%]*)".format(name=NAME)
|
||||
AM = r"(?P<param>AM)(?P<name>{name})\*(?P<macro>[^%]*)".format(name=NAME)
|
||||
|
||||
# begin deprecated
|
||||
|
|
@ -233,8 +255,7 @@ class GerberParser(object):
|
|||
return self.parse_raw(data, filename)
|
||||
|
||||
def parse_raw(self, data, filename=None):
|
||||
lines = [line for line in StringIO(data)]
|
||||
for stmt in self._parse(lines):
|
||||
for stmt in self._parse(self._split_commands(data)):
|
||||
self.evaluate(stmt)
|
||||
self.statements.append(stmt)
|
||||
|
||||
|
|
@ -242,7 +263,38 @@ class GerberParser(object):
|
|||
for stmt in self.statements:
|
||||
stmt.units = self.settings.units
|
||||
|
||||
return GerberFile(self.statements, self.settings, self.primitives, filename)
|
||||
return GerberFile(self.statements, self.settings, self.primitives, self.apertures.values(), filename)
|
||||
|
||||
def _split_commands(self, data):
|
||||
"""
|
||||
Split the data into commands. Commands end with * (and also newline to help with some badly formatted files)
|
||||
"""
|
||||
|
||||
length = len(data)
|
||||
start = 0
|
||||
in_header = True
|
||||
|
||||
for cur in range(0, length):
|
||||
|
||||
val = data[cur]
|
||||
|
||||
if val == '%' and start == cur:
|
||||
in_header = True
|
||||
continue
|
||||
|
||||
if val == '\r' or val == '\n':
|
||||
if start != cur:
|
||||
yield data[start:cur]
|
||||
start = cur + 1
|
||||
|
||||
elif not in_header and val == '*':
|
||||
yield data[start:cur + 1]
|
||||
start = cur + 1
|
||||
|
||||
elif in_header and val == '%':
|
||||
yield data[start:cur + 1]
|
||||
start = cur + 1
|
||||
in_header = False
|
||||
|
||||
def dump_json(self):
|
||||
stmts = {"statements": [stmt.__dict__ for stmt in self.statements]}
|
||||
|
|
@ -257,7 +309,7 @@ class GerberParser(object):
|
|||
def _parse(self, data):
|
||||
oldline = ''
|
||||
|
||||
for i, line in enumerate(data):
|
||||
for line in data:
|
||||
line = oldline + line.strip()
|
||||
|
||||
# skip empty lines
|
||||
|
|
@ -273,6 +325,12 @@ class GerberParser(object):
|
|||
while did_something and len(line) > 0:
|
||||
did_something = False
|
||||
|
||||
# consume empty data blocks
|
||||
if line[0] == '*':
|
||||
line = line[1:]
|
||||
did_something = True
|
||||
continue
|
||||
|
||||
# coord
|
||||
(coord, r) = _match_one(self.COORD_STMT, line)
|
||||
if coord:
|
||||
|
|
@ -285,7 +343,6 @@ class GerberParser(object):
|
|||
(aperture, r) = _match_one(self.APERTURE_STMT, line)
|
||||
if aperture:
|
||||
yield ApertureStmt(**aperture)
|
||||
|
||||
did_something = True
|
||||
line = r
|
||||
continue
|
||||
|
|
@ -309,7 +366,9 @@ class GerberParser(object):
|
|||
elif param["param"] == "AD":
|
||||
yield ADParamStmt.from_dict(param)
|
||||
elif param["param"] == "AM":
|
||||
yield AMParamStmt.from_dict(param)
|
||||
stmt = AMParamStmt.from_dict(param)
|
||||
stmt.units = self.settings.units
|
||||
yield stmt
|
||||
elif param["param"] == "OF":
|
||||
yield OFParamStmt.from_dict(param)
|
||||
elif param["param"] == "IN":
|
||||
|
|
@ -432,19 +491,46 @@ class GerberParser(object):
|
|||
aperture = None
|
||||
if shape == 'C':
|
||||
diameter = modifiers[0][0]
|
||||
aperture = Circle(position=None, diameter=diameter)
|
||||
|
||||
if len(modifiers[0]) >= 2:
|
||||
hole_diameter = modifiers[0][1]
|
||||
else:
|
||||
hole_diameter = None
|
||||
|
||||
aperture = Circle(position=None, diameter=diameter, hole_diameter=hole_diameter, units=self.settings.units)
|
||||
elif shape == 'R':
|
||||
width = modifiers[0][0]
|
||||
height = modifiers[0][1]
|
||||
aperture = Rectangle(position=None, width=width, height=height)
|
||||
|
||||
if len(modifiers[0]) >= 3:
|
||||
hole_diameter = modifiers[0][2]
|
||||
else:
|
||||
hole_diameter = None
|
||||
|
||||
aperture = Rectangle(position=None, width=width, height=height, hole_diameter=hole_diameter, units=self.settings.units)
|
||||
elif shape == 'O':
|
||||
width = modifiers[0][0]
|
||||
height = modifiers[0][1]
|
||||
aperture = Obround(position=None, width=width, height=height)
|
||||
|
||||
if len(modifiers[0]) >= 3:
|
||||
hole_diameter = modifiers[0][2]
|
||||
else:
|
||||
hole_diameter = None
|
||||
|
||||
aperture = Obround(position=None, width=width, height=height, hole_diameter=hole_diameter, units=self.settings.units)
|
||||
elif shape == 'P':
|
||||
diameter = modifiers[0][0]
|
||||
sides = modifiers[0][1]
|
||||
aperture = Polygon(position=None, radius=diameter/2.0, sides=sides)
|
||||
outer_diameter = modifiers[0][0]
|
||||
number_vertices = int(modifiers[0][1])
|
||||
if len(modifiers[0]) > 2:
|
||||
rotation = modifiers[0][2]
|
||||
else:
|
||||
rotation = 0
|
||||
|
||||
if len(modifiers[0]) > 3:
|
||||
hole_diameter = modifiers[0][3]
|
||||
else:
|
||||
hole_diameter = None
|
||||
aperture = Polygon(position=None, sides=number_vertices, radius=outer_diameter/2.0, hole_diameter=hole_diameter, rotation=rotation)
|
||||
else:
|
||||
aperture = self.macros[shape].build(modifiers)
|
||||
|
||||
|
|
@ -454,8 +540,11 @@ class GerberParser(object):
|
|||
def _evaluate_mode(self, stmt):
|
||||
if stmt.type == 'RegionMode':
|
||||
if self.region_mode == 'on' and stmt.mode == 'off':
|
||||
self.primitives.append(Region(self.current_region,
|
||||
level_polarity=self.level_polarity))
|
||||
# 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.current_region = None
|
||||
self.region_mode = stmt.mode
|
||||
elif stmt.type == 'QuadrantMode':
|
||||
|
|
@ -488,13 +577,19 @@ class GerberParser(object):
|
|||
self.direction = ('clockwise' if stmt.function in
|
||||
('G02', 'G2') else 'counterclockwise')
|
||||
|
||||
if stmt.only_function:
|
||||
# Sometimes we get a coordinate statement
|
||||
# that only sets the function. If so, don't
|
||||
# try futher otherwise that might draw/flash something
|
||||
return
|
||||
|
||||
if stmt.op:
|
||||
self.op = stmt.op
|
||||
else:
|
||||
# no implicit op allowed, force here if coord block doesn't have it
|
||||
stmt.op = self.op
|
||||
|
||||
if self.op == "D01":
|
||||
if self.op == "D01" or self.op == "D1":
|
||||
start = (self.x, self.y)
|
||||
end = (x, y)
|
||||
|
||||
|
|
@ -507,9 +602,10 @@ class GerberParser(object):
|
|||
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
|
||||
# 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,
|
||||
|
|
@ -525,31 +621,38 @@ class GerberParser(object):
|
|||
else:
|
||||
i = 0 if stmt.i is None else stmt.i
|
||||
j = 0 if stmt.j is None else stmt.j
|
||||
center = (start[0] + i, start[1] + j)
|
||||
center = self._find_center(start, end, (i, j))
|
||||
if self.region_mode == 'off':
|
||||
self.primitives.append(Arc(start, end, center, self.direction,
|
||||
self.apertures[self.aperture],
|
||||
quadrant_mode=self.quadrant_mode,
|
||||
level_polarity=self.level_polarity,
|
||||
units=self.settings.units))
|
||||
else:
|
||||
if self.current_region is None:
|
||||
self.current_region = [Arc(start, end, center, self.direction,
|
||||
self.apertures[self.aperture],
|
||||
self.apertures.get(self.aperture, Circle((0,0), 0)),
|
||||
quadrant_mode=self.quadrant_mode,
|
||||
level_polarity=self.level_polarity,
|
||||
units=self.settings.units), ]
|
||||
units=self.settings.units),]
|
||||
else:
|
||||
self.current_region.append(Arc(start, end, center, self.direction,
|
||||
self.apertures[self.aperture],
|
||||
self.apertures.get(self.aperture, Circle((0,0), 0)),
|
||||
quadrant_mode=self.quadrant_mode,
|
||||
level_polarity=self.level_polarity,
|
||||
units=self.settings.units))
|
||||
|
||||
elif self.op == "D02":
|
||||
pass
|
||||
elif self.op == "D02" or self.op == "D2":
|
||||
|
||||
elif self.op == "D03":
|
||||
if self.region_mode == "on":
|
||||
# D02 in the middle of a region finishes that region and starts a new one
|
||||
if self.current_region and len(self.current_region) > 1:
|
||||
self.primitives.append(Region(self.current_region, level_polarity=self.level_polarity, units=self.settings.units))
|
||||
self.current_region = None
|
||||
|
||||
elif self.op == "D03" or self.op == "D3":
|
||||
primitive = copy.deepcopy(self.apertures[self.aperture])
|
||||
|
||||
|
||||
if primitive is not None:
|
||||
|
||||
if not isinstance(primitive, AMParamStmt):
|
||||
|
|
@ -567,6 +670,35 @@ class GerberParser(object):
|
|||
self.primitives.append(renderable)
|
||||
self.x, self.y = x, y
|
||||
|
||||
def _find_center(self, start, end, offsets):
|
||||
"""
|
||||
In single quadrant mode, the offsets are always positive, which means there are 4 possible centers.
|
||||
The correct center is the only one that results in an arc with sweep angle of less than or equal to 90 degrees
|
||||
"""
|
||||
|
||||
if self.quadrant_mode == 'single-quadrant':
|
||||
|
||||
# The Gerber spec says single quadrant only has one possible center, and you can detect
|
||||
# based on the angle. But for real files, this seems to work better - there is usually
|
||||
# only one option that makes sense for the center (since the distance should be the same
|
||||
# from start and end). Find the center that makes the most sense
|
||||
sqdist_diff_min = sys.maxint
|
||||
center = None
|
||||
for factors in [(1, 1), (1, -1), (-1, 1), (-1, -1)]:
|
||||
|
||||
test_center = (start[0] + offsets[0] * factors[0], start[1] + offsets[1] * factors[1])
|
||||
|
||||
sqdist_start = sq_distance(start, test_center)
|
||||
sqdist_end = sq_distance(end, test_center)
|
||||
|
||||
if abs(sqdist_start - sqdist_end) < sqdist_diff_min:
|
||||
center = test_center
|
||||
sqdist_diff_min = abs(sqdist_start - sqdist_end)
|
||||
|
||||
return center
|
||||
else:
|
||||
return (start[0] + offsets[0], start[1] + offsets[1])
|
||||
|
||||
def _evaluate_aperture(self, stmt):
|
||||
self.aperture = stmt.d
|
||||
|
||||
|
|
|
|||
BIN
gerber/tests/golden/example_am_exposure_modifier.png
Normal file
|
After Width: | Height: | Size: 9.9 KiB |
BIN
gerber/tests/golden/example_coincident_hole.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
gerber/tests/golden/example_cutin_multiple.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
gerber/tests/golden/example_flash_circle.png
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
gerber/tests/golden/example_flash_obround.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
gerber/tests/golden/example_flash_polygon.png
Normal file
|
After Width: | Height: | Size: 4 KiB |
BIN
gerber/tests/golden/example_flash_rectangle.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
gerber/tests/golden/example_fully_coincident.png
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
gerber/tests/golden/example_holes_dont_clear.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
gerber/tests/golden/example_not_overlapping_contour.png
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
gerber/tests/golden/example_not_overlapping_touching.png
Normal file
|
After Width: | Height: | Size: 94 KiB |
BIN
gerber/tests/golden/example_overlapping_contour.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
gerber/tests/golden/example_overlapping_touching.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
gerber/tests/golden/example_simple_contour.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
gerber/tests/golden/example_single_contour.png
Normal file
|
After Width: | Height: | Size: 556 B |
BIN
gerber/tests/golden/example_single_contour_3.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
16
gerber/tests/golden/example_single_quadrant.gbr
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
%FSLAX23Y23*%
|
||||
%MOIN*%
|
||||
%ADD10C,0.01*%
|
||||
G74*
|
||||
D10*
|
||||
%LPD*%
|
||||
G01X1100Y600D02*
|
||||
G03X700Y1000I-400J0D01*
|
||||
G03X300Y600I0J-400D01*
|
||||
G03X700Y200I400J0D01*
|
||||
G03X1100Y600I0J400D01*
|
||||
G01X300D02*
|
||||
X1100D01*
|
||||
X700Y200D02*
|
||||
Y1000D01*
|
||||
M02*
|
||||
BIN
gerber/tests/golden/example_single_quadrant.png
Normal file
|
After Width: | Height: | Size: 9.4 KiB |
16
gerber/tests/golden/example_two_square_boxes.gbr
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
%FSLAX25Y25*%
|
||||
%MOMM*%
|
||||
%ADD10C,0.01*%
|
||||
D10*
|
||||
%LPD*%
|
||||
G01X0Y0D02*
|
||||
X500000D01*
|
||||
Y500000D01*
|
||||
X0D01*
|
||||
Y0D01*
|
||||
X600000D02*
|
||||
X1100000D01*
|
||||
Y500000D01*
|
||||
X600000D01*
|
||||
Y0D01*
|
||||
M02*
|
||||
BIN
gerber/tests/golden/example_two_square_boxes.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
16
gerber/tests/resources/example_am_exposure_modifier.gbr
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
G04 Umaco example for exposure modifier and clearing area*
|
||||
%FSLAX26Y26*%
|
||||
%MOIN*%
|
||||
%AMSQUAREWITHHOLE*
|
||||
21,0.1,1,1,0,0,0*
|
||||
1,0,0.5,0,0*%
|
||||
%ADD10SQUAREWITHHOLE*%
|
||||
%ADD11C,1*%
|
||||
G01*
|
||||
%LPD*%
|
||||
D11*
|
||||
X-1000000Y-250000D02*
|
||||
X1000000Y250000D01*
|
||||
D10*
|
||||
X0Y0D03*
|
||||
M02*
|
||||
24
gerber/tests/resources/example_coincident_hole.gbr
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
G04 ex2: overlapping*
|
||||
%FSLAX24Y24*%
|
||||
%MOMM*%
|
||||
%SRX1Y1I0.000J0.000*%
|
||||
%ADD10C,1.00000*%
|
||||
G01*
|
||||
%LPD*%
|
||||
G36*
|
||||
X0Y50000D02*
|
||||
Y100000D01*
|
||||
X100000D01*
|
||||
Y0D01*
|
||||
X0D01*
|
||||
Y50000D01*
|
||||
G04 first fully coincident linear segment*
|
||||
X10000D01*
|
||||
X50000Y10000D01*
|
||||
X90000Y50000D01*
|
||||
X50000Y90000D01*
|
||||
X10000Y50000D01*
|
||||
G04 second fully coincident linear segment*
|
||||
X0D01*
|
||||
G37*
|
||||
M02*
|
||||
18
gerber/tests/resources/example_cutin.gbr
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
G04 Umaco uut-in example*
|
||||
%FSLAX24Y24*%
|
||||
G75*
|
||||
G36*
|
||||
X20000Y100000D02*
|
||||
G01*
|
||||
X120000D01*
|
||||
Y20000D01*
|
||||
X20000D01*
|
||||
Y60000D01*
|
||||
X50000D01*
|
||||
G03*
|
||||
X50000Y60000I30000J0D01*
|
||||
G01*
|
||||
X20000D01*
|
||||
Y100000D01*
|
||||
G37*
|
||||
M02*
|
||||
28
gerber/tests/resources/example_cutin_multiple.gbr
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
G04 multiple cutins*
|
||||
%FSLAX24Y24*%
|
||||
%MOMM*%
|
||||
%SRX1Y1I0.000J0.000*%
|
||||
%ADD10C,1.00000*%
|
||||
%LPD*%
|
||||
G36*
|
||||
X1220000Y2570000D02*
|
||||
G01*
|
||||
Y2720000D01*
|
||||
X1310000D01*
|
||||
Y2570000D01*
|
||||
X1250000D01*
|
||||
Y2600000D01*
|
||||
X1290000D01*
|
||||
Y2640000D01*
|
||||
X1250000D01*
|
||||
Y2670000D01*
|
||||
X1290000D01*
|
||||
Y2700000D01*
|
||||
X1250000D01*
|
||||
Y2670000D01*
|
||||
Y2640000D01*
|
||||
Y2600000D01*
|
||||
Y2570000D01*
|
||||
X1220000D01*
|
||||
G37*
|
||||
M02*
|
||||
10
gerber/tests/resources/example_flash_circle.gbr
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
G04 Flashes of circular apertures*
|
||||
%FSLAX24Y24*%
|
||||
%MOMM*%
|
||||
%ADD10C,0.5*%
|
||||
%ADD11C,0.5X0.25*%
|
||||
D10*
|
||||
X000000Y000000D03*
|
||||
D11*
|
||||
X010000D03*
|
||||
M02*
|
||||
10
gerber/tests/resources/example_flash_obround.gbr
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
G04 Flashes of rectangular apertures*
|
||||
%FSLAX24Y24*%
|
||||
%MOMM*%
|
||||
%ADD10O,0.46X0.26*%
|
||||
%ADD11O,0.46X0.26X0.19*%
|
||||
D10*
|
||||
X000000Y000000D03*
|
||||
D11*
|
||||
X010000D03*
|
||||
M02*
|
||||
10
gerber/tests/resources/example_flash_polygon.gbr
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
G04 Flashes of rectangular apertures*
|
||||
%FSLAX24Y24*%
|
||||
%MOMM*%
|
||||
%ADD10P,.40X6*%
|
||||
%ADD11P,.40X6X0.0X0.19*%
|
||||
D10*
|
||||
X000000Y000000D03*
|
||||
D11*
|
||||
X010000D03*
|
||||
M02*
|
||||
10
gerber/tests/resources/example_flash_rectangle.gbr
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
G04 Flashes of rectangular apertures*
|
||||
%FSLAX24Y24*%
|
||||
%MOMM*%
|
||||
%ADD10R,0.44X0.25*%
|
||||
%ADD11R,0.44X0.25X0.19*%
|
||||
D10*
|
||||
X000000Y000000D03*
|
||||
D11*
|
||||
X010000D03*
|
||||
M02*
|
||||
23
gerber/tests/resources/example_fully_coincident.gbr
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
G04 ex1: non overlapping*
|
||||
%FSLAX24Y24*%
|
||||
%MOMM*%
|
||||
%ADD10C,1.00000*%
|
||||
G01*
|
||||
%LPD*%
|
||||
G36*
|
||||
X0Y50000D02*
|
||||
Y100000D01*
|
||||
X100000D01*
|
||||
Y0D01*
|
||||
X0D01*
|
||||
Y50000D01*
|
||||
G04 first fully coincident linear segment*
|
||||
X-10000D01*
|
||||
X-50000Y10000D01*
|
||||
X-90000Y50000D01*
|
||||
X-50000Y90000D01*
|
||||
X-10000Y50000D01*
|
||||
G04 second fully coincident linear segment*
|
||||
X0D01*
|
||||
G37*
|
||||
M02*
|
||||
13
gerber/tests/resources/example_holes_dont_clear.gbr
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
G04 Demonstrates that apertures with holes do not clear the area - only the aperture hole*
|
||||
%FSLAX26Y26*%
|
||||
%MOIN*%
|
||||
%ADD10C,1X0.5*%
|
||||
%ADD11C,0.1*%
|
||||
G01*
|
||||
%LPD*%
|
||||
D11*
|
||||
X-1000000Y-250000D02*
|
||||
X1000000Y250000D01*
|
||||
D10*
|
||||
X0Y0D03*
|
||||
M02*
|
||||
39
gerber/tests/resources/example_level_holes.gbr
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
G04 This file illustrates how to use levels to create holes*
|
||||
%FSLAX25Y25*%
|
||||
%MOMM*%
|
||||
G01*
|
||||
G04 First level: big square - dark polarity*
|
||||
%LPD*%
|
||||
G36*
|
||||
X250000Y250000D02*
|
||||
X1750000D01*
|
||||
Y1750000D01*
|
||||
X250000D01*
|
||||
Y250000D01*
|
||||
G37*
|
||||
G04 Second level: big circle - clear polarity*
|
||||
%LPC*%
|
||||
G36*
|
||||
G75*
|
||||
X500000Y1000000D02*
|
||||
G03*
|
||||
X500000Y1000000I500000J0D01*
|
||||
G37*
|
||||
G04 Third level: small square - dark polarity*
|
||||
%LPD*%
|
||||
G36*
|
||||
X750000Y750000D02*
|
||||
X1250000D01*
|
||||
Y1250000D01*
|
||||
X750000D01*
|
||||
Y750000D01*
|
||||
G37*
|
||||
G04 Fourth level: small circle - clear polarity*
|
||||
%LPC*%
|
||||
G36*
|
||||
G75*
|
||||
X1150000Y1000000D02*
|
||||
G03*
|
||||
X1150000Y1000000I250000J0D01*
|
||||
G37*
|
||||
M02*
|
||||
20
gerber/tests/resources/example_not_overlapping_contour.gbr
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
G04 Non-overlapping contours*
|
||||
%FSLAX24Y24*%
|
||||
%MOMM*%
|
||||
%ADD10C,1.00000*%
|
||||
G01*
|
||||
%LPD*%
|
||||
G36*
|
||||
X0Y50000D02*
|
||||
Y100000D01*
|
||||
X100000D01*
|
||||
Y0D01*
|
||||
X0D01*
|
||||
Y50000D01*
|
||||
X-10000D02*
|
||||
X-50000Y10000D01*
|
||||
X-90000Y50000D01*
|
||||
X-50000Y90000D01*
|
||||
X-10000Y50000D01*
|
||||
G37*
|
||||
M02*
|
||||
20
gerber/tests/resources/example_not_overlapping_touching.gbr
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
G04 Non-overlapping and touching*
|
||||
%FSLAX24Y24*%
|
||||
%MOMM*%
|
||||
%ADD10C,1.00000*%
|
||||
G01*
|
||||
%LPD*%
|
||||
G36*
|
||||
X0Y50000D02*
|
||||
Y100000D01*
|
||||
X100000D01*
|
||||
Y0D01*
|
||||
X0D01*
|
||||
Y50000D01*
|
||||
D02*
|
||||
X-50000Y10000D01*
|
||||
X-90000Y50000D01*
|
||||
X-50000Y90000D01*
|
||||
X0Y50000D01*
|
||||
G37*
|
||||
M02*
|
||||
20
gerber/tests/resources/example_overlapping_contour.gbr
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
G04 Overlapping contours*
|
||||
%FSLAX24Y24*%
|
||||
%MOMM*%
|
||||
%ADD10C,1.00000*%
|
||||
G01*
|
||||
%LPD*%
|
||||
G36*
|
||||
X0Y50000D02*
|
||||
Y100000D01*
|
||||
X100000D01*
|
||||
Y0D01*
|
||||
X0D01*
|
||||
Y50000D01*
|
||||
X10000D02*
|
||||
X50000Y10000D01*
|
||||
X90000Y50000D01*
|
||||
X50000Y90000D01*
|
||||
X10000Y50000D01*
|
||||
G37*
|
||||
M02*
|
||||
20
gerber/tests/resources/example_overlapping_touching.gbr
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
G04 Overlapping and touching*
|
||||
%FSLAX24Y24*%
|
||||
%MOMM*%
|
||||
%ADD10C,1.00000*%
|
||||
G01*
|
||||
%LPD*%
|
||||
G36*
|
||||
X0Y50000D02*
|
||||
Y100000D01*
|
||||
X100000D01*
|
||||
Y0D01*
|
||||
X0D01*
|
||||
Y50000D01*
|
||||
D02*
|
||||
X50000Y10000D01*
|
||||
X90000Y50000D01*
|
||||
X50000Y90000D01*
|
||||
X0Y50000D01*
|
||||
G37*
|
||||
M02*
|
||||
16
gerber/tests/resources/example_simple_contour.gbr
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
G04 Ucamco ex. 4.6.4: Simple contour*
|
||||
%FSLAX25Y25*%
|
||||
%MOIN*%
|
||||
%ADD10C,0.010*%
|
||||
G36*
|
||||
X200000Y300000D02*
|
||||
G01*
|
||||
X700000D01*
|
||||
Y100000D01*
|
||||
X1100000Y500000D01*
|
||||
X700000Y900000D01*
|
||||
Y700000D01*
|
||||
X200000D01*
|
||||
Y300000D01*
|
||||
G37*
|
||||
M02*
|
||||
15
gerber/tests/resources/example_single_contour_1.gbr
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
G04 Ucamco ex. 4.6.5: Single contour #1*
|
||||
%FSLAX25Y25*%
|
||||
%MOMM*%
|
||||
%ADD11C,0.01*%
|
||||
G01*
|
||||
D11*
|
||||
X3000Y5000D01*
|
||||
G36*
|
||||
X50000Y50000D02*
|
||||
X60000D01*
|
||||
Y60000D01*
|
||||
X50000D01*
|
||||
Y50000Y50000D01*
|
||||
G37*
|
||||
M02*
|
||||
15
gerber/tests/resources/example_single_contour_2.gbr
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
G04 Ucamco ex. 4.6.5: Single contour #2*
|
||||
%FSLAX25Y25*%
|
||||
%MOMM*%
|
||||
%ADD11C,0.01*%
|
||||
G01*
|
||||
D11*
|
||||
X3000Y5000D01*
|
||||
X50000Y50000D02*
|
||||
G36*
|
||||
X60000D01*
|
||||
Y60000D01*
|
||||
X50000D01*
|
||||
Y50000Y50000D01*
|
||||
G37*
|
||||
M02*
|
||||
15
gerber/tests/resources/example_single_contour_3.gbr
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
G04 Ucamco ex. 4.6.5: Single contour #2*
|
||||
%FSLAX25Y25*%
|
||||
%MOMM*%
|
||||
%ADD11C,0.01*%
|
||||
G01*
|
||||
D11*
|
||||
X3000Y5000D01*
|
||||
X50000Y50000D01*
|
||||
G36*
|
||||
X60000D01*
|
||||
Y60000D01*
|
||||
X50000D01*
|
||||
Y50000Y50000D01*
|
||||
G37*
|
||||
M02*
|
||||
18
gerber/tests/resources/example_single_quadrant.gbr
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
G04 Ucamco ex. 4.5.8: Single quadrant*
|
||||
%FSLAX23Y23*%
|
||||
%MOIN*%
|
||||
%ADD10C,0.010*%
|
||||
G74*
|
||||
D10*
|
||||
X1100Y600D02*
|
||||
G03*
|
||||
X700Y1000I400J0D01*
|
||||
X300Y600I0J400D01*
|
||||
X700Y200I400J0D01*
|
||||
X1100Y600I0J400D01*
|
||||
X300D02*
|
||||
G01*
|
||||
X1100D01*
|
||||
X700Y200D02*
|
||||
Y1000D01*
|
||||
M02*
|
||||
19
gerber/tests/resources/example_two_square_boxes.gbr
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
G04 Ucamco ex. 1: Two square boxes*
|
||||
%FSLAX25Y25*%
|
||||
%MOMM*%
|
||||
%TF.Part,Other*%
|
||||
%LPD*%
|
||||
%ADD10C,0.010*%
|
||||
D10*
|
||||
X0Y0D02*
|
||||
G01*
|
||||
X500000Y0D01*
|
||||
Y500000D01*
|
||||
X0D01*
|
||||
Y0D01*
|
||||
X600000D02*
|
||||
X1100000D01*
|
||||
Y500000D01*
|
||||
X600000D01*
|
||||
Y0D01*
|
||||
M02*
|
||||
|
|
@ -160,7 +160,11 @@ def test_AMOutlinePrimitive_factory():
|
|||
|
||||
def test_AMOUtlinePrimitive_dump():
|
||||
o = AMOutlinePrimitive(4, 'on', (0, 0), [(3, 3), (3, 0), (0, 0)], 0)
|
||||
assert_equal(o.to_gerber(), '4,1,3,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():
|
||||
|
|
@ -253,33 +257,40 @@ def test_AMMoirePrimitive_conversion():
|
|||
|
||||
|
||||
def test_AMThermalPrimitive_validation():
|
||||
assert_raises(ValueError, AMThermalPrimitive, 8, (0.0, 0.0), 7, 5, 0.2)
|
||||
assert_raises(TypeError, AMThermalPrimitive, 7, (0.0, '0'), 7, 5, 0.2)
|
||||
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*')
|
||||
t = AMThermalPrimitive.from_gerber('7,0,0,7,6,0.2,45*')
|
||||
assert_equal(t.code, 7)
|
||||
assert_equal(t.position, (0, 0))
|
||||
assert_equal(t.outer_diameter, 7)
|
||||
assert_equal(t.inner_diameter, 6)
|
||||
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*')
|
||||
assert_equal(t.to_gerber(), '7,0,0,7.0,6.0,0.2*')
|
||||
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)
|
||||
t = AMThermalPrimitive(7, (25.4, 25.4), 25.4, 25.4, 25.4, 0.0)
|
||||
t.to_inch()
|
||||
assert_equal(t.position, (1., 1.))
|
||||
assert_equal(t.outer_diameter, 1.)
|
||||
assert_equal(t.inner_diameter, 1.)
|
||||
assert_equal(t.gap, 1.)
|
||||
|
||||
t = AMThermalPrimitive(7, (1, 1), 1, 1, 1)
|
||||
t = AMThermalPrimitive(7, (1, 1), 1, 1, 1, 0)
|
||||
t.to_metric()
|
||||
assert_equal(t.position, (25.4, 25.4))
|
||||
assert_equal(t.outer_diameter, 25.4)
|
||||
|
|
|
|||
189
gerber/tests/test_cairo_backend.py
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
#! /usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Author: Garret Fick <garret@ficksworkshop.com>
|
||||
import os
|
||||
|
||||
|
||||
from ..render.cairo_backend import GerberCairoContext
|
||||
from ..rs274x import read
|
||||
from .tests import *
|
||||
from nose.tools import assert_tuple_equal
|
||||
|
||||
def _DISABLED_test_render_two_boxes():
|
||||
"""Umaco exapmle of two boxes"""
|
||||
_test_render('resources/example_two_square_boxes.gbr', 'golden/example_two_square_boxes.png')
|
||||
|
||||
|
||||
def _DISABLED_test_render_single_quadrant():
|
||||
"""Umaco exapmle of a single quadrant arc"""
|
||||
_test_render('resources/example_single_quadrant.gbr', 'golden/example_single_quadrant.png')
|
||||
|
||||
|
||||
def _DISABLED_test_render_simple_contour():
|
||||
"""Umaco exapmle of a simple arrow-shaped contour"""
|
||||
gerber = _test_render('resources/example_simple_contour.gbr', 'golden/example_simple_contour.png')
|
||||
|
||||
# Check the resulting dimensions
|
||||
assert_tuple_equal(((2.0, 11.0), (1.0, 9.0)), gerber.bounding_box)
|
||||
|
||||
|
||||
def _DISABLED_test_render_single_contour_1():
|
||||
"""Umaco example of a single contour
|
||||
|
||||
The resulting image for this test is used by other tests because they must generate the same output."""
|
||||
_test_render('resources/example_single_contour_1.gbr', 'golden/example_single_contour.png')
|
||||
|
||||
|
||||
def _DISABLED_test_render_single_contour_2():
|
||||
"""Umaco exapmle of a single contour, alternate contour end order
|
||||
|
||||
The resulting image for this test is used by other tests because they must generate the same output."""
|
||||
_test_render('resources/example_single_contour_2.gbr', 'golden/example_single_contour.png')
|
||||
|
||||
|
||||
def _DISABLED_test_render_single_contour_3():
|
||||
"""Umaco exapmle of a single contour with extra line"""
|
||||
_test_render('resources/example_single_contour_3.gbr', 'golden/example_single_contour_3.png')
|
||||
|
||||
|
||||
def _DISABLED_test_render_not_overlapping_contour():
|
||||
"""Umaco example of D02 staring a second contour"""
|
||||
_test_render('resources/example_not_overlapping_contour.gbr', 'golden/example_not_overlapping_contour.png')
|
||||
|
||||
def _DISABLED_test_render_not_overlapping_touching():
|
||||
"""Umaco example of D02 staring a second contour"""
|
||||
_test_render('resources/example_not_overlapping_touching.gbr', 'golden/example_not_overlapping_touching.png')
|
||||
|
||||
|
||||
def test_render_overlapping_touching():
|
||||
"""Umaco example of D02 staring a second contour"""
|
||||
_test_render('resources/example_overlapping_touching.gbr', 'golden/example_overlapping_touching.png')
|
||||
|
||||
|
||||
def test_render_overlapping_contour():
|
||||
"""Umaco example of D02 staring a second contour"""
|
||||
_test_render('resources/example_overlapping_contour.gbr', 'golden/example_overlapping_contour.png')
|
||||
|
||||
|
||||
def _DISABLED_test_render_level_holes():
|
||||
"""Umaco example of using multiple levels to create multiple holes"""
|
||||
|
||||
# TODO This is clearly rendering wrong. I'm temporarily checking this in because there are more
|
||||
# rendering fixes in the related repository that may resolve these.
|
||||
_test_render('resources/example_level_holes.gbr', 'golden/example_overlapping_contour.png')
|
||||
|
||||
|
||||
def _DISABLED_test_render_cutin():
|
||||
"""Umaco example of using a cutin"""
|
||||
|
||||
# TODO This is clearly rendering wrong.
|
||||
_test_render('resources/example_cutin.gbr', 'golden/example_cutin.png', '/Users/ham/Desktop/cutin.png')
|
||||
|
||||
|
||||
def _DISABLED_test_render_fully_coincident():
|
||||
"""Umaco example of coincident lines rendering two contours"""
|
||||
|
||||
_test_render('resources/example_fully_coincident.gbr', 'golden/example_fully_coincident.png')
|
||||
|
||||
|
||||
def _DISABLED_test_render_coincident_hole():
|
||||
"""Umaco example of coincident lines rendering a hole in the contour"""
|
||||
|
||||
_test_render('resources/example_coincident_hole.gbr', 'golden/example_coincident_hole.png')
|
||||
|
||||
|
||||
def test_render_cutin_multiple():
|
||||
"""Umaco example of a region with multiple cutins"""
|
||||
|
||||
_test_render('resources/example_cutin_multiple.gbr', 'golden/example_cutin_multiple.png')
|
||||
|
||||
|
||||
def _DISABLED_test_flash_circle():
|
||||
"""Umaco example a simple circular flash with and without a hole"""
|
||||
|
||||
_test_render('resources/example_flash_circle.gbr', 'golden/example_flash_circle.png',
|
||||
'/Users/ham/Desktop/flashcircle.png')
|
||||
|
||||
|
||||
def _DISABLED_test_flash_rectangle():
|
||||
"""Umaco example a simple rectangular flash with and without a hole"""
|
||||
|
||||
_test_render('resources/example_flash_rectangle.gbr', 'golden/example_flash_rectangle.png')
|
||||
|
||||
|
||||
def _DISABLED_test_flash_obround():
|
||||
"""Umaco example a simple obround flash with and without a hole"""
|
||||
|
||||
_test_render('resources/example_flash_obround.gbr', 'golden/example_flash_obround.png')
|
||||
|
||||
|
||||
def _DISABLED_test_flash_polygon():
|
||||
"""Umaco example a simple polygon flash with and without a hole"""
|
||||
|
||||
_test_render('resources/example_flash_polygon.gbr', 'golden/example_flash_polygon.png')
|
||||
|
||||
|
||||
def _DISABLED_test_holes_dont_clear():
|
||||
"""Umaco example that an aperture with a hole does not clear the area"""
|
||||
|
||||
_test_render('resources/example_holes_dont_clear.gbr', 'golden/example_holes_dont_clear.png')
|
||||
|
||||
|
||||
def _DISABLED_test_render_am_exposure_modifier():
|
||||
"""Umaco example that an aperture macro with a hole does not clear the area"""
|
||||
|
||||
_test_render('resources/example_am_exposure_modifier.gbr', 'golden/example_am_exposure_modifier.png')
|
||||
|
||||
|
||||
def _resolve_path(path):
|
||||
return os.path.join(os.path.dirname(__file__),
|
||||
path)
|
||||
|
||||
|
||||
def _test_render(gerber_path, png_expected_path, create_output_path = None):
|
||||
"""Render the gerber file and compare to the expected PNG output.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
gerber_path : string
|
||||
Path to Gerber file to open
|
||||
png_expected_path : string
|
||||
Path to the PNG file to compare to
|
||||
create_output : string|None
|
||||
If not None, write the generated PNG to the specified path.
|
||||
This is primarily to help with
|
||||
"""
|
||||
|
||||
gerber_path = _resolve_path(gerber_path)
|
||||
png_expected_path = _resolve_path(png_expected_path)
|
||||
if create_output_path:
|
||||
create_output_path = _resolve_path(create_output_path)
|
||||
|
||||
gerber = read(gerber_path)
|
||||
|
||||
# Create PNG image to the memory stream
|
||||
ctx = GerberCairoContext()
|
||||
gerber.render(ctx)
|
||||
|
||||
actual_bytes = ctx.dump(None)
|
||||
|
||||
# If we want to write the file bytes, do it now. This happens
|
||||
if create_output_path:
|
||||
with open(create_output_path, 'wb') as out_file:
|
||||
out_file.write(actual_bytes)
|
||||
# Creating the output is dangerous - it could overwrite the expected result.
|
||||
# So if we are creating the output, we make the test fail on purpose so you
|
||||
# won't forget to disable this
|
||||
assert_false(True, 'Test created the output %s. This needs to be disabled to make sure the test behaves correctly' % (create_output_path,))
|
||||
|
||||
# Read the expected PNG file
|
||||
|
||||
with open(png_expected_path, 'rb') as expected_file:
|
||||
expected_bytes = expected_file.read()
|
||||
|
||||
# Don't directly use assert_equal otherwise any failure pollutes the test results
|
||||
equal = (expected_bytes == actual_bytes)
|
||||
assert_true(equal)
|
||||
|
||||
return gerber
|
||||
|
|
@ -116,14 +116,22 @@ 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)
|
||||
|
||||
# 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',
|
||||
'inch', 'leading', (2, 5), 'leading')
|
||||
assert_raises(ValueError, FileSettings, 'absolute',
|
||||
'inch', 'following', (2, 5), None)
|
||||
|
||||
# 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', None, (2, 5), 'following')
|
||||
assert_raises(ValueError, FileSettings, 'absolute',
|
||||
|
|
|
|||
|
|
@ -38,4 +38,4 @@ def test_load_from_string():
|
|||
def test_file_type_validation():
|
||||
""" Test file format validation
|
||||
"""
|
||||
assert_raises(ParseError, read, 'LICENSE')
|
||||
assert_raises(ParseError, read, __file__)
|
||||
|
|
|
|||
|
|
@ -81,7 +81,8 @@ def test_conversion():
|
|||
assert_equal(i_tool, m_tool)
|
||||
|
||||
for m, i in zip(ncdrill.primitives, inch_primitives):
|
||||
assert_equal(m, i)
|
||||
assert_equal(m.position, i.position, '%s not equal to %s' % (m, i))
|
||||
assert_equal(m.diameter, i.diameter, '%s not equal to %s' % (m, i))
|
||||
|
||||
|
||||
def test_parser_hole_count():
|
||||
|
|
|
|||
|
|
@ -485,9 +485,14 @@ def test_AMParamStmt_dump():
|
|||
macro = '5,1,8,25.4,25.4,25.4,0.0'
|
||||
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*%')
|
||||
|
||||
#TODO - Store Equations and update on unit change...
|
||||
s = AMParamStmt.from_dict({'param': 'AM', 'name': 'OC8', 'macro': '5,1,8,0,0,1.08239X$1,22.5'})
|
||||
s.build()
|
||||
#assert_equal(s.to_gerber(), '%AMOC8*5,1,8,0,0,1.08239X$1,22.5*%')
|
||||
assert_equal(s.to_gerber(), '%AMOC8*5,1,8,0,0,0,22.5*%')
|
||||
|
||||
|
||||
def test_AMParamStmt_string():
|
||||
name = 'POLYGON'
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ def test_read():
|
|||
assert(isinstance(ipcfile, IPCNetlist))
|
||||
|
||||
|
||||
|
||||
def test_parser():
|
||||
ipcfile = read(IPC_D_356_FILE)
|
||||
assert_equal(ipcfile.settings.units, 'inch')
|
||||
|
|
|
|||
|
|
@ -2,17 +2,29 @@
|
|||
# -*- 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()
|
||||
try:
|
||||
p.bounding_box
|
||||
assert_false(True, 'should have thrown the exception')
|
||||
except NotImplementedError:
|
||||
pass
|
||||
#assert_raises(NotImplementedError, p.bounding_box)
|
||||
|
||||
p.to_metric()
|
||||
p.to_inch()
|
||||
p.offset(1, 1)
|
||||
#try:
|
||||
# p.offset(1, 1)
|
||||
# assert_false(True, 'should have thrown the exception')
|
||||
#except NotImplementedError:
|
||||
# pass
|
||||
|
||||
|
||||
|
||||
def test_line_angle():
|
||||
|
|
@ -159,7 +171,7 @@ def test_arc_radius():
|
|||
((0, 1), (1, 0), (0, 0), 1), ]
|
||||
|
||||
for start, end, center, radius in cases:
|
||||
a = Arc(start, end, center, 'clockwise', 0)
|
||||
a = Arc(start, end, center, 'clockwise', 0, 'single-quadrant')
|
||||
assert_equal(a.radius, radius)
|
||||
|
||||
|
||||
|
|
@ -172,8 +184,8 @@ def test_arc_sweep_angle():
|
|||
((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)
|
||||
c = Circle((0,0), 1)
|
||||
a = Arc(start, end, center, direction, c, 'single-quadrant')
|
||||
assert_equal(a.sweep_angle, sweep)
|
||||
|
||||
|
||||
|
|
@ -186,15 +198,15 @@ def test_arc_bounds():
|
|||
# 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)
|
||||
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, units='metric')
|
||||
'clockwise', c, 'single-quadrant', units='metric')
|
||||
|
||||
# No effect
|
||||
a.to_metric()
|
||||
|
|
@ -218,7 +230,7 @@ def test_arc_conversion():
|
|||
|
||||
c = Circle((0, 0), 1.0, units='inch')
|
||||
a = Arc((0.1, 1.0), (10.0, 100.0), (1000.0, 10000.0),
|
||||
'clockwise', c, units='inch')
|
||||
'clockwise', c, 'single-quadrant', units='inch')
|
||||
a.to_metric()
|
||||
assert_equal(a.start, (2.54, 25.4))
|
||||
assert_equal(a.end, (254.0, 2540.0))
|
||||
|
|
@ -228,7 +240,7 @@ def test_arc_conversion():
|
|||
|
||||
def test_arc_offset():
|
||||
c = Circle((0, 0), 1)
|
||||
a = Arc((0, 0), (1, 1), (2, 2), 'clockwise', c)
|
||||
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.end, (2., 1.))
|
||||
|
|
@ -246,6 +258,13 @@ def test_circle_radius():
|
|||
assert_equal(c.radius, 1)
|
||||
|
||||
|
||||
def test_circle_hole_radius():
|
||||
""" Test Circle primitive hole radius calculation
|
||||
"""
|
||||
c = Circle((1, 1), 4, 2)
|
||||
assert_equal(c.hole_radius, 1)
|
||||
|
||||
|
||||
def test_circle_bounds():
|
||||
""" Test Circle bounding box calculation
|
||||
"""
|
||||
|
|
@ -254,35 +273,81 @@ def test_circle_bounds():
|
|||
|
||||
|
||||
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
|
||||
assert_equal(c.position, (2.54, 25.4))
|
||||
assert_equal(c.diameter, 254.)
|
||||
assert_equal(c.hole_diameter, None)
|
||||
|
||||
c.to_inch()
|
||||
assert_equal(c.position, (0.1, 1.))
|
||||
assert_equal(c.diameter, 10.)
|
||||
assert_equal(c.hole_diameter, None)
|
||||
|
||||
# no effect
|
||||
c.to_inch()
|
||||
assert_equal(c.position, (0.1, 1.))
|
||||
assert_equal(c.diameter, 10.)
|
||||
assert_equal(c.hole_diameter, None)
|
||||
|
||||
# Circle initially metric, with hole
|
||||
c = Circle((2.54, 25.4), 254.0, 127.0, units='metric')
|
||||
|
||||
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, 127.)
|
||||
|
||||
c.to_inch()
|
||||
assert_equal(c.position, (0.1, 1.))
|
||||
assert_equal(c.diameter, 10.)
|
||||
assert_equal(c.hole_diameter, 5.)
|
||||
|
||||
# no effect
|
||||
c.to_inch()
|
||||
assert_equal(c.position, (0.1, 1.))
|
||||
assert_equal(c.diameter, 10.)
|
||||
assert_equal(c.hole_diameter, 5.)
|
||||
|
||||
# Circle initially inch, no hole
|
||||
c = Circle((0.1, 1.0), 10.0, units='inch')
|
||||
# No effect
|
||||
c.to_inch()
|
||||
assert_equal(c.position, (0.1, 1.))
|
||||
assert_equal(c.diameter, 10.)
|
||||
assert_equal(c.hole_diameter, None)
|
||||
|
||||
c.to_metric()
|
||||
assert_equal(c.position, (2.54, 25.4))
|
||||
assert_equal(c.diameter, 254.)
|
||||
assert_equal(c.hole_diameter, None)
|
||||
|
||||
# no effect
|
||||
c.to_metric()
|
||||
assert_equal(c.position, (2.54, 25.4))
|
||||
assert_equal(c.diameter, 254.)
|
||||
assert_equal(c.hole_diameter, None)
|
||||
|
||||
c = Circle((0.1, 1.0), 10.0, 5.0, units='inch')
|
||||
#No effect
|
||||
c.to_inch()
|
||||
assert_equal(c.position, (0.1, 1.))
|
||||
assert_equal(c.diameter, 10.)
|
||||
assert_equal(c.hole_diameter, 5.)
|
||||
|
||||
c.to_metric()
|
||||
assert_equal(c.position, (2.54, 25.4))
|
||||
assert_equal(c.diameter, 254.)
|
||||
assert_equal(c.hole_diameter, 127.)
|
||||
|
||||
# no effect
|
||||
c.to_metric()
|
||||
assert_equal(c.position, (2.54, 25.4))
|
||||
assert_equal(c.diameter, 254.)
|
||||
assert_equal(c.hole_diameter, 127.)
|
||||
|
||||
|
||||
def test_circle_offset():
|
||||
|
|
@ -373,6 +438,16 @@ def test_rectangle_ctor():
|
|||
assert_equal(r.width, width)
|
||||
assert_equal(r.height, height)
|
||||
|
||||
def test_rectangle_hole_radius():
|
||||
""" Test rectangle hole diameter calculation
|
||||
"""
|
||||
r = Rectangle((0,0), 2, 2)
|
||||
assert_equal(0, r.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
|
||||
|
|
@ -388,6 +463,9 @@ def test_rectangle_bounds():
|
|||
|
||||
|
||||
def test_rectangle_conversion():
|
||||
"""Test converting rectangles between units"""
|
||||
|
||||
# Initially metric no hole
|
||||
r = Rectangle((2.54, 25.4), 254.0, 2540.0, units='metric')
|
||||
|
||||
r.to_metric()
|
||||
|
|
@ -405,6 +483,28 @@ def test_rectangle_conversion():
|
|||
assert_equal(r.width, 10.0)
|
||||
assert_equal(r.height, 100.0)
|
||||
|
||||
# Initially metric with hole
|
||||
r = Rectangle((2.54, 25.4), 254.0, 2540.0, 127.0, units='metric')
|
||||
|
||||
r.to_metric()
|
||||
assert_equal(r.position, (2.54,25.4))
|
||||
assert_equal(r.width, 254.0)
|
||||
assert_equal(r.height, 2540.0)
|
||||
assert_equal(r.hole_diameter, 127.0)
|
||||
|
||||
r.to_inch()
|
||||
assert_equal(r.position, (0.1, 1.0))
|
||||
assert_equal(r.width, 10.0)
|
||||
assert_equal(r.height, 100.0)
|
||||
assert_equal(r.hole_diameter, 5.0)
|
||||
|
||||
r.to_inch()
|
||||
assert_equal(r.position, (0.1, 1.0))
|
||||
assert_equal(r.width, 10.0)
|
||||
assert_equal(r.height, 100.0)
|
||||
assert_equal(r.hole_diameter, 5.0)
|
||||
|
||||
# Initially inch, no hole
|
||||
r = Rectangle((0.1, 1.0), 10.0, 100.0, units='inch')
|
||||
r.to_inch()
|
||||
assert_equal(r.position, (0.1, 1.0))
|
||||
|
|
@ -421,6 +521,26 @@ def test_rectangle_conversion():
|
|||
assert_equal(r.width, 254.0)
|
||||
assert_equal(r.height, 2540.0)
|
||||
|
||||
# Initially inch with hole
|
||||
r = Rectangle((0.1, 1.0), 10.0, 100.0, 5.0, units='inch')
|
||||
r.to_inch()
|
||||
assert_equal(r.position, (0.1, 1.0))
|
||||
assert_equal(r.width, 10.0)
|
||||
assert_equal(r.height, 100.0)
|
||||
assert_equal(r.hole_diameter, 5.0)
|
||||
|
||||
r.to_metric()
|
||||
assert_equal(r.position, (2.54,25.4))
|
||||
assert_equal(r.width, 254.0)
|
||||
assert_equal(r.height, 2540.0)
|
||||
assert_equal(r.hole_diameter, 127.0)
|
||||
|
||||
r.to_metric()
|
||||
assert_equal(r.position, (2.54, 25.4))
|
||||
assert_equal(r.width, 254.0)
|
||||
assert_equal(r.height, 2540.0)
|
||||
assert_equal(r.hole_diameter, 127.0)
|
||||
|
||||
|
||||
def test_rectangle_offset():
|
||||
r = Rectangle((0, 0), 1, 2)
|
||||
|
|
@ -756,31 +876,32 @@ def test_obround_offset():
|
|||
def test_polygon_ctor():
|
||||
""" Test polygon creation
|
||||
"""
|
||||
test_cases = (((0, 0), 3, 5),
|
||||
((0, 0), 5, 6),
|
||||
((1, 1), 7, 7))
|
||||
for pos, sides, radius in test_cases:
|
||||
p = Polygon(pos, sides, radius)
|
||||
test_cases = (((0, 0), 3, 5, 0),
|
||||
((0, 0), 5, 6, 0),
|
||||
((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)
|
||||
assert_equal(p.sides, sides)
|
||||
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)
|
||||
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)
|
||||
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, units='metric')
|
||||
p = Polygon((2.54, 25.4), 3, 254.0, 0, units='metric')
|
||||
|
||||
# No effect
|
||||
p.to_metric()
|
||||
|
|
@ -796,7 +917,7 @@ def test_polygon_conversion():
|
|||
assert_equal(p.position, (0.1, 1.0))
|
||||
assert_equal(p.radius, 10.0)
|
||||
|
||||
p = Polygon((0.1, 1.0), 3, 10.0, units='inch')
|
||||
p = Polygon((0.1, 1.0), 3, 10.0, 0, units='inch')
|
||||
|
||||
# No effect
|
||||
p.to_inch()
|
||||
|
|
@ -814,7 +935,7 @@ def test_polygon_conversion():
|
|||
|
||||
|
||||
def test_polygon_offset():
|
||||
p = Polygon((0, 0), 5, 10)
|
||||
p = Polygon((0, 0), 5, 10, 0)
|
||||
p.offset(1, 0)
|
||||
assert_equal(p.position, (1., 0.))
|
||||
p.offset(0, 1)
|
||||
|
|
@ -1074,7 +1195,7 @@ def test_drill_ctor():
|
|||
"""
|
||||
test_cases = (((0, 0), 2), ((1, 1), 3), ((2, 2), 5))
|
||||
for position, diameter in test_cases:
|
||||
d = Drill(position, diameter)
|
||||
d = Drill(position, diameter, None)
|
||||
assert_equal(d.position, position)
|
||||
assert_equal(d.diameter, diameter)
|
||||
assert_equal(d.radius, diameter / 2.)
|
||||
|
|
@ -1083,25 +1204,26 @@ def test_drill_ctor():
|
|||
def test_drill_ctor_validation():
|
||||
""" Test drill argument validation
|
||||
"""
|
||||
assert_raises(TypeError, Drill, 3, 5)
|
||||
assert_raises(TypeError, Drill, (3, 4, 5), 5)
|
||||
assert_raises(TypeError, Drill, 3, 5, None)
|
||||
assert_raises(TypeError, Drill, (3,4,5), 5, None)
|
||||
|
||||
|
||||
|
||||
def test_drill_bounds():
|
||||
d = Drill((0, 0), 2)
|
||||
d = Drill((0, 0), 2, None)
|
||||
xbounds, ybounds = d.bounding_box
|
||||
assert_array_almost_equal(xbounds, (-1, 1))
|
||||
assert_array_almost_equal(ybounds, (-1, 1))
|
||||
d = Drill((1, 2), 2)
|
||||
d = Drill((1, 2), 2, None)
|
||||
xbounds, ybounds = d.bounding_box
|
||||
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., units='metric')
|
||||
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)
|
||||
|
|
@ -1110,12 +1232,12 @@ def test_drill_conversion():
|
|||
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., units='inch')
|
||||
d = Drill((0.1, 1.0), 10., None, units='inch')
|
||||
|
||||
# No effect
|
||||
d.to_inch()
|
||||
|
|
@ -1133,7 +1255,7 @@ def test_drill_conversion():
|
|||
|
||||
|
||||
def test_drill_offset():
|
||||
d = Drill((0, 0), 1.)
|
||||
d = Drill((0, 0), 1., None)
|
||||
d.offset(1, 0)
|
||||
assert_equal(d.position, (1., 0.))
|
||||
d.offset(0, 1)
|
||||
|
|
@ -1141,8 +1263,8 @@ def test_drill_offset():
|
|||
|
||||
|
||||
def test_drill_equality():
|
||||
d = Drill((2.54, 25.4), 254.)
|
||||
d1 = Drill((2.54, 25.4), 254.)
|
||||
d = Drill((2.54, 25.4), 254., None)
|
||||
d1 = Drill((2.54, 25.4), 254., None)
|
||||
assert_equal(d, d1)
|
||||
d1 = Drill((2.54, 25.4), 254.2)
|
||||
d1 = Drill((2.54, 25.4), 254.2, None)
|
||||
assert_not_equal(d, d1)
|
||||
|
|
|
|||
|
|
@ -39,10 +39,9 @@ def test_size_parameter():
|
|||
|
||||
|
||||
def test_conversion():
|
||||
import copy
|
||||
top_copper = read(TOP_COPPER_FILE)
|
||||
assert_equal(top_copper.units, 'inch')
|
||||
top_copper_inch = copy.deepcopy(top_copper)
|
||||
top_copper_inch = read(TOP_COPPER_FILE)
|
||||
top_copper.to_metric()
|
||||
for statement in top_copper_inch.statements:
|
||||
statement.to_metric()
|
||||
|
|
|
|||
185
gerber/tests/test_rs274x_backend.py
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
#! /usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Author: Garret Fick <garret@ficksworkshop.com>
|
||||
import io
|
||||
import os
|
||||
|
||||
from ..render.rs274x_backend import Rs274xContext
|
||||
from ..rs274x import read
|
||||
from .tests import *
|
||||
|
||||
def test_render_two_boxes():
|
||||
"""Umaco exapmle of two boxes"""
|
||||
_test_render('resources/example_two_square_boxes.gbr', 'golden/example_two_square_boxes.gbr')
|
||||
|
||||
|
||||
def _test_render_single_quadrant():
|
||||
"""Umaco exapmle of a single quadrant arc"""
|
||||
|
||||
# TODO there is probably a bug here
|
||||
_test_render('resources/example_single_quadrant.gbr', 'golden/example_single_quadrant.gbr')
|
||||
|
||||
|
||||
def _test_render_simple_contour():
|
||||
"""Umaco exapmle of a simple arrow-shaped contour"""
|
||||
_test_render('resources/example_simple_contour.gbr', 'golden/example_simple_contour.gbr')
|
||||
|
||||
|
||||
def _test_render_single_contour_1():
|
||||
"""Umaco example of a single contour
|
||||
|
||||
The resulting image for this test is used by other tests because they must generate the same output."""
|
||||
_test_render('resources/example_single_contour_1.gbr', 'golden/example_single_contour.gbr')
|
||||
|
||||
|
||||
def _test_render_single_contour_2():
|
||||
"""Umaco exapmle of a single contour, alternate contour end order
|
||||
|
||||
The resulting image for this test is used by other tests because they must generate the same output."""
|
||||
_test_render('resources/example_single_contour_2.gbr', 'golden/example_single_contour.gbr')
|
||||
|
||||
|
||||
def _test_render_single_contour_3():
|
||||
"""Umaco exapmle of a single contour with extra line"""
|
||||
_test_render('resources/example_single_contour_3.gbr', 'golden/example_single_contour_3.gbr')
|
||||
|
||||
|
||||
def _test_render_not_overlapping_contour():
|
||||
"""Umaco example of D02 staring a second contour"""
|
||||
_test_render('resources/example_not_overlapping_contour.gbr', 'golden/example_not_overlapping_contour.gbr')
|
||||
|
||||
|
||||
def _test_render_not_overlapping_touching():
|
||||
"""Umaco example of D02 staring a second contour"""
|
||||
_test_render('resources/example_not_overlapping_touching.gbr', 'golden/example_not_overlapping_touching.gbr')
|
||||
|
||||
|
||||
def _test_render_overlapping_touching():
|
||||
"""Umaco example of D02 staring a second contour"""
|
||||
_test_render('resources/example_overlapping_touching.gbr', 'golden/example_overlapping_touching.gbr')
|
||||
|
||||
|
||||
def _test_render_overlapping_contour():
|
||||
"""Umaco example of D02 staring a second contour"""
|
||||
_test_render('resources/example_overlapping_contour.gbr', 'golden/example_overlapping_contour.gbr')
|
||||
|
||||
|
||||
def _DISABLED_test_render_level_holes():
|
||||
"""Umaco example of using multiple levels to create multiple holes"""
|
||||
|
||||
# TODO This is clearly rendering wrong. I'm temporarily checking this in because there are more
|
||||
# rendering fixes in the related repository that may resolve these.
|
||||
_test_render('resources/example_level_holes.gbr', 'golden/example_overlapping_contour.gbr')
|
||||
|
||||
|
||||
def _DISABLED_test_render_cutin():
|
||||
"""Umaco example of using a cutin"""
|
||||
|
||||
# TODO This is clearly rendering wrong.
|
||||
_test_render('resources/example_cutin.gbr', 'golden/example_cutin.gbr')
|
||||
|
||||
|
||||
def _test_render_fully_coincident():
|
||||
"""Umaco example of coincident lines rendering two contours"""
|
||||
|
||||
_test_render('resources/example_fully_coincident.gbr', 'golden/example_fully_coincident.gbr')
|
||||
|
||||
|
||||
def _test_render_coincident_hole():
|
||||
"""Umaco example of coincident lines rendering a hole in the contour"""
|
||||
|
||||
_test_render('resources/example_coincident_hole.gbr', 'golden/example_coincident_hole.gbr')
|
||||
|
||||
|
||||
def _test_render_cutin_multiple():
|
||||
"""Umaco example of a region with multiple cutins"""
|
||||
|
||||
_test_render('resources/example_cutin_multiple.gbr', 'golden/example_cutin_multiple.gbr')
|
||||
|
||||
|
||||
def _test_flash_circle():
|
||||
"""Umaco example a simple circular flash with and without a hole"""
|
||||
|
||||
_test_render('resources/example_flash_circle.gbr', 'golden/example_flash_circle.gbr')
|
||||
|
||||
|
||||
def _test_flash_rectangle():
|
||||
"""Umaco example a simple rectangular flash with and without a hole"""
|
||||
|
||||
_test_render('resources/example_flash_rectangle.gbr', 'golden/example_flash_rectangle.gbr')
|
||||
|
||||
|
||||
def _test_flash_obround():
|
||||
"""Umaco example a simple obround flash with and without a hole"""
|
||||
|
||||
_test_render('resources/example_flash_obround.gbr', 'golden/example_flash_obround.gbr')
|
||||
|
||||
|
||||
def _test_flash_polygon():
|
||||
"""Umaco example a simple polygon flash with and without a hole"""
|
||||
|
||||
_test_render('resources/example_flash_polygon.gbr', 'golden/example_flash_polygon.gbr')
|
||||
|
||||
|
||||
def _test_holes_dont_clear():
|
||||
"""Umaco example that an aperture with a hole does not clear the area"""
|
||||
|
||||
_test_render('resources/example_holes_dont_clear.gbr', 'golden/example_holes_dont_clear.gbr')
|
||||
|
||||
|
||||
def _test_render_am_exposure_modifier():
|
||||
"""Umaco example that an aperture macro with a hole does not clear the area"""
|
||||
|
||||
_test_render('resources/example_am_exposure_modifier.gbr', 'golden/example_am_exposure_modifier.gbr')
|
||||
|
||||
|
||||
def _resolve_path(path):
|
||||
return os.path.join(os.path.dirname(__file__),
|
||||
path)
|
||||
|
||||
|
||||
def _test_render(gerber_path, png_expected_path, create_output_path = None):
|
||||
"""Render the gerber file and compare to the expected PNG output.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
gerber_path : string
|
||||
Path to Gerber file to open
|
||||
png_expected_path : string
|
||||
Path to the PNG file to compare to
|
||||
create_output : string|None
|
||||
If not None, write the generated PNG to the specified path.
|
||||
This is primarily to help with
|
||||
"""
|
||||
|
||||
gerber_path = _resolve_path(gerber_path)
|
||||
png_expected_path = _resolve_path(png_expected_path)
|
||||
if create_output_path:
|
||||
create_output_path = _resolve_path(create_output_path)
|
||||
|
||||
gerber = read(gerber_path)
|
||||
|
||||
# Create GBR output from the input file
|
||||
ctx = Rs274xContext(gerber.settings)
|
||||
gerber.render(ctx)
|
||||
|
||||
actual_contents = ctx.dump()
|
||||
|
||||
# If we want to write the file bytes, do it now. This happens
|
||||
if create_output_path:
|
||||
with open(create_output_path, 'wb') as out_file:
|
||||
out_file.write(actual_contents.getvalue())
|
||||
# Creating the output is dangerous - it could overwrite the expected result.
|
||||
# So if we are creating the output, we make the test fail on purpose so you
|
||||
# won't forget to disable this
|
||||
assert_false(True, 'Test created the output %s. This needs to be disabled to make sure the test behaves correctly' % (create_output_path,))
|
||||
|
||||
# Read the expected PNG file
|
||||
|
||||
with open(png_expected_path, 'r') as expected_file:
|
||||
expected_contents = expected_file.read()
|
||||
|
||||
assert_equal(expected_contents, actual_contents.getvalue())
|
||||
|
||||
return gerber
|
||||
|
|
@ -291,10 +291,25 @@ def rotate_point(point, angle, center=(0.0, 0.0)):
|
|||
`point` rotated about `center` by `angle` degrees.
|
||||
"""
|
||||
angle = radians(angle)
|
||||
x_delta, y_delta = tuple(map(sub, point, center))
|
||||
x = center[0] + (cos(angle) * x_delta) - (sin(angle) * y_delta)
|
||||
y = center[1] + (sin(angle) * x_delta) - (cos(angle) * y_delta)
|
||||
return (x, y)
|
||||
|
||||
cos_angle = cos(angle)
|
||||
sin_angle = sin(angle)
|
||||
|
||||
return (
|
||||
cos_angle * (point[0] - center[0]) - sin_angle * (point[1] - center[1]) + center[0],
|
||||
sin_angle * (point[0] - center[0]) + cos_angle * (point[1] - center[1]) + center[1])
|
||||
|
||||
def nearly_equal(point1, point2, ndigits = 6):
|
||||
'''Are the points nearly equal'''
|
||||
|
||||
return round(point1[0] - point2[0], ndigits) == 0 and round(point1[1] - point2[1], ndigits) == 0
|
||||
|
||||
|
||||
def sq_distance(point1, point2):
|
||||
|
||||
diff1 = point1[0] - point2[0]
|
||||
diff2 = point1[1] - point2[1]
|
||||
return diff1 * diff1 + diff2 * diff2
|
||||
|
||||
|
||||
def listdir(directory, ignore_hidden=True, ignore_os=True):
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
# Test requirements
|
||||
cairocffi==0.6
|
||||
coverage==3.7.1
|
||||
nose==1.3.4
|
||||
|
|
|
|||