From 5af19af190c1fb0f0c5be029d46d63e657dde4d9 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Thu, 21 Jan 2016 03:57:44 -0500 Subject: Commit partial merge so I can work on the plane --- gerber/am_statements.py | 3 + gerber/cam.py | 3 +- gerber/excellon.py | 19 ++-- gerber/gerber_statements.py | 3 +- gerber/primitives.py | 172 ++++++++++++++++++++----------------- gerber/render/cairo_backend.py | 92 +++++++++++++++++++- gerber/render/render.py | 1 + gerber/rs274x.py | 22 +++-- gerber/tests/test_am_statements.py | 4 + gerber/tests/test_cam.py | 11 +++ gerber/tests/test_primitives.py | 18 ++-- 11 files changed, 244 insertions(+), 104 deletions(-) diff --git a/gerber/am_statements.py b/gerber/am_statements.py index 9c09085..726df2f 100644 --- a/gerber/am_statements.py +++ b/gerber/am_statements.py @@ -19,10 +19,13 @@ from math import asin import math +from .primitives import * from .primitives import Circle, Line, Outline, Polygon, Rectangle +from .utils import validate_coordinates, inch, metric from .utils import validate_coordinates, inch, metric, rotate_point + # TODO: Add support for aperture macro variables __all__ = ['AMPrimitive', 'AMCommentPrimitive', 'AMCirclePrimitive', 'AMVectorLinePrimitive', 'AMOutlinePrimitive', 'AMPolygonPrimitive', diff --git a/gerber/cam.py b/gerber/cam.py index 0e19b05..c5b8938 100644 --- a/gerber/cam.py +++ b/gerber/cam.py @@ -267,8 +267,7 @@ class CamFile(object): filename : string If provided, save the rendered image to `filename` """ - - ctx.set_bounds(self.bounding_box) + ctx.set_bounds(self.bounds) ctx._paint_background() ctx.invert = invert ctx._new_render_layer() diff --git a/gerber/excellon.py b/gerber/excellon.py index a5da42a..0626819 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -80,7 +80,7 @@ def loads(data, settings = None, tools = None): settings = FileSettings(**detect_excellon_format(data)) return ExcellonParser(settings, tools).parse_raw(data) - + class DrillHit(object): """Drill feature that is a single drill hole. @@ -91,8 +91,7 @@ class DrillHit(object): position : tuple(float, float) Center position of the drill. - """ - + """ def __init__(self, tool, position): self.tool = tool self.position = position @@ -194,7 +193,7 @@ class ExcellonFile(CamFile): self.hits = hits @property - def primitives(self): + def primitives(self): """ Gets the primitives. Note that unlike Gerber, this generates new objects """ @@ -262,7 +261,7 @@ class ExcellonFile(CamFile): for hit in self.hits: if hit.tool.number == tool.number: f.write(CoordinateStmt( - *hit.position).to_excellon(self.settings) + '\n') + *hit.position).to_excellon(self.settings) + '\n') f.write(EndOfProgramStmt().to_excellon() + '\n') def to_inch(self): @@ -276,7 +275,7 @@ class ExcellonFile(CamFile): for tool in iter(self.tools.values()): tool.to_inch() for primitive in self.primitives: - primitive.to_inch() + primitive.to_inch() for hit in self.hits: hit.to_inch() @@ -298,7 +297,7 @@ class ExcellonFile(CamFile): for statement in self.statements: statement.offset(x_offset, y_offset) for primitive in self.primitives: - primitive.offset(x_offset, y_offset) + primitive.offset(x_offset, y_offset) for hit in self. hits: hit.offset(x_offset, y_offset) @@ -359,7 +358,7 @@ class ExcellonParser(object): Parameters ---------- settings : FileSettings or dict-like - Excellon file settings to use when interpreting the excellon file. + Excellon file settings to use when interpreting the excellon file. """ def __init__(self, settings=None, ext_tools=None): self.notation = 'absolute' @@ -614,12 +613,12 @@ class ExcellonParser(object): stmt = ToolSelectionStmt.from_excellon(line) self.statements.append(stmt) - # T0 is used as END marker, just ignore + # T0 is used as END marker, just ignore if stmt.tool != 0: tool = self._get_tool(stmt.tool) if not tool: - # FIXME: for weird files with no tools defined, original calc from gerbv + # FIXME: for weird files with no tools defined, original calc from gerb if self._settings().units == "inch": diameter = (16 + 8 * stmt.tool) / 1000.0 else: diff --git a/gerber/gerber_statements.py b/gerber/gerber_statements.py index 08dbd82..33fb4ec 100644 --- a/gerber/gerber_statements.py +++ b/gerber/gerber_statements.py @@ -337,7 +337,8 @@ class ADParamStmt(ParamStmt): if isinstance(modifiers, tuple): self.modifiers = modifiers elif modifiers: - self.modifiers = [tuple([float(x) for x in m.split("X") if len(x)]) for m in modifiers.split(",") if len(m)] + self.modifiers = [tuple([float(x) for x in m.split("X") if len(x)]) + for m in modifiers.split(",") if len(m)] else: self.modifiers = [tuple()] diff --git a/gerber/primitives.py b/gerber/primitives.py index d78c6d9..a291c26 100644 --- a/gerber/primitives.py +++ b/gerber/primitives.py @@ -16,11 +16,11 @@ # limitations under the License. -from itertools import combinations + import math from operator import add - -from .utils import validate_coordinates, inch, metric, convex_hull, rotate_point, nearly_equal +from itertools import combinations +from .utils import validate_coordinates, inch, metric, convex_hull, rotate_point, nearly_equal @@ -50,9 +50,9 @@ class Primitive(object): def __init__(self, level_polarity='dark', rotation=0, units=None, net_name=None): self.level_polarity = level_polarity - self.net_name = net_name + self.net_name = net_name self._to_convert = list() - self.id = id + self.id = id self._memoized = list() self._units = units self._rotation = rotation @@ -60,18 +60,21 @@ class Primitive(object): self._sin_theta = math.sin(math.radians(rotation)) self._bounding_box = None self._vertices = None - self._segments = None + self._segments = None @property def flashed(self): '''Is this a flashed primitive''' raise NotImplementedError('Is flashed must be ' - 'implemented in subclass') + 'implemented in subclass') + def __eq__(self, other): + return self.__dict__ == other.__dict__ + @property def units(self): - return self._units + return self._units @units.setter def units(self, value): @@ -81,7 +84,7 @@ class Primitive(object): @property def rotation(self): return self._rotation - + @rotation.setter def rotation(self, value): self._changed() @@ -172,8 +175,8 @@ class Primitive(object): except: if value is not None: setattr(self, attr, metric(value)) - - def offset(self, x_offset=0, y_offset=0): + + def offset(self, x_offset=0, y_offset=0): """ Move the primitive by the specified x and y offset amount. values are specified in the primitive's native units @@ -183,10 +186,7 @@ class Primitive(object): self.position = tuple([coord + offset for coord, offset in zip(self.position, (x_offset, y_offset))]) - - def __eq__(self, other): - return self.__dict__ == other.__dict__ - + def to_statement(self): pass @@ -201,9 +201,8 @@ class Primitive(object): self._bounding_box = None self._vertices = None self._segments = None - for attr in self._memoized: - setattr(self, attr, None) - + for attr in self._memoized: + setattr(self, attr, None) class Line(Primitive): """ @@ -238,7 +237,6 @@ class Line(Primitive): self._changed() self._end = value - @property def angle(self): delta_x, delta_y = tuple( @@ -246,7 +244,7 @@ class Line(Primitive): angle = math.atan2(delta_y, delta_x) return angle - @property + @property def bounding_box(self): if self._bounding_box is None: if isinstance(self.aperture, Circle): @@ -261,7 +259,7 @@ class Line(Primitive): max_y = max(self.start[1], self.end[1]) + height_2 self._bounding_box = ((min_x, max_x), (min_y, max_y)) return self._bounding_box - + @property def bounding_box_no_aperture(self): '''Gets the bounding box without the aperture''' @@ -293,13 +291,13 @@ class Line(Primitive): # The line is defined by the convex hull of the points self._vertices = convex_hull((start_ll, start_lr, start_ul, start_ur, end_ll, end_lr, end_ul, end_ur)) return self._vertices - - def offset(self, x_offset=0, y_offset=0): + + def offset(self, x_offset=0, y_offset=0): + self._changed() self.start = tuple([coord + offset for coord, offset in zip(self.start, (x_offset, y_offset))]) self.end = tuple([coord + offset for coord, offset in zip(self.end, (x_offset, y_offset))]) - self._changed() def equivalent(self, other, offset): @@ -308,12 +306,14 @@ class Line(Primitive): equiv_start = tuple(map(add, other.start, offset)) equiv_end = tuple(map(add, other.end, offset)) + return nearly_equal(self.start, equiv_start) and nearly_equal(self.end, equiv_end) class Arc(Primitive): """ - """ + """ + def __init__(self, start, end, center, direction, aperture, quadrant_mode, **kwargs): super(Arc, self).__init__(**kwargs) self._start = start @@ -436,7 +436,7 @@ class Arc(Primitive): min_y = min(y) - self.aperture.radius max_y = max(y) + self.aperture.radius self._bounding_box = ((min_x, max_x), (min_y, max_y)) - return self._bounding_box + return self._bounding_box @property def bounding_box_no_aperture(self): @@ -488,12 +488,13 @@ class Arc(Primitive): class Circle(Primitive): """ - """ - def __init__(self, position, diameter, hole_diameter = None, **kwargs): + """ + + def __init__(self, position, diameter, hole_diameter = None, **kwargs): super(Circle, self).__init__(**kwargs) validate_coordinates(position) self._position = position - self._diameter = diameter + self._diameter = diameter self.hole_diameter = hole_diameter self._to_convert = ['position', 'diameter', 'hole_diameter'] @@ -537,7 +538,7 @@ class Circle(Primitive): min_y = self.position[1] - self.radius max_y = self.position[1] + self.radius self._bounding_box = ((min_x, max_x), (min_y, max_y)) - return self._bounding_box + return self._bounding_box def offset(self, x_offset=0, y_offset=0): self.position = tuple(map(add, self.position, (x_offset, y_offset))) @@ -553,7 +554,7 @@ class Circle(Primitive): equiv_position = tuple(map(add, other.position, offset)) - return nearly_equal(self.position, equiv_position) + return nearly_equal(self.position, equiv_position) class Ellipse(Primitive): @@ -575,7 +576,7 @@ class Ellipse(Primitive): @property def position(self): return self._position - + @position.setter def position(self, value): self._changed() @@ -625,18 +626,19 @@ class Ellipse(Primitive): class Rectangle(Primitive): - """ + """ When rotated, the rotation is about the center point. Only aperture macro generated Rectangle objects can be rotated. If you aren't in a AMGroup, then you don't need to worry about rotation - """ - def __init__(self, position, width, height, hole_diameter=0, **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._height = height self.hole_diameter = hole_diameter self._to_convert = ['position', 'width', 'height', 'hole_diameter'] # TODO These are probably wrong when rotated @@ -656,14 +658,14 @@ class Rectangle(Primitive): self._changed() self._position = value - @property + @property def width(self): return self._width @width.setter def width(self, value): self._changed() - self._width = value + self._width = value @property def height(self): @@ -685,7 +687,7 @@ class Rectangle(Primitive): def upper_right(self): return (self.position[0] + (self._abs_width / 2.), self.position[1] + (self._abs_height / 2.)) - + @property def lower_left(self): return (self.position[0] - (self.axis_aligned_width / 2.), @@ -765,7 +767,7 @@ class Diamond(Primitive): @position.setter def position(self, value): self._changed() - self._position = value + self._position = value @property def width(self): @@ -776,7 +778,7 @@ class Diamond(Primitive): self._changed() self._width = value - @property + @property def height(self): return self._height @@ -950,7 +952,7 @@ class RoundRectangle(Primitive): @height.setter def height(self, value): self._changed() - self._height = value + self._height = value @property def radius(self): @@ -985,21 +987,22 @@ class RoundRectangle(Primitive): return (self._cos_theta * self.width + self._sin_theta * self.height) - @property + @property def axis_aligned_height(self): return (self._cos_theta * self.height + self._sin_theta * self.width) class Obround(Primitive): - """ """ - def __init__(self, position, width, height, hole_diameter=0, **kwargs): + """ + + 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._height = height self.hole_diameter = hole_diameter self._to_convert = ['position', 'width', 'height', 'hole_diameter'] @@ -1014,7 +1017,7 @@ class Obround(Primitive): @position.setter def position(self, value): self._changed() - self._position = value + self._position = value @property def width(self): @@ -1030,7 +1033,7 @@ class Obround(Primitive): return (self.position[0] + (self._abs_width / 2.), self.position[1] + (self._abs_height / 2.)) - @property + @property def height(self): return self._height @@ -1093,7 +1096,7 @@ class Obround(Primitive): class Polygon(Primitive): - """ + """ Polygon flash defined by a set number of sides. """ def __init__(self, position, sides, radius, hole_diameter, **kwargs): @@ -1126,7 +1129,7 @@ class Polygon(Primitive): @position.setter def position(self, value): self._changed() - self._position = value + self._position = value @property def radius(self): @@ -1162,6 +1165,18 @@ class Polygon(Primitive): return points + @property + def vertices(self): + if self._vertices is None: + theta = math.radians(360/self.sides) + vertices = [(self.position[0] + (math.cos(theta * side) * self.radius), + self.position[1] + (math.sin(theta * side) * self.radius)) + for side in range(self.sides)] + self._vertices = [(((x * self._cos_theta) - (y * self._sin_theta)), + ((x * self._sin_theta) + (y * self._cos_theta))) + for x, y in vertices] + return self._vertices + def equivalent(self, other, offset): """ Is this the outline the same as the other, ignoring the position offset? @@ -1170,7 +1185,7 @@ class Polygon(Primitive): # Quick check if it even makes sense to compare them if type(self) != type(other) or self.sides != other.sides or self.radius != other.radius: return False - + equiv_pos = tuple(map(add, other.position, offset)) return nearly_equal(self.position, equiv_pos) @@ -1178,7 +1193,7 @@ class Polygon(Primitive): class AMGroup(Primitive): """ - """ + """ def __init__(self, amprimitives, stmt = None, **kwargs): """ @@ -1281,6 +1296,7 @@ class Outline(Primitive): Outlines only exist as the rendering for a apeture macro outline. They don't exist outside of AMGroup objects """ + def __init__(self, primitives, **kwargs): super(Outline, self).__init__(**kwargs) self.primitives = primitives @@ -1295,16 +1311,19 @@ class Outline(Primitive): @property def bounding_box(self): - xlims, ylims = zip(*[p.bounding_box for p in self.primitives]) - minx, maxx = zip(*xlims) - miny, maxy = zip(*ylims) - min_x = min(minx) - max_x = max(maxx) - min_y = min(miny) - max_y = max(maxy) - return ((min_x, max_x), (min_y, max_y)) + if self._bounding_box is None: + xlims, ylims = zip(*[p.bounding_box for p in self.primitives]) + minx, maxx = zip(*xlims) + miny, maxy = zip(*ylims) + min_x = min(minx) + max_x = max(maxx) + min_y = min(miny) + max_y = max(maxy) + self._bounding_box = ((min_x, max_x), (min_y, max_y)) + return self._bounding_box def offset(self, x_offset=0, y_offset=0): + self._changed() for p in self.primitives: p.offset(x_offset, y_offset) @@ -1416,11 +1435,11 @@ class SquareButterfly(Primitive): self._to_convert = ['position', 'side'] # TODO This does not reset bounding box correctly - + @property def flashed(self): return True - + @property def bounding_box(self): if self._bounding_box is None: @@ -1456,9 +1475,10 @@ class Donut(Primitive): else: # Hexagon self.width = 0.5 * math.sqrt(3.) * outer_diameter - self.height = outer_diameter + self.height = outer_diameter - self._to_convert = ['position', 'width', 'height', 'inner_diameter', 'outer_diameter'] + self._to_convert = ['position', 'width', + 'height', 'inner_diameter', 'outer_diameter'] # TODO This does not reset bounding box correctly @@ -1474,7 +1494,7 @@ class Donut(Primitive): @property def upper_right(self): return (self.position[0] + (self.width / 2.), - self.position[1] + (self.height / 2.)) + self.position[1] + (self.height / 2.) @property def bounding_box(self): @@ -1526,11 +1546,13 @@ class Drill(Primitive): self.hit = hit self._to_convert = ['position', 'diameter', 'hit'] + # TODO Ths won't handle the hit updates correctly + @property def flashed(self): return False - @property + @property def position(self): return self._position @@ -1588,19 +1610,13 @@ class Slot(Primitive): @property def flashed(self): return False - - @property - def radius(self): - return self.diameter / 2. - - @property + def bounding_box(self): - radius = self.radius - min_x = min(self.start[0], self.end[0]) - radius - max_x = max(self.start[0], self.end[0]) + radius - min_y = min(self.start[1], self.end[1]) - radius - max_y = max(self.start[1], self.end[1]) + radius - return ((min_x, max_x), (min_y, max_y)) + if self._bounding_box is None: + ll = tuple([c - self.outer_diameter / 2. for c in self.position]) + ur = tuple([c + self.outer_diameter / 2. for c in self.position]) + self._bounding_box = ((ll[0], ur[0]), (ll[1], ur[1])) + return self._bounding_box def offset(self, x_offset=0, y_offset=0): self.start = tuple(map(add, self.start, (x_offset, y_offset))) diff --git a/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py index dc39607..8c7232f 100644 --- a/gerber/render/cairo_backend.py +++ b/gerber/render/cairo_backend.py @@ -12,6 +12,7 @@ # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + # See the License for the specific language governing permissions and # limitations under the License. @@ -22,14 +23,14 @@ except ImportError: import math from operator import mul, div - import tempfile +import cairocffi as cairo + from ..primitives import * from .render import GerberContext, RenderSettings from .theme import THEMES - try: from cStringIO import StringIO except(ImportError): @@ -138,20 +139,35 @@ class GerberCairoContext(GerberContext): start = [pos * scale for pos, scale in zip(line.start, self.scale)] end = [pos * scale for pos, scale in zip(line.end, self.scale)] if not self.invert: +<<<<<<< HEAD self.ctx.set_source_rgba(color[0], color[1], color[2], alpha=self.alpha) self.ctx.set_operator(cairo.OPERATOR_OVER if line.level_polarity == "dark" else cairo.OPERATOR_CLEAR) +======= + self.ctx.set_source_rgba(*color, alpha=self.alpha) + self.ctx.set_operator(cairo.OPERATOR_OVER + if line.level_polarity == 'dark' + else cairo.OPERATOR_CLEAR) +>>>>>>> 5476da8... Fix a bunch of rendering bugs. else: self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) self.ctx.set_operator(cairo.OPERATOR_CLEAR) if isinstance(line.aperture, Circle): +<<<<<<< HEAD width = line.aperture.diameter +======= + width = line.aperture.diameter +>>>>>>> 5476da8... Fix a bunch of rendering bugs. self.ctx.set_line_width(width * self.scale[0]) self.ctx.set_line_cap(cairo.LINE_CAP_ROUND) self.ctx.move_to(*start) self.ctx.line_to(*end) +<<<<<<< HEAD self.ctx.stroke() +======= + self.ctx.stroke() +>>>>>>> 5476da8... Fix a bunch of rendering bugs. elif isinstance(line.aperture, Rectangle): points = [self.scale_point(x) for x in line.vertices] self.ctx.set_line_width(0) @@ -176,6 +192,7 @@ class GerberCairoContext(GerberContext): width = max(arc.aperture.width, arc.aperture.height, 0.001) if not self.invert: +<<<<<<< HEAD self.ctx.set_source_rgba(color[0], color[1], color[2], alpha=self.alpha) self.ctx.set_operator(cairo.OPERATOR_OVER if arc.level_polarity == "dark"\ @@ -184,25 +201,50 @@ class GerberCairoContext(GerberContext): self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) self.ctx.set_operator(cairo.OPERATOR_CLEAR) +======= + self.ctx.set_source_rgba(*color, alpha=self.alpha) + self.ctx.set_operator(cairo.OPERATOR_OVER + if arc.level_polarity == 'dark' + else cairo.OPERATOR_CLEAR) + else: + self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) + self.ctx.set_operator(cairo.OPERATOR_CLEAR) +>>>>>>> 5476da8... Fix a bunch of rendering bugs. self.ctx.set_line_width(width * self.scale[0]) self.ctx.set_line_cap(cairo.LINE_CAP_ROUND) self.ctx.move_to(*start) # You actually have to do this... if arc.direction == 'counterclockwise': +<<<<<<< HEAD self.ctx.arc(center[0], center[1], radius, angle1, angle2) else: self.ctx.arc_negative(center[0], center[1], radius, angle1, angle2) +======= + self.ctx.arc(*center, radius=radius, angle1=angle1, angle2=angle2) + else: + self.ctx.arc_negative(*center, radius=radius, + angle1=angle1, angle2=angle2) +>>>>>>> 5476da8... Fix a bunch of rendering bugs. self.ctx.move_to(*end) # ...lame def _render_region(self, region, color): if not self.invert: +<<<<<<< HEAD self.ctx.set_source_rgba(color[0], color[1], color[2], alpha=self.alpha) self.ctx.set_operator(cairo.OPERATOR_OVER if region.level_polarity == "dark" +======= + self.ctx.set_source_rgba(*color, alpha=self.alpha) + self.ctx.set_operator(cairo.OPERATOR_OVER + if region.level_polarity == 'dark' +>>>>>>> 5476da8... Fix a bunch of rendering bugs. else cairo.OPERATOR_CLEAR) else: self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) self.ctx.set_operator(cairo.OPERATOR_CLEAR) +<<<<<<< HEAD +======= +>>>>>>> 5476da8... Fix a bunch of rendering bugs. self.ctx.set_line_width(0) self.ctx.set_line_cap(cairo.LINE_CAP_ROUND) self.ctx.move_to(*self.scale_point(region.primitives[0].start)) @@ -220,6 +262,7 @@ class GerberCairoContext(GerberContext): else: self.ctx.arc_negative(*center, radius=radius, angle1=angle1, angle2=angle2) +<<<<<<< HEAD self.ctx.fill() def _render_circle(self, circle, color): center = self.scale_point(circle.position) @@ -249,10 +292,28 @@ class GerberCairoContext(GerberContext): self.ctx.pop_group_to_source() self.ctx.paint_with_alpha(1) +======= + self.ctx.fill() + + def _render_circle(self, circle, color): + center = self.scale_point(circle.position) + if not self.invert: + self.ctx.set_source_rgba(*color, alpha=self.alpha) + self.ctx.set_operator( + cairo.OPERATOR_OVER if circle.level_polarity == 'dark' else cairo.OPERATOR_CLEAR) + else: + self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) + self.ctx.set_operator(cairo.OPERATOR_CLEAR) + self.ctx.set_line_width(0) + self.ctx.arc(*center, radius=circle.radius * + self.scale[0], angle1=0, angle2=2 * math.pi) + self.ctx.fill() +>>>>>>> 5476da8... Fix a bunch of rendering bugs. def _render_rectangle(self, rectangle, color): lower_left = self.scale_point(rectangle.lower_left) width, height = tuple([abs(coord) for coord in self.scale_point((rectangle.width, rectangle.height))]) +<<<<<<< HEAD if not self.invert: self.ctx.set_source_rgba(color[0], color[1], color[2], alpha=self.alpha) @@ -295,6 +356,19 @@ class GerberCairoContext(GerberContext): if rectangle.rotation != 0: self.ctx.restore() +======= + + if not self.invert: + self.ctx.set_source_rgba(*color, alpha=self.alpha) + self.ctx.set_operator( + cairo.OPERATOR_OVER if rectangle.level_polarity == 'dark' else cairo.OPERATOR_CLEAR) + else: + self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) + self.ctx.set_operator(cairo.OPERATOR_CLEAR) + self.ctx.set_line_width(0) + self.ctx.rectangle(*lower_left, width=width, height=height) + self.ctx.fill() +>>>>>>> 5476da8... Fix a bunch of rendering bugs. def _render_obround(self, obround, color): @@ -424,7 +498,11 @@ class GerberCairoContext(GerberContext): def _flatten(self): self.output_ctx.set_operator(cairo.OPERATOR_OVER) +<<<<<<< HEAD ptn = cairo.SurfacePattern(self.active_layer) +======= + ptn = cairo.SurfacePattern(self.active_layer) +>>>>>>> 5476da8... Fix a bunch of rendering bugs. ptn.set_matrix(self._xform_matrix) self.output_ctx.set_source(ptn) self.output_ctx.paint() @@ -435,8 +513,16 @@ class GerberCairoContext(GerberContext): if (not self.bg) or force: self.bg = True self.output_ctx.set_operator(cairo.OPERATOR_OVER) +<<<<<<< HEAD self.output_ctx.set_source_rgba(self.background_color[0], self.background_color[1], self.background_color[2], alpha=1.0) self.output_ctx.paint() def scale_point(self, point): - return tuple([coord * scale for coord, scale in zip(point, self.scale)]) \ No newline at end of file + return tuple([coord * scale for coord, scale in zip(point, self.scale)]) +======= + self.output_ctx.set_source_rgba(*self.background_color, alpha=1.0) + self.output_ctx.paint() + + def scale_point(self, point): + return tuple([coord * scale for coord, scale in zip(point, self.scale)]) +>>>>>>> 5476da8... Fix a bunch of rendering bugs. diff --git a/gerber/render/render.py b/gerber/render/render.py index 7bd4c00..b319648 100644 --- a/gerber/render/render.py +++ b/gerber/render/render.py @@ -182,6 +182,7 @@ class GerberContext(object): return + def _render_line(self, primitive, color): pass diff --git a/gerber/rs274x.py b/gerber/rs274x.py index 7fec64f..e84c161 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -95,6 +95,7 @@ class GerberFile(CamFile): `bounds` is stored as ((min x, max x), (min y, max y)) """ + def __init__(self, statements, settings, primitives, apertures, filename=None): super(GerberFile, self).__init__(statements, settings, primitives, filename) @@ -568,7 +569,7 @@ class GerberParser(object): self.interpolation = 'arc' self.direction = ('clockwise' if stmt.function in ('G02', 'G2') else 'counterclockwise') - + if stmt.only_function: # Sometimes we get a coordinate statement # that only sets the function. If so, don't @@ -594,7 +595,6 @@ class GerberParser(object): else: # from gerber spec revision J3, Section 4.5, page 55: # The segments are not graphics objects in themselves; segments are part of region which is the graphics object. The segments have no thickness. - # The current aperture is associated with the region. # This has no graphical effect, but allows all its attributes to # be applied to the region. @@ -616,12 +616,24 @@ class GerberParser(object): j = 0 if stmt.j is None else stmt.j center = self._find_center(start, end, (i, j)) if self.region_mode == 'off': - self.primitives.append(Arc(start, end, center, self.direction, self.apertures[self.aperture], quadrant_mode=self.quadrant_mode, level_polarity=self.level_polarity, units=self.settings.units)) + self.primitives.append(Arc(start, end, center, self.direction, + self.apertures[self.aperture], + quadrant_mode=self.quadrant_mode, + level_polarity=self.level_polarity, + units=self.settings.units)) else: if self.current_region is None: - self.current_region = [Arc(start, end, center, self.direction, self.apertures.get(self.aperture, Circle((0,0), 0)), quadrant_mode=self.quadrant_mode, level_polarity=self.level_polarity, units=self.settings.units),] + self.current_region = [Arc(start, end, center, self.direction, + self.apertures.get(self.aperture, Circle((0,0), 0)), + quadrant_mode=self.quadrant_mode, + level_polarity=self.level_polarity, + units=self.settings.units),] else: - self.current_region.append(Arc(start, end, center, self.direction, self.apertures.get(self.aperture, Circle((0,0), 0)), quadrant_mode=self.quadrant_mode, level_polarity=self.level_polarity, units=self.settings.units)) + self.current_region.append(Arc(start, end, center, self.direction, + self.apertures.get(self.aperture, Circle((0,0), 0)), + quadrant_mode=self.quadrant_mode, + level_polarity=self.level_polarity, + units=self.settings.units)) elif self.op == "D02" or self.op == "D2": diff --git a/gerber/tests/test_am_statements.py b/gerber/tests/test_am_statements.py index c5ae6ae..98a7332 100644 --- a/gerber/tests/test_am_statements.py +++ b/gerber/tests/test_am_statements.py @@ -165,6 +165,7 @@ def test_AMOUtlinePrimitive_dump(): assert_equal(o.to_gerber().replace('\n', ''), '4,1,3,0,0,3,3,3,0,0,0,0*') + def test_AMOutlinePrimitive_conversion(): o = AMOutlinePrimitive( 4, 'on', (0, 0), [(25.4, 25.4), (25.4, 0), (0, 0)], 0) @@ -259,6 +260,7 @@ def test_AMThermalPrimitive_validation(): assert_raises(TypeError, AMThermalPrimitive, 7, (0.0, '0'), 7, 5, 0.2, 0.0) + def test_AMThermalPrimitive_factory(): t = AMThermalPrimitive.from_gerber('7,0,0,7,6,0.2,45*') assert_equal(t.code, 7) @@ -269,11 +271,13 @@ def test_AMThermalPrimitive_factory(): assert_equal(t.rotation, 45) + def test_AMThermalPrimitive_dump(): t = AMThermalPrimitive.from_gerber('7,0,0,7,6,0.2,30*') assert_equal(t.to_gerber(), '7,0,0,7.0,6.0,0.2,30.0*') + def test_AMThermalPrimitive_conversion(): t = AMThermalPrimitive(7, (25.4, 25.4), 25.4, 25.4, 25.4, 0.0) t.to_inch() diff --git a/gerber/tests/test_cam.py b/gerber/tests/test_cam.py index 24f2b9b..a557e8c 100644 --- a/gerber/tests/test_cam.py +++ b/gerber/tests/test_cam.py @@ -116,6 +116,7 @@ def test_zeros(): def test_filesettings_validation(): """ Test FileSettings constructor argument validation """ +<<<<<<< HEAD # absolute-ish is not a valid notation assert_raises(ValueError, FileSettings, 'absolute-ish', 'inch', None, (2, 5), None) @@ -132,6 +133,16 @@ def test_filesettings_validation(): #assert_raises(ValueError, FileSettings, 'absolute', # 'inch', 'following', (2, 5), None) +======= + assert_raises(ValueError, FileSettings, 'absolute-ish', + 'inch', None, (2, 5), None) + assert_raises(ValueError, FileSettings, 'absolute', + 'degrees kelvin', None, (2, 5), None) + assert_raises(ValueError, FileSettings, 'absolute', + 'inch', 'leading', (2, 5), 'leading') + assert_raises(ValueError, FileSettings, 'absolute', + 'inch', 'following', (2, 5), None) +>>>>>>> 5476da8... Fix a bunch of rendering bugs. assert_raises(ValueError, FileSettings, 'absolute', 'inch', None, (2, 5), 'following') assert_raises(ValueError, FileSettings, 'absolute', diff --git a/gerber/tests/test_primitives.py b/gerber/tests/test_primitives.py index e23d5f4..c49b558 100644 --- a/gerber/tests/test_primitives.py +++ b/gerber/tests/test_primitives.py @@ -204,7 +204,8 @@ def test_arc_bounds(): def test_arc_conversion(): c = Circle((0, 0), 25.4, units='metric') - a = Arc((2.54, 25.4), (254.0, 2540.0), (25400.0, 254000.0),'clockwise', c, 'single-quadrant', units='metric') + a = Arc((2.54, 25.4), (254.0, 2540.0), (25400.0, 254000.0), + 'clockwise', c, 'single-quadrant', units='metric') # No effect a.to_metric() @@ -227,7 +228,8 @@ def test_arc_conversion(): assert_equal(a.aperture.diameter, 1.0) c = Circle((0, 0), 1.0, units='inch') - a = Arc((0.1, 1.0), (10.0, 100.0), (1000.0, 10000.0),'clockwise', c, 'single-quadrant', units='inch') + a = Arc((0.1, 1.0), (10.0, 100.0), (1000.0, 10000.0), + 'clockwise', c, 'single-quadrant', units='inch') a.to_metric() assert_equal(a.start, (2.54, 25.4)) assert_equal(a.end, (254.0, 2540.0)) @@ -254,12 +256,14 @@ 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 """ @@ -301,7 +305,7 @@ def test_circle_conversion(): assert_equal(c.diameter, 10.) assert_equal(c.hole_diameter, 5.) - #no effect + # no effect c.to_inch() assert_equal(c.position, (0.1, 1.)) assert_equal(c.diameter, 10.) @@ -338,13 +342,14 @@ def test_circle_conversion(): assert_equal(c.diameter, 254.) assert_equal(c.hole_diameter, 127.) - #no effect + # 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) c.offset(1, 0) @@ -443,6 +448,7 @@ def test_rectangle_hole_radius(): assert_equal(0.5, r.hole_radius) + def test_rectangle_bounds(): """ Test rectangle bounding box calculation """ @@ -530,7 +536,7 @@ def test_rectangle_conversion(): assert_equal(r.hole_diameter, 127.0) r.to_metric() - assert_equal(r.position, (2.54,25.4)) + assert_equal(r.position, (2.54, 25.4)) assert_equal(r.width, 254.0) assert_equal(r.height, 2540.0) assert_equal(r.hole_diameter, 127.0) @@ -881,6 +887,7 @@ def test_polygon_ctor(): assert_equal(p.hole_diameter, hole_diameter) + def test_polygon_bounds(): """ Test polygon bounding box calculation """ @@ -1201,6 +1208,7 @@ def test_drill_ctor_validation(): assert_raises(TypeError, Drill, 3, 5, None) assert_raises(TypeError, Drill, (3,4,5), 5, None) + def test_drill_bounds(): d = Drill((0, 0), 2, None) xbounds, ybounds = d.bounding_box -- cgit