diff options
author | Garret Fick <garret@ficksworkshop.com> | 2016-07-24 17:08:47 +0800 |
---|---|---|
committer | Garret Fick <garret@ficksworkshop.com> | 2016-07-24 17:08:47 +0800 |
commit | 7cd6acf12670f3773113f67ed2acb35cb21c32a0 (patch) | |
tree | ce2d79376375b8c7b4ee8c0f519f23d3f3fc1db9 /gerber | |
parent | cd4614f9973eb9f4ce858b3864573367614f54b6 (diff) | |
download | gerbonara-7cd6acf12670f3773113f67ed2acb35cb21c32a0.tar.gz gerbonara-7cd6acf12670f3773113f67ed2acb35cb21c32a0.tar.bz2 gerbonara-7cd6acf12670f3773113f67ed2acb35cb21c32a0.zip |
Add many render tests based on the Umaco gerger specification. Fix multiple rendering bugs, especially related to holes in flashed apertures
Diffstat (limited to 'gerber')
42 files changed, 699 insertions, 48 deletions
diff --git a/gerber/cam.py b/gerber/cam.py index 28918cb..f64aa34 100644 --- a/gerber/cam.py +++ b/gerber/cam.py @@ -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: diff --git a/gerber/gerber_statements.py b/gerber/gerber_statements.py index 52e7ac3..3212c1c 100644 --- a/gerber/gerber_statements.py +++ b/gerber/gerber_statements.py @@ -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): diff --git a/gerber/primitives.py b/gerber/primitives.py index 90b6fb9..b8ee344 100644 --- a/gerber/primitives.py +++ b/gerber/primitives.py @@ -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):
diff --git a/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py index 0cb230b..78ccf34 100644 --- a/gerber/render/cairo_backend.py +++ b/gerber/render/cairo_backend.py @@ -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)
diff --git a/gerber/render/rs274x_backend.py b/gerber/render/rs274x_backend.py index 972edcb..15e9154 100644 --- a/gerber/render/rs274x_backend.py +++ b/gerber/render/rs274x_backend.py @@ -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): diff --git a/gerber/rs274x.py b/gerber/rs274x.py index 260fbf8..e88bba7 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -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]) diff --git a/gerber/tests/golden/example_coincident_hole.png b/gerber/tests/golden/example_coincident_hole.png Binary files differnew file mode 100644 index 0000000..9855b11 --- /dev/null +++ b/gerber/tests/golden/example_coincident_hole.png diff --git a/gerber/tests/golden/example_cutin_multiple.png b/gerber/tests/golden/example_cutin_multiple.png Binary files differnew file mode 100644 index 0000000..ebc1191 --- /dev/null +++ b/gerber/tests/golden/example_cutin_multiple.png diff --git a/gerber/tests/golden/example_flash_circle.png b/gerber/tests/golden/example_flash_circle.png Binary files differnew file mode 100644 index 0000000..0c407f6 --- /dev/null +++ b/gerber/tests/golden/example_flash_circle.png diff --git a/gerber/tests/golden/example_flash_obround.png b/gerber/tests/golden/example_flash_obround.png Binary files differnew file mode 100644 index 0000000..2fd4dc3 --- /dev/null +++ b/gerber/tests/golden/example_flash_obround.png diff --git a/gerber/tests/golden/example_flash_polygon.png b/gerber/tests/golden/example_flash_polygon.png Binary files differnew file mode 100644 index 0000000..89a964b --- /dev/null +++ b/gerber/tests/golden/example_flash_polygon.png diff --git a/gerber/tests/golden/example_flash_rectangle.png b/gerber/tests/golden/example_flash_rectangle.png Binary files differnew file mode 100644 index 0000000..797e0c3 --- /dev/null +++ b/gerber/tests/golden/example_flash_rectangle.png diff --git a/gerber/tests/golden/example_fully_coincident.png b/gerber/tests/golden/example_fully_coincident.png Binary files differnew file mode 100644 index 0000000..4e522ff --- /dev/null +++ b/gerber/tests/golden/example_fully_coincident.png diff --git a/gerber/tests/golden/example_not_overlapping_contour.png b/gerber/tests/golden/example_not_overlapping_contour.png Binary files differnew file mode 100644 index 0000000..4e522ff --- /dev/null +++ b/gerber/tests/golden/example_not_overlapping_contour.png diff --git a/gerber/tests/golden/example_not_overlapping_touching.png b/gerber/tests/golden/example_not_overlapping_touching.png Binary files differnew file mode 100644 index 0000000..d485495 --- /dev/null +++ b/gerber/tests/golden/example_not_overlapping_touching.png diff --git a/gerber/tests/golden/example_overlapping_contour.png b/gerber/tests/golden/example_overlapping_contour.png Binary files differnew file mode 100644 index 0000000..7504311 --- /dev/null +++ b/gerber/tests/golden/example_overlapping_contour.png diff --git a/gerber/tests/golden/example_overlapping_touching.png b/gerber/tests/golden/example_overlapping_touching.png Binary files differnew file mode 100644 index 0000000..7504311 --- /dev/null +++ b/gerber/tests/golden/example_overlapping_touching.png diff --git a/gerber/tests/golden/example_simple_contour.png b/gerber/tests/golden/example_simple_contour.png Binary files differnew file mode 100644 index 0000000..564ae14 --- /dev/null +++ b/gerber/tests/golden/example_simple_contour.png diff --git a/gerber/tests/golden/example_single_contour.png b/gerber/tests/golden/example_single_contour.png Binary files differnew file mode 100644 index 0000000..3341638 --- /dev/null +++ b/gerber/tests/golden/example_single_contour.png diff --git a/gerber/tests/golden/example_single_contour_3.png b/gerber/tests/golden/example_single_contour_3.png Binary files differnew file mode 100644 index 0000000..1eecfee --- /dev/null +++ b/gerber/tests/golden/example_single_contour_3.png diff --git a/gerber/tests/golden/example_single_quadrant.png b/gerber/tests/golden/example_single_quadrant.png Binary files differnew file mode 100644 index 0000000..89b763f --- /dev/null +++ b/gerber/tests/golden/example_single_quadrant.png diff --git a/gerber/tests/golden/example_two_square_boxes.png b/gerber/tests/golden/example_two_square_boxes.png Binary files differindex 4732995..98d0518 100644 --- a/gerber/tests/golden/example_two_square_boxes.png +++ b/gerber/tests/golden/example_two_square_boxes.png diff --git a/gerber/tests/resources/example_coincident_hole.gbr b/gerber/tests/resources/example_coincident_hole.gbr new file mode 100644 index 0000000..4f896ea --- /dev/null +++ b/gerber/tests/resources/example_coincident_hole.gbr @@ -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*
\ No newline at end of file diff --git a/gerber/tests/resources/example_cutin.gbr b/gerber/tests/resources/example_cutin.gbr new file mode 100644 index 0000000..365e5e1 --- /dev/null +++ b/gerber/tests/resources/example_cutin.gbr @@ -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*
\ No newline at end of file diff --git a/gerber/tests/resources/example_cutin_multiple.gbr b/gerber/tests/resources/example_cutin_multiple.gbr new file mode 100644 index 0000000..8e19429 --- /dev/null +++ b/gerber/tests/resources/example_cutin_multiple.gbr @@ -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*
\ No newline at end of file diff --git a/gerber/tests/resources/example_flash_circle.gbr b/gerber/tests/resources/example_flash_circle.gbr new file mode 100644 index 0000000..20b2566 --- /dev/null +++ b/gerber/tests/resources/example_flash_circle.gbr @@ -0,0 +1,10 @@ +G04 Flashes of circular apertures* +%FSLAX24Y24*% +%MOMM*% +%ADD10C,0.5*% +%ADD11C,0.5X0.25*% +D10* +X000000Y000000D03* +D11* +X010000D03* +M02*
\ No newline at end of file diff --git a/gerber/tests/resources/example_flash_obround.gbr b/gerber/tests/resources/example_flash_obround.gbr new file mode 100644 index 0000000..5313f82 --- /dev/null +++ b/gerber/tests/resources/example_flash_obround.gbr @@ -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*
\ No newline at end of file diff --git a/gerber/tests/resources/example_flash_polygon.gbr b/gerber/tests/resources/example_flash_polygon.gbr new file mode 100644 index 0000000..177cf9b --- /dev/null +++ b/gerber/tests/resources/example_flash_polygon.gbr @@ -0,0 +1,10 @@ +G04 Flashes of rectangular apertures* +%FSLAX24Y24*% +%MOMM*% +%ADD10P,.40X6*% +%ADD11P,.40X6X0.0X0.19*% +D10* +X000000Y000000D03* +D11* +X010000D03* +M02*
\ No newline at end of file diff --git a/gerber/tests/resources/example_flash_rectangle.gbr b/gerber/tests/resources/example_flash_rectangle.gbr new file mode 100644 index 0000000..8fde812 --- /dev/null +++ b/gerber/tests/resources/example_flash_rectangle.gbr @@ -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*
\ No newline at end of file diff --git a/gerber/tests/resources/example_fully_coincident.gbr b/gerber/tests/resources/example_fully_coincident.gbr new file mode 100644 index 0000000..3764128 --- /dev/null +++ b/gerber/tests/resources/example_fully_coincident.gbr @@ -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*
\ No newline at end of file diff --git a/gerber/tests/resources/example_level_holes.gbr b/gerber/tests/resources/example_level_holes.gbr new file mode 100644 index 0000000..1b4e189 --- /dev/null +++ b/gerber/tests/resources/example_level_holes.gbr @@ -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*
\ No newline at end of file diff --git a/gerber/tests/resources/example_not_overlapping_contour.gbr b/gerber/tests/resources/example_not_overlapping_contour.gbr new file mode 100644 index 0000000..e3ea631 --- /dev/null +++ b/gerber/tests/resources/example_not_overlapping_contour.gbr @@ -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*
\ No newline at end of file diff --git a/gerber/tests/resources/example_not_overlapping_touching.gbr b/gerber/tests/resources/example_not_overlapping_touching.gbr new file mode 100644 index 0000000..3b9b955 --- /dev/null +++ b/gerber/tests/resources/example_not_overlapping_touching.gbr @@ -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*
\ No newline at end of file diff --git a/gerber/tests/resources/example_overlapping_contour.gbr b/gerber/tests/resources/example_overlapping_contour.gbr new file mode 100644 index 0000000..74886a2 --- /dev/null +++ b/gerber/tests/resources/example_overlapping_contour.gbr @@ -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*
\ No newline at end of file diff --git a/gerber/tests/resources/example_overlapping_touching.gbr b/gerber/tests/resources/example_overlapping_touching.gbr new file mode 100644 index 0000000..27fce15 --- /dev/null +++ b/gerber/tests/resources/example_overlapping_touching.gbr @@ -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*
\ No newline at end of file diff --git a/gerber/tests/resources/example_simple_contour.gbr b/gerber/tests/resources/example_simple_contour.gbr new file mode 100644 index 0000000..d851760 --- /dev/null +++ b/gerber/tests/resources/example_simple_contour.gbr @@ -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*
\ No newline at end of file diff --git a/gerber/tests/resources/example_single_contour_1.gbr b/gerber/tests/resources/example_single_contour_1.gbr new file mode 100644 index 0000000..e9f9a75 --- /dev/null +++ b/gerber/tests/resources/example_single_contour_1.gbr @@ -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*
\ No newline at end of file diff --git a/gerber/tests/resources/example_single_contour_2.gbr b/gerber/tests/resources/example_single_contour_2.gbr new file mode 100644 index 0000000..085c72c --- /dev/null +++ b/gerber/tests/resources/example_single_contour_2.gbr @@ -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*
\ No newline at end of file diff --git a/gerber/tests/resources/example_single_contour_3.gbr b/gerber/tests/resources/example_single_contour_3.gbr new file mode 100644 index 0000000..40de149 --- /dev/null +++ b/gerber/tests/resources/example_single_contour_3.gbr @@ -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*
\ No newline at end of file diff --git a/gerber/tests/resources/example_single_quadrant.gbr b/gerber/tests/resources/example_single_quadrant.gbr new file mode 100644 index 0000000..c398601 --- /dev/null +++ b/gerber/tests/resources/example_single_quadrant.gbr @@ -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*
\ No newline at end of file diff --git a/gerber/tests/test_cairo_backend.py b/gerber/tests/test_cairo_backend.py index e298439..38cffba 100644 --- a/gerber/tests/test_cairo_backend.py +++ b/gerber/tests/test_cairo_backend.py @@ -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_polygon(): +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_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) - _test_render(TWO_BOXES_FILE, TWO_BOXES_EXPECTED) 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 diff --git a/gerber/tests/test_primitives.py b/gerber/tests/test_primitives.py index a88497c..bc67891 100644 --- a/gerber/tests/test_primitives.py +++ b/gerber/tests/test_primitives.py @@ -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) |