Add many render tests based on the Umaco gerger specification. Fix multiple rendering bugs, especially related to holes in flashed apertures
|
|
@ -260,7 +260,7 @@ class CamFile(object):
|
|||
If provided, save the rendered image to `filename`
|
||||
"""
|
||||
|
||||
ctx.set_bounds(self.bounds)
|
||||
ctx.set_bounds(self.bounding_box)
|
||||
ctx._paint_background()
|
||||
|
||||
if invert:
|
||||
|
|
|
|||
|
|
@ -279,9 +279,9 @@ class ADParamStmt(ParamStmt):
|
|||
return cls('AD', dcode, 'R', ([width, height],))
|
||||
|
||||
@classmethod
|
||||
def circle(cls, dcode, diameter):
|
||||
def circle(cls, dcode, diameter, hole_diameter):
|
||||
'''Create a circular aperture definition statement'''
|
||||
return cls('AD', dcode, 'C', ([diameter],))
|
||||
return cls('AD', dcode, 'C', ([diameter, hole_diameter],))
|
||||
|
||||
@classmethod
|
||||
def obround(cls, dcode, width, height):
|
||||
|
|
|
|||
|
|
@ -370,12 +370,13 @@ class Arc(Primitive):
|
|||
class Circle(Primitive):
|
||||
"""
|
||||
"""
|
||||
def __init__(self, position, diameter, **kwargs):
|
||||
def __init__(self, position, diameter, hole_diameter = 0, **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):
|
||||
|
|
@ -384,6 +385,10 @@ class Circle(Primitive):
|
|||
@property
|
||||
def radius(self):
|
||||
return self.diameter / 2.
|
||||
|
||||
@property
|
||||
def hole_radius(self):
|
||||
return self.hole_diameter / 2.
|
||||
|
||||
@property
|
||||
def bounding_box(self):
|
||||
|
|
@ -402,7 +407,7 @@ class Circle(Primitive):
|
|||
if not isinstance(other, Circle):
|
||||
return False
|
||||
|
||||
if self.diameter != other.diameter:
|
||||
if self.diameter != other.diameter or self.hole_diameter != other.hole_diameter:
|
||||
return False
|
||||
|
||||
equiv_position = tuple(map(add, other.position, offset))
|
||||
|
|
@ -456,13 +461,14 @@ class Rectangle(Primitive):
|
|||
Only aperture macro generated Rectangle objects can be rotated. If you aren't in a AMGroup,
|
||||
then you don't need to worry about rotation
|
||||
"""
|
||||
def __init__(self, position, width, height, **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']
|
||||
|
||||
@property
|
||||
def flashed(self):
|
||||
|
|
@ -477,6 +483,11 @@ class Rectangle(Primitive):
|
|||
def upper_right(self):
|
||||
return (self.position[0] + (self._abs_width / 2.),
|
||||
self.position[1] + (self._abs_height / 2.))
|
||||
|
||||
@property
|
||||
def hole_radius(self):
|
||||
"""The radius of the hole. If there is no hole, returns 0"""
|
||||
return self.hole_diameter / 2.
|
||||
|
||||
@property
|
||||
def bounding_box(self):
|
||||
|
|
@ -499,12 +510,12 @@ class Rectangle(Primitive):
|
|||
math.sin(math.radians(self.rotation)) * self.width)
|
||||
|
||||
def equivalent(self, other, offset):
|
||||
'''Is this the same as the other rect, ignoring the offiset?'''
|
||||
"""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:
|
||||
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))
|
||||
|
|
@ -655,13 +666,14 @@ class RoundRectangle(Primitive):
|
|||
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):
|
||||
|
|
@ -676,6 +688,11 @@ class Obround(Primitive):
|
|||
def upper_right(self):
|
||||
return (self.position[0] + (self._abs_width / 2.),
|
||||
self.position[1] + (self._abs_height / 2.))
|
||||
|
||||
@property
|
||||
def hole_radius(self):
|
||||
"""The radius of the hole. If there is no hole, returns 0"""
|
||||
return self.hole_diameter / 2.
|
||||
|
||||
@property
|
||||
def orientation(self):
|
||||
|
|
|
|||
|
|
@ -20,13 +20,14 @@ try:
|
|||
except ImportError:
|
||||
import cairocffi as cairo
|
||||
|
||||
from operator import mul, div
|
||||
import math
|
||||
from operator import mul, div
|
||||
import tempfile
|
||||
|
||||
from ..primitives import *
|
||||
from .render import GerberContext, RenderSettings
|
||||
from .theme import THEMES
|
||||
from ..primitives import *
|
||||
|
||||
|
||||
try:
|
||||
from cStringIO import StringIO
|
||||
|
|
@ -219,15 +220,30 @@ class GerberCairoContext(GerberContext):
|
|||
center = tuple(map(mul, circle.position, self.scale))
|
||||
if not self.invert:
|
||||
ctx = self.ctx
|
||||
ctx.set_source_rgba(*color, alpha=self.alpha)
|
||||
ctx.set_source_rgba(color[0], color[1], color[2], alpha=self.alpha)
|
||||
ctx.set_operator(cairo.OPERATOR_OVER if circle.level_polarity == "dark" else cairo.OPERATOR_CLEAR)
|
||||
else:
|
||||
ctx = self.mask_ctx
|
||||
ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0)
|
||||
ctx.set_operator(cairo.OPERATOR_CLEAR)
|
||||
|
||||
if circle.hole_diameter > 0:
|
||||
ctx.push_group()
|
||||
|
||||
ctx.set_line_width(0)
|
||||
ctx.arc(center[0], center[1], radius=circle.radius * self.scale[0], angle1=0, angle2=2 * math.pi)
|
||||
ctx.fill()
|
||||
ctx.fill()
|
||||
|
||||
if circle.hole_diameter > 0:
|
||||
# Render the center clear
|
||||
|
||||
ctx.set_source_rgba(color[0], color[1], color[2], self.alpha)
|
||||
ctx.set_operator(cairo.OPERATOR_CLEAR)
|
||||
ctx.arc(center[0], center[1], radius=circle.hole_radius * self.scale[0], angle1=0, angle2=2 * math.pi)
|
||||
ctx.fill()
|
||||
|
||||
ctx.pop_group_to_source()
|
||||
ctx.paint_with_alpha(1)
|
||||
|
||||
def _render_rectangle(self, rectangle, color):
|
||||
ll = map(mul, rectangle.lower_left, self.scale)
|
||||
|
|
@ -253,48 +269,95 @@ class GerberCairoContext(GerberContext):
|
|||
ll[1] = ll[1] - center[1]
|
||||
matrix.rotate(rectangle.rotation)
|
||||
ctx.transform(matrix)
|
||||
|
||||
|
||||
if rectangle.hole_diameter > 0:
|
||||
ctx.push_group()
|
||||
|
||||
ctx.set_line_width(0)
|
||||
ctx.rectangle(ll[0], ll[1], width, height)
|
||||
ctx.fill()
|
||||
|
||||
if rectangle.hole_diameter > 0:
|
||||
# Render the center clear
|
||||
ctx.set_source_rgba(color[0], color[1], color[2], self.alpha)
|
||||
ctx.set_operator(cairo.OPERATOR_CLEAR)
|
||||
center = map(mul, rectangle.position, self.scale)
|
||||
ctx.arc(center[0], center[1], radius=rectangle.hole_radius * self.scale[0], angle1=0, angle2=2 * math.pi)
|
||||
ctx.fill()
|
||||
|
||||
ctx.pop_group_to_source()
|
||||
ctx.paint_with_alpha(1)
|
||||
|
||||
if rectangle.rotation != 0:
|
||||
ctx.restore()
|
||||
|
||||
def _render_obround(self, obround, color):
|
||||
|
||||
if not self.invert:
|
||||
ctx = self.ctx
|
||||
ctx.set_source_rgba(color[0], color[1], color[2], alpha=self.alpha)
|
||||
ctx.set_operator(cairo.OPERATOR_OVER if obround.level_polarity == "dark" else cairo.OPERATOR_CLEAR)
|
||||
else:
|
||||
ctx = self.mask_ctx
|
||||
ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0)
|
||||
ctx.set_operator(cairo.OPERATOR_CLEAR)
|
||||
|
||||
if obround.hole_diameter > 0:
|
||||
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
|
||||
ctx.set_source_rgba(color[0], color[1], color[2], self.alpha)
|
||||
ctx.set_operator(cairo.OPERATOR_CLEAR)
|
||||
center = map(mul, obround.position, self.scale)
|
||||
ctx.arc(center[0], center[1], radius=obround.hole_radius * self.scale[0], angle1=0, angle2=2 * math.pi)
|
||||
ctx.fill()
|
||||
|
||||
ctx.pop_group_to_source()
|
||||
ctx.paint_with_alpha(1)
|
||||
|
||||
def _render_polygon(self, polygon, color):
|
||||
|
||||
# TODO Ths does not handle rotation of a polygon
|
||||
if not self.invert:
|
||||
ctx = self.ctx
|
||||
ctx.set_source_rgba(color[0], color[1], color[2], alpha=self.alpha)
|
||||
ctx.set_operator(cairo.OPERATOR_OVER if polygon.level_polarity == "dark" else cairo.OPERATOR_CLEAR)
|
||||
else:
|
||||
ctx = self.mask_ctx
|
||||
ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0)
|
||||
ctx.set_operator(cairo.OPERATOR_CLEAR)
|
||||
|
||||
if polygon.hole_radius > 0:
|
||||
self.ctx.push_group()
|
||||
ctx.push_group()
|
||||
|
||||
vertices = polygon.vertices
|
||||
|
||||
self.ctx.set_source_rgba(color[0], color[1], color[2], self.alpha)
|
||||
self.ctx.set_operator(cairo.OPERATOR_OVER if (polygon.level_polarity == "dark" and not self.invert) else cairo.OPERATOR_CLEAR)
|
||||
self.ctx.set_line_width(0)
|
||||
self.ctx.set_line_cap(cairo.LINE_CAP_ROUND)
|
||||
|
||||
ctx.set_line_width(0)
|
||||
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))
|
||||
ctx.move_to(*map(mul, vertices[-1], self.scale))
|
||||
for v in vertices:
|
||||
self.ctx.line_to(*map(mul, v, self.scale))
|
||||
ctx.line_to(*map(mul, v, self.scale))
|
||||
|
||||
self.ctx.fill()
|
||||
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)
|
||||
self.ctx.set_line_width(0)
|
||||
self.ctx.arc(center[0], center[1], polygon.hole_radius * self.scale[0], 0, 2 * math.pi)
|
||||
self.ctx.fill()
|
||||
ctx.set_source_rgba(color[0], color[1], color[2], self.alpha)
|
||||
ctx.set_operator(cairo.OPERATOR_CLEAR)
|
||||
ctx.set_line_width(0)
|
||||
ctx.arc(center[0], center[1], polygon.hole_radius * self.scale[0], 0, 2 * math.pi)
|
||||
ctx.fill()
|
||||
|
||||
self.ctx.pop_group_to_source()
|
||||
self.ctx.paint_with_alpha(1)
|
||||
ctx.pop_group_to_source()
|
||||
ctx.paint_with_alpha(1)
|
||||
|
||||
def _render_drill(self, circle, color):
|
||||
self._render_circle(circle, color)
|
||||
|
|
|
|||
|
|
@ -151,7 +151,7 @@ class Rs274xContext(GerberContext):
|
|||
# Select the right aperture if not already selected
|
||||
if aperture:
|
||||
if isinstance(aperture, Circle):
|
||||
aper = self._get_circle(aperture.diameter)
|
||||
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):
|
||||
|
|
@ -275,10 +275,10 @@ class Rs274xContext(GerberContext):
|
|||
|
||||
self._pos = primitive.position
|
||||
|
||||
def _get_circle(self, diameter, dcode = None):
|
||||
def _get_circle(self, diameter, hole_diameter, dcode = None):
|
||||
'''Define a circlar aperture'''
|
||||
|
||||
aper = self._circles.get(diameter, None)
|
||||
aper = self._circles.get((diameter, hole_diameter), None)
|
||||
|
||||
if not aper:
|
||||
if not dcode:
|
||||
|
|
@ -287,15 +287,15 @@ class Rs274xContext(GerberContext):
|
|||
else:
|
||||
self._next_dcode = max(dcode + 1, self._next_dcode)
|
||||
|
||||
aper = ADParamStmt.circle(dcode, diameter)
|
||||
self._circles[diameter] = aper
|
||||
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)
|
||||
aper = self._get_circle(circle.diameter, circle.hole_diameter)
|
||||
self._render_flash(circle, aper)
|
||||
|
||||
def _get_rectangle(self, width, height, dcode = None):
|
||||
|
|
|
|||
|
|
@ -482,15 +482,33 @@ class GerberParser(object):
|
|||
aperture = None
|
||||
if shape == 'C':
|
||||
diameter = modifiers[0][0]
|
||||
aperture = Circle(position=None, diameter=diameter, units=self.settings.units)
|
||||
|
||||
if len(modifiers[0]) >= 2:
|
||||
hole_diameter = modifiers[0][1]
|
||||
else:
|
||||
hole_diameter = 0
|
||||
|
||||
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, units=self.settings.units)
|
||||
|
||||
if len(modifiers[0]) >= 3:
|
||||
hole_diameter = modifiers[0][2]
|
||||
else:
|
||||
hole_diameter = 0
|
||||
|
||||
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, units=self.settings.units)
|
||||
|
||||
if len(modifiers[0]) >= 3:
|
||||
hole_diameter = modifiers[0][2]
|
||||
else:
|
||||
hole_diameter = 0
|
||||
|
||||
aperture = Obround(position=None, width=width, height=height, hole_diameter=hole_diameter, units=self.settings.units)
|
||||
elif shape == 'P':
|
||||
outer_diameter = modifiers[0][0]
|
||||
number_vertices = int(modifiers[0][1])
|
||||
|
|
|
|||
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_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 |
BIN
gerber/tests/golden/example_single_quadrant.png
Normal file
|
After Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
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*
|
||||
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*
|
||||
|
|
@ -8,16 +8,125 @@ import os
|
|||
from ..render.cairo_backend import GerberCairoContext
|
||||
from ..rs274x import read, GerberFile
|
||||
from .tests import *
|
||||
from nose.tools import assert_tuple_equal
|
||||
|
||||
def test_render_two_boxes():
|
||||
"""Umaco exapmle of two boxes"""
|
||||
_test_render('resources/example_two_square_boxes.gbr', 'golden/example_two_square_boxes.png')
|
||||
|
||||
|
||||
TWO_BOXES_FILE = os.path.join(os.path.dirname(__file__),
|
||||
'resources/example_two_square_boxes.gbr')
|
||||
TWO_BOXES_EXPECTED = os.path.join(os.path.dirname(__file__),
|
||||
'golden/example_two_square_boxes.png')
|
||||
def test_render_single_quadrant():
|
||||
"""Umaco exapmle of a single quadrant arc"""
|
||||
_test_render('resources/example_single_quadrant.gbr', 'golden/example_single_quadrant.png')
|
||||
|
||||
def test_render_polygon():
|
||||
|
||||
_test_render(TWO_BOXES_FILE, TWO_BOXES_EXPECTED)
|
||||
def 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 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 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 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 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 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')
|
||||
|
||||
|
||||
def 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 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 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')
|
||||
|
||||
|
||||
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.png')
|
||||
|
||||
|
||||
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.png')
|
||||
|
||||
|
||||
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.png', 'golden/example_flash_polygon.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.
|
||||
|
|
@ -33,6 +142,11 @@ def _test_render(gerber_path, png_expected_path, create_output_path = None):
|
|||
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
|
||||
|
|
@ -56,3 +170,5 @@ def _test_render(gerber_path, png_expected_path, create_output_path = None):
|
|||
expected_bytes = expected_file.read()
|
||||
|
||||
assert_equal(expected_bytes, actual_bytes)
|
||||
|
||||
return gerber
|
||||
|
|
|
|||
|
|
@ -236,6 +236,12 @@ def test_circle_radius():
|
|||
c = Circle((1, 1), 2)
|
||||
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
|
||||
"""
|
||||
|
|
@ -243,35 +249,81 @@ def test_circle_bounds():
|
|||
assert_equal(c.bounding_box, ((0, 2), (0, 2)))
|
||||
|
||||
def test_circle_conversion():
|
||||
"""Circle conversion of units"""
|
||||
# Circle initially metric, no hole
|
||||
c = Circle((2.54, 25.4), 254.0, units='metric')
|
||||
|
||||
c.to_metric() #shouldn't do antyhing
|
||||
assert_equal(c.position, (2.54, 25.4))
|
||||
assert_equal(c.diameter, 254.)
|
||||
assert_equal(c.hole_diameter, 0.)
|
||||
|
||||
c.to_inch()
|
||||
assert_equal(c.position, (0.1, 1.))
|
||||
assert_equal(c.diameter, 10.)
|
||||
assert_equal(c.hole_diameter, 0)
|
||||
|
||||
#no effect
|
||||
c.to_inch()
|
||||
assert_equal(c.position, (0.1, 1.))
|
||||
assert_equal(c.diameter, 10.)
|
||||
assert_equal(c.hole_diameter, 0)
|
||||
|
||||
# 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, 0)
|
||||
|
||||
c.to_metric()
|
||||
assert_equal(c.position, (2.54, 25.4))
|
||||
assert_equal(c.diameter, 254.)
|
||||
assert_equal(c.hole_diameter, 0)
|
||||
|
||||
#no effect
|
||||
c.to_metric()
|
||||
assert_equal(c.position, (2.54, 25.4))
|
||||
assert_equal(c.diameter, 254.)
|
||||
assert_equal(c.hole_diameter, 0)
|
||||
|
||||
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():
|
||||
c = Circle((0, 0), 1)
|
||||
|
|
@ -355,6 +407,15 @@ def test_rectangle_ctor():
|
|||
assert_equal(r.position, pos)
|
||||
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
|
||||
|
|
@ -369,6 +430,9 @@ def test_rectangle_bounds():
|
|||
assert_array_almost_equal(ybounds, (-math.sqrt(2), math.sqrt(2)))
|
||||
|
||||
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()
|
||||
|
|
@ -385,7 +449,29 @@ def test_rectangle_conversion():
|
|||
assert_equal(r.position, (0.1, 1.0))
|
||||
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))
|
||||
|
|
@ -401,6 +487,26 @@ def test_rectangle_conversion():
|
|||
assert_equal(r.position, (2.54,25.4))
|
||||
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)
|
||||
|
|
|
|||