From 7cd6acf12670f3773113f67ed2acb35cb21c32a0 Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Sun, 24 Jul 2016 17:08:47 +0800 Subject: Add many render tests based on the Umaco gerger specification. Fix multiple rendering bugs, especially related to holes in flashed apertures --- gerber/cam.py | 2 +- gerber/gerber_statements.py | 4 +- gerber/primitives.py | 35 ++++-- gerber/render/cairo_backend.py | 105 +++++++++++++---- gerber/render/rs274x_backend.py | 12 +- gerber/rs274x.py | 24 +++- gerber/tests/golden/example_coincident_hole.png | Bin 0 -> 47261 bytes gerber/tests/golden/example_cutin_multiple.png | Bin 0 -> 1348 bytes gerber/tests/golden/example_flash_circle.png | Bin 0 -> 5978 bytes gerber/tests/golden/example_flash_obround.png | Bin 0 -> 3443 bytes gerber/tests/golden/example_flash_polygon.png | Bin 0 -> 4087 bytes gerber/tests/golden/example_flash_rectangle.png | Bin 0 -> 1731 bytes gerber/tests/golden/example_fully_coincident.png | Bin 0 -> 71825 bytes .../golden/example_not_overlapping_contour.png | Bin 0 -> 71825 bytes .../golden/example_not_overlapping_touching.png | Bin 0 -> 96557 bytes .../tests/golden/example_overlapping_contour.png | Bin 0 -> 33301 bytes .../tests/golden/example_overlapping_touching.png | Bin 0 -> 33301 bytes gerber/tests/golden/example_simple_contour.png | Bin 0 -> 31830 bytes gerber/tests/golden/example_single_contour.png | Bin 0 -> 556 bytes gerber/tests/golden/example_single_contour_3.png | Bin 0 -> 2297 bytes gerber/tests/golden/example_single_quadrant.png | Bin 0 -> 9658 bytes gerber/tests/golden/example_two_square_boxes.png | Bin 18247 -> 18219 bytes gerber/tests/resources/example_coincident_hole.gbr | 24 ++++ gerber/tests/resources/example_cutin.gbr | 18 +++ gerber/tests/resources/example_cutin_multiple.gbr | 28 +++++ gerber/tests/resources/example_flash_circle.gbr | 10 ++ gerber/tests/resources/example_flash_obround.gbr | 10 ++ gerber/tests/resources/example_flash_polygon.gbr | 10 ++ gerber/tests/resources/example_flash_rectangle.gbr | 10 ++ .../tests/resources/example_fully_coincident.gbr | 23 ++++ gerber/tests/resources/example_level_holes.gbr | 39 +++++++ .../resources/example_not_overlapping_contour.gbr | 20 ++++ .../resources/example_not_overlapping_touching.gbr | 20 ++++ .../resources/example_overlapping_contour.gbr | 20 ++++ .../resources/example_overlapping_touching.gbr | 20 ++++ gerber/tests/resources/example_simple_contour.gbr | 16 +++ .../tests/resources/example_single_contour_1.gbr | 15 +++ .../tests/resources/example_single_contour_2.gbr | 15 +++ .../tests/resources/example_single_contour_3.gbr | 15 +++ gerber/tests/resources/example_single_quadrant.gbr | 18 +++ gerber/tests/test_cairo_backend.py | 128 ++++++++++++++++++++- gerber/tests/test_primitives.py | 106 +++++++++++++++++ 42 files changed, 699 insertions(+), 48 deletions(-) create mode 100644 gerber/tests/golden/example_coincident_hole.png create mode 100644 gerber/tests/golden/example_cutin_multiple.png create mode 100644 gerber/tests/golden/example_flash_circle.png create mode 100644 gerber/tests/golden/example_flash_obround.png create mode 100644 gerber/tests/golden/example_flash_polygon.png create mode 100644 gerber/tests/golden/example_flash_rectangle.png create mode 100644 gerber/tests/golden/example_fully_coincident.png create mode 100644 gerber/tests/golden/example_not_overlapping_contour.png create mode 100644 gerber/tests/golden/example_not_overlapping_touching.png create mode 100644 gerber/tests/golden/example_overlapping_contour.png create mode 100644 gerber/tests/golden/example_overlapping_touching.png create mode 100644 gerber/tests/golden/example_simple_contour.png create mode 100644 gerber/tests/golden/example_single_contour.png create mode 100644 gerber/tests/golden/example_single_contour_3.png create mode 100644 gerber/tests/golden/example_single_quadrant.png create mode 100644 gerber/tests/resources/example_coincident_hole.gbr create mode 100644 gerber/tests/resources/example_cutin.gbr create mode 100644 gerber/tests/resources/example_cutin_multiple.gbr create mode 100644 gerber/tests/resources/example_flash_circle.gbr create mode 100644 gerber/tests/resources/example_flash_obround.gbr create mode 100644 gerber/tests/resources/example_flash_polygon.gbr create mode 100644 gerber/tests/resources/example_flash_rectangle.gbr create mode 100644 gerber/tests/resources/example_fully_coincident.gbr create mode 100644 gerber/tests/resources/example_level_holes.gbr create mode 100644 gerber/tests/resources/example_not_overlapping_contour.gbr create mode 100644 gerber/tests/resources/example_not_overlapping_touching.gbr create mode 100644 gerber/tests/resources/example_overlapping_contour.gbr create mode 100644 gerber/tests/resources/example_overlapping_touching.gbr create mode 100644 gerber/tests/resources/example_simple_contour.gbr create mode 100644 gerber/tests/resources/example_single_contour_1.gbr create mode 100644 gerber/tests/resources/example_single_contour_2.gbr create mode 100644 gerber/tests/resources/example_single_contour_3.gbr create mode 100644 gerber/tests/resources/example_single_quadrant.gbr 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 new file mode 100644 index 0000000..9855b11 Binary files /dev/null and b/gerber/tests/golden/example_coincident_hole.png differ diff --git a/gerber/tests/golden/example_cutin_multiple.png b/gerber/tests/golden/example_cutin_multiple.png new file mode 100644 index 0000000..ebc1191 Binary files /dev/null and b/gerber/tests/golden/example_cutin_multiple.png differ diff --git a/gerber/tests/golden/example_flash_circle.png b/gerber/tests/golden/example_flash_circle.png new file mode 100644 index 0000000..0c407f6 Binary files /dev/null and b/gerber/tests/golden/example_flash_circle.png differ diff --git a/gerber/tests/golden/example_flash_obround.png b/gerber/tests/golden/example_flash_obround.png new file mode 100644 index 0000000..2fd4dc3 Binary files /dev/null and b/gerber/tests/golden/example_flash_obround.png differ diff --git a/gerber/tests/golden/example_flash_polygon.png b/gerber/tests/golden/example_flash_polygon.png new file mode 100644 index 0000000..89a964b Binary files /dev/null and b/gerber/tests/golden/example_flash_polygon.png differ diff --git a/gerber/tests/golden/example_flash_rectangle.png b/gerber/tests/golden/example_flash_rectangle.png new file mode 100644 index 0000000..797e0c3 Binary files /dev/null and b/gerber/tests/golden/example_flash_rectangle.png differ diff --git a/gerber/tests/golden/example_fully_coincident.png b/gerber/tests/golden/example_fully_coincident.png new file mode 100644 index 0000000..4e522ff Binary files /dev/null and b/gerber/tests/golden/example_fully_coincident.png differ diff --git a/gerber/tests/golden/example_not_overlapping_contour.png b/gerber/tests/golden/example_not_overlapping_contour.png new file mode 100644 index 0000000..4e522ff Binary files /dev/null and b/gerber/tests/golden/example_not_overlapping_contour.png differ diff --git a/gerber/tests/golden/example_not_overlapping_touching.png b/gerber/tests/golden/example_not_overlapping_touching.png new file mode 100644 index 0000000..d485495 Binary files /dev/null and b/gerber/tests/golden/example_not_overlapping_touching.png differ diff --git a/gerber/tests/golden/example_overlapping_contour.png b/gerber/tests/golden/example_overlapping_contour.png new file mode 100644 index 0000000..7504311 Binary files /dev/null and b/gerber/tests/golden/example_overlapping_contour.png differ diff --git a/gerber/tests/golden/example_overlapping_touching.png b/gerber/tests/golden/example_overlapping_touching.png new file mode 100644 index 0000000..7504311 Binary files /dev/null and b/gerber/tests/golden/example_overlapping_touching.png differ diff --git a/gerber/tests/golden/example_simple_contour.png b/gerber/tests/golden/example_simple_contour.png new file mode 100644 index 0000000..564ae14 Binary files /dev/null and b/gerber/tests/golden/example_simple_contour.png differ diff --git a/gerber/tests/golden/example_single_contour.png b/gerber/tests/golden/example_single_contour.png new file mode 100644 index 0000000..3341638 Binary files /dev/null and b/gerber/tests/golden/example_single_contour.png differ diff --git a/gerber/tests/golden/example_single_contour_3.png b/gerber/tests/golden/example_single_contour_3.png new file mode 100644 index 0000000..1eecfee Binary files /dev/null and b/gerber/tests/golden/example_single_contour_3.png differ diff --git a/gerber/tests/golden/example_single_quadrant.png b/gerber/tests/golden/example_single_quadrant.png new file mode 100644 index 0000000..89b763f Binary files /dev/null and b/gerber/tests/golden/example_single_quadrant.png differ diff --git a/gerber/tests/golden/example_two_square_boxes.png b/gerber/tests/golden/example_two_square_boxes.png index 4732995..98d0518 100644 Binary files a/gerber/tests/golden/example_two_square_boxes.png and b/gerber/tests/golden/example_two_square_boxes.png differ 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) -- cgit