diff options
Diffstat (limited to 'gerber')
88 files changed, 12438 insertions, 4546 deletions
diff --git a/gerber/__init__.py b/gerber/__init__.py index 1a11159..5cfdad7 100644 --- a/gerber/__init__.py +++ b/gerber/__init__.py @@ -23,4 +23,5 @@ gerber-tools provides utilities for working with Gerber (RS-274X) and Excellon files in python. """ -from .common import read
\ No newline at end of file +from .common import read, loads +from .pcb import PCB diff --git a/gerber/__main__.py b/gerber/__main__.py index 71e3bfc..6643b54 100644 --- a/gerber/__main__.py +++ b/gerber/__main__.py @@ -16,24 +16,24 @@ # the License. if __name__ == '__main__': - from .common import read - from .render import GerberSvgContext + from gerber.common import read + from gerber.render import GerberCairoContext import sys if len(sys.argv) < 2: - print >> sys.stderr, "Usage: python -m gerber <filename> <filename>..." + sys.stderr.write("Usage: python -m gerber <filename> <filename>...\n") sys.exit(1) - ctx = GerberSvgContext() - ctx.set_alpha(0.95) + ctx = GerberCairoContext() + ctx.alpha = 0.95 for filename in sys.argv[1:]: - print "parsing %s" % filename + print("parsing %s" % filename) if 'GTO' in filename or 'GBO' in filename: - ctx.set_color((1, 1, 1)) - ctx.set_alpha(0.8) + ctx.color = (1, 1, 1) + ctx.alpha = 0.8 elif 'GTS' in filename or 'GBS' in filename: - ctx.set_color((0.2, 0.2, 0.75)) - ctx.set_alpha(0.8) + ctx.color = (0.2, 0.2, 0.75) + ctx.alpha = 0.8 gerberfile = read(filename) gerberfile.render(ctx) diff --git a/gerber/am_eval.py b/gerber/am_eval.py new file mode 100644 index 0000000..3a7e1ed --- /dev/null +++ b/gerber/am_eval.py @@ -0,0 +1,109 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be> +# copyright 2014 Paulo Henrique Silva <ph.silva@gmail.com> +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" This module provides RS-274-X AM macro evaluation. +""" + + +class OpCode: + PUSH = 1 + LOAD = 2 + STORE = 3 + ADD = 4 + SUB = 5 + MUL = 6 + DIV = 7 + PRIM = 8 + + @staticmethod + def str(opcode): + if opcode == OpCode.PUSH: + return "OPCODE_PUSH" + elif opcode == OpCode.LOAD: + return "OPCODE_LOAD" + elif opcode == OpCode.STORE: + return "OPCODE_STORE" + elif opcode == OpCode.ADD: + return "OPCODE_ADD" + elif opcode == OpCode.SUB: + return "OPCODE_SUB" + elif opcode == OpCode.MUL: + return "OPCODE_MUL" + elif opcode == OpCode.DIV: + return "OPCODE_DIV" + elif opcode == OpCode.PRIM: + return "OPCODE_PRIM" + else: + return "UNKNOWN" + + +def eval_macro(instructions, parameters={}): + + if not isinstance(parameters, type({})): + p = {} + for i, val in enumerate(parameters): + p[i + 1] = val + + parameters = p + + stack = [] + + def pop(): + return stack.pop() + + def push(op): + stack.append(op) + + def top(): + return stack[-1] + + def empty(): + return len(stack) == 0 + + for opcode, argument in instructions: + if opcode == OpCode.PUSH: + push(argument) + + elif opcode == OpCode.LOAD: + push(parameters.get(argument, 0)) + + elif opcode == OpCode.STORE: + parameters[argument] = pop() + + elif opcode == OpCode.ADD: + op1 = pop() + op2 = pop() + push(op2 + op1) + + elif opcode == OpCode.SUB: + op1 = pop() + op2 = pop() + push(op2 - op2) + + elif opcode == OpCode.MUL: + op1 = pop() + op2 = pop() + push(op2 * op1) + + elif opcode == OpCode.DIV: + op1 = pop() + op2 = pop() + push(op2 / op1) + + elif opcode == OpCode.PRIM: + yield "%d,%s" % (argument, ",".join([str(x) for x in stack])) + stack = [] diff --git a/gerber/am_read.py b/gerber/am_read.py new file mode 100644 index 0000000..4aff00b --- /dev/null +++ b/gerber/am_read.py @@ -0,0 +1,255 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be> +# copyright 2014 Paulo Henrique Silva <ph.silva@gmail.com> +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" This module provides RS-274-X AM macro modifiers parsing. +""" + +from .am_eval import OpCode, eval_macro + +import string + + +class Token: + ADD = "+" + SUB = "-" + # compatibility as many gerber writes do use non compliant X + MULT = ("x", "X") + DIV = "/" + OPERATORS = (ADD, SUB, MULT[0], MULT[1], DIV) + LEFT_PARENS = "(" + RIGHT_PARENS = ")" + EQUALS = "=" + EOF = "EOF" + + +def token_to_opcode(token): + if token == Token.ADD: + return OpCode.ADD + elif token == Token.SUB: + return OpCode.SUB + elif token in Token.MULT: + return OpCode.MUL + elif token == Token.DIV: + return OpCode.DIV + else: + return None + + +def precedence(token): + if token == Token.ADD or token == Token.SUB: + return 1 + elif token in Token.MULT or token == Token.DIV: + return 2 + else: + return 0 + + +def is_op(token): + return token in Token.OPERATORS + + +class Scanner: + + def __init__(self, s): + self.buff = s + self.n = 0 + + def eof(self): + return self.n == len(self.buff) + + def peek(self): + if not self.eof(): + return self.buff[self.n] + + return Token.EOF + + def ungetc(self): + if self.n > 0: + self.n -= 1 + + def getc(self): + if self.eof(): + return "" + + c = self.buff[self.n] + self.n += 1 + return c + + def readint(self): + n = "" + while not self.eof() and (self.peek() in string.digits): + n += self.getc() + return int(n) + + def readfloat(self): + n = "" + while not self.eof() and (self.peek() in string.digits or self.peek() == "."): + n += self.getc() + # weird case where zero is ommited inthe last modifider, like in ',0.' + if n == ".": + return 0 + return float(n) + + def readstr(self, end="*"): + s = "" + while not self.eof() and self.peek() != end: + s += self.getc() + return s.strip() + + +def print_instructions(instructions): + for opcode, argument in instructions: + print("%s %s" % (OpCode.str(opcode), + str(argument) if argument is not None else "")) + + +def read_macro(macro): + instructions = [] + + for block in macro.split("*"): + + is_primitive = False + is_equation = False + + found_equation_left_side = False + found_primitive_code = False + + equation_left_side = 0 + primitive_code = 0 + + unary_minus_allowed = False + unary_minus = False + + if Token.EQUALS in block: + is_equation = True + else: + is_primitive = True + + scanner = Scanner(block) + + # inlined here for compactness and convenience + op_stack = [] + + def pop(): + return op_stack.pop() + + def push(op): + op_stack.append(op) + + def top(): + return op_stack[-1] + + def empty(): + return len(op_stack) == 0 + + while not scanner.eof(): + + c = scanner.getc() + + if c == ",": + found_primitive_code = True + + # add all instructions on the stack to finish last modifier + while not empty(): + instructions.append((token_to_opcode(pop()), None)) + + unary_minus_allowed = True + + elif c in Token.OPERATORS: + if c == Token.SUB and unary_minus_allowed: + unary_minus = True + unary_minus_allowed = False + continue + + while not empty() and is_op(top()) and precedence(top()) >= precedence(c): + instructions.append((token_to_opcode(pop()), None)) + + push(c) + + elif c == Token.LEFT_PARENS: + push(c) + + elif c == Token.RIGHT_PARENS: + while not empty() and top() != Token.LEFT_PARENS: + instructions.append((token_to_opcode(pop()), None)) + + if empty(): + raise ValueError("unbalanced parentheses") + + # discard "(" + pop() + + elif c.startswith("$"): + n = scanner.readint() + + if is_equation and not found_equation_left_side: + equation_left_side = n + else: + instructions.append((OpCode.LOAD, n)) + + elif c == Token.EQUALS: + found_equation_left_side = True + + elif c == "0": + if is_primitive and not found_primitive_code: + instructions.append((OpCode.PUSH, scanner.readstr("*"))) + found_primitive_code = True + else: + # decimal or integer disambiguation + if scanner.peek() not in '.' or scanner.peek() == Token.EOF: + instructions.append((OpCode.PUSH, 0)) + + elif c in "123456789.": + scanner.ungetc() + + if is_primitive and not found_primitive_code: + primitive_code = scanner.readint() + else: + n = scanner.readfloat() + if unary_minus: + unary_minus = False + n *= -1 + + instructions.append((OpCode.PUSH, n)) + else: + # whitespace or unknown char + pass + + # add all instructions on the stack to finish last modifier (if any) + while not empty(): + instructions.append((token_to_opcode(pop()), None)) + + # at end, we either have a primitive or a equation + if is_primitive and found_primitive_code: + instructions.append((OpCode.PRIM, primitive_code)) + + if is_equation: + instructions.append((OpCode.STORE, equation_left_side)) + + return instructions + +if __name__ == '__main__': + import sys + + instructions = read_macro(sys.argv[1]) + + print("insructions:") + print_instructions(instructions) + + print("eval:") + for primitive in eval_macro(instructions): + print(primitive) diff --git a/gerber/am_statements.py b/gerber/am_statements.py new file mode 100644 index 0000000..726df2f --- /dev/null +++ b/gerber/am_statements.py @@ -0,0 +1,1060 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# copyright 2015 Hamilton Kibbe <ham@hamiltonkib.be> and Paulo Henrique Silva +# <ph.silva@gmail.com> + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +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', + 'AMMoirePrimitive', 'AMThermalPrimitive', 'AMCenterLinePrimitive', + 'AMLowerLeftLinePrimitive', 'AMUnsupportPrimitive'] + + +class AMPrimitive(object): + """ Aperture Macro Primitive Base Class + + Parameters + ---------- + code : int + primitive shape code + + exposure : str + on or off Primitives with exposure on create a slid part of + the macro aperture, and primitives with exposure off erase the + solid part created previously in the aperture macro definition. + .. note:: + The erasing effect is limited to the aperture definition in + which it occurs. + + Returns + ------- + primitive : :class: `gerber.am_statements.AMPrimitive` + + Raises + ------ + TypeError, ValueError + """ + + def __init__(self, code, exposure=None): + VALID_CODES = (0, 1, 2, 4, 5, 6, 7, 20, 21, 22, 9999) + if not isinstance(code, int): + raise TypeError('Aperture Macro Primitive code must be an integer') + elif code not in VALID_CODES: + raise ValueError('Invalid Code. Valid codes are %s.' % + ', '.join(map(str, VALID_CODES))) + if exposure is not None and exposure.lower() not in ('on', 'off'): + raise ValueError('Exposure must be either on or off') + self.code = code + self.exposure = exposure.lower() if exposure is not None else None + + def to_inch(self): + raise NotImplementedError('Subclass must implement `to-inch`') + + def to_metric(self): + raise NotImplementedError('Subclass must implement `to-metric`') + + @property + def _level_polarity(self): + if self.exposure == 'off': + return 'clear' + return 'dark' + + def to_primitive(self, units): + """ Return a Primitive instance based on the specified macro params. + """ + print('Rendering {}s is not supported yet.'.format(str(self.__class__))) + + def __eq__(self, other): + return self.__dict__ == other.__dict__ + + +class AMCommentPrimitive(AMPrimitive): + """ Aperture Macro Comment primitive. Code 0 + + The comment primitive has no image meaning. It is used to include human- + readable comments into the AM command. + + .. seealso:: + `The Gerber File Format Specification <http://www.ucamco.com/files/downloads/file/81/the_gerber_file_format_specification.pdf>`_ + **Section 4.12.3.1:** Comment, primitive code 0 + + Parameters + ---------- + code : int + Aperture Macro primitive code. 0 Indicates an AMCommentPrimitive + + comment : str + The comment as a string. + + Returns + ------- + CommentPrimitive : :class:`gerber.am_statements.AMCommentPrimitive` + An Initialized AMCommentPrimitive + + Raises + ------ + ValueError + """ + @classmethod + def from_gerber(cls, primitive): + primitive = primitive.strip() + code = int(primitive[0]) + comment = primitive[1:] + return cls(code, comment) + + def __init__(self, code, comment): + if code != 0: + raise ValueError('Not a valid Aperture Macro Comment statement') + super(AMCommentPrimitive, self).__init__(code) + self.comment = comment.strip(' *') + + def to_inch(self): + pass + + def to_metric(self): + pass + + def to_gerber(self, settings=None): + return '0 %s *' % self.comment + + def to_primitive(self, units): + """ + Returns None - has not primitive representation + """ + return None + + def __str__(self): + return '<Aperture Macro Comment: %s>' % self.comment + + +class AMCirclePrimitive(AMPrimitive): + """ Aperture macro Circle primitive. Code 1 + + A circle primitive is defined by its center point and diameter. + + .. seealso:: + `The Gerber File Format Specification <http://www.ucamco.com/files/downloads/file/81/the_gerber_file_format_specification.pdf>`_ + **Section 4.12.3.2:** Circle, primitive code 1 + + Parameters + ---------- + code : int + Circle Primitive code. Must be 1 + + exposure : string + 'on' or 'off' + + diameter : float + Circle diameter + + position : tuple (<float>, <float>) + Position of the circle relative to the macro origin + + Returns + ------- + CirclePrimitive : :class:`gerber.am_statements.AMCirclePrimitive` + An initialized AMCirclePrimitive + + Raises + ------ + ValueError, TypeError + """ + @classmethod + def from_gerber(cls, primitive): + modifiers = primitive.strip(' *').split(',') + code = int(modifiers[0]) + exposure = 'on' if float(modifiers[1]) == 1 else 'off' + diameter = float(modifiers[2]) + position = (float(modifiers[3]), float(modifiers[4])) + return cls(code, exposure, diameter, position) + + @classmethod + def from_primitive(cls, primitive): + return cls(1, 'on', primitive.diameter, primitive.position) + + def __init__(self, code, exposure, diameter, position): + validate_coordinates(position) + if code != 1: + raise ValueError('CirclePrimitive code is 1') + super(AMCirclePrimitive, self).__init__(code, exposure) + self.diameter = diameter + self.position = position + + def to_inch(self): + self.diameter = inch(self.diameter) + self.position = tuple([inch(x) for x in self.position]) + + def to_metric(self): + self.diameter = metric(self.diameter) + self.position = tuple([metric(x) for x in self.position]) + + def to_gerber(self, settings=None): + data = dict(code=self.code, + exposure='1' if self.exposure == 'on' else 0, + diameter=self.diameter, + x=self.position[0], + y=self.position[1]) + return '{code},{exposure},{diameter},{x},{y}*'.format(**data) + + def to_primitive(self, units): + return Circle((self.position), self.diameter, units=units, level_polarity=self._level_polarity) + + +class AMVectorLinePrimitive(AMPrimitive): + """ Aperture Macro Vector Line primitive. Code 2 or 20. + + A vector line is a rectangle defined by its line width, start, and end + points. The line ends are rectangular. + + .. seealso:: + `The Gerber File Format Specification <http://www.ucamco.com/files/downloads/file/81/the_gerber_file_format_specification.pdf>`_ + **Section 4.12.3.3:** Vector Line, primitive code 2 or 20. + + Parameters + ---------- + code : int + Vector Line Primitive code. Must be either 2 or 20. + + exposure : string + 'on' or 'off' + + width : float + Line width + + start : tuple (<float>, <float>) + coordinate of line start point + + end : tuple (<float>, <float>) + coordinate of line end point + + rotation : float + Line rotation about the origin. + + Returns + ------- + LinePrimitive : :class:`gerber.am_statements.AMVectorLinePrimitive` + An initialized AMVectorLinePrimitive + + Raises + ------ + ValueError, TypeError + """ + + @classmethod + def from_primitive(cls, primitive): + return cls(2, 'on', primitive.aperture.width, primitive.start, primitive.end, 0) + + @classmethod + def from_gerber(cls, primitive): + modifiers = primitive.strip(' *').split(',') + code = int(modifiers[0]) + exposure = 'on' if float(modifiers[1]) == 1 else 'off' + width = float(modifiers[2]) + start = (float(modifiers[3]), float(modifiers[4])) + end = (float(modifiers[5]), float(modifiers[6])) + rotation = float(modifiers[7]) + return cls(code, exposure, width, start, end, rotation) + + def __init__(self, code, exposure, width, start, end, rotation): + validate_coordinates(start) + validate_coordinates(end) + if code not in (2, 20): + raise ValueError('VectorLinePrimitive codes are 2 or 20') + super(AMVectorLinePrimitive, self).__init__(code, exposure) + self.width = width + self.start = start + self.end = end + self.rotation = rotation + + def to_inch(self): + self.width = inch(self.width) + self.start = tuple([inch(x) for x in self.start]) + self.end = tuple([inch(x) for x in self.end]) + + def to_metric(self): + self.width = metric(self.width) + self.start = tuple([metric(x) for x in self.start]) + self.end = tuple([metric(x) for x in self.end]) + + def to_gerber(self, settings=None): + fmtstr = '{code},{exp},{width},{startx},{starty},{endx},{endy},{rotation}*' + data = dict(code=self.code, + exp=1 if self.exposure == 'on' else 0, + width=self.width, + startx=self.start[0], + starty=self.start[1], + endx=self.end[0], + endy=self.end[1], + rotation=self.rotation) + return fmtstr.format(**data) + + def to_primitive(self, units): + """ + Convert this to a primitive. We use the Outline to represent this (instead of Line) + because the behaviour of the end caps is different for aperture macros compared to Lines + when rotated. + """ + + # Use a line to generate our vertices easily + line = Line(self.start, self.end, Rectangle(None, self.width, self.width)) + vertices = line.vertices + + aperture = Circle((0, 0), 0) + + lines = [] + prev_point = rotate_point(vertices[-1], self.rotation, (0, 0)) + for point in vertices: + cur_point = rotate_point(point, self.rotation, (0, 0)) + + lines.append(Line(prev_point, cur_point, aperture)) + + return Outline(lines, units=units, level_polarity=self._level_polarity) + + +class AMOutlinePrimitive(AMPrimitive): + """ Aperture Macro Outline primitive. Code 4. + + An outline primitive is an area enclosed by an n-point polygon defined by + its start point and n subsequent points. The outline must be closed, i.e. + the last point must be equal to the start point. Self intersecting + outlines are not allowed. + + .. seealso:: + `The Gerber File Format Specification <http://www.ucamco.com/files/downloads/file/81/the_gerber_file_format_specification.pdf>`_ + **Section 4.12.3.6:** Outline, primitive code 4. + + Parameters + ---------- + code : int + OutlinePrimitive code. Must be 6. + + exposure : string + 'on' or 'off' + + start_point : tuple (<float>, <float>) + coordinate of outline start point + + points : list of tuples (<float>, <float>) + coordinates of subsequent points + + rotation : float + outline rotation about the origin. + + Returns + ------- + OutlinePrimitive : :class:`gerber.am_statements.AMOutlineinePrimitive` + An initialized AMOutlinePrimitive + + Raises + ------ + ValueError, TypeError + """ + + @classmethod + def from_primitive(cls, primitive): + + start_point = (round(primitive.primitives[0].start[0], 6), round(primitive.primitives[0].start[1], 6)) + points = [] + for prim in primitive.primitives: + points.append((round(prim.end[0], 6), round(prim.end[1], 6))) + + rotation = 0.0 + + return cls(4, 'on', start_point, points, rotation) + + @classmethod + def from_gerber(cls, primitive): + modifiers = primitive.strip(' *').split(",") + + code = int(modifiers[0]) + exposure = "on" if float(modifiers[1]) == 1 else "off" + n = int(float(modifiers[2])) + start_point = (float(modifiers[3]), float(modifiers[4])) + points = [] + for i in range(n): + points.append((float(modifiers[5 + i * 2]), + float(modifiers[5 + i * 2 + 1]))) + rotation = float(modifiers[-1]) + return cls(code, exposure, start_point, points, rotation) + + def __init__(self, code, exposure, start_point, points, rotation): + """ Initialize AMOutlinePrimitive + """ + validate_coordinates(start_point) + for point in points: + validate_coordinates(point) + if code != 4: + raise ValueError('OutlinePrimitive code is 4') + super(AMOutlinePrimitive, self).__init__(code, exposure) + self.start_point = start_point + if points[-1] != start_point: + raise ValueError('OutlinePrimitive must be closed') + self.points = points + self.rotation = rotation + + def to_inch(self): + self.start_point = tuple([inch(x) for x in self.start_point]) + self.points = tuple([(inch(x), inch(y)) for x, y in self.points]) + + def to_metric(self): + self.start_point = tuple([metric(x) for x in self.start_point]) + self.points = tuple([(metric(x), metric(y)) for x, y in self.points]) + + def to_gerber(self, settings=None): + data = dict( + code=self.code, + exposure="1" if self.exposure == "on" else "0", + n_points=len(self.points), + start_point="%.6g,%.6g" % self.start_point, + points=",\n".join(["%.6g,%.6g" % point for point in self.points]), + rotation=str(self.rotation) + ) + # TODO I removed a closing asterix - not sure if this works for items with multiple statements + return "{code},{exposure},{n_points},{start_point},{points},\n{rotation}*".format(**data) + + def to_primitive(self, units): + """ + Convert this to a drawable primitive. This uses the Outline instead of Line + primitive to handle differences in end caps when rotated. + """ + + lines = [] + prev_point = rotate_point(self.start_point, self.rotation) + for point in self.points: + cur_point = rotate_point(point, self.rotation) + + lines.append(Line(prev_point, cur_point, Circle((0,0), 0))) + + prev_point = cur_point + + if lines[0].start != lines[-1].end: + raise ValueError('Outline must be closed') + + return Outline(lines, units=units, level_polarity=self._level_polarity) + + +class AMPolygonPrimitive(AMPrimitive): + """ Aperture Macro Polygon primitive. Code 5. + + A polygon primitive is a regular polygon defined by the number of + vertices, the center point, and the diameter of the circumscribed circle. + + .. seealso:: + `The Gerber File Format Specification <http://www.ucamco.com/files/downloads/file/81/the_gerber_file_format_specification.pdf>`_ + **Section 4.12.3.8:** Polygon, primitive code 5. + + Parameters + ---------- + code : int + PolygonPrimitive code. Must be 5. + + exposure : string + 'on' or 'off' + + vertices : int, 3 <= vertices <= 12 + Number of vertices + + position : tuple (<float>, <float>) + X and Y coordinates of polygon center + + diameter : float + diameter of circumscribed circle. + + rotation : float + polygon rotation about the origin. + + Returns + ------- + PolygonPrimitive : :class:`gerber.am_statements.AMPolygonPrimitive` + An initialized AMPolygonPrimitive + + Raises + ------ + ValueError, TypeError + """ + + @classmethod + def from_primitive(cls, primitive): + return cls(5, 'on', primitive.sides, primitive.position, primitive.diameter, primitive.rotation) + + @classmethod + def from_gerber(cls, primitive): + modifiers = primitive.strip(' *').split(",") + code = int(modifiers[0]) + exposure = "on" if float(modifiers[1]) == 1 else "off" + vertices = int(float(modifiers[2])) + position = (float(modifiers[3]), float(modifiers[4])) + try: + diameter = float(modifiers[5]) + except: + diameter = 0 + + rotation = float(modifiers[6]) + return cls(code, exposure, vertices, position, diameter, rotation) + + def __init__(self, code, exposure, vertices, position, diameter, rotation): + """ Initialize AMPolygonPrimitive + """ + if code != 5: + raise ValueError('PolygonPrimitive code is 5') + super(AMPolygonPrimitive, self).__init__(code, exposure) + if vertices < 3 or vertices > 12: + raise ValueError('Number of vertices must be between 3 and 12') + self.vertices = vertices + validate_coordinates(position) + self.position = position + self.diameter = diameter + self.rotation = rotation + + def to_inch(self): + self.position = tuple([inch(x) for x in self.position]) + self.diameter = inch(self.diameter) + + def to_metric(self): + self.position = tuple([metric(x) for x in self.position]) + self.diameter = metric(self.diameter) + + def to_gerber(self, settings=None): + data = dict( + code=self.code, + exposure="1" if self.exposure == "on" else "0", + vertices=self.vertices, + position="%.4g,%.4g" % self.position, + diameter='%.4g' % self.diameter, + rotation=str(self.rotation) + ) + fmt = "{code},{exposure},{vertices},{position},{diameter},{rotation}*" + return fmt.format(**data) + + def to_primitive(self, units): + return Polygon(self.position, self.vertices, self.diameter / 2.0, 0, rotation=math.radians(self.rotation), units=units, level_polarity=self._level_polarity) + + +class AMMoirePrimitive(AMPrimitive): + """ Aperture Macro Moire primitive. Code 6. + + The moire primitive is a cross hair centered on concentric rings (annuli). + Exposure is always on. + + .. seealso:: + `The Gerber File Format Specification <http://www.ucamco.com/files/downloads/file/81/the_gerber_file_format_specification.pdf>`_ + **Section 4.12.3.9:** Moire, primitive code 6. + + Parameters + ---------- + code : int + Moire Primitive code. Must be 6. + + position : tuple (<float>, <float>) + X and Y coordinates of moire center + + diameter : float + outer diameter of outer ring. + + ring_thickness : float + thickness of concentric rings. + + gap : float + gap between concentric rings. + + max_rings : float + maximum number of rings + + crosshair_thickness : float + thickness of crosshairs + + crosshair_length : float + length of crosshairs + + rotation : float + moire rotation about the origin. + + Returns + ------- + MoirePrimitive : :class:`gerber.am_statements.AMMoirePrimitive` + An initialized AMMoirePrimitive + + Raises + ------ + ValueError, TypeError + """ + @classmethod + def from_gerber(cls, primitive): + modifiers = primitive.strip(' *').split(",") + code = int(modifiers[0]) + position = (float(modifiers[1]), float(modifiers[2])) + diameter = float(modifiers[3]) + ring_thickness = float(modifiers[4]) + gap = float(modifiers[5]) + max_rings = int(float(modifiers[6])) + crosshair_thickness = float(modifiers[7]) + crosshair_length = float(modifiers[8]) + rotation = float(modifiers[9]) + return cls(code, position, diameter, ring_thickness, gap, max_rings, crosshair_thickness, crosshair_length, rotation) + + def __init__(self, code, position, diameter, ring_thickness, gap, max_rings, crosshair_thickness, crosshair_length, rotation): + """ Initialize AMoirePrimitive + """ + if code != 6: + raise ValueError('MoirePrimitive code is 6') + super(AMMoirePrimitive, self).__init__(code, 'on') + validate_coordinates(position) + self.position = position + self.diameter = diameter + self.ring_thickness = ring_thickness + self.gap = gap + self.max_rings = max_rings + self.crosshair_thickness = crosshair_thickness + self.crosshair_length = crosshair_length + self.rotation = rotation + + def to_inch(self): + self.position = tuple([inch(x) for x in self.position]) + self.diameter = inch(self.diameter) + self.ring_thickness = inch(self.ring_thickness) + self.gap = inch(self.gap) + self.crosshair_thickness = inch(self.crosshair_thickness) + self.crosshair_length = inch(self.crosshair_length) + + def to_metric(self): + self.position = tuple([metric(x) for x in self.position]) + self.diameter = metric(self.diameter) + self.ring_thickness = metric(self.ring_thickness) + self.gap = metric(self.gap) + self.crosshair_thickness = metric(self.crosshair_thickness) + self.crosshair_length = metric(self.crosshair_length) + + def to_gerber(self, settings=None): + data = dict( + code=self.code, + position="%.4g,%.4g" % self.position, + diameter=self.diameter, + ring_thickness=self.ring_thickness, + gap=self.gap, + max_rings=self.max_rings, + crosshair_thickness=self.crosshair_thickness, + crosshair_length=self.crosshair_length, + rotation=self.rotation + ) + fmt = "{code},{position},{diameter},{ring_thickness},{gap},{max_rings},{crosshair_thickness},{crosshair_length},{rotation}*" + return fmt.format(**data) + + def to_primitive(self, units): + raise NotImplementedError() + + +class AMThermalPrimitive(AMPrimitive): + """ Aperture Macro Thermal primitive. Code 7. + + The thermal primitive is a ring (annulus) interrupted by four gaps. + Exposure is always on. + + .. seealso:: + `The Gerber File Format Specification <http://www.ucamco.com/files/downloads/file/81/the_gerber_file_format_specification.pdf>`_ + **Section 4.12.3.10:** Thermal, primitive code 7. + + Parameters + ---------- + code : int + Thermal Primitive code. Must be 7. + + position : tuple (<float>, <float>) + X and Y coordinates of thermal center + + outer_diameter : float + outer diameter of thermal. + + inner_diameter : float + inner diameter of thermal. + + gap : float + gap thickness + + rotation : float + thermal rotation about the origin. + + Returns + ------- + ThermalPrimitive : :class:`gerber.am_statements.AMThermalPrimitive` + An initialized AMThermalPrimitive + + Raises + ------ + ValueError, TypeError + """ + @classmethod + def from_gerber(cls, primitive): + modifiers = primitive.strip(' *').split(",") + code = int(modifiers[0]) + position = (float(modifiers[1]), float(modifiers[2])) + outer_diameter = float(modifiers[3]) + inner_diameter = float(modifiers[4]) + gap = float(modifiers[5]) + rotation = float(modifiers[6]) + return cls(code, position, outer_diameter, inner_diameter, gap, rotation) + + def __init__(self, code, position, outer_diameter, inner_diameter, gap, rotation): + if code != 7: + raise ValueError('ThermalPrimitive code is 7') + super(AMThermalPrimitive, self).__init__(code, 'on') + validate_coordinates(position) + self.position = position + self.outer_diameter = outer_diameter + self.inner_diameter = inner_diameter + self.gap = gap + self.rotation = rotation + + def to_inch(self): + self.position = tuple([inch(x) for x in self.position]) + self.outer_diameter = inch(self.outer_diameter) + self.inner_diameter = inch(self.inner_diameter) + self.gap = inch(self.gap) + + def to_metric(self): + self.position = tuple([metric(x) for x in self.position]) + self.outer_diameter = metric(self.outer_diameter) + self.inner_diameter = metric(self.inner_diameter) + self.gap = metric(self.gap) + + def to_gerber(self, settings=None): + data = dict( + code=self.code, + position="%.4g,%.4g" % self.position, + outer_diameter = self.outer_diameter, + inner_diameter = self.inner_diameter, + gap = self.gap, + rotation = self.rotation + ) + fmt = "{code},{position},{outer_diameter},{inner_diameter},{gap},{rotation}*" + return fmt.format(**data) + + def _approximate_arc_cw(self, start_angle, end_angle, radius, center): + """ + Get an arc as a series of points + + Parameters + ---------- + start_angle : The start angle in radians + end_angle : The end angle in radians + radius`: Radius of the arc + center : The center point of the arc (x, y) tuple + + Returns + ------- + array of point tuples + """ + + # The total sweep + sweep_angle = end_angle - start_angle + num_steps = 10 + + angle_step = sweep_angle / num_steps + + radius = radius + center = center + + points = [] + + for i in range(num_steps + 1): + current_angle = start_angle + (angle_step * i) + + nextx = (center[0] + math.cos(current_angle) * radius) + nexty = (center[1] + math.sin(current_angle) * radius) + + points.append((nextx, nexty)) + + return points + + def to_primitive(self, units): + + # We start with calculating the top right section, then duplicate it + + inner_radius = self.inner_diameter / 2.0 + outer_radius = self.outer_diameter / 2.0 + + # Calculate the start angle relative to the horizontal axis + inner_offset_angle = asin(self.gap / 2.0 / inner_radius) + outer_offset_angle = asin(self.gap / 2.0 / outer_radius) + + rotation_rad = math.radians(self.rotation) + inner_start_angle = inner_offset_angle + rotation_rad + inner_end_angle = math.pi / 2 - inner_offset_angle + rotation_rad + + outer_start_angle = outer_offset_angle + rotation_rad + outer_end_angle = math.pi / 2 - outer_offset_angle + rotation_rad + + outlines = [] + aperture = Circle((0, 0), 0) + + points = (self._approximate_arc_cw(inner_start_angle, inner_end_angle, inner_radius, self.position) + + list(reversed(self._approximate_arc_cw(outer_start_angle, outer_end_angle, outer_radius, self.position)))) + # Add in the last point since outlines should be closed + points.append(points[0]) + + # There are four outlines at rotated sections + for rotation in [0, 90.0, 180.0, 270.0]: + + lines = [] + prev_point = rotate_point(points[0], rotation, self.position) + for point in points[1:]: + cur_point = rotate_point(point, rotation, self.position) + + lines.append(Line(prev_point, cur_point, aperture)) + + prev_point = cur_point + + outlines.append(Outline(lines, units=units, level_polarity=self._level_polarity)) + + return outlines + + +class AMCenterLinePrimitive(AMPrimitive): + """ Aperture Macro Center Line primitive. Code 21. + + The center line primitive is a rectangle defined by its width, height, and center point. + + .. seealso:: + `The Gerber File Format Specification <http://www.ucamco.com/files/downloads/file/81/the_gerber_file_format_specification.pdf>`_ + **Section 4.12.3.4:** Center Line, primitive code 21. + + Parameters + ---------- + code : int + Center Line Primitive code. Must be 21. + + exposure : str + 'on' or 'off' + + width : float + Width of rectangle + + height : float + Height of rectangle + + center : tuple (<float>, <float>) + X and Y coordinates of line center + + rotation : float + rectangle rotation about its center. + + Returns + ------- + CenterLinePrimitive : :class:`gerber.am_statements.AMCenterLinePrimitive` + An initialized AMCenterLinePrimitive + + Raises + ------ + ValueError, TypeError + """ + + @classmethod + def from_primitive(cls, primitive): + width = primitive.width + height = primitive.height + center = primitive.position + rotation = math.degrees(primitive.rotation) + return cls(21, 'on', width, height, center, rotation) + + @classmethod + def from_gerber(cls, primitive): + modifiers = primitive.strip(' *').split(",") + code = int(modifiers[0]) + exposure = 'on' if float(modifiers[1]) == 1 else 'off' + width = float(modifiers[2]) + height = float(modifiers[3]) + center = (float(modifiers[4]), float(modifiers[5])) + rotation = float(modifiers[6]) + return cls(code, exposure, width, height, center, rotation) + + def __init__(self, code, exposure, width, height, center, rotation): + if code != 21: + raise ValueError('CenterLinePrimitive code is 21') + super(AMCenterLinePrimitive, self).__init__(code, exposure) + self.width = width + self.height = height + validate_coordinates(center) + self.center = center + self.rotation = rotation + + def to_inch(self): + self.center = tuple([inch(x) for x in self.center]) + self.width = inch(self.width) + self.height = inch(self.height) + + def to_metric(self): + self.center = tuple([metric(x) for x in self.center]) + self.width = metric(self.width) + self.height = metric(self.height) + + def to_gerber(self, settings=None): + data = dict( + code=self.code, + exposure='1' if self.exposure == 'on' else '0', + width=self.width, + height=self.height, + center="%.4g,%.4g" % self.center, + rotation=self.rotation + ) + fmt = "{code},{exposure},{width},{height},{center},{rotation}*" + return fmt.format(**data) + + def to_primitive(self, units): + + x = self.center[0] + y = self.center[1] + half_width = self.width / 2.0 + half_height = self.height / 2.0 + + points = [] + points.append((x - half_width, y + half_height)) + points.append((x - half_width, y - half_height)) + points.append((x + half_width, y - half_height)) + points.append((x + half_width, y + half_height)) + + aperture = Circle((0, 0), 0) + + lines = [] + prev_point = rotate_point(points[3], self.rotation, self.center) + for point in points: + cur_point = rotate_point(point, self.rotation, self.center) + + lines.append(Line(prev_point, cur_point, aperture)) + + return Outline(lines, units=units, level_polarity=self._level_polarity) + + +class AMLowerLeftLinePrimitive(AMPrimitive): + """ Aperture Macro Lower Left Line primitive. Code 22. + + The lower left line primitive is a rectangle defined by its width, height, and the lower left point. + + .. seealso:: + `The Gerber File Format Specification <http://www.ucamco.com/files/downloads/file/81/the_gerber_file_format_specification.pdf>`_ + **Section 4.12.3.5:** Lower Left Line, primitive code 22. + + Parameters + ---------- + code : int + Center Line Primitive code. Must be 22. + + exposure : str + 'on' or 'off' + + width : float + Width of rectangle + + height : float + Height of rectangle + + lower_left : tuple (<float>, <float>) + X and Y coordinates of lower left corner + + rotation : float + rectangle rotation about its origin. + + Returns + ------- + LowerLeftLinePrimitive : :class:`gerber.am_statements.AMLowerLeftLinePrimitive` + An initialized AMLowerLeftLinePrimitive + + Raises + ------ + ValueError, TypeError + """ + @classmethod + def from_gerber(cls, primitive): + modifiers = primitive.strip(' *').split(",") + code = int(modifiers[0]) + exposure = 'on' if float(modifiers[1]) == 1 else 'off' + width = float(modifiers[2]) + height = float(modifiers[3]) + lower_left = (float(modifiers[4]), float(modifiers[5])) + rotation = float(modifiers[6]) + return cls(code, exposure, width, height, lower_left, rotation) + + def __init__(self, code, exposure, width, height, lower_left, rotation): + if code != 22: + raise ValueError('LowerLeftLinePrimitive code is 22') + super(AMLowerLeftLinePrimitive, self).__init__(code, exposure) + self.width = width + self.height = height + validate_coordinates(lower_left) + self.lower_left = lower_left + self.rotation = rotation + + def to_inch(self): + self.lower_left = tuple([inch(x) for x in self.lower_left]) + self.width = inch(self.width) + self.height = inch(self.height) + + def to_metric(self): + self.lower_left = tuple([metric(x) for x in self.lower_left]) + self.width = metric(self.width) + self.height = metric(self.height) + + def to_primitive(self, units): + # TODO I think I have merged this wrong + # Offset the primitive from macro position + position = tuple([pos + offset for pos, offset in + zip(self.lower_left, (self.width/2, self.height/2))]) + # Return a renderable primitive + return Rectangle(position, self.width, self.height, + level_polarity=self._level_polarity, units=units) + + def to_gerber(self, settings=None): + data = dict( + code=self.code, + exposure='1' if self.exposure == 'on' else '0', + width=self.width, + height=self.height, + lower_left="%.4g,%.4g" % self.lower_left, + rotation=self.rotation + ) + fmt = "{code},{exposure},{width},{height},{lower_left},{rotation}*" + return fmt.format(**data) + + +class AMUnsupportPrimitive(AMPrimitive): + + @classmethod + def from_gerber(cls, primitive): + return cls(primitive) + + def __init__(self, primitive): + super(AMUnsupportPrimitive, self).__init__(9999) + self.primitive = primitive + + def to_inch(self): + pass + + def to_metric(self): + pass + + def to_gerber(self, settings=None): + return self.primitive + + def to_primitive(self, units): + return None
\ No newline at end of file diff --git a/gerber/cam.py b/gerber/cam.py index 051c3b5..c5b8938 100644 --- a/gerber/cam.py +++ b/gerber/cam.py @@ -27,9 +27,36 @@ class FileSettings(object): """ CAM File Settings Provides a common representation of gerber/excellon file settings + + Parameters + ---------- + notation: string + notation format. either 'absolute' or 'incremental' + + units : string + Measurement units. 'inch' or 'metric' + + zero_suppression: string + 'leading' to suppress leading zeros, 'trailing' to suppress trailing zeros. + This is the convention used in Gerber files. + + format : tuple (int, int) + Decimal format + + zeros : string + 'leading' to include leading zeros, 'trailing to include trailing zeros. + This is the convention used in Excellon files + + Notes + ----- + Either `zeros` or `zero_suppression` should be specified, there is no need to + specify both. `zero_suppression` will take on the opposite value of `zeros` + and vice versa """ + def __init__(self, notation='absolute', units='inch', - zero_suppression='trailing', format=(2, 5)): + zero_suppression=None, format=(2, 5), zeros=None, + angle_units='degrees'): if notation not in ['absolute', 'incremental']: raise ValueError('Notation must be either absolute or incremental') self.notation = notation @@ -38,15 +65,52 @@ class FileSettings(object): raise ValueError('Units must be either inch or metric') self.units = units - if zero_suppression not in ['leading', 'trailing']: - raise ValueError('Zero suppression must be either leading or \ - trailling') - self.zero_suppression = zero_suppression + if zero_suppression is None and zeros is None: + self.zero_suppression = 'trailing' + + elif zero_suppression == zeros: + raise ValueError('Zeros and Zero Suppression must be different. \ + Best practice is to specify only one.') + + elif zero_suppression is not None: + if zero_suppression not in ['leading', 'trailing']: + # This is a common problem in Eagle files, so just suppress it + self.zero_suppression = 'leading' + else: + self.zero_suppression = zero_suppression + + elif zeros is not None: + if zeros not in ['leading', 'trailing']: + raise ValueError('Zeros must be either leading or trailling') + self.zeros = zeros if len(format) != 2: raise ValueError('Format must be a tuple(n=2) of integers') self.format = format + if angle_units not in ('degrees', 'radians'): + raise ValueError('Angle units may be degrees or radians') + self.angle_units = angle_units + + @property + def zero_suppression(self): + return self._zero_suppression + + @zero_suppression.setter + def zero_suppression(self, value): + self._zero_suppression = value + self._zeros = 'leading' if value == 'trailing' else 'trailing' + + @property + def zeros(self): + return self._zeros + + @zeros.setter + def zeros(self, value): + + self._zeros = value + self._zero_suppression = 'leading' if value == 'trailing' else 'trailing' + def __getitem__(self, key): if key == 'notation': return self.notation @@ -54,8 +118,12 @@ class FileSettings(object): return self.units elif key == 'zero_suppression': return self.zero_suppression + elif key == 'zeros': + return self.zeros elif key == 'format': return self.format + elif key == 'angle_units': + return self.angle_units else: raise KeyError() @@ -69,22 +137,41 @@ class FileSettings(object): if value not in ['inch', 'metric']: raise ValueError('Units must be either inch or metric') self.units = value + elif key == 'zero_suppression': if value not in ['leading', 'trailing']: raise ValueError('Zero suppression must be either leading or \ trailling') self.zero_suppression = value + + elif key == 'zeros': + if value not in ['leading', 'trailing']: + raise ValueError('Zeros must be either leading or trailling') + self.zeros = value + elif key == 'format': if len(value) != 2: raise ValueError('Format must be a tuple(n=2) of integers') self.format = value + elif key == 'angle_units': + if value not in ('degrees', 'radians'): + raise ValueError('Angle units may be degrees or radians') + self.angle_units = value + + else: + raise KeyError('%s is not a valid key' % key) + def __eq__(self, other): return (self.notation == other.notation and self.units == other.units and self.zero_suppression == other.zero_suppression and - self.format == other.format) - + self.format == other.format and + self.angle_units == other.angle_units) + + def __str__(self): + return ('<Settings: %s %s %s %s %s>' % + (self.units, self.notation, self.zero_suppression, self.format, self.angle_units)) class CamFile(object): @@ -131,14 +218,17 @@ class CamFile(object): self.notation = settings['notation'] self.units = settings['units'] self.zero_suppression = settings['zero_suppression'] + self.zeros = settings['zeros'] self.format = settings['format'] else: self.notation = 'absolute' self.units = 'inch' self.zero_suppression = 'trailing' + self.zeros = 'leading' self.format = (2, 5) self.statements = statements if statements is not None else [] - self.primitives = primitives + if primitives is not None: + self.primitives = primitives self.filename = filename self.layer_name = layer_name @@ -156,11 +246,17 @@ class CamFile(object): @property def bounds(self): - """ File baundaries + """ File boundaries """ pass - def render(self, ctx, filename=None): + def to_inch(self): + pass + + def to_metric(self): + pass + + def render(self, ctx, invert=False, filename=None): """ Generate image of layer. Parameters @@ -172,7 +268,12 @@ class CamFile(object): If provided, save the rendered image to `filename` """ ctx.set_bounds(self.bounds) + ctx._paint_background() + ctx.invert = invert + ctx._new_render_layer() for p in self.primitives: ctx.render(p) + ctx._flatten() + if filename is not None: ctx.dump(filename) diff --git a/gerber/common.py b/gerber/common.py index 6e8c862..cf137dd 100644 --- a/gerber/common.py +++ b/gerber/common.py @@ -15,6 +15,12 @@ # See the License for the specific language governing permissions and # limitations under the License. +from . import rs274x +from . import excellon +from . import ipc356 +from .exceptions import ParseError +from .utils import detect_file_format + def read(filename): """ Read a gerber or excellon file and return a representative object. @@ -30,13 +36,39 @@ def read(filename): CncFile object representing the file, either GerberFile or ExcellonFile. Returns None if file is not an Excellon or Gerber file. """ - import rs274x - import excellon - from utils import detect_file_format - fmt = detect_file_format(filename) + with open(filename, 'rU') as f: + data = f.read() + fmt = detect_file_format(data) if fmt == 'rs274x': return rs274x.read(filename) elif fmt == 'excellon': return excellon.read(filename) + elif fmt == 'ipc_d_356': + return ipc356.read(filename) + else: + raise ParseError('Unable to detect file format') + + +def loads(data): + """ Read gerber or excellon file contents from a string and return a + representative object. + + Parameters + ---------- + data : string + gerber or excellon file contents as a string. + + Returns + ------- + file : CncFile subclass + CncFile object representing the file, either GerberFile or + ExcellonFile. Returns None if file is not an Excellon or Gerber file. + """ + + fmt = detect_file_format(data) + if fmt == 'rs274x': + return rs274x.loads(data) + elif fmt == 'excellon': + return excellon.loads(data) else: - return None + raise TypeError('Unable to detect file format') diff --git a/gerber/excellon.py b/gerber/excellon.py index 9d09576..0626819 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*-
# Copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be>
- +
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
@@ -13,8 +13,8 @@ # 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. - +# limitations under the License.
+
"""
Excellon File module
====================
@@ -23,11 +23,22 @@ Excellon File module This module provides Excellon file classes and parsing utilities
"""
+import math
+import operator
-from .excellon_statements import *
from .cam import CamFile, FileSettings
-from .primitives import Drill
-import math
+from .excellon_statements import *
+from .excellon_tool import ExcellonToolDefinitionParser
+from .primitives import Drill, Slot
+from .utils import inch, metric
+
+
+try:
+ from cStringIO import StringIO
+except(ImportError):
+ from io import StringIO
+
+
def read(filename):
""" Read data from filename and return an ExcellonFile
@@ -42,10 +53,108 @@ def read(filename): An ExcellonFile created from the specified file.
"""
- detected_settings = detect_excellon_format(filename)
- settings = FileSettings(**detected_settings)
- zeros = ''
+ # File object should use settings from source file by default.
+ with open(filename, 'rU') as f:
+ data = f.read()
+ settings = FileSettings(**detect_excellon_format(data))
return ExcellonParser(settings).parse(filename)
+ +def loads(data, settings = None, tools = None): + """ Read data from string and return an ExcellonFile
+ Parameters
+ ----------
+ data : string
+ string containing Excellon file contents
+
+ tools: dict (optional)
+ externally defined tools
+
+ Returns
+ -------
+ file : :class:`gerber.excellon.ExcellonFile`
+ An ExcellonFile created from the specified file.
+
+ """
+ # File object should use settings from source file by default.
+ if not settings:
+ settings = FileSettings(**detect_excellon_format(data))
+ return ExcellonParser(settings, tools).parse_raw(data)
+
+ +class DrillHit(object): + """Drill feature that is a single drill hole.
+
+ Attributes
+ ----------
+ tool : ExcellonTool
+ Tool to drill the hole. Defines the size of the hole that is generated.
+ position : tuple(float, float)
+ Center position of the drill.
+
+ """ + def __init__(self, tool, position):
+ self.tool = tool
+ self.position = position
+
+ def to_inch(self):
+ self.position = tuple(map(inch, self.position))
+
+ def to_metric(self):
+ self.position = tuple(map(metric, self.position))
+
+ @property
+ def bounding_box(self):
+ position = self.position
+ radius = self.tool.diameter / 2.
+
+ min_x = position[0] - radius
+ max_x = position[0] + radius
+ min_y = position[1] - radius
+ max_y = position[1] + radius
+ return ((min_x, max_x), (min_y, max_y))
+
+ def offset(self, x_offset, y_offset):
+ self.position = tuple(map(operator.add, self.position, (x_offset, y_offset)))
+
+ def __str__(self):
+ return 'Hit (%f, %f) {%s}' % (self.position[0], self.position[1], self.tool)
+
+class DrillSlot(object):
+ """
+ A slot is created between two points. The way the slot is created depends on the statement used to create it
+ """
+
+ TYPE_ROUT = 1
+ TYPE_G85 = 2
+
+ def __init__(self, tool, start, end, slot_type):
+ self.tool = tool
+ self.start = start
+ self.end = end
+ self.slot_type = slot_type
+
+ def to_inch(self):
+ self.start = tuple(map(inch, self.start))
+ self.end = tuple(map(inch, self.end))
+
+ def to_metric(self):
+ self.start = tuple(map(metric, self.start))
+ self.end = tuple(map(metric, self.end))
+
+ @property
+ def bounding_box(self):
+ start = self.start
+ end = self.end
+ radius = self.tool.diameter / 2.
+ min_x = min(start[0], end[0]) - radius
+ max_x = max(start[0], end[0]) + radius
+ min_y = min(start[1], end[1]) - radius
+ max_y = max(start[1], end[1]) + radius
+ return ((min_x, max_x), (min_y, max_y))
+
+ def offset(self, x_offset, y_offset):
+ self.start = tuple(map(operator.add, self.start, (x_offset, y_offset)))
+ self.end = tuple(map(operator.add, self.end, (x_offset, y_offset)))
class ExcellonFile(CamFile):
@@ -53,6 +162,9 @@ class ExcellonFile(CamFile): The ExcellonFile class represents a single excellon file.
+ http://www.excellon.com/manuals/program.htm
+ (archived version at https://web.archive.org/web/20150920001043/http://www.excellon.com/manuals/program.htm)
+
Parameters
----------
tools : list
@@ -72,39 +184,172 @@ class ExcellonFile(CamFile): either 'inch' or 'metric'.
"""
+
def __init__(self, statements, tools, hits, settings, filename=None):
super(ExcellonFile, self).__init__(statements=statements,
settings=settings,
filename=filename)
self.tools = tools
self.hits = hits
- self.primitives = [Drill(position, tool.diameter)
- for tool, position in self.hits]
+
+ @property
+ def primitives(self): + """
+ Gets the primitives. Note that unlike Gerber, this generates new objects
+ """
+ primitives = []
+ for hit in self.hits:
+ if isinstance(hit, DrillHit):
+ primitives.append(Drill(hit.position, hit.tool.diameter, hit, units=self.settings.units))
+ elif isinstance(hit, DrillSlot):
+ primitives.append(Slot(hit.start, hit.end, hit.tool.diameter, hit, units=self.settings.units))
+ else:
+ raise ValueError('Unknown hit type')
+
+ return primitives @property
def bounds(self):
xmin = ymin = 100000000000
xmax = ymax = -100000000000
- for tool, position in self.hits:
- radius = tool.diameter / 2.
- x = position[0]
- y = position[1]
- xmin = min(x - radius, xmin)
- xmax = max(x + radius, xmax)
- ymin = min(y - radius, ymin)
- ymax = max(y + radius, ymax)
+ for hit in self.hits:
+ bbox = hit.bounding_box
+ xmin = min(bbox[0][0], xmin)
+ xmax = max(bbox[0][1], xmax)
+ ymin = min(bbox[1][0], ymin)
+ ymax = max(bbox[1][1], ymax)
return ((xmin, xmax), (ymin, ymax))
- def report(self):
- """ Print drill report
+ def report(self, filename=None):
+ """ Print or save drill report
"""
- pass
-
-
- def write(self, filename):
- with open(filename, 'w') as f:
+ if self.settings.units == 'inch':
+ toolfmt = ' T{:0>2d} {:%d.%df} {: >3d} {:f}in.\n' % self.settings.format
+ else:
+ toolfmt = ' T{:0>2d} {:%d.%df} {: >3d} {:f}mm\n' % self.settings.format
+ rprt = '=====================\nExcellon Drill Report\n=====================\n'
+ if self.filename is not None:
+ rprt += 'NC Drill File: %s\n\n' % self.filename
+ rprt += 'Drill File Info:\n----------------\n'
+ rprt += (' Data Mode %s\n' % 'Absolute'
+ if self.settings.notation == 'absolute' else 'Incremental')
+ rprt += (' Units %s\n' % 'Inches'
+ if self.settings.units == 'inch' else 'Millimeters')
+ rprt += '\nTool List:\n----------\n\n'
+ rprt += ' Code Size Hits Path Length\n'
+ rprt += ' --------------------------------------\n'
+ for tool in iter(self.tools.values()):
+ rprt += toolfmt.format(tool.number, tool.diameter,
+ tool.hit_count, self.path_length(tool.number))
+ if filename is not None:
+ with open(filename, 'w') as f:
+ f.write(rprt)
+ return rprt
+
+ def write(self, filename=None):
+ filename = filename if filename is not None else self.filename
+ with open(filename, 'w') as f:
+ for statement in self.statements:
+ if not isinstance(statement, ToolSelectionStmt):
+ f.write(statement.to_excellon(self.settings) + '\n')
+ else:
+ break
+
+ # Write out coordinates for drill hits by tool
+ for tool in iter(self.tools.values()):
+ f.write(ToolSelectionStmt(tool.number).to_excellon(self.settings) + '\n')
+ for hit in self.hits:
+ if hit.tool.number == tool.number:
+ f.write(CoordinateStmt(
+ *hit.position).to_excellon(self.settings) + '\n') + f.write(EndOfProgramStmt().to_excellon() + '\n') +
+ def to_inch(self):
+ """
+ Convert units to inches
+ """
+ if self.units != 'inch':
+ self.units = 'inch'
+ for statement in self.statements:
+ statement.to_inch()
+ for tool in iter(self.tools.values()):
+ tool.to_inch()
+ for primitive in self.primitives:
+ primitive.to_inch() + for hit in self.hits: + hit.to_inch() +
+ def to_metric(self):
+ """ Convert units to metric
+ """
+ if self.units != 'metric':
+ self.units = 'metric'
for statement in self.statements:
- f.write(statement.to_excellon() + '\n')
+ statement.to_metric()
+ for tool in iter(self.tools.values()):
+ tool.to_metric()
+ for primitive in self.primitives:
+ primitive.to_metric()
+ for hit in self.hits:
+ hit.to_metric()
+
+ def offset(self, x_offset=0, y_offset=0):
+ for statement in self.statements:
+ statement.offset(x_offset, y_offset)
+ for primitive in self.primitives:
+ primitive.offset(x_offset, y_offset) + for hit in self. hits: + hit.offset(x_offset, y_offset) +
+ def path_length(self, tool_number=None):
+ """ Return the path length for a given tool
+ """
+ lengths = {}
+ positions = {}
+ for hit in self.hits:
+ tool = hit.tool
+ num = tool.number
+ positions[num] = (0, 0) if positions.get(
+ num) is None else positions[num]
+ lengths[num] = 0.0 if lengths.get(num) is None else lengths[num]
+ lengths[num] = lengths[
+ num] + math.hypot(*tuple(map(operator.sub, positions[num], hit.position)))
+ positions[num] = hit.position
+
+ if tool_number is None:
+ return lengths
+ else:
+ return lengths.get(tool_number)
+
+ def hit_count(self, tool_number=None):
+ counts = {}
+ for tool in iter(self.tools.values()):
+ counts[tool.number] = tool.hit_count
+ if tool_number is None:
+ return counts
+ else:
+ return counts.get(tool_number)
+
+ def update_tool(self, tool_number, **kwargs):
+ """ Change parameters of a tool
+ """
+ if kwargs.get('feed_rate') is not None:
+ self.tools[tool_number].feed_rate = kwargs.get('feed_rate')
+ if kwargs.get('retract_rate') is not None:
+ self.tools[tool_number].retract_rate = kwargs.get('retract_rate')
+ if kwargs.get('rpm') is not None:
+ self.tools[tool_number].rpm = kwargs.get('rpm')
+ if kwargs.get('diameter') is not None:
+ self.tools[tool_number].diameter = kwargs.get('diameter')
+ if kwargs.get('max_hit_count') is not None:
+ self.tools[tool_number].max_hit_count = kwargs.get('max_hit_count')
+ if kwargs.get('depth_offset') is not None:
+ self.tools[tool_number].depth_offset = kwargs.get('depth_offset')
+ # Update drill hits
+ newtool = self.tools[tool_number]
+ for hit in self.hits:
+ if hit.tool.number == newtool.number:
+ hit.tool = newtool
class ExcellonParser(object):
@@ -113,26 +358,30 @@ class ExcellonParser(object): Parameters
----------
settings : FileSettings or dict-like
- Excellon file settings to use when interpreting the excellon file.
- """
- def __init__(self, settings=None):
+ Excellon file settings to use when interpreting the excellon file. + """ + def __init__(self, settings=None, ext_tools=None): self.notation = 'absolute'
self.units = 'inch'
- self.zero_suppression = 'trailing'
- self.format = (2, 5)
+ self.zeros = 'leading'
+ self.format = (2, 4)
self.state = 'INIT'
self.statements = []
self.tools = {}
+ self.ext_tools = ext_tools or {}
+ self.comment_tools = {}
self.hits = []
self.active_tool = None
- self.pos = [0., 0.] + self.pos = [0., 0.]
+ self.drill_down = False
+ # Default for lated is None, which means we don't know
+ self.plated = ExcellonTool.PLATED_UNKNOWN
if settings is not None:
self.units = settings.units
- self.zero_suppression = settings.zero_suppression
+ self.zeros = settings.zeros
self.notation = settings.notation
self.format = settings.format
-
@property
def coordinates(self):
return [(stmt.x, stmt.y) for stmt in self.statements if isinstance(stmt, CoordinateStmt)]
@@ -159,18 +408,51 @@ class ExcellonParser(object): return len(self.hits)
def parse(self, filename):
- with open(filename, 'r') as f:
- for line in f:
- self._parse(line.strip())
+ with open(filename, 'rU') as f:
+ data = f.read()
+ return self.parse_raw(data, filename)
+
+ def parse_raw(self, data, filename=None):
+ for line in StringIO(data):
+ self._parse_line(line.strip())
+ for stmt in self.statements:
+ stmt.units = self.units
return ExcellonFile(self.statements, self.tools, self.hits,
self._settings(), filename)
- def _parse(self, line):
- #line = line.strip()
- zs = self._settings().zero_suppression
- fmt = self._settings().format
+ def _parse_line(self, line):
+ # skip empty lines
+ if not line.strip():
+ return
+
if line[0] == ';':
- self.statements.append(CommentStmt.from_excellon(line))
+ comment_stmt = CommentStmt.from_excellon(line)
+ self.statements.append(comment_stmt)
+
+ # get format from altium comment
+ if "FILE_FORMAT" in comment_stmt.comment:
+ detected_format = tuple(
+ [int(x) for x in comment_stmt.comment.split('=')[1].split(":")])
+ if detected_format:
+ self.format = detected_format
+
+ if "TYPE=PLATED" in comment_stmt.comment:
+ self.plated = ExcellonTool.PLATED_YES
+
+ if "TYPE=NON_PLATED" in comment_stmt.comment:
+ self.plated = ExcellonTool.PLATED_NO
+
+ if "HEADER:" in comment_stmt.comment:
+ self.state = "HEADER"
+
+ if " Holesize " in comment_stmt.comment:
+ self.state = "HEADER"
+
+ # Parse this as a hole definition
+ tools = ExcellonToolDefinitionParser(self._settings()).parse_raw(comment_stmt.comment)
+ if len(tools) == 1:
+ tool = tools[tools.keys()[0]]
+ self._add_comment_tool(tool)
elif line[:3] == 'M48':
self.statements.append(HeaderBeginStmt())
@@ -180,30 +462,109 @@ class ExcellonParser(object): self.statements.append(RewindStopStmt())
if self.state == 'HEADER':
self.state = 'DRILL'
+ elif self.state == 'INIT':
+ self.state = 'HEADER'
+
+ elif line[:3] == 'M00' and self.state == 'DRILL':
+ if self.active_tool:
+ cur_tool_number = self.active_tool.number
+ next_tool = self._get_tool(cur_tool_number + 1)
+
+ self.statements.append(NextToolSelectionStmt(self.active_tool, next_tool))
+ self.active_tool = next_tool
+ else:
+ raise Exception('Invalid state exception')
elif line[:3] == 'M95':
self.statements.append(HeaderEndStmt())
if self.state == 'HEADER':
self.state = 'DRILL'
+ elif line[:3] == 'M15':
+ self.statements.append(ZAxisRoutPositionStmt())
+ self.drill_down = True
+
+ elif line[:3] == 'M16':
+ self.statements.append(RetractWithClampingStmt())
+ self.drill_down = False
+
+ elif line[:3] == 'M17':
+ self.statements.append(RetractWithoutClampingStmt())
+ self.drill_down = False
+
elif line[:3] == 'M30':
- stmt = EndOfProgramStmt.from_excellon(line)
+ stmt = EndOfProgramStmt.from_excellon(line, self._settings())
self.statements.append(stmt)
elif line[:3] == 'G00':
+ self.statements.append(RouteModeStmt())
self.state = 'ROUT'
+ stmt = CoordinateStmt.from_excellon(line[3:], self._settings())
+ stmt.mode = self.state
+
+ x = stmt.x
+ y = stmt.y
+ self.statements.append(stmt)
+ if self.notation == 'absolute':
+ if x is not None:
+ self.pos[0] = x
+ if y is not None:
+ self.pos[1] = y
+ else:
+ if x is not None:
+ self.pos[0] += x
+ if y is not None:
+ self.pos[1] += y
+
+ elif line[:3] == 'G01':
+ self.statements.append(RouteModeStmt())
+ self.state = 'LINEAR'
+
+ stmt = CoordinateStmt.from_excellon(line[3:], self._settings())
+ stmt.mode = self.state
+
+ # The start position is where we were before the rout command
+ start = (self.pos[0], self.pos[1])
+
+ x = stmt.x
+ y = stmt.y
+ self.statements.append(stmt)
+ if self.notation == 'absolute':
+ if x is not None:
+ self.pos[0] = x
+ if y is not None:
+ self.pos[1] = y
+ else:
+ if x is not None:
+ self.pos[0] += x
+ if y is not None:
+ self.pos[1] += y
+
+ # Our ending position
+ end = (self.pos[0], self.pos[1])
+
+ if self.drill_down:
+ if not self.active_tool:
+ self.active_tool = self._get_tool(1)
+
+ self.hits.append(DrillSlot(self.active_tool, start, end, DrillSlot.TYPE_ROUT))
+ self.active_tool._hit()
+
elif line[:3] == 'G05':
+ self.statements.append(DrillModeStmt())
+ self.drill_down = False
self.state = 'DRILL'
- elif (('INCH' in line or 'METRIC' in line) and
- ('LZ' in line or 'TZ' in line)):
+ elif 'INCH' in line or 'METRIC' in line:
stmt = UnitStmt.from_excellon(line)
self.units = stmt.units
- self.zero_suppression = stmt.zero_suppression
+ self.zeros = stmt.zeros
+ if stmt.format:
+ self.format = stmt.format
self.statements.append(stmt)
- elif line[:3] == 'M71' or line [:3] == 'M72':
+ elif line[:3] == 'M71' or line[:3] == 'M72':
stmt = MeasuringModeStmt.from_excellon(line)
self.units = stmt.units
self.statements.append(stmt)
@@ -220,52 +581,191 @@ class ExcellonParser(object): elif line[:4] == 'FMAT':
stmt = FormatStmt.from_excellon(line)
self.statements.append(stmt)
+ self.format = stmt.format_tuple
+
+ elif line[:3] == 'G40':
+ self.statements.append(CutterCompensationOffStmt())
+
+ elif line[:3] == 'G41':
+ self.statements.append(CutterCompensationLeftStmt())
+
+ elif line[:3] == 'G42':
+ self.statements.append(CutterCompensationRightStmt())
+
+ elif line[:3] == 'G90':
+ self.statements.append(AbsoluteModeStmt())
+ self.notation = 'absolute'
+
+ elif line[0] == 'F':
+ infeed_rate_stmt = ZAxisInfeedRateStmt.from_excellon(line)
+ self.statements.append(infeed_rate_stmt)
elif line[0] == 'T' and self.state == 'HEADER':
- tool = ExcellonTool.from_excellon(line, self._settings())
- self.tools[tool.number] = tool
- self.statements.append(tool)
+ if not ',OFF' in line and not ',ON' in line:
+ tool = ExcellonTool.from_excellon(line, self._settings(), None, self.plated)
+ self._merge_properties(tool)
+ self.tools[tool.number] = tool
+ self.statements.append(tool)
+ else:
+ self.statements.append(UnknownStmt.from_excellon(line))
elif line[0] == 'T' and self.state != 'HEADER':
stmt = ToolSelectionStmt.from_excellon(line)
- self.active_tool = self.tools[stmt.tool]
self.statements.append(stmt)
+ # 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 gerb + if self._settings().units == "inch":
+ diameter = (16 + 8 * stmt.tool) / 1000.0
+ else:
+ diameter = metric((16 + 8 * stmt.tool) / 1000.0)
+
+ tool = ExcellonTool(
+ self._settings(), number=stmt.tool, diameter=diameter)
+ self.tools[tool.number] = tool
+
+ # FIXME: need to add this tool definition inside header to
+ # make sure it is properly written
+ for i, s in enumerate(self.statements):
+ if isinstance(s, ToolSelectionStmt) or isinstance(s, ExcellonTool):
+ self.statements.insert(i, tool)
+ break
+
+ self.active_tool = tool
+
+ elif line[0] == 'R' and self.state != 'HEADER':
+ stmt = RepeatHoleStmt.from_excellon(line, self._settings())
+ self.statements.append(stmt)
+ for i in range(stmt.count):
+ self.pos[0] += stmt.xdelta if stmt.xdelta is not None else 0
+ self.pos[1] += stmt.ydelta if stmt.ydelta is not None else 0
+ self.hits.append(DrillHit(self.active_tool, tuple(self.pos)))
+ self.active_tool._hit()
+
elif line[0] in ['X', 'Y']:
- stmt = CoordinateStmt.from_excellon(line, fmt, zs)
- x = stmt.x
- y = stmt.y
- self.statements.append(stmt) - if self.notation == 'absolute':
- if x is not None:
- self.pos[0] = x
- if y is not None:
- self.pos[1] = y
+ if 'G85' in line:
+ stmt = SlotStmt.from_excellon(line, self._settings())
+
+ # I don't know if this is actually correct, but it makes sense that this is where the tool would end
+ x = stmt.x_end
+ y = stmt.y_end
+
+ self.statements.append(stmt)
+
+ if self.notation == 'absolute':
+ if x is not None:
+ self.pos[0] = x
+ if y is not None:
+ self.pos[1] = y
+ else:
+ if x is not None:
+ self.pos[0] += x
+ if y is not None:
+ self.pos[1] += y
+
+ if self.state == 'DRILL' or self.state == 'HEADER':
+ if not self.active_tool:
+ self.active_tool = self._get_tool(1)
+
+ self.hits.append(DrillSlot(self.active_tool, (stmt.x_start, stmt.y_start), (stmt.x_end, stmt.y_end), DrillSlot.TYPE_G85))
+ self.active_tool._hit()
else:
- if x is not None:
- self.pos[0] += x
- if y is not None:
- self.pos[1] += y
- if self.state == 'DRILL': - self.hits.append((self.active_tool, tuple(self.pos)))
- self.active_tool._hit()
+ stmt = CoordinateStmt.from_excellon(line, self._settings())
+
+ # We need this in case we are in rout mode
+ start = (self.pos[0], self.pos[1])
+
+ x = stmt.x
+ y = stmt.y
+ self.statements.append(stmt)
+ if self.notation == 'absolute':
+ if x is not None:
+ self.pos[0] = x
+ if y is not None:
+ self.pos[1] = y
+ else:
+ if x is not None:
+ self.pos[0] += x
+ if y is not None:
+ self.pos[1] += y
+
+ if self.state == 'LINEAR' and self.drill_down:
+ if not self.active_tool:
+ self.active_tool = self._get_tool(1)
+
+ self.hits.append(DrillSlot(self.active_tool, start, tuple(self.pos), DrillSlot.TYPE_ROUT))
+
+ elif self.state == 'DRILL' or self.state == 'HEADER':
+ # Yes, drills in the header doesn't follow the specification, but it there are many
+ # files like this
+ if not self.active_tool:
+ self.active_tool = self._get_tool(1)
+
+ self.hits.append(DrillHit(self.active_tool, tuple(self.pos)))
+ self.active_tool._hit()
+
else:
self.statements.append(UnknownStmt.from_excellon(line))
def _settings(self):
return FileSettings(units=self.units, format=self.format,
- zero_suppression=self.zero_suppression,
- notation=self.notation)
-
-
-def detect_excellon_format(filename):
+ zeros=self.zeros, notation=self.notation)
+
+ def _add_comment_tool(self, tool):
+ """
+ Add a tool that was defined in the comments to this file.
+
+ If we have already found this tool, then we will merge this comment tool definition into
+ the information for the tool
+ """
+
+ existing = self.tools.get(tool.number)
+ if existing and existing.plated == None:
+ existing.plated = tool.plated
+
+ self.comment_tools[tool.number] = tool
+
+ def _merge_properties(self, tool):
+ """
+ When we have externally defined tools, merge the properties of that tool into this one
+
+ For now, this is only plated
+ """
+
+ if tool.plated == ExcellonTool.PLATED_UNKNOWN:
+ ext_tool = self.ext_tools.get(tool.number)
+
+ if ext_tool:
+ tool.plated = ext_tool.plated
+
+ def _get_tool(self, toolid):
+
+ tool = self.tools.get(toolid)
+ if not tool:
+ tool = self.comment_tools.get(toolid)
+ if tool:
+ tool.settings = self._settings()
+ self.tools[toolid] = tool
+
+ if not tool:
+ tool = self.ext_tools.get(toolid)
+ if tool:
+ tool.settings = self._settings()
+ self.tools[toolid] = tool
+
+ return tool
+
+def detect_excellon_format(data=None, filename=None):
""" Detect excellon file decimal format and zero-suppression settings.
Parameters
----------
- filename : string
- Name of the file to parse. This does not check if the file is actually
- an Excellon file, so do that before calling this.
+ data : string
+ String containing contents of Excellon file.
Returns
-------
@@ -277,15 +777,21 @@ def detect_excellon_format(filename): results = {}
detected_zeros = None
detected_format = None
- zs_options = ('leading', 'trailing', )
+ zeros_options = ('leading', 'trailing', )
format_options = ((2, 4), (2, 5), (3, 3),)
+ if data is None and filename is None:
+ raise ValueError('Either data or filename arguments must be provided')
+ if data is None:
+ with open(filename, 'rU') as f:
+ data = f.read()
+
# Check for obvious clues:
p = ExcellonParser()
- p.parse(filename)
+ p.parse_raw(data)
# Get zero_suppression from a unit statement
- zero_statements = [stmt.zero_suppression for stmt in p.statements
+ zero_statements = [stmt.zeros for stmt in p.statements
if isinstance(stmt, UnitStmt)]
# get format from altium comment
@@ -294,40 +800,40 @@ def detect_excellon_format(filename): and 'FILE_FORMAT' in stmt.comment]
detected_format = (tuple([int(val) for val in
- format_comment[0].split('=')[1].split(':')])
+ format_comment[0].split('=')[1].split(':')])
if len(format_comment) == 1 else None)
detected_zeros = zero_statements[0] if len(zero_statements) == 1 else None
# Bail out here if possible
if detected_format is not None and detected_zeros is not None:
- return {'format': detected_format, 'zero_suppression': detected_zeros}
+ return {'format': detected_format, 'zeros': detected_zeros}
# Only look at remaining options
if detected_format is not None:
format_options = (detected_format,)
if detected_zeros is not None:
- zs_options = (detected_zeros,)
+ zeros_options = (detected_zeros,)
# Brute force all remaining options, and pick the best looking one...
- for zs in zs_options:
+ for zeros in zeros_options:
for fmt in format_options:
- key = (fmt, zs)
- settings = FileSettings(zero_suppression=zs, format=fmt)
+ key = (fmt, zeros)
+ settings = FileSettings(zeros=zeros, format=fmt)
try:
p = ExcellonParser(settings)
- p.parse(filename)
- size = tuple([t[1] - t[0] for t in p.bounds])
+ p.parse_raw(data)
+ size = tuple([t[0] - t[1] for t in p.bounds])
hole_area = 0.0
for hit in p.hits:
- tool = hit[0]
+ tool = hit.tool
hole_area += math.pow(math.pi * tool.diameter / 2., 2)
results[key] = (size, p.hole_count, hole_area)
except:
pass
# See if any of the dimensions are left with only a single option
- formats = set(key[0] for key in results.iterkeys())
- zeros = set(key[1] for key in results.iterkeys())
+ formats = set(key[0] for key in iter(results.keys()))
+ zeros = set(key[1] for key in iter(results.keys()))
if len(formats) == 1:
detected_format = formats.pop()
if len(zeros) == 1:
@@ -335,7 +841,7 @@ def detect_excellon_format(filename): # Bail out here if we got everything....
if detected_format is not None and detected_zeros is not None:
- return {'format': detected_format, 'zero_suppression': detected_zeros}
+ return {'format': detected_format, 'zeros': detected_zeros}
# Otherwise score each option and pick the best candidate
else:
@@ -344,9 +850,9 @@ def detect_excellon_format(filename): size, count, diameter = results[key]
scores[key] = _layer_size_score(size, count, diameter)
minscore = min(scores.values())
- for key in scores.iterkeys():
+ for key in iter(scores.keys()):
if scores[key] == minscore:
- return {'format': key[0], 'zero_suppression': key[1]}
+ return {'format': key[0], 'zeros': key[1]}
def _layer_size_score(size, hole_count, hole_area):
@@ -354,7 +860,11 @@ def _layer_size_score(size, hole_count, hole_area): Lower is better.
"""
board_area = size[0] * size[1]
+ if board_area == 0:
+ return 0
+
hole_percentage = hole_area / board_area
hole_score = (hole_percentage - 0.25) ** 2
- size_score = (board_area - 8) **2
+ size_score = (board_area - 8) ** 2
return hole_score * size_score
+
\ No newline at end of file diff --git a/gerber/excellon_report/excellon_drr.py b/gerber/excellon_report/excellon_drr.py new file mode 100644 index 0000000..ab9e857 --- /dev/null +++ b/gerber/excellon_report/excellon_drr.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright 2015 Garret Fick <garret@ficksworkshop.com> + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Excellon DRR File module +==================== +**Excellon file classes** + +Extra parsers for allegro misc files that can be useful when the Excellon file doesn't contain parameter information +""" + diff --git a/gerber/excellon_settings.py b/gerber/excellon_settings.py new file mode 100644 index 0000000..4dbe0ca --- /dev/null +++ b/gerber/excellon_settings.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from argparse import PARSER + +# Copyright 2015 Garret Fick <garret@ficksworkshop.com> + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Excellon Settings Definition File module +==================== +**Excellon file classes** + +This module provides Excellon file classes and parsing utilities +""" + +import re +try: + from cStringIO import StringIO +except(ImportError): + from io import StringIO + +from .cam import FileSettings + +def loads(data): + """ Read settings file information and return an FileSettings + Parameters + ---------- + data : string + string containing Excellon settings file contents + + Returns + ------- + file settings: FileSettings + + """ + + return ExcellonSettingsParser().parse_raw(data) + +def map_coordinates(value): + if value == 'ABSOLUTE': + return 'absolute' + return 'relative' + +def map_units(value): + if value == 'ENGLISH': + return 'inch' + return 'metric' + +def map_boolean(value): + return value == 'YES' + +SETTINGS_KEYS = { + 'INTEGER-PLACES': (int, 'format-int'), + 'DECIMAL-PLACES': (int, 'format-dec'), + 'COORDINATES': (map_coordinates, 'notation'), + 'OUTPUT-UNITS': (map_units, 'units'), + } + +class ExcellonSettingsParser(object): + """Excellon Settings PARSER + + Parameters + ---------- + None + """ + + def __init__(self): + self.values = {} + self.settings = None + + def parse_raw(self, data): + for line in StringIO(data): + self._parse(line.strip()) + + # Create the FileSettings object + self.settings = FileSettings( + notation=self.values['notation'], + units=self.values['units'], + format=(self.values['format-int'], self.values['format-dec']) + ) + + return self.settings + + def _parse(self, line): + + line_items = line.split() + if len(line_items) == 2: + + item_type_info = SETTINGS_KEYS.get(line_items[0]) + if item_type_info: + # Convert the value to the expected type + item_value = item_type_info[0](line_items[1]) + + self.values[item_type_info[1]] = item_value
\ No newline at end of file diff --git a/gerber/excellon_statements.py b/gerber/excellon_statements.py index feeda44..ac9c528 100644 --- a/gerber/excellon_statements.py +++ b/gerber/excellon_statements.py @@ -21,27 +21,54 @@ Excellon Statements """ -from .utils import parse_gerber_value, write_gerber_value, decimal_string import re +import uuid +from .utils import (parse_gerber_value, write_gerber_value, decimal_string, + inch, metric) + __all__ = ['ExcellonTool', 'ToolSelectionStmt', 'CoordinateStmt', 'CommentStmt', 'HeaderBeginStmt', 'HeaderEndStmt', 'RewindStopStmt', 'EndOfProgramStmt', 'UnitStmt', 'IncrementalModeStmt', 'VersionStmt', 'FormatStmt', 'LinkToolStmt', - 'MeasuringModeStmt', 'UnknownStmt', - ] + 'MeasuringModeStmt', 'RouteModeStmt', 'LinearModeStmt', 'DrillModeStmt', + 'AbsoluteModeStmt', 'RepeatHoleStmt', 'UnknownStmt', + 'ExcellonStatement', 'ZAxisRoutPositionStmt', + 'RetractWithClampingStmt', 'RetractWithoutClampingStmt', + 'CutterCompensationOffStmt', 'CutterCompensationLeftStmt', + 'CutterCompensationRightStmt', 'ZAxisInfeedRateStmt', + 'NextToolSelectionStmt', 'SlotStmt'] class ExcellonStatement(object): """ Excellon Statement abstract base class """ + @classmethod def from_excellon(cls, line): - pass + raise NotImplementedError('from_excellon must be implemented in a ' + 'subclass') + + def __init__(self, unit='inch', id=None): + self.units = unit + self.id = uuid.uuid4().int if id is None else id + + def to_excellon(self, settings=None): + raise NotImplementedError('to_excellon must be implemented in a ' + 'subclass') - def to_excellon(self): + def to_inch(self): + self.units = 'inch' + + def to_metric(self): + self.units = 'metric' + + def offset(self, x_offset=0, y_offset=0): pass + def __eq__(self, other): + return self.__dict__ == other.__dict__ + class ExcellonTool(ExcellonStatement): """ Excellon Tool class @@ -86,9 +113,29 @@ class ExcellonTool(ExcellonStatement): hit_count : integer Number of tool hits in excellon file. """ + + PLATED_UNKNOWN = None + PLATED_YES = 'plated' + PLATED_NO = 'nonplated' + PLATED_OPTIONAL = 'optional' + + @classmethod + def from_tool(cls, tool): + args = {} + + args['depth_offset'] = tool.depth_offset + args['diameter'] = tool.diameter + args['feed_rate'] = tool.feed_rate + args['max_hit_count'] = tool.max_hit_count + args['number'] = tool.number + args['plated'] = tool.plated + args['retract_rate'] = tool.retract_rate + args['rpm'] = tool.rpm + + return cls(None, **args) @classmethod - def from_excellon(cls, line, settings): + def from_excellon(cls, line, settings, id=None, plated=None): """ Create a Tool from an excellon file tool definition line. Parameters @@ -107,6 +154,7 @@ class ExcellonTool(ExcellonStatement): commands = re.split('([BCFHSTZ])', line)[1:] commands = [(command, value) for command, value in pairwise(commands)] args = {} + args['id'] = id nformat = settings.format zero_suppression = settings.zero_suppression for cmd, val in commands: @@ -124,6 +172,10 @@ class ExcellonTool(ExcellonStatement): args['number'] = int(val) elif cmd == 'Z': args['depth_offset'] = parse_gerber_value(val, nformat, zero_suppression) + + if plated != ExcellonTool.PLATED_UNKNOWN: + # Sometimees we can can parse the + args['plated'] = plated return cls(settings, **args) @classmethod @@ -143,9 +195,11 @@ class ExcellonTool(ExcellonStatement): tool : ExcellonTool An ExcellonTool initialized with the parameters in tool_dict. """ - return cls(settings, tool_dict) + return cls(settings, **tool_dict) def __init__(self, settings, **kwargs): + if kwargs.get('id') is not None: + super(ExcellonTool, self).__init__(id=kwargs.get('id')) self.settings = settings self.number = kwargs.get('number') self.feed_rate = kwargs.get('feed_rate') @@ -154,12 +208,16 @@ class ExcellonTool(ExcellonStatement): self.diameter = kwargs.get('diameter') self.max_hit_count = kwargs.get('max_hit_count') self.depth_offset = kwargs.get('depth_offset') + self.plated = kwargs.get('plated') + self.hit_count = 0 - def to_excellon(self): - fmt = self.settings.format - zs = self.settings.format - stmt = 'T%d' % self.number + def to_excellon(self, settings=None): + if self.settings and not settings: + settings = self.settings + fmt = settings.format + zs = settings.zero_suppression + stmt = 'T%02d' % self.number if self.retract_rate is not None: stmt += 'B%s' % write_gerber_value(self.retract_rate, fmt, zs) if self.feed_rate is not None: @@ -170,25 +228,55 @@ class ExcellonTool(ExcellonStatement): if self.rpm < 100000.: stmt += 'S%s' % write_gerber_value(self.rpm / 1000., fmt, zs) else: - stmt += 'S%g' % self.rpm / 1000. + stmt += 'S%g' % (self.rpm / 1000.) if self.diameter is not None: stmt += 'C%s' % decimal_string(self.diameter, fmt[1], True) if self.depth_offset is not None: stmt += 'Z%s' % write_gerber_value(self.depth_offset, fmt, zs) return stmt + def to_inch(self): + if self.settings.units != 'inch': + self.settings.units = 'inch' + if self.diameter is not None: + self.diameter = inch(self.diameter) + + def to_metric(self): + if self.settings.units != 'metric': + self.settings.units = 'metric' + if self.diameter is not None: + self.diameter = metric(self.diameter) + def _hit(self): self.hit_count += 1 + + def equivalent(self, other): + """ + Is the other tool equal to this, ignoring the tool number, and other file specified properties + """ + + if type(self) != type(other): + return False + + return (self.diameter == other.diameter + and self.feed_rate == other.feed_rate + and self.retract_rate == other.retract_rate + and self.rpm == other.rpm + and self.depth_offset == other.depth_offset + and self.max_hit_count == other.max_hit_count + and self.plated == other.plated + and self.settings.units == other.settings.units) def __repr__(self): unit = 'in.' if self.settings.units == 'inch' else 'mm' - return '<ExcellonTool %d: %0.3f%s dia.>' % (self.number, self.diameter, unit) + fmtstr = '<ExcellonTool %%02d: %%%d.%dg%%s dia.>' % self.settings.format + return fmtstr % (self.number, self.diameter, unit) class ToolSelectionStmt(ExcellonStatement): @classmethod - def from_excellon(cls, line): + def from_excellon(cls, line, **kwargs): """ Create a ToolSelectionStmt from an excellon file line. Parameters @@ -203,103 +291,335 @@ class ToolSelectionStmt(ExcellonStatement): """ line = line[1:] compensation_index = None - tool = int(line[:2]) - if len(line) > 2: + + # up to 3 characters for tool number (Frizting uses that) + if len(line) <= 3: + tool = int(line) + else: + tool = int(line[:2]) compensation_index = int(line[2:]) - return cls(tool, compensation_index) - def __init__(self, tool, compensation_index=None): + return cls(tool, compensation_index, **kwargs) + + def __init__(self, tool, compensation_index=None, **kwargs): + super(ToolSelectionStmt, self).__init__(**kwargs) tool = int(tool) compensation_index = (int(compensation_index) if compensation_index is not None else None) self.tool = tool self.compensation_index = compensation_index - def to_excellon(self): + def to_excellon(self, settings=None): stmt = 'T%02d' % self.tool if self.compensation_index is not None: stmt += '%02d' % self.compensation_index return stmt + +class NextToolSelectionStmt(ExcellonStatement): + + # TODO the statement exists outside of the context of the file, + # so it is imposible to know that it is really the next tool + + def __init__(self, cur_tool, next_tool, **kwargs): + """ + Select the next tool in the wheel. + Parameters + ---------- + cur_tool : the tool that is currently selected + next_tool : the that that is now selected + """ + super(NextToolSelectionStmt, self).__init__(**kwargs) + + self.cur_tool = cur_tool + self.next_tool = next_tool + + def to_excellon(self, settings=None): + stmt = 'M00' + return stmt + +class ZAxisInfeedRateStmt(ExcellonStatement): + + @classmethod + def from_excellon(cls, line, **kwargs): + """ Create a ZAxisInfeedRate from an excellon file line. + + Parameters + ---------- + line : string + Line from an Excellon file + + Returns + ------- + z_axis_infeed_rate : ToolSelectionStmt + ToolSelectionStmt representation of `line.` + """ + rate = int(line[1:]) + + return cls(rate, **kwargs) + + def __init__(self, rate, **kwargs): + super(ZAxisInfeedRateStmt, self).__init__(**kwargs) + self.rate = rate + + def to_excellon(self, settings=None): + return 'F%02d' % self.rate class CoordinateStmt(ExcellonStatement): @classmethod - def from_excellon(cls, line, nformat=(2, 5), zero_suppression='trailing'): + def from_point(cls, point, mode=None): + + stmt = cls(point[0], point[1]) + if mode: + stmt.mode = mode + return stmt + + @classmethod + def from_excellon(cls, line, settings, **kwargs): x_coord = None y_coord = None if line[0] == 'X': splitline = line.strip('X').split('Y') - x_coord = parse_gerber_value(splitline[0], nformat, - zero_suppression) + x_coord = parse_gerber_value(splitline[0], settings.format, + settings.zero_suppression) if len(splitline) == 2: - y_coord = parse_gerber_value(splitline[1], nformat, - zero_suppression) + y_coord = parse_gerber_value(splitline[1], settings.format, + settings.zero_suppression) else: - y_coord = parse_gerber_value(line.strip(' Y'), nformat, - zero_suppression) - return cls(x_coord, y_coord) - - def __init__(self, x=None, y=None): + y_coord = parse_gerber_value(line.strip(' Y'), settings.format, + settings.zero_suppression) + c = cls(x_coord, y_coord, **kwargs) + c.units = settings.units + return c + + def __init__(self, x=None, y=None, **kwargs): + super(CoordinateStmt, self).__init__(**kwargs) self.x = x self.y = y + self.mode = None - def to_excellon(self): + def to_excellon(self, settings): stmt = '' + if self.mode == "ROUT": + stmt += "G00" + if self.mode == "LINEAR": + stmt += "G01" if self.x is not None: - stmt += 'X%s' % write_gerber_value(self.x) + stmt += 'X%s' % write_gerber_value(self.x, settings.format, + settings.zero_suppression) if self.y is not None: - stmt += 'Y%s' % write_gerber_value(self.y) + stmt += 'Y%s' % write_gerber_value(self.y, settings.format, + settings.zero_suppression) return stmt + def to_inch(self): + if self.units == 'metric': + self.units = 'inch' + if self.x is not None: + self.x = inch(self.x) + if self.y is not None: + self.y = inch(self.y) + + def to_metric(self): + if self.units == 'inch': + self.units = 'metric' + if self.x is not None: + self.x = metric(self.x) + if self.y is not None: + self.y = metric(self.y) + + def offset(self, x_offset=0, y_offset=0): + if self.x is not None: + self.x += x_offset + if self.y is not None: + self.y += y_offset + + def __str__(self): + coord_str = '' + if self.x is not None: + coord_str += 'X: %g ' % self.x + if self.y is not None: + coord_str += 'Y: %g ' % self.y + + return '<Coordinate Statement: %s>' % coord_str + + +class RepeatHoleStmt(ExcellonStatement): + + @classmethod + def from_excellon(cls, line, settings, **kwargs): + match = re.compile(r'R(?P<rcount>[0-9]*)X?(?P<xdelta>[+\-]?\d*\.?\d*)?Y?' + '(?P<ydelta>[+\-]?\d*\.?\d*)?').match(line) + stmt = match.groupdict() + count = int(stmt['rcount']) + xdelta = (parse_gerber_value(stmt['xdelta'], settings.format, + settings.zero_suppression) + if stmt['xdelta'] is not '' else None) + ydelta = (parse_gerber_value(stmt['ydelta'], settings.format, + settings.zero_suppression) + if stmt['ydelta'] is not '' else None) + c = cls(count, xdelta, ydelta, **kwargs) + c.units = settings.units + return c + + def __init__(self, count, xdelta=0.0, ydelta=0.0, **kwargs): + super(RepeatHoleStmt, self).__init__(**kwargs) + self.count = count + self.xdelta = xdelta + self.ydelta = ydelta + + def to_excellon(self, settings): + stmt = 'R%d' % self.count + if self.xdelta is not None and self.xdelta != 0.0: + stmt += 'X%s' % write_gerber_value(self.xdelta, settings.format, + settings.zero_suppression) + if self.ydelta is not None and self.ydelta != 0.0: + stmt += 'Y%s' % write_gerber_value(self.ydelta, settings.format, + settings.zero_suppression) + return stmt + + def to_inch(self): + if self.units == 'metric': + self.units = 'inch' + if self.xdelta is not None: + self.xdelta = inch(self.xdelta) + if self.ydelta is not None: + self.ydelta = inch(self.ydelta) + + def to_metric(self): + if self.units == 'inch': + self.units = 'metric' + if self.xdelta is not None: + self.xdelta = metric(self.xdelta) + if self.ydelta is not None: + self.ydelta = metric(self.ydelta) + + def __str__(self): + return '<Repeat Hole: %d times, offset X: %g Y: %g>' % ( + self.count, + self.xdelta if self.xdelta is not None else 0, + self.ydelta if self.ydelta is not None else 0) + class CommentStmt(ExcellonStatement): @classmethod - def from_excellon(cls, line): + def from_excellon(cls, line, **kwargs): return cls(line.lstrip(';')) - def __init__(self, comment): + def __init__(self, comment, **kwargs): + super(CommentStmt, self).__init__(**kwargs) self.comment = comment - def to_excellon(self): + def to_excellon(self, settings=None): return ';%s' % self.comment class HeaderBeginStmt(ExcellonStatement): - def __init__(self): - pass + def __init__(self, **kwargs): + super(HeaderBeginStmt, self).__init__(**kwargs) - def to_excellon(self): + def to_excellon(self, settings=None): return 'M48' class HeaderEndStmt(ExcellonStatement): - def __init__(self): - pass + def __init__(self, **kwargs): + super(HeaderEndStmt, self).__init__(**kwargs) - def to_excellon(self): + def to_excellon(self, settings=None): return 'M95' class RewindStopStmt(ExcellonStatement): - def __init__(self): - pass + def __init__(self, **kwargs): + super(RewindStopStmt, self).__init__(**kwargs) - def to_excellon(self): + def to_excellon(self, settings=None): return '%' +class ZAxisRoutPositionStmt(ExcellonStatement): + + def __init__(self, **kwargs): + super(ZAxisRoutPositionStmt, self).__init__(**kwargs) + + def to_excellon(self, settings=None): + return 'M15' + + +class RetractWithClampingStmt(ExcellonStatement): + + def __init__(self, **kwargs): + super(RetractWithClampingStmt, self).__init__(**kwargs) + + def to_excellon(self, settings=None): + return 'M16' + + +class RetractWithoutClampingStmt(ExcellonStatement): + + def __init__(self, **kwargs): + super(RetractWithoutClampingStmt, self).__init__(**kwargs) + + def to_excellon(self, settings=None): + return 'M17' + + +class CutterCompensationOffStmt(ExcellonStatement): + + def __init__(self, **kwargs): + super(CutterCompensationOffStmt, self).__init__(**kwargs) + + def to_excellon(self, settings=None): + return 'G40' + + +class CutterCompensationLeftStmt(ExcellonStatement): + + def __init__(self, **kwargs): + super(CutterCompensationLeftStmt, self).__init__(**kwargs) + + def to_excellon(self, settings=None): + return 'G41' + + +class CutterCompensationRightStmt(ExcellonStatement): + + def __init__(self, **kwargs): + super(CutterCompensationRightStmt, self).__init__(**kwargs) + + def to_excellon(self, settings=None): + return 'G42' + + class EndOfProgramStmt(ExcellonStatement): - def __init__(self, x=None, y=None): + @classmethod + def from_excellon(cls, line, settings, **kwargs): + match = re.compile(r'M30X?(?P<x>\d*\.?\d*)?Y?' + '(?P<y>\d*\.?\d*)?').match(line) + stmt = match.groupdict() + x = (parse_gerber_value(stmt['x'], settings.format, + settings.zero_suppression) + if stmt['x'] is not '' else None) + y = (parse_gerber_value(stmt['y'], settings.format, + settings.zero_suppression) + if stmt['y'] is not '' else None) + c = cls(x, y, **kwargs) + c.units = settings.units + return c + + def __init__(self, x=None, y=None, **kwargs): + super(EndOfProgramStmt, self).__init__(**kwargs) self.x = x self.y = y - def to_excellon(self): + def to_excellon(self, settings=None): stmt = 'M30' if self.x is not None: stmt += 'X%s' % write_gerber_value(self.x) @@ -307,119 +627,346 @@ class EndOfProgramStmt(ExcellonStatement): stmt += 'Y%s' % write_gerber_value(self.y) return stmt + def to_inch(self): + if self.units == 'metric': + self.units = 'inch' + if self.x is not None: + self.x = inch(self.x) + if self.y is not None: + self.y = inch(self.y) + + def to_metric(self): + if self.units == 'inch': + self.units = 'metric' + if self.x is not None: + self.x = metric(self.x) + if self.y is not None: + self.y = metric(self.y) + + def offset(self, x_offset=0, y_offset=0): + if self.x is not None: + self.x += x_offset + if self.y is not None: + self.y += y_offset + class UnitStmt(ExcellonStatement): + + @classmethod + def from_settings(cls, settings): + """Create the unit statement from the FileSettings""" + + return cls(settings.units, settings.zeros) @classmethod - def from_excellon(cls, line): + def from_excellon(cls, line, **kwargs): units = 'inch' if 'INCH' in line else 'metric' - zero_suppression = 'trailing' if 'LZ' in line else 'leading' - return cls(units, zero_suppression) + zeros = 'leading' if 'LZ' in line else 'trailing' + if '0000.00' in line: + format = (4, 2) + elif '000.000' in line: + format = (3, 3) + elif '00.0000' in line: + format = (2, 4) + else: + format = None + return cls(units, zeros, format, **kwargs) - def __init__(self, units='inch', zero_suppression='trailing'): + def __init__(self, units='inch', zeros='leading', format=None, **kwargs): + super(UnitStmt, self).__init__(**kwargs) self.units = units.lower() - self.zero_suppression = zero_suppression + self.zeros = zeros + self.format = format - def to_excellon(self): + def to_excellon(self, settings=None): + # TODO This won't export the invalid format statement if it exists stmt = '%s,%s' % ('INCH' if self.units == 'inch' else 'METRIC', - 'LZ' if self.zero_suppression == 'trailing' + 'LZ' if self.zeros == 'leading' else 'TZ') return stmt + def to_inch(self): + self.units = 'inch' + + def to_metric(self): + self.units = 'metric' + class IncrementalModeStmt(ExcellonStatement): @classmethod - def from_excellon(cls, line): - return cls('off') if 'OFF' in line else cls('on') + def from_excellon(cls, line, **kwargs): + return cls('off', **kwargs) if 'OFF' in line else cls('on', **kwargs) - def __init__(self, mode='off'): + def __init__(self, mode='off', **kwargs): + super(IncrementalModeStmt, self).__init__(**kwargs) if mode.lower() not in ['on', 'off']: raise ValueError('Mode may be "on" or "off"') self.mode = mode - def to_excellon(self): + def to_excellon(self, settings=None): return 'ICI,%s' % ('OFF' if self.mode == 'off' else 'ON') class VersionStmt(ExcellonStatement): @classmethod - def from_excellon(cls, line): + def from_excellon(cls, line, **kwargs): version = int(line.split(',')[1]) - return cls(version) + return cls(version, **kwargs) - def __init__(self, version=1): + def __init__(self, version=1, **kwargs): + super(VersionStmt, self).__init__(**kwargs) version = int(version) if version not in [1, 2]: raise ValueError('Valid versions are 1 or 2') self.version = version - def to_excellon(self): + def to_excellon(self, settings=None): return 'VER,%d' % self.version class FormatStmt(ExcellonStatement): @classmethod - def from_excellon(cls, line): + def from_excellon(cls, line, **kwargs): fmt = int(line.split(',')[1]) - return cls(fmt) + return cls(fmt, **kwargs) - def __init__(self, format=1): + def __init__(self, format=1, **kwargs): + super(FormatStmt, self).__init__(**kwargs) format = int(format) if format not in [1, 2]: raise ValueError('Valid formats are 1 or 2') self.format = format - def to_excellon(self): + def to_excellon(self, settings=None): return 'FMAT,%d' % self.format + + @property + def format_tuple(self): + return (self.format, 6 - self.format) class LinkToolStmt(ExcellonStatement): @classmethod - def from_excellon(cls, line): + def from_excellon(cls, line, **kwargs): linked = [int(tool) for tool in line.split('/')] - return cls(linked) + return cls(linked, **kwargs) - def __init__(self, linked_tools): + def __init__(self, linked_tools, **kwargs): + super(LinkToolStmt, self).__init__(**kwargs) self.linked_tools = [int(x) for x in linked_tools] - def to_excellon(self): + def to_excellon(self, settings=None): return '/'.join([str(x) for x in self.linked_tools]) class MeasuringModeStmt(ExcellonStatement): @classmethod - def from_excellon(cls, line): + def from_excellon(cls, line, **kwargs): if not ('M71' in line or 'M72' in line): raise ValueError('Not a measuring mode statement') - return cls('inch') if 'M72' in line else cls('metric') + return cls('inch', **kwargs) if 'M72' in line else cls('metric', **kwargs) - def __init__(self, units='inch'): + def __init__(self, units='inch', **kwargs): + super(MeasuringModeStmt, self).__init__(**kwargs) units = units.lower() if units not in ['inch', 'metric']: raise ValueError('units must be "inch" or "metric"') self.units = units - def to_excellon(self): + def to_excellon(self, settings=None): return 'M72' if self.units == 'inch' else 'M71' + def to_inch(self): + self.units = 'inch' + + def to_metric(self): + self.units = 'metric' + + +class RouteModeStmt(ExcellonStatement): + + def __init__(self, **kwargs): + super(RouteModeStmt, self).__init__(**kwargs) + + def to_excellon(self, settings=None): + return 'G00' + + +class LinearModeStmt(ExcellonStatement): + + def __init__(self, **kwargs): + super(LinearModeStmt, self).__init__(**kwargs) + + def to_excellon(self, settings=None): + return 'G01' + + +class DrillModeStmt(ExcellonStatement): + + def __init__(self, **kwargs): + super(DrillModeStmt, self).__init__(**kwargs) + + def to_excellon(self, settings=None): + return 'G05' + + +class AbsoluteModeStmt(ExcellonStatement): + + def __init__(self, **kwargs): + super(AbsoluteModeStmt, self).__init__(**kwargs) + + def to_excellon(self, settings=None): + return 'G90' + class UnknownStmt(ExcellonStatement): @classmethod - def from_excellon(cls, line): - return cls(line) + def from_excellon(cls, line, **kwargs): + return cls(line, **kwargs) - def __init__(self, stmt): + def __init__(self, stmt, **kwargs): + super(UnknownStmt, self).__init__(**kwargs) self.stmt = stmt - def to_excellon(self): + def to_excellon(self, settings=None): return self.stmt + def __str__(self): + return "<Unknown Statement: %s>" % self.stmt + + +class SlotStmt(ExcellonStatement): + """ + G85 statement. Defines a slot created by multiple drills between two specified points. + + Format is two coordinates, split by G85in the middle, for example, XnY0nG85XnYn + """ + + @classmethod + def from_points(cls, start, end): + + return cls(start[0], start[1], end[0], end[1]) + + @classmethod + def from_excellon(cls, line, settings, **kwargs): + # Split the line based on the G85 separator + sub_coords = line.split('G85') + (x_start_coord, y_start_coord) = SlotStmt.parse_sub_coords(sub_coords[0], settings) + (x_end_coord, y_end_coord) = SlotStmt.parse_sub_coords(sub_coords[1], settings) + + # Some files seem to specify only one of the coordinates + if x_end_coord == None: + x_end_coord = x_start_coord + if y_end_coord == None: + y_end_coord = y_start_coord + + c = cls(x_start_coord, y_start_coord, x_end_coord, y_end_coord, **kwargs) + c.units = settings.units + return c + + @staticmethod + def parse_sub_coords(line, settings): + + x_coord = None + y_coord = None + + if line[0] == 'X': + splitline = line.strip('X').split('Y') + x_coord = parse_gerber_value(splitline[0], settings.format, + settings.zero_suppression) + if len(splitline) == 2: + y_coord = parse_gerber_value(splitline[1], settings.format, + settings.zero_suppression) + else: + y_coord = parse_gerber_value(line.strip(' Y'), settings.format, + settings.zero_suppression) + + return (x_coord, y_coord) + + + def __init__(self, x_start=None, y_start=None, x_end=None, y_end=None, **kwargs): + super(SlotStmt, self).__init__(**kwargs) + self.x_start = x_start + self.y_start = y_start + self.x_end = x_end + self.y_end = y_end + self.mode = None + + def to_excellon(self, settings): + stmt = '' + + if self.x_start is not None: + stmt += 'X%s' % write_gerber_value(self.x_start, settings.format, + settings.zero_suppression) + if self.y_start is not None: + stmt += 'Y%s' % write_gerber_value(self.y_start, settings.format, + settings.zero_suppression) + + stmt += 'G85' + + if self.x_end is not None: + stmt += 'X%s' % write_gerber_value(self.x_end, settings.format, + settings.zero_suppression) + if self.y_end is not None: + stmt += 'Y%s' % write_gerber_value(self.y_end, settings.format, + settings.zero_suppression) + + return stmt + + def to_inch(self): + if self.units == 'metric': + self.units = 'inch' + if self.x_start is not None: + self.x_start = inch(self.x_start) + if self.y_start is not None: + self.y_start = inch(self.y_start) + if self.x_end is not None: + self.x_end = inch(self.x_end) + if self.y_end is not None: + self.y_end = inch(self.y_end) + + def to_metric(self): + if self.units == 'inch': + self.units = 'metric' + if self.x_start is not None: + self.x_start = metric(self.x_start) + if self.y_start is not None: + self.y_start = metric(self.y_start) + if self.x_end is not None: + self.x_end = metric(self.x_end) + if self.y_end is not None: + self.y_end = metric(self.y_end) + + def offset(self, x_offset=0, y_offset=0): + if self.x_start is not None: + self.x_start += x_offset + if self.y_start is not None: + self.y_start += y_offset + if self.x_end is not None: + self.x_end += x_offset + if self.y_end is not None: + self.y_end += y_offset + + def __str__(self): + start_str = '' + if self.x_start is not None: + start_str += 'X: %g ' % self.x_start + if self.y_start is not None: + start_str += 'Y: %g ' % self.y_start + + end_str = '' + if self.x_end is not None: + end_str += 'X: %g ' % self.x_end + if self.y_end is not None: + end_str += 'Y: %g ' % self.y_end + + return '<Slot Statement: %s to %s>' % (start_str, end_str) def pairwise(iterator): """ Iterate over list taking two elements at a time. @@ -428,4 +975,4 @@ def pairwise(iterator): """ itr = iter(iterator) while True: - yield tuple([itr.next() for i in range(2)]) + yield tuple([next(itr) for i in range(2)]) diff --git a/gerber/excellon_tool.py b/gerber/excellon_tool.py new file mode 100644 index 0000000..bd76e54 --- /dev/null +++ b/gerber/excellon_tool.py @@ -0,0 +1,186 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright 2015 Garret Fick <garret@ficksworkshop.com> + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Excellon Tool Definition File module +==================== +**Excellon file classes** + +This module provides Excellon file classes and parsing utilities +""" + +import re +try: + from cStringIO import StringIO +except(ImportError): + from io import StringIO + +from .excellon_statements import ExcellonTool + +def loads(data, settings=None): + """ Read tool file information and return a map of tools + Parameters + ---------- + data : string + string containing Excellon Tool Definition file contents + + Returns + ------- + dict tool name: ExcellonTool + + """ + return ExcellonToolDefinitionParser(settings).parse_raw(data) + +class ExcellonToolDefinitionParser(object): + """ Excellon File Parser + + Parameters + ---------- + None + """ + + allegro_tool = re.compile(r'(?P<size>[0-9/.]+)\s+(?P<plated>P|N)\s+T(?P<toolid>[0-9]{2})\s+(?P<xtol>[0-9/.]+)\s+(?P<ytol>[0-9/.]+)') + allegro_comment_mils = re.compile('Holesize (?P<toolid>[0-9]{1,2})\. = (?P<size>[0-9/.]+) Tolerance = \+(?P<xtol>[0-9/.]+)/-(?P<ytol>[0-9/.]+) (?P<plated>(PLATED)|(NON_PLATED)|(OPTIONAL)) MILS Quantity = [0-9]+') + allegro2_comment_mils = re.compile('T(?P<toolid>[0-9]{1,2}) Holesize (?P<toolid2>[0-9]{1,2})\. = (?P<size>[0-9/.]+) Tolerance = \+(?P<xtol>[0-9/.]+)/-(?P<ytol>[0-9/.]+) (?P<plated>(PLATED)|(NON_PLATED)|(OPTIONAL)) MILS Quantity = [0-9]+') + allegro_comment_mm = re.compile('Holesize (?P<toolid>[0-9]{1,2})\. = (?P<size>[0-9/.]+) Tolerance = \+(?P<xtol>[0-9/.]+)/-(?P<ytol>[0-9/.]+) (?P<plated>(PLATED)|(NON_PLATED)|(OPTIONAL)) MM Quantity = [0-9]+') + allegro2_comment_mm = re.compile('T(?P<toolid>[0-9]{1,2}) Holesize (?P<toolid2>[0-9]{1,2})\. = (?P<size>[0-9/.]+) Tolerance = \+(?P<xtol>[0-9/.]+)/-(?P<ytol>[0-9/.]+) (?P<plated>(PLATED)|(NON_PLATED)|(OPTIONAL)) MM Quantity = [0-9]+') + + matchers = [ + (allegro_tool, 'mils'), + (allegro_comment_mils, 'mils'), + (allegro2_comment_mils, 'mils'), + (allegro_comment_mm, 'mm'), + (allegro2_comment_mm, 'mm'), + ] + + def __init__(self, settings=None): + self.tools = {} + self.settings = settings + + def parse_raw(self, data): + for line in StringIO(data): + self._parse(line.strip()) + + return self.tools + + def _parse(self, line): + + for matcher in ExcellonToolDefinitionParser.matchers: + m = matcher[0].match(line) + if m: + unit = matcher[1] + + size = float(m.group('size')) + platedstr = m.group('plated') + toolid = int(m.group('toolid')) + xtol = float(m.group('xtol')) + ytol = float(m.group('ytol')) + + size = self._convert_length(size, unit) + xtol = self._convert_length(xtol, unit) + ytol = self._convert_length(ytol, unit) + + if platedstr == 'PLATED': + plated = ExcellonTool.PLATED_YES + elif platedstr == 'NON_PLATED': + plated = ExcellonTool.PLATED_NO + elif platedstr == 'OPTIONAL': + plated = ExcellonTool.PLATED_OPTIONAL + else: + plated = ExcellonTool.PLATED_UNKNOWN + + tool = ExcellonTool(None, number=toolid, diameter=size, plated=plated) + + self.tools[tool.number] = tool + + break + + def _convert_length(self, value, unit): + + # Convert the value to mm + if unit == 'mils': + value /= 39.3700787402 + + # Now convert to the settings unit + if self.settings.units == 'inch': + return value / 25.4 + else: + # Already in mm + return value + +def loads_rep(data, settings=None): + """ Read tool report information generated by PADS and return a map of tools + Parameters + ---------- + data : string + string containing Excellon Report file contents + + Returns + ------- + dict tool name: ExcellonTool + + """ + return ExcellonReportParser(settings).parse_raw(data) + +class ExcellonReportParser(object): + + # We sometimes get files with different encoding, so we can't actually + # match the text - the best we can do it detect the table header + header = re.compile(r'====\s+====\s+====\s+====\s+=====\s+===') + + def __init__(self, settings=None): + self.tools = {} + self.settings = settings + + self.found_header = False + + def parse_raw(self, data): + for line in StringIO(data): + self._parse(line.strip()) + + return self.tools + + def _parse(self, line): + + # skip empty lines and "comments" + if not line.strip(): + return + + if not self.found_header: + # Try to find the heaader, since we need that to be sure we understand the contents correctly. + if ExcellonReportParser.header.match(line): + self.found_header = True + + elif line[0] != '=': + # Already found the header, so we know to to map the contents + parts = line.split() + if len(parts) == 6: + toolid = int(parts[0]) + size = float(parts[1]) + if parts[2] == 'x': + plated = ExcellonTool.PLATED_YES + elif parts[2] == '-': + plated = ExcellonTool.PLATED_NO + else: + plated = ExcellonTool.PLATED_UNKNOWN + feedrate = int(parts[3]) + speed = int(parts[4]) + qty = int(parts[5]) + + tool = ExcellonTool(None, number=toolid, diameter=size, plated=plated, feed_rate=feedrate, rpm=speed) + + self.tools[tool.number] = tool
\ No newline at end of file diff --git a/gerber/exceptions.py b/gerber/exceptions.py new file mode 100644 index 0000000..65ae905 --- /dev/null +++ b/gerber/exceptions.py @@ -0,0 +1,36 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright 2015 Hamilton Kibbe <ham@hamiltonkib.be> + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +class ParseError(Exception): + pass + + +class GerberParseError(ParseError): + pass + + +class ExcellonParseError(ParseError): + pass + + +class ExcellonFileError(IOError): + pass + + +class GerberFileError(IOError): + pass diff --git a/gerber/gerber_statements.py b/gerber/gerber_statements.py index 32d3784..33fb4ec 100644 --- a/gerber/gerber_statements.py +++ b/gerber/gerber_statements.py @@ -1,19 +1,32 @@ -#! /usr/bin/env python +#!/usr/bin/env python # -*- coding: utf-8 -*- + +# copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be> +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. """ Gerber (RS-274X) Statements =========================== **Gerber RS-274X file statement classes** """ -from .utils import parse_gerber_value, write_gerber_value, decimal_string +from .utils import (parse_gerber_value, write_gerber_value, decimal_string, + inch, metric) - -__all__ = ['FSParamStmt', 'MOParamStmt', 'IPParamStmt', 'OFParamStmt', - 'LPParamStmt', 'ADParamStmt', 'AMParamStmt', 'INParamStmt', - 'LNParamStmt', 'CoordStmt', 'ApertureStmt', 'CommentStmt', - 'EofStmt', 'QuadrantModeStmt', 'RegionModeStmt', 'UnknownStmt', - 'ParamStmt'] +from .am_statements import * +from .am_read import read_macro +from .am_eval import eval_macro +from .primitives import AMGroup class Statement(object): @@ -31,8 +44,10 @@ class Statement(object): type : string String identifying the statement type. """ - def __init__(self, stype): + + def __init__(self, stype, units='inch'): self.type = stype + self.units = units def __str__(self): s = "<{0} ".format(self.__class__.__name__) @@ -43,6 +58,18 @@ class Statement(object): s = s.rstrip() + ">" return s + def to_inch(self): + self.units = 'inch' + + def to_metric(self): + self.units = 'metric' + + def offset(self, x_offset=0, y_offset=0): + pass + + def __eq__(self, other): + return self.__dict__ == other.__dict__ + class ParamStmt(Statement): """ Gerber parameter statement Base class @@ -59,6 +86,7 @@ class ParamStmt(Statement): param : string Parameter type code """ + def __init__(self, param): Statement.__init__(self, "PARAM") self.param = param @@ -67,16 +95,27 @@ class ParamStmt(Statement): class FSParamStmt(ParamStmt): """ FS - Gerber Format Specification Statement """ + + @classmethod + def from_settings(cls, settings): + + return cls('FS', settings.zero_suppression, settings.notation, settings.format) @classmethod def from_dict(cls, stmt_dict): """ """ param = stmt_dict.get('param') - zeros = 'leading' if stmt_dict.get('zero') == 'L' else 'trailing' + + if stmt_dict.get('zero') == 'L': + zeros = 'leading' + elif stmt_dict.get('zero') == 'T': + zeros = 'trailing' + else: + zeros = 'none' + notation = 'absolute' if stmt_dict.get('notation') == 'A' else 'incremental' - x = map(int, stmt_dict.get('x')) - fmt = (x[0], x[1]) + fmt = tuple(map(int, stmt_dict.get('x'))) return cls(param, zeros, notation, fmt) def __init__(self, param, zero_suppression='leading', @@ -94,7 +133,7 @@ class FSParamStmt(ParamStmt): Parameter. zero_suppression : string - Zero-suppression mode. May be either 'leading' or 'trailing' + Zero-suppression mode. May be either 'leading', 'trailing' or 'none' (all zeros are present) notation : string Notation mode. May be either 'absolute' or 'incremental' @@ -114,32 +153,42 @@ class FSParamStmt(ParamStmt): self.notation = notation self.format = format - def to_gerber(self): - zero_suppression = 'L' if self.zero_suppression == 'leading' else 'T' - notation = 'A' if self.notation == 'absolute' else 'I' - fmt = ''.join(map(str, self.format)) - return '%FS{0}{1}X{2}Y{3}*%'.format(zero_suppression, notation, - fmt, fmt) + def to_gerber(self, settings=None): + if settings: + zero_suppression = 'L' if settings.zero_suppression == 'leading' else 'T' + notation = 'A' if settings.notation == 'absolute' else 'I' + fmt = ''.join(map(str, settings.format)) + else: + zero_suppression = 'L' if self.zero_suppression == 'leading' else 'T' + notation = 'A' if self.notation == 'absolute' else 'I' + fmt = ''.join(map(str, self.format)) + + return '%FS{0}{1}X{2}Y{3}*%'.format(zero_suppression, notation, fmt, fmt) def __str__(self): return ('<Format Spec: %d:%d %s zero suppression %s notation>' % - (self.format[0], self.format[1], self.zero_suppression, - self.notation)) + (self.format[0], self.format[1], self.zero_suppression, self.notation)) class MOParamStmt(ParamStmt): """ MO - Gerber Mode (measurement units) Statement. """ + + @classmethod + def from_units(cls, units): + return cls(None, units) @classmethod def from_dict(cls, stmt_dict): param = stmt_dict.get('param') - if stmt_dict.get('mo').lower() == 'in': + if stmt_dict.get('mo') is None: + mo = None + elif stmt_dict.get('mo').lower() not in ('in', 'mm'): + raise ValueError('Mode may be mm or in') + elif stmt_dict.get('mo').lower() == 'in': mo = 'inch' - elif stmt_dict.get('mo').lower() == 'mm': - mo = 'metric' else: - mo = None + mo = 'metric' return cls(param, mo) def __init__(self, param, mo): @@ -162,102 +211,19 @@ class MOParamStmt(ParamStmt): ParamStmt.__init__(self, param) self.mode = mo - def to_gerber(self): + def to_gerber(self, settings=None): mode = 'MM' if self.mode == 'metric' else 'IN' return '%MO{0}*%'.format(mode) - def __str__(self): - mode_str = 'millimeters' if self.mode == 'metric' else 'inches' - return ('<Mode: %s>' % mode_str) - + def to_inch(self): + self.mode = 'inch' -class IPParamStmt(ParamStmt): - """ IP - Gerber Image Polarity Statement. (Deprecated) - """ - @classmethod - def from_dict(cls, stmt_dict): - param = stmt_dict.get('param') - ip = 'positive' if stmt_dict.get('ip') == 'POS' else 'negative' - return cls(param, ip) - - def __init__(self, param, ip): - """ Initialize IPParamStmt class - - Parameters - ---------- - param : string - Parameter string. - - ip : string - Image polarity. May be either'positive' or 'negative' - - Returns - ------- - ParamStmt : IPParamStmt - Initialized IPParamStmt class. - - """ - ParamStmt.__init__(self, param) - self.ip = ip - - def to_gerber(self): - ip = 'POS' if self.ip == 'positive' else 'NEG' - return '%IP{0}*%'.format(ip) + def to_metric(self): + self.mode = 'metric' def __str__(self): - return ('<Image Polarity: %s>' % self.ip) - - -class OFParamStmt(ParamStmt): - """ OF - Gerber Offset statement (Deprecated) - """ - - @classmethod - def from_dict(cls, stmt_dict): - param = stmt_dict.get('param') - a = float(stmt_dict.get('a')) - b = float(stmt_dict.get('b')) - return cls(param, a, b) - - def __init__(self, param, a, b): - """ Initialize OFParamStmt class - - Parameters - ---------- - param : string - Parameter - - a : float - Offset along the output device A axis - - b : float - Offset along the output device B axis - - Returns - ------- - ParamStmt : OFParamStmt - Initialized OFParamStmt class. - - """ - ParamStmt.__init__(self, param) - self.a = a - self.b = b - - def to_gerber(self): - ret = '%OF' - if self.a: - ret += 'A' + decimal_string(self.a, precision=6) - if self.b: - ret += 'B' + decimal_string(self.b, precision=6) - return ret + '*%' - - def __str__(self): - offset_str = '' - if self.a: - offset_str += ('X: %f' % self.a) - if self.b: - offset_str += ('Y: %f' % self.b) - return ('<Offset: %s>' % offset_str) + mode_str = 'millimeters' if self.mode == 'metric' else 'inches' + return ('<Mode: %s>' % mode_str) class LPParamStmt(ParamStmt): @@ -269,6 +235,11 @@ class LPParamStmt(ParamStmt): param = stmt_dict['param'] lp = 'clear' if stmt_dict.get('lp') == 'C' else 'dark' return cls(param, lp) + + @classmethod + def from_region(cls, region): + #todo what is the first param? + return cls(None, region.level_polarity) def __init__(self, param, lp): """ Initialize LPParamStmt class @@ -290,7 +261,7 @@ class LPParamStmt(ParamStmt): ParamStmt.__init__(self, param) self.lp = lp - def to_gerber(self): + def to_gerber(self, settings=None): lp = 'C' if self.lp == 'clear' else 'D' return '%LP{0}*%'.format(lp) @@ -301,7 +272,34 @@ class LPParamStmt(ParamStmt): class ADParamStmt(ParamStmt): """ AD - Gerber Aperture Definition Statement """ - + + @classmethod + def rect(cls, dcode, width, height): + '''Create a rectangular aperture definition statement''' + return cls('AD', dcode, 'R', ([width, height],)) + + @classmethod + def circle(cls, dcode, diameter, hole_diameter): + '''Create a circular aperture definition statement''' + + if hole_diameter != None: + return cls('AD', dcode, 'C', ([diameter, hole_diameter],)) + return cls('AD', dcode, 'C', ([diameter],)) + + @classmethod + def obround(cls, dcode, width, height): + '''Create an obround aperture definition statement''' + return cls('AD', dcode, 'O', ([width, height],)) + + @classmethod + def polygon(cls, dcode, diameter, num_vertices, rotation, hole_diameter): + '''Create a polygon aperture definition statement''' + return cls('AD', dcode, 'P', ([diameter, num_vertices, rotation, hole_diameter],)) + + @classmethod + def macro(cls, dcode, name): + return cls('AD', dcode, name, '') + @classmethod def from_dict(cls, stmt_dict): param = stmt_dict.get('param') @@ -329,21 +327,38 @@ class ADParamStmt(ParamStmt): Returns ------- - ParamStmt : LPParamStmt - Initialized LPParamStmt class. + ParamStmt : ADParamStmt + Initialized ADParamStmt class. """ ParamStmt.__init__(self, param) self.d = d self.shape = shape - if modifiers is not None: - self.modifiers = [[x for x in m.split("X")] for m in modifiers.split(",")] + if isinstance(modifiers, tuple): + self.modifiers = modifiers + elif modifiers: + self.modifiers = [tuple([float(x) for x in m.split("X") if len(x)]) + for m in modifiers.split(",") if len(m)] else: - self.modifiers = [] - - def to_gerber(self, settings): - return '%ADD{0}{1},{2}*%'.format(self.d, self.shape, - ','.join(['X'.join(e) for e in self.modifiers])) + self.modifiers = [tuple()] + + def to_inch(self): + if self.units == 'metric': + self.units = 'inch' + self.modifiers = [tuple([inch(x) for x in modifier]) + for modifier in self.modifiers] + + def to_metric(self): + if self.units == 'inch': + self.units = 'metric' + self.modifiers = [tuple([metric(x) for x in modifier]) + for modifier in self.modifiers] + + def to_gerber(self, settings=None): + if any(self.modifiers): + return '%ADD{0}{1},{2}*%'.format(self.d, self.shape, ','.join(['X'.join(["%.4g" % x for x in modifier]) for modifier in self.modifiers])) + else: + return '%ADD{0}{1}*%'.format(self.d, self.shape) def __str__(self): if self.shape == 'C': @@ -351,7 +366,7 @@ class ADParamStmt(ParamStmt): elif self.shape == 'R': shape = 'rectangle' elif self.shape == 'O': - shape = 'oblong' + shape = 'obround' else: shape = self.shape @@ -390,15 +405,97 @@ class AMParamStmt(ParamStmt): self.name = name self.macro = macro - def to_gerber(self): - return '%AM{0}*{1}*%'.format(self.name, self.macro) + self.instructions = self.read(macro) + self.primitives = [] + + def read(self, macro): + return read_macro(macro) + + def build(self, modifiers=[[]]): + self.primitives = [] + + for primitive in eval_macro(self.instructions, modifiers[0]): + if primitive[0] == '0': + self.primitives.append(AMCommentPrimitive.from_gerber(primitive)) + elif primitive[0] == '1': + self.primitives.append(AMCirclePrimitive.from_gerber(primitive)) + elif primitive[0:2] in ('2,', '20'): + self.primitives.append(AMVectorLinePrimitive.from_gerber(primitive)) + elif primitive[0:2] == '21': + self.primitives.append(AMCenterLinePrimitive.from_gerber(primitive)) + elif primitive[0:2] == '22': + self.primitives.append(AMLowerLeftLinePrimitive.from_gerber(primitive)) + elif primitive[0] == '4': + self.primitives.append(AMOutlinePrimitive.from_gerber(primitive)) + elif primitive[0] == '5': + self.primitives.append(AMPolygonPrimitive.from_gerber(primitive)) + elif primitive[0] == '6': + self.primitives.append(AMMoirePrimitive.from_gerber(primitive)) + elif primitive[0] == '7': + self.primitives.append( + AMThermalPrimitive.from_gerber(primitive)) + else: + self.primitives.append(AMUnsupportPrimitive.from_gerber(primitive)) + + return AMGroup(self.primitives, stmt=self, units=self.units) + + def to_inch(self): + if self.units == 'metric': + self.units = 'inch' + for primitive in self.primitives: + primitive.to_inch() + + def to_metric(self): + if self.units == 'inch': + self.units = 'metric' + for primitive in self.primitives: + primitive.to_metric() + + def to_gerber(self, settings=None): + return '%AM{0}*{1}%'.format(self.name, "".join([primitive.to_gerber() for primitive in self.primitives])) def __str__(self): return '<Aperture Macro %s: %s>' % (self.name, self.macro) +class ASParamStmt(ParamStmt): + """ AS - Axis Select. (Deprecated) + """ + @classmethod + def from_dict(cls, stmt_dict): + param = stmt_dict.get('param') + mode = stmt_dict.get('mode') + return cls(param, mode) + + def __init__(self, param, mode): + """ Initialize ASParamStmt class + + Parameters + ---------- + param : string + Parameter string. + + mode : string + Axis select. May be either 'AXBY' or 'AYBX' + + Returns + ------- + ParamStmt : ASParamStmt + Initialized ASParamStmt class. + + """ + ParamStmt.__init__(self, param) + self.mode = mode + + def to_gerber(self, settings=None): + return '%AS{0}*%'.format(self.mode) + + def __str__(self): + return ('<Axis Select: %s>' % self.mode) + + class INParamStmt(ParamStmt): - """ IN - Image Name Statement + """ IN - Image Name Statement (Deprecated) """ @classmethod def from_dict(cls, stmt_dict): @@ -424,13 +521,280 @@ class INParamStmt(ParamStmt): ParamStmt.__init__(self, param) self.name = name - def to_gerber(self): + def to_gerber(self, settings=None): return '%IN{0}*%'.format(self.name) def __str__(self): return '<Image Name: %s>' % self.name +class IPParamStmt(ParamStmt): + """ IP - Gerber Image Polarity Statement. (Deprecated) + """ + @classmethod + def from_dict(cls, stmt_dict): + param = stmt_dict.get('param') + ip = 'positive' if stmt_dict.get('ip') == 'POS' else 'negative' + return cls(param, ip) + + def __init__(self, param, ip): + """ Initialize IPParamStmt class + + Parameters + ---------- + param : string + Parameter string. + + ip : string + Image polarity. May be either'positive' or 'negative' + + Returns + ------- + ParamStmt : IPParamStmt + Initialized IPParamStmt class. + + """ + ParamStmt.__init__(self, param) + self.ip = ip + + def to_gerber(self, settings=None): + ip = 'POS' if self.ip == 'positive' else 'NEG' + return '%IP{0}*%'.format(ip) + + def __str__(self): + return ('<Image Polarity: %s>' % self.ip) + + +class IRParamStmt(ParamStmt): + """ IR - Image Rotation Param (Deprecated) + """ + @classmethod + def from_dict(cls, stmt_dict): + angle = int(stmt_dict['angle']) + return cls(stmt_dict['param'], angle) + + def __init__(self, param, angle): + """ Initialize IRParamStmt class + + Parameters + ---------- + param : string + Parameter code + + angle : int + Image angle + + Returns + ------- + ParamStmt : IRParamStmt + Initialized IRParamStmt class. + + """ + ParamStmt.__init__(self, param) + self.angle = angle + + def to_gerber(self, settings=None): + return '%IR{0}*%'.format(self.angle) + + def __str__(self): + return '<Image Angle: %s>' % self.angle + + +class MIParamStmt(ParamStmt): + """ MI - Image Mirror Param (Deprecated) + """ + @classmethod + def from_dict(cls, stmt_dict): + param = stmt_dict.get('param') + a = int(stmt_dict.get('a', 0)) + b = int(stmt_dict.get('b', 0)) + return cls(param, a, b) + + def __init__(self, param, a, b): + """ Initialize MIParamStmt class + + Parameters + ---------- + param : string + Parameter code + + a : int + Mirror for A output devices axis (0=disabled, 1=mirrored) + + b : int + Mirror for B output devices axis (0=disabled, 1=mirrored) + + Returns + ------- + ParamStmt : MIParamStmt + Initialized MIParamStmt class. + + """ + ParamStmt.__init__(self, param) + self.a = a + self.b = b + + def to_gerber(self, settings=None): + ret = "%MI" + if self.a is not None: + ret += "A{0}".format(self.a) + if self.b is not None: + ret += "B{0}".format(self.b) + ret += "*%" + return ret + + def __str__(self): + return '<Image Mirror: A=%d B=%d>' % (self.a, self.b) + + +class OFParamStmt(ParamStmt): + """ OF - Gerber Offset statement (Deprecated) + """ + + @classmethod + def from_dict(cls, stmt_dict): + param = stmt_dict.get('param') + a = float(stmt_dict.get('a', 0)) + b = float(stmt_dict.get('b', 0)) + return cls(param, a, b) + + def __init__(self, param, a, b): + """ Initialize OFParamStmt class + + Parameters + ---------- + param : string + Parameter + + a : float + Offset along the output device A axis + + b : float + Offset along the output device B axis + + Returns + ------- + ParamStmt : OFParamStmt + Initialized OFParamStmt class. + + """ + ParamStmt.__init__(self, param) + self.a = a + self.b = b + + def to_gerber(self, settings=None): + ret = '%OF' + if self.a is not None: + ret += 'A' + decimal_string(self.a, precision=5) + if self.b is not None: + ret += 'B' + decimal_string(self.b, precision=5) + return ret + '*%' + + def to_inch(self): + if self.units == 'metric': + self.units = 'inch' + if self.a is not None: + self.a = inch(self.a) + if self.b is not None: + self.b = inch(self.b) + + def to_metric(self): + if self.units == 'inch': + self.units = 'metric' + if self.a is not None: + self.a = metric(self.a) + if self.b is not None: + self.b = metric(self.b) + + def offset(self, x_offset=0, y_offset=0): + if self.a is not None: + self.a += x_offset + if self.b is not None: + self.b += y_offset + + def __str__(self): + offset_str = '' + if self.a is not None: + offset_str += ('X: %f ' % self.a) + if self.b is not None: + offset_str += ('Y: %f ' % self.b) + return ('<Offset: %s>' % offset_str) + + +class SFParamStmt(ParamStmt): + """ SF - Scale Factor Param (Deprecated) + """ + + @classmethod + def from_dict(cls, stmt_dict): + param = stmt_dict.get('param') + a = float(stmt_dict.get('a', 1)) + b = float(stmt_dict.get('b', 1)) + return cls(param, a, b) + + def __init__(self, param, a, b): + """ Initialize OFParamStmt class + + Parameters + ---------- + param : string + Parameter + + a : float + Scale factor for the output device A axis + + b : float + Scale factor for the output device B axis + + Returns + ------- + ParamStmt : SFParamStmt + Initialized SFParamStmt class. + + """ + ParamStmt.__init__(self, param) + self.a = a + self.b = b + + def to_gerber(self, settings=None): + ret = '%SF' + if self.a is not None: + ret += 'A' + decimal_string(self.a, precision=5) + if self.b is not None: + ret += 'B' + decimal_string(self.b, precision=5) + return ret + '*%' + + def to_inch(self): + if self.units == 'metric': + self.units = 'inch' + if self.a is not None: + self.a = inch(self.a) + if self.b is not None: + self.b = inch(self.b) + + def to_metric(self): + if self.units == 'inch': + self.units = 'metric' + if self.a is not None: + self.a = metric(self.a) + if self.b is not None: + self.b = metric(self.b) + + def offset(self, x_offset=0, y_offset=0): + if self.a is not None: + self.a += x_offset + if self.b is not None: + self.b += y_offset + + def __str__(self): + scale_factor = '' + if self.a is not None: + scale_factor += ('X: %g ' % self.a) + if self.b is not None: + scale_factor += ('Y: %g' % self.b) + return ('<Scale Factor: %s>' % scale_factor) + + class LNParamStmt(ParamStmt): """ LN - Level Name Statement (Deprecated) """ @@ -458,21 +822,58 @@ class LNParamStmt(ParamStmt): ParamStmt.__init__(self, param) self.name = name - def to_gerber(self): + def to_gerber(self, settings=None): return '%LN{0}*%'.format(self.name) def __str__(self): return '<Level Name: %s>' % self.name +class DeprecatedStmt(Statement): + """ Unimportant deprecated statement, will be parsed but not emitted. + """ + @classmethod + def from_gerber(cls, line): + return cls(line) + + def __init__(self, line): + """ Initialize DeprecatedStmt class + + Parameters + ---------- + line : string + Deprecated statement text + + Returns + ------- + DeprecatedStmt + Initialized DeprecatedStmt class. + + """ + Statement.__init__(self, "DEPRECATED") + self.line = line + + def to_gerber(self, settings=None): + return self.line + + def __str__(self): + return '<Deprecated Statement: \'%s\'>' % self.line + + class CoordStmt(Statement): """ Coordinate Data Block """ + OP_DRAW = 'D01' + OP_MOVE = 'D02' + OP_FLASH = 'D03' + + FUNC_LINEAR = 'G01' + FUNC_ARC_CW = 'G02' + FUNC_ARC_CCW = 'G03' + @classmethod def from_dict(cls, stmt_dict, settings): - zeros = settings.zero_suppression - format = settings.format function = stmt_dict['function'] x = stmt_dict.get('x') y = stmt_dict.get('y') @@ -481,18 +882,44 @@ class CoordStmt(Statement): op = stmt_dict.get('op') if x is not None: - x = parse_gerber_value(stmt_dict.get('x'), - format, zeros) + x = parse_gerber_value(stmt_dict.get('x'), settings.format, + settings.zero_suppression) if y is not None: - y = parse_gerber_value(stmt_dict.get('y'), - format, zeros) + y = parse_gerber_value(stmt_dict.get('y'), settings.format, + settings.zero_suppression) if i is not None: - i = parse_gerber_value(stmt_dict.get('i'), - format, zeros) + i = parse_gerber_value(stmt_dict.get('i'), settings.format, + settings.zero_suppression) if j is not None: - j = parse_gerber_value(stmt_dict.get('j'), - format, zeros) + j = parse_gerber_value(stmt_dict.get('j'), settings.format, + settings.zero_suppression) return cls(function, x, y, i, j, op, settings) + + @classmethod + def move(cls, func, point): + if point: + return cls(func, point[0], point[1], None, None, CoordStmt.OP_MOVE, None) + # No point specified, so just write the function. This is normally for ending a region (D02*) + return cls(func, None, None, None, None, CoordStmt.OP_MOVE, None) + + @classmethod + def line(cls, func, point): + return cls(func, point[0], point[1], None, None, CoordStmt.OP_DRAW, None) + + @classmethod + def mode(cls, func): + return cls(func, None, None, None, None, None, None) + + @classmethod + def arc(cls, func, point, center): + return cls(func, point[0], point[1], center[0], center[1], CoordStmt.OP_DRAW, None) + + @classmethod + def flash(cls, point): + if point: + return cls(None, point[0], point[1], None, None, CoordStmt.OP_FLASH, None) + else: + return cls(None, None, None, None, None, CoordStmt.OP_FLASH, None) def __init__(self, function, x, y, i, j, op, settings): """ Initialize CoordStmt class @@ -527,8 +954,6 @@ class CoordStmt(Statement): """ Statement.__init__(self, "COORD") - self.zero_suppression = settings.zero_suppression - self.format = settings.format self.function = function self.x = x self.y = y @@ -536,38 +961,76 @@ class CoordStmt(Statement): self.j = j self.op = op - def to_gerber(self): + def to_gerber(self, settings=None): ret = '' if self.function: ret += self.function - if self.x: - ret += 'X{0}'.format(write_gerber_value(self.x, self.zeros, - self.format)) - if self.y: - ret += 'Y{0}'.format(write_gerber_value(self.y, self. zeros, - self.format)) - if self.i: - ret += 'I{0}'.format(write_gerber_value(self.i, self.zeros, - self.format)) - if self.j: - ret += 'J{0}'.format(write_gerber_value(self.j, self.zeros, - self.format)) + if self.x is not None: + ret += 'X{0}'.format(write_gerber_value(self.x, settings.format, + settings.zero_suppression)) + if self.y is not None: + ret += 'Y{0}'.format(write_gerber_value(self.y, settings.format, + settings.zero_suppression)) + if self.i is not None: + ret += 'I{0}'.format(write_gerber_value(self.i, settings.format, + settings.zero_suppression)) + if self.j is not None: + ret += 'J{0}'.format(write_gerber_value(self.j, settings.format, + settings.zero_suppression)) if self.op: ret += self.op return ret + '*' + def to_inch(self): + if self.units == 'metric': + self.units = 'inch' + if self.x is not None: + self.x = inch(self.x) + if self.y is not None: + self.y = inch(self.y) + if self.i is not None: + self.i = inch(self.i) + if self.j is not None: + self.j = inch(self.j) + if self.function == "G71": + self.function = "G70" + + def to_metric(self): + if self.units == 'inch': + self.units = 'metric' + if self.x is not None: + self.x = metric(self.x) + if self.y is not None: + self.y = metric(self.y) + if self.i is not None: + self.i = metric(self.i) + if self.j is not None: + self.j = metric(self.j) + if self.function == "G70": + self.function = "G71" + + def offset(self, x_offset=0, y_offset=0): + if self.x is not None: + self.x += x_offset + if self.y is not None: + self.y += y_offset + if self.i is not None: + self.i += x_offset + if self.j is not None: + self.j += y_offset + def __str__(self): coord_str = '' if self.function: coord_str += 'Fn: %s ' % self.function - if self.x: - coord_str += 'X: %f ' % self.x - if self.y: - coord_str += 'Y: %f ' % self.y - if self.i: - coord_str += 'I: %f ' % self.i - if self.j: - coord_str += 'J: %f ' % self.j + if self.x is not None: + coord_str += 'X: %g ' % self.x + if self.y is not None: + coord_str += 'Y: %g ' % self.y + if self.i is not None: + coord_str += 'I: %g ' % self.i + if self.j is not None: + coord_str += 'J: %g ' % self.j if self.op: if self.op == 'D01': op = 'Lights On' @@ -580,17 +1043,32 @@ class CoordStmt(Statement): coord_str += 'Op: %s' % op return '<Coordinate Statement: %s>' % coord_str + + @property + def only_function(self): + """ + Returns if the statement only set the function. + """ + + # TODO I would like to refactor this so that the function is handled separately and then + # TODO this isn't required + return self.function != None and self.op == None and self.x == None and self.y == None and self.i == None and self.j == None class ApertureStmt(Statement): """ Aperture Statement """ - def __init__(self, d): + + def __init__(self, d, deprecated=None): Statement.__init__(self, "APERTURE") self.d = int(d) + self.deprecated = True if deprecated is not None and deprecated is not False else False - def to_gerber(self): - return 'G54D{0}*'.format(self.d) + def to_gerber(self, settings=None): + if self.deprecated: + return 'G54D{0}*'.format(self.d) + else: + return 'D{0}*'.format(self.d) def __str__(self): return '<Aperture: %d>' % self.d @@ -602,9 +1080,9 @@ class CommentStmt(Statement): def __init__(self, comment): Statement.__init__(self, "COMMENT") - self.comment = comment + self.comment = comment if comment is not None else "" - def to_gerber(self): + def to_gerber(self, settings=None): return 'G04{0}*'.format(self.comment) def __str__(self): @@ -614,10 +1092,11 @@ class CommentStmt(Statement): class EofStmt(Statement): """ EOF Statement """ + def __init__(self): Statement.__init__(self, "EOF") - def to_gerber(self): + def to_gerber(self, settings=None): return 'M02*' def __str__(self): @@ -625,6 +1104,14 @@ class EofStmt(Statement): class QuadrantModeStmt(Statement): + + @classmethod + def single(cls): + return cls('single-quadrant') + + @classmethod + def multi(cls): + return cls('multi-quadrant') @classmethod def from_gerber(cls, line): @@ -642,7 +1129,7 @@ class QuadrantModeStmt(Statement): or "multi-quadrant"') self.mode = mode - def to_gerber(self): + def to_gerber(self, settings=None): return 'G74*' if self.mode == 'single-quadrant' else 'G75*' @@ -653,6 +1140,14 @@ class RegionModeStmt(Statement): if 'G36' not in line and 'G37' not in line: raise ValueError('%s is not a valid region mode statement' % line) return (cls('on') if line[:3] == 'G36' else cls('off')) + + @classmethod + def on(cls): + return cls('on') + + @classmethod + def off(cls): + return cls('off') def __init__(self, mode): super(RegionModeStmt, self).__init__('RegionMode') @@ -661,16 +1156,20 @@ class RegionModeStmt(Statement): raise ValueError('Valid modes are "on" or "off"') self.mode = mode - def to_gerber(self): + def to_gerber(self, settings=None): return 'G36*' if self.mode == 'on' else 'G37*' class UnknownStmt(Statement): """ Unknown Statement """ + def __init__(self, line): Statement.__init__(self, "UNKNOWN") self.line = line - def to_gerber(self): + def to_gerber(self, settings=None): return self.line + + def __str__(self): + return '<Unknown Statement: \'%s\'>' % self.line diff --git a/gerber/ipc356.py b/gerber/ipc356.py new file mode 100644 index 0000000..7dadd22 --- /dev/null +++ b/gerber/ipc356.py @@ -0,0 +1,461 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be> +# Modified from parser.py by Paulo Henrique Silva <ph.silva@gmail.com> +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import math +import re +from .cam import CamFile, FileSettings +from .primitives import TestRecord + +# Net Name Variables +_NNAME = re.compile(r'^NNAME\d+$') + +# Board Edge Coordinates +_COORD = re.compile(r'X?(?P<x>[\d\s]*)?Y?(?P<y>[\d\s]*)?') + +_SM_FIELD = { + '0': 'none', + '1': 'primary side', + '2': 'secondary side', + '3': 'both'} + + +def read(filename): + """ Read data from filename and return an IPC_D_356 + Parameters + ---------- + filename : string + Filename of file to parse + + Returns + ------- + file : :class:`gerber.ipc356.IPC_D_356` + An IPC_D_356 object created from the specified file. + + """ + # File object should use settings from source file by default. + return IPC_D_356.from_file(filename) + + +class IPC_D_356(CamFile): + + @classmethod + def from_file(cls, filename): + parser = IPC_D_356_Parser() + return parser.parse(filename) + + def __init__(self, statements, settings, primitives=None, filename=None): + self.statements = statements + self.units = settings.units + self.angle_units = settings.angle_units + self.primitives = [TestRecord((rec.x_coord, rec.y_coord), rec.net_name, + rec.access) for rec in self.test_records] + self.filename = filename + + @property + def settings(self): + return FileSettings(units=self.units, angle_units=self.angle_units) + + @property + def comments(self): + return [record for record in self.statements + if isinstance(record, IPC356_Comment)] + + @property + def parameters(self): + return [record for record in self.statements + if isinstance(record, IPC356_Parameter)] + + @property + def test_records(self): + return [record for record in self.statements + if isinstance(record, IPC356_TestRecord)] + + @property + def nets(self): + nets = [] + for net in list(set([rec.net_name for rec in self.test_records + if rec.net_name is not None])): + adjacent_nets = set() + for record in self.adjacency_records: + if record.net == net: + adjacent_nets = adjacent_nets.update(record.adjacent_nets) + elif net in record.adjacent_nets: + adjacent_nets.add(record.net) + nets.append(IPC356_Net(net, adjacent_nets)) + return nets + + @property + def components(self): + return list(set([rec.id for rec in self.test_records + if rec.id is not None and rec.id != 'VIA'])) + + @property + def vias(self): + return [rec.id for rec in self.test_records if rec.id == 'VIA'] + + @property + def outlines(self): + return [stmt for stmt in self.statements + if isinstance(stmt, IPC356_Outline)] + + @property + def adjacency_records(self): + return [record for record in self.statements + if isinstance(record, IPC356_Adjacency)] + + def render(self, ctx, layer='both', filename=None): + for p in self.primitives: + if layer == 'both' and p.layer in ('top', 'bottom', 'both'): + ctx.render(p) + elif layer == 'top' and p.layer in ('top', 'both'): + ctx.render(p) + elif layer == 'bottom' and p.layer in ('bottom', 'both'): + ctx.render(p) + if filename is not None: + ctx.dump(filename) + + +class IPC_D_356_Parser(object): + # TODO: Allow multi-line statements (e.g. Altium board edge) + + def __init__(self): + self.units = 'inch' + self.angle_units = 'degrees' + self.statements = [] + self.nnames = {} + + @property + def settings(self): + return FileSettings(units=self.units, angle_units=self.angle_units) + + def parse(self, filename): + with open(filename, 'rU') as f: + oldline = '' + for line in f: + # Check for existing multiline data... + if oldline != '': + if len(line) and line[0] == '0': + oldline = oldline.rstrip('\r\n') + line[3:].rstrip() + else: + self._parse_line(oldline) + oldline = line + else: + oldline = line + self._parse_line(oldline) + + return IPC_D_356(self.statements, self.settings, filename=filename) + + def _parse_line(self, line): + if not len(line): + return + if line[0] == 'C': + # Comment + self.statements.append(IPC356_Comment.from_line(line)) + + elif line[0] == 'P': + # Parameter + p = IPC356_Parameter.from_line(line) + if p.parameter == 'UNITS': + if p.value in ('CUST', 'CUST 0'): + self.units = 'inch' + self.angle_units = 'degrees' + elif p.value == 'CUST 1': + self.units = 'metric' + self.angle_units = 'degrees' + elif p.value == 'CUST 2': + self.units = 'inch' + self.angle_units = 'radians' + self.statements.append(p) + if _NNAME.match(p.parameter): + # Add to list of net name variables + self.nnames[p.parameter] = p.value + + elif line[0] == '9': + self.statements.append(IPC356_EndOfFile()) + + elif line[0:3] in ('317', '327', '367'): + # Test Record + record = IPC356_TestRecord.from_line(line, self.settings) + + # Substitute net name variables + net = record.net_name + if (_NNAME.match(net) and net in self.nnames.keys()): + record.net_name = self.nnames[record.net_name] + self.statements.append(record) + + elif line[0:3] == '378': + # Conductor + self.statements.append( + IPC356_Conductor.from_line( + line, self.settings)) + + elif line[0:3] == '379': + # Net Adjacency + self.statements.append(IPC356_Adjacency.from_line(line)) + + elif line[0:3] == '389': + # Outline + self.statements.append( + IPC356_Outline.from_line( + line, self.settings)) + + +class IPC356_Comment(object): + + @classmethod + def from_line(cls, line): + if line[0] != 'C': + raise ValueError('Not a valid comment statment') + comment = line[2:].strip() + return cls(comment) + + def __init__(self, comment): + self.comment = comment + + def __repr__(self): + return '<IPC-D-356 Comment: %s>' % self.comment + + +class IPC356_Parameter(object): + + @classmethod + def from_line(cls, line): + if line[0] != 'P': + raise ValueError('Not a valid parameter statment') + splitline = line[2:].split() + parameter = splitline[0].strip() + value = ' '.join(splitline[1:]).strip() + return cls(parameter, value) + + def __init__(self, parameter, value): + self.parameter = parameter + self.value = value + + def __repr__(self): + return '<IPC-D-356 Parameter: %s=%s>' % (self.parameter, self.value) + + +class IPC356_TestRecord(object): + + @classmethod + def from_line(cls, line, settings): + offset = 0 + units = settings.units + angle = settings.angle_units + feature_types = {'1': 'through-hole', '2': 'smt', + '3': 'tooling-feature', '4': 'tooling-hole'} + access = ['both', 'top', 'layer2', 'layer3', 'layer4', 'layer5', + 'layer6', 'layer7', 'bottom'] + record = {} + line = line.strip() + if line[0] != '3': + raise ValueError('Not a valid test record statment') + record['feature_type'] = feature_types[line[1]] + + end = len(line) - 1 if len(line) < 18 else 17 + record['net_name'] = line[3:end].strip() + + if len(line) >= 27 and line[26] != '-': + offset = line[26:].find('-') + offset = 0 if offset == -1 else offset + end = len(line) - 1 if len(line) < (27 + offset) else (26 + offset) + record['id'] = line[20:end].strip() + + end = len(line) - 1 if len(line) < (32 + offset) else (31 + offset) + record['pin'] = (line[27 + offset:end].strip() if line[27 + offset:end].strip() != '' + else None) + + record['location'] = 'middle' if line[31 + offset] == 'M' else 'end' + if line[32 + offset] == 'D': + end = len(line) - 1 if len(line) < (38 + offset) else (37 + offset) + dia = int(line[33 + offset:end].strip()) + record['hole_diameter'] = (dia * 0.0001 if units == 'inch' + else dia * 0.001) + if len(line) >= (38 + offset): + record['plated'] = (line[37 + offset] == 'P') + + if len(line) >= (40 + offset): + end = len(line) - 1 if len(line) < (42 + offset) else (41 + offset) + record['access'] = access[int(line[39 + offset:end])] + + if len(line) >= (43 + offset): + end = len(line) - 1 if len(line) < (50 + offset) else (49 + offset) + coord = int(line[42 + offset:end].strip()) + record['x_coord'] = (coord * 0.0001 if units == 'inch' + else coord * 0.001) + + if len(line) >= (51 + offset): + end = len(line) - 1 if len(line) < (58 + offset) else (57 + offset) + coord = int(line[50 + offset:end].strip()) + record['y_coord'] = (coord * 0.0001 if units == 'inch' + else coord * 0.001) + + if len(line) >= (59 + offset): + end = len(line) - 1 if len(line) < (63 + offset) else (62 + offset) + dim = line[58 + offset:end].strip() + if dim != '': + record['rect_x'] = (int(dim) * 0.0001 if units == 'inch' + else int(dim) * 0.001) + + if len(line) >= (64 + offset): + end = len(line) - 1 if len(line) < (68 + offset) else (67 + offset) + dim = line[63 + offset:end].strip() + if dim != '': + record['rect_y'] = (int(dim) * 0.0001 if units == 'inch' + else int(dim) * 0.001) + + if len(line) >= (69 + offset): + end = len(line) - 1 if len(line) < (72 + offset) else (71 + offset) + rot = line[68 + offset:end].strip() + if rot != '': + record['rect_rotation'] = (int(rot) if angle == 'degrees' + else math.degrees(rot)) + + if len(line) >= (74 + offset): + end = 74 + offset + sm_info = line[73 + offset:end].strip() + record['soldermask_info'] = _SM_FIELD.get(sm_info) + + if len(line) >= (76 + offset): + end = len(line) - 1 if len(line) < (80 + offset) else 79 + offset + record['optional_info'] = line[75 + offset:end] + + return cls(**record) + + def __init__(self, **kwargs): + for key in kwargs: + setattr(self, key, kwargs[key]) + + def __repr__(self): + return '<IPC-D-356 %s Test Record: %s>' % (self.net_name, + self.feature_type) + + +class IPC356_Outline(object): + + @classmethod + def from_line(cls, line, settings): + type = line[3:17].strip() + scale = 0.0001 if settings.units == 'inch' else 0.001 + points = [] + x = 0 + y = 0 + coord_strings = line.strip().split()[1:] + for coord in coord_strings: + coord_dict = _COORD.match(coord).groupdict() + x = int(coord_dict['x']) if coord_dict['x'] is not '' else x + y = int(coord_dict['y']) if coord_dict['y'] is not '' else y + points.append((x * scale, y * scale)) + return cls(type, points) + + def __init__(self, type, points): + self.type = type + self.points = points + + def __repr__(self): + return '<IPC-D-356 %s Outline Definition>' % self.type + + +class IPC356_Conductor(object): + + @classmethod + def from_line(cls, line, settings): + if line[0:3] != '378': + raise ValueError('Not a valid IPC-D-356 Conductor statement') + + scale = 0.0001 if settings.units == 'inch' else 0.001 + net_name = line[3:17].strip() + layer = int(line[19:21]) + + # Parse out aperture definiting + raw_aperture = line[22:].split()[0] + aperture_dict = _COORD.match(raw_aperture).groupdict() + x = 0 + y = 0 + x = int(aperture_dict['x']) * \ + scale if aperture_dict['x'] is not '' else None + y = int(aperture_dict['y']) * \ + scale if aperture_dict['y'] is not '' else None + aperture = (x, y) + + # Parse out conductor shapes + shapes = [] + coord_list = ' '.join(line[22:].split()[1:]) + raw_shapes = coord_list.split('*') + for rshape in raw_shapes: + x = 0 + y = 0 + shape = [] + coords = rshape.split() + for coord in coords: + coord_dict = _COORD.match(coord).groupdict() + x = int(coord_dict['x']) if coord_dict['x'] is not '' else x + y = int(coord_dict['y']) if coord_dict['y'] is not '' else y + shape.append((x * scale, y * scale)) + shapes.append(tuple(shape)) + return cls(net_name, layer, aperture, tuple(shapes)) + + def __init__(self, net_name, layer, aperture, shapes): + self.net_name = net_name + self.layer = layer + self.aperture = aperture + self.shapes = shapes + + def __repr__(self): + return '<IPC-D-356 %s Conductor Record>' % self.net_name + + +class IPC356_Adjacency(object): + + @classmethod + def from_line(cls, line): + if line[0:3] != '379': + raise ValueError('Not a valid IPC-D-356 Conductor statement') + nets = line[3:].strip().split() + + return cls(nets[0], nets[1:]) + + def __init__(self, net, adjacent_nets): + self.net = net + self.adjacent_nets = adjacent_nets + + def __repr__(self): + return '<IPC-D-356 %s Adjacency Record>' % self.net + + +class IPC356_EndOfFile(object): + + def __init__(self): + pass + + def to_netlist(self): + return '999' + + def __repr__(self): + return '<IPC-D-356 EOF>' + + +class IPC356_Net(object): + + def __init__(self, name, adjacent_nets): + self.name = name + self.adjacent_nets = set( + adjacent_nets) if adjacent_nets is not None else set() + + def __repr__(self): + return '<IPC-D-356 Net %s>' % self.name diff --git a/gerber/layers.py b/gerber/layers.py index b10cf16..29e452b 100644 --- a/gerber/layers.py +++ b/gerber/layers.py @@ -15,40 +15,217 @@ # See the License for the specific language governing permissions and
# limitations under the License.
-top_copper_ext = ['gtl', 'cmp', 'top', ]
-top_copper_name = ['art01', 'top', 'GTL', 'layer1', 'soldcom', 'comp', ]
-
-bottom_copper_ext = ['gbl', 'sld', 'bot', 'sol', ]
-bottom_coppper_name = ['art02', 'bottom', 'bot', 'GBL', 'layer2', 'soldsold', ]
-
-internal_layer_ext = ['in', 'gt1', 'gt2', 'gt3', 'gt4', 'gt5', 'gt6', 'g1',
- 'g2', 'g3', 'g4', 'g5', 'g6', ]
-internal_layer_name = ['art', 'internal']
-
-power_plane_name = ['pgp', 'pwr', ]
-ground_plane_name = ['gp1', 'gp2', 'gp3', 'gp4', 'gt5', 'gp6', 'gnd',
- 'ground', ]
-
-top_silk_ext = ['gto', 'sst', 'plc', 'ts', 'skt', ]
-top_silk_name = ['sst01', 'topsilk', 'silk', 'slk', 'sst', ]
-
-bottom_silk_ext = ['gbo', 'ssb', 'pls', 'bs', 'skb', ]
-bottom_silk_name = ['sst', 'bsilk', 'ssb', 'botsilk', ]
-
-top_mask_ext = ['gts', 'stc', 'tmk', 'smt', 'tr', ]
-top_mask_name = ['sm01', 'cmask', 'tmask', 'mask1', 'maskcom', 'topmask',
- 'mst', ]
-
-bottom_mask_ext = ['gbs', 'sts', 'bmk', 'smb', 'br', ]
-bottom_mask_name = ['sm', 'bmask', 'mask2', 'masksold', 'botmask', 'msb', ]
-
-top_paste_ext = ['gtp', 'tm']
-top_paste_name = ['sp01', 'toppaste', 'pst']
-
-bottom_paste_ext = ['gbp', 'bm']
-bottom_paste_name = ['sp02', 'botpaste', 'psb']
-
-board_outline_ext = ['gko']
-board_outline_name = ['BDR', 'border', 'out', ]
-
-
+import os
+import re
+from collections import namedtuple
+
+from .excellon import ExcellonFile
+from .ipc356 import IPC_D_356
+
+
+Hint = namedtuple('Hint', 'layer ext name')
+
+hints = [
+ Hint(layer='top',
+ ext=['gtl', 'cmp', 'top', ],
+ name=['art01', 'top', 'GTL', 'layer1', 'soldcom', 'comp', ]
+ ),
+ Hint(layer='bottom',
+ ext=['gbl', 'sld', 'bot', 'sol', 'bottom', ],
+ name=['art02', 'bottom', 'bot', 'GBL', 'layer2', 'soldsold', ]
+ ),
+ Hint(layer='internal',
+ ext=['in', 'gt1', 'gt2', 'gt3', 'gt4', 'gt5', 'gt6', 'g1',
+ 'g2', 'g3', 'g4', 'g5', 'g6', ],
+ name=['art', 'internal', 'pgp', 'pwr', 'gp1', 'gp2', 'gp3', 'gp4',
+ 'gt5', 'gp6', 'gnd', 'ground', ]
+ ),
+ Hint(layer='topsilk',
+ ext=['gto', 'sst', 'plc', 'ts', 'skt', 'topsilk', ],
+ name=['sst01', 'topsilk', 'silk', 'slk', 'sst', ]
+ ),
+ Hint(layer='bottomsilk',
+ ext=['gbo', 'ssb', 'pls', 'bs', 'skb', 'bottomsilk', ],
+ name=['bsilk', 'ssb', 'botsilk', ]
+ ),
+ Hint(layer='topmask',
+ ext=['gts', 'stc', 'tmk', 'smt', 'tr', 'topmask', ],
+ name=['sm01', 'cmask', 'tmask', 'mask1', 'maskcom', 'topmask',
+ 'mst', ]
+ ),
+ Hint(layer='bottommask',
+ ext=['gbs', 'sts', 'bmk', 'smb', 'br', 'bottommask', ],
+ name=['sm', 'bmask', 'mask2', 'masksold', 'botmask', 'msb', ]
+ ),
+ Hint(layer='toppaste',
+ ext=['gtp', 'tm', 'toppaste', ],
+ name=['sp01', 'toppaste', 'pst']
+ ),
+ Hint(layer='bottompaste',
+ ext=['gbp', 'bm', 'bottompaste', ],
+ name=['sp02', 'botpaste', 'psb']
+ ),
+ Hint(layer='outline',
+ ext=['gko', 'outline', ],
+ name=['BDR', 'border', 'out', ]
+ ),
+ Hint(layer='ipc_netlist',
+ ext=['ipc'],
+ name=[],
+ ),
+]
+
+
+def guess_layer_class(filename):
+ try:
+ directory, name = os.path.split(filename)
+ name, ext = os.path.splitext(name.lower())
+ for hint in hints:
+ patterns = [r'^(\w*[.-])*{}([.-]\w*)?$'.format(x) for x in hint.name]
+ if ext[1:] in hint.ext or any(re.findall(p, name, re.IGNORECASE) for p in patterns):
+ return hint.layer
+ except:
+ pass
+ return 'unknown'
+
+
+def sort_layers(layers):
+ layer_order = ['outline', 'toppaste', 'topsilk', 'topmask', 'top',
+ 'internal', 'bottom', 'bottommask', 'bottomsilk',
+ 'bottompaste', 'drill', ]
+ output = []
+ drill_layers = [layer for layer in layers if layer.layer_class == 'drill']
+ internal_layers = list(sorted([layer for layer in layers
+ if layer.layer_class == 'internal']))
+
+ for layer_class in layer_order:
+ if layer_class == 'internal':
+ output += internal_layers
+ elif layer_class == 'drill':
+ output += drill_layers
+ else:
+ for layer in layers:
+ if layer.layer_class == layer_class:
+ output.append(layer)
+ return output
+
+
+class PCBLayer(object):
+ """ Base class for PCB Layers
+
+ Parameters
+ ----------
+ source : CAMFile
+ CAMFile representing the layer
+
+
+ Attributes
+ ----------
+ filename : string
+ Source Filename
+
+ """
+ @classmethod
+ def from_gerber(cls, camfile):
+ filename = camfile.filename
+ layer_class = guess_layer_class(filename)
+ if isinstance(camfile, ExcellonFile) or (layer_class == 'drill'):
+ return DrillLayer.from_gerber(camfile)
+ elif layer_class == 'internal':
+ return InternalLayer.from_gerber(camfile)
+ if isinstance(camfile, IPC_D_356):
+ layer_class = 'ipc_netlist'
+ return cls(filename, layer_class, camfile)
+
+ def __init__(self, filename=None, layer_class=None, cam_source=None, **kwargs):
+ super(PCBLayer, self).__init__(**kwargs)
+ self.filename = filename
+ self.layer_class = layer_class
+ self.cam_source = cam_source
+ self.surface = None
+ self.primitives = cam_source.primitives if cam_source is not None else []
+
+ @property
+ def bounds(self):
+ if self.cam_source is not None:
+ return self.cam_source.bounds
+ else:
+ return None
+
+ def __repr__(self):
+ return '<PCBLayer: {}>'.format(self.layer_class)
+
+class DrillLayer(PCBLayer):
+ @classmethod
+ def from_gerber(cls, camfile):
+ return cls(camfile.filename, camfile)
+
+ def __init__(self, filename=None, cam_source=None, layers=None, **kwargs):
+ super(DrillLayer, self).__init__(filename, 'drill', cam_source, **kwargs)
+ self.layers = layers if layers is not None else ['top', 'bottom']
+
+
+class InternalLayer(PCBLayer):
+
+ @classmethod
+ def from_gerber(cls, camfile):
+ filename = camfile.filename
+ try:
+ order = int(re.search(r'\d+', filename).group())
+ except:
+ order = 0
+ return cls(filename, camfile, order)
+
+ def __init__(self, filename=None, cam_source=None, order=0, **kwargs):
+ super(InternalLayer, self).__init__(filename, 'internal', cam_source, **kwargs)
+ self.order = order
+
+ def __eq__(self, other):
+ if not hasattr(other, 'order'):
+ raise TypeError()
+ return (self.order == other.order)
+
+ def __ne__(self, other):
+ if not hasattr(other, 'order'):
+ raise TypeError()
+ return (self.order != other.order)
+
+ def __gt__(self, other):
+ if not hasattr(other, 'order'):
+ raise TypeError()
+ return (self.order > other.order)
+
+ def __lt__(self, other):
+ if not hasattr(other, 'order'):
+ raise TypeError()
+ return (self.order < other.order)
+
+ def __ge__(self, other):
+ if not hasattr(other, 'order'):
+ raise TypeError()
+ return (self.order >= other.order)
+
+ def __le__(self, other):
+ if not hasattr(other, 'order'):
+ raise TypeError()
+ return (self.order <= other.order)
+
+
+class LayerSet(object):
+
+ def __init__(self, name, layers, **kwargs):
+ super(LayerSet, self).__init__(**kwargs)
+ self.name = name
+ self.layers = list(layers)
+
+ def __len__(self):
+ return len(self.layers)
+
+ def __getitem__(self, item):
+ return self.layers[item]
+
+ def to_render(self):
+ return self.layers
+
+ def apply_theme(self, theme):
+ pass
diff --git a/gerber/ncparam/allegro.py b/gerber/ncparam/allegro.py new file mode 100644 index 0000000..a67bcf1 --- /dev/null +++ b/gerber/ncparam/allegro.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright 2015 Garret Fick <garret@ficksworkshop.com> + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Allegro File module +==================== +**Excellon file classes** + +Extra parsers for allegro misc files that can be useful when the Excellon file doesn't contain parameter information +""" + diff --git a/gerber/operations.py b/gerber/operations.py new file mode 100644 index 0000000..d06876e --- /dev/null +++ b/gerber/operations.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# copyright 2015 Hamilton Kibbe <ham@hamiltonkib.be> +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +CAM File Operations +=================== +**Transformations and other operations performed on Gerber and Excellon files** + +""" +import copy + + +def to_inch(cam_file): + """ Convert Gerber or Excellon file units to imperial + + Parameters + ---------- + cam_file : :class:`gerber.cam.CamFile` subclass + Gerber or Excellon file to convert + + Returns + ------- + cam_file : :class:`gerber.cam.CamFile` subclass + A deep copy of the source file with units converted to imperial. + """ + cam_file = copy.deepcopy(cam_file) + cam_file.to_inch() + return cam_file + + +def to_metric(cam_file): + """ Convert Gerber or Excellon file units to metric + + Parameters + ---------- + cam_file : :class:`gerber.cam.CamFile` subclass + Gerber or Excellon file to convert + + Returns + ------- + cam_file : :class:`gerber.cam.CamFile` subclass + A deep copy of the source file with units converted to metric. + """ + cam_file = copy.deepcopy(cam_file) + cam_file.to_metric() + return cam_file + + +def offset(cam_file, x_offset, y_offset): + """ Offset a Cam file by a specified amount in the X and Y directions. + + Parameters + ---------- + cam_file : :class:`gerber.cam.CamFile` subclass + Gerber or Excellon file to offset + + x_offset : float + Amount to offset the file in the X direction + + y_offset : float + Amount to offset the file in the Y direction + + Returns + ------- + cam_file : :class:`gerber.cam.CamFile` subclass + An offset deep copy of the source file. + """ + cam_file = copy.deepcopy(cam_file) + cam_file.offset(x_offset, y_offset) + return cam_file + + +def scale(cam_file, x_scale, y_scale): + """ Scale a Cam file by a specified amount in the X and Y directions. + + Parameters + ---------- + cam_file : :class:`gerber.cam.CamFile` subclass + Gerber or Excellon file to scale + + x_scale : float + X-axis scale factor + + y_scale : float + Y-axis scale factor + + Returns + ------- + cam_file : :class:`gerber.cam.CamFile` subclass + An scaled deep copy of the source file. + """ + # TODO + pass + + +def rotate(cam_file, angle): + """ Rotate a Cam file a specified amount about the origin. + + Parameters + ---------- + cam_file : :class:`gerber.cam.CamFile` subclass + Gerber or Excellon file to rotate + + angle : float + Angle to rotate the file in degrees. + + Returns + ------- + cam_file : :class:`gerber.cam.CamFile` subclass + An rotated deep copy of the source file. + """ + # TODO + pass diff --git a/gerber/pcb.py b/gerber/pcb.py new file mode 100644 index 0000000..92a1f28 --- /dev/null +++ b/gerber/pcb.py @@ -0,0 +1,101 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# copyright 2015 Hamilton Kibbe <ham@hamiltonkib.be> +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import os +from .exceptions import ParseError +from .layers import PCBLayer, LayerSet, sort_layers +from .common import read as gerber_read +from .utils import listdir + + +class PCB(object): + + @classmethod + def from_directory(cls, directory, board_name=None, verbose=False): + layers = [] + names = set() + # Validate + directory = os.path.abspath(directory) + if not os.path.isdir(directory): + raise TypeError('{} is not a directory.'.format(directory)) + # Load gerber files + for filename in listdir(directory, True, True): + try: + camfile = gerber_read(os.path.join(directory, filename)) + layer = PCBLayer.from_gerber(camfile) + layers.append(layer) + names.add(os.path.splitext(filename)[0]) + if verbose: + print('Added {} layer <{}>'.format(layer.layer_class, filename)) + except ParseError: + if verbose: + print('Skipping file {}'.format(filename)) + # Try to guess board name + if board_name is None: + if len(names) == 1: + board_name = names.pop() + else: + board_name = os.path.basename(directory) + # Return PCB + return cls(layers, board_name) + + def __init__(self, layers, name=None): + self.layers = sort_layers(layers) + self.name = name + + def __len__(self): + return len(self.layers) + + @property + def top_layers(self): + board_layers = [l for l in reversed(self.layers) if l.layer_class in + ('topsilk', 'topmask', 'top')] + drill_layers = [l for l in self.drill_layers if 'top' in l.layers] + return board_layers + drill_layers + + @property + def bottom_layers(self): + board_layers = [l for l in self.layers if l.layer_class in + ('bottomsilk', 'bottommask', 'bottom')] + drill_layers = [l for l in self.drill_layers if 'bottom' in l.layers] + return board_layers + drill_layers + + @property + def drill_layers(self): + return [l for l in self.layers if l.layer_class == 'drill'] + + @property + def copper_layers(self): + return [layer for layer in self.layers if layer.layer_class in + ('top', 'bottom', 'internal')] + + @property + def layer_count(self): + """ Number of *COPPER* layers + """ + return len([l for l in self.layers if l.layer_class in + ('top', 'bottom', 'internal')]) + + @property + def board_bounds(self): + for layer in self.layers: + if layer.layer_class == 'outline': + return layer.bounds + for layer in self.layers: + if layer.layer_class == 'top': + return layer.bounds diff --git a/gerber/primitives.py b/gerber/primitives.py index b3869e1..a291c26 100644 --- a/gerber/primitives.py +++ b/gerber/primitives.py @@ -1,7 +1,7 @@ #! /usr/bin/env python
# -*- coding: utf-8 -*-
-# copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be>
+# copyright 2016 Hamilton Kibbe <ham@hamiltonkib.be>
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -14,223 +14,1561 @@ # 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.
+
+
+ import math
-from operator import sub
+from operator import add
+from itertools import combinations
+from .utils import validate_coordinates, inch, metric, convex_hull, rotate_point, nearly_equal + +
class Primitive(object):
-
- def __init__(self, level_polarity='dark'):
+ """ Base class for all Cam file primitives
+
+ Parameters
+ ---------
+ level_polarity : string
+ Polarity of the parameter. May be 'dark' or 'clear'. Dark indicates
+ a "positive" primitive, i.e. indicating where coppper should remain,
+ and clear indicates a negative primitive, such as where copper should
+ be removed. clear primitives are often used to create cutouts in region
+ pours.
+
+ rotation : float
+ Rotation of a primitive about its origin in degrees. Positive rotation
+ is counter-clockwise as viewed from the board top.
+
+ units : string
+ Units in which primitive was defined. 'inch' or 'metric'
+
+ net_name : string
+ Name of the electrical net the primitive belongs to
+ """
+
+ def __init__(self, level_polarity='dark', rotation=0, units=None, net_name=None):
self.level_polarity = level_polarity
+ self.net_name = net_name + self._to_convert = list() + self.id = id + self._memoized = list()
+ self._units = units
+ self._rotation = rotation
+ self._cos_theta = math.cos(math.radians(rotation))
+ self._sin_theta = math.sin(math.radians(rotation))
+ self._bounding_box = None
+ self._vertices = None
+ self._segments = None + @property
+ def flashed(self):
+ '''Is this a flashed primitive'''
+
+ raise NotImplementedError('Is flashed must be '
+ 'implemented in subclass') +
+ def __eq__(self, other):
+ return self.__dict__ == other.__dict__
+
+ @property
+ def units(self):
+ return self._units +
+ @units.setter
+ def units(self, value):
+ self._changed()
+ self._units = value
+
+ @property
+ def rotation(self):
+ return self._rotation
+ + @rotation.setter
+ def rotation(self, value):
+ self._changed()
+ self._rotation = value
+ self._cos_theta = math.cos(math.radians(value))
+ self._sin_theta = math.sin(math.radians(value))
+
+ @property
+ def vertices(self):
+ return None
+
+ @property
+ def segments(self):
+ if self._segments is None:
+ if self.vertices is not None and len(self.vertices):
+ self._segments = [segment for segment in
+ combinations(self.vertices, 2)]
+ return self._segments
+ + @property
def bounding_box(self):
- """ Calculate bounding box
+ """ Calculate axis-aligned bounding box
will be helpful for sweep & prune during DRC clearance checks.
Return ((min x, max x), (min y, max y))
"""
+ raise NotImplementedError('Bounding box calculation must be '
+ 'implemented in subclass')
+
+ @property
+ def bounding_box_no_aperture(self):
+ """ Calculate bouxing box without considering the aperture
+
+ for most objects, this is the same as the bounding_box, but is different for
+ Lines and Arcs (which are not flashed)
+
+ Return ((min x, max x), (min y, max y))
+ """
+ return self.bounding_box
+
+ def to_inch(self):
+ """ Convert primitive units to inches.
+ """
+ if self.units == 'metric':
+ self.units = 'inch'
+ for attr, value in [(attr, getattr(self, attr))
+ for attr in self._to_convert]:
+ if hasattr(value, 'to_inch'):
+ value.to_inch()
+ else:
+ try:
+ if len(value) > 1:
+ if hasattr(value[0], 'to_inch'):
+ for v in value:
+ v.to_inch()
+ elif isinstance(value[0], tuple):
+ setattr(self, attr,
+ [tuple(map(inch, point))
+ for point in value])
+ else:
+ setattr(self, attr, tuple(map(inch, value)))
+ except:
+ if value is not None:
+ setattr(self, attr, inch(value))
+
+ def to_metric(self):
+ """ Convert primitive units to metric.
+ """
+ if self.units == 'inch':
+ self.units = 'metric'
+ for attr, value in [(attr, getattr(self, attr))
+ for attr in self._to_convert]:
+ if hasattr(value, 'to_metric'):
+ value.to_metric()
+ else:
+ try:
+ if len(value) > 1:
+ if hasattr(value[0], 'to_metric'):
+ for v in value:
+ v.to_metric()
+ elif isinstance(value[0], tuple):
+ setattr(self, attr,
+ [tuple(map(metric, point))
+ for point in value])
+ else:
+ setattr(self, attr, tuple(map(metric, value)))
+ except:
+ if value is not None:
+ setattr(self, attr, metric(value))
+ + 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
+ """
+ if hasattr(self, 'position'):
+ self._changed()
+ self.position = tuple([coord + offset for coord, offset
+ in zip(self.position,
+ (x_offset, y_offset))])
+
+ def to_statement(self):
pass
+ def _changed(self):
+ """ Clear memoized properties.
+
+ Forces a recalculation next time any memoized propery is queried.
+ This must be called from a subclass every time a parameter that affects
+ a memoized property is changed. The easiest way to do this is to call
+ _changed() from property.setter methods.
+ """
+ self._bounding_box = None
+ self._vertices = None
+ self._segments = None
+ for attr in self._memoized: + setattr(self, attr, None)
class Line(Primitive):
"""
"""
- def __init__(self, start, end, width, level_polarity='dark'):
- super(Line, self).__init__(level_polarity)
- self.start = start
- self.end = end
- self.width = width
+
+ def __init__(self, start, end, aperture, **kwargs):
+ super(Line, self).__init__(**kwargs)
+ self._start = start
+ self._end = end
+ self.aperture = aperture
+ self._to_convert = ['start', 'end', 'aperture']
+
+ @property
+ def flashed(self):
+ return False
+
+ @property
+ def start(self):
+ return self._start
+
+ @start.setter
+ def start(self, value):
+ self._changed()
+ self._start = value
+
+ @property
+ def end(self):
+ return self._end
+
+ @end.setter
+ def end(self, value):
+ self._changed()
+ self._end = value
@property
def angle(self):
- delta_x, delta_y = tuple(map(sub, end, start))
- angle = degrees(math.tan(delta_y/delta_x))
+ delta_x, delta_y = tuple(
+ [end - start for end, start in zip(self.end, self.start)])
+ angle = math.atan2(delta_y, delta_x)
return angle
+ @property + def bounding_box(self): + if self._bounding_box is None:
+ if isinstance(self.aperture, Circle):
+ width_2 = self.aperture.radius
+ height_2 = width_2
+ else:
+ width_2 = self.aperture.width / 2.
+ height_2 = self.aperture.height / 2.
+ min_x = min(self.start[0], self.end[0]) - width_2
+ max_x = max(self.start[0], self.end[0]) + width_2
+ min_y = min(self.start[1], self.end[1]) - height_2
+ 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(self):
- width_2 = self.width / 2.
- min_x = min(self.start[0], self.end[0]) - width_2
- max_x = max(self.start[0], self.end[0]) + width_2
- min_y = min(self.start[1], self.end[1]) - width_2
- max_y = max(self.start[1], self.end[1]) + width_2
- return ((min_x, max_x), (min_y, max_y))
+ def bounding_box_no_aperture(self):
+ '''Gets the bounding box without the aperture'''
+ min_x = min(self.start[0], self.end[0])
+ max_x = max(self.start[0], self.end[0])
+ min_y = min(self.start[1], self.end[1])
+ max_y = max(self.start[1], self.end[1])
+ return ((min_x, max_x), (min_y, max_y)) +
+ @property
+ def vertices(self):
+ if self._vertices is None:
+ if isinstance(self.aperture, Rectangle):
+ start = self.start
+ end = self.end
+ width = self.aperture.width
+ height = self.aperture.height
+
+ # Find all the corners of the start and end position
+ start_ll = (start[0] - (width / 2.), start[1] - (height / 2.))
+ start_lr = (start[0] + (width / 2.), start[1] - (height / 2.))
+ start_ul = (start[0] - (width / 2.), start[1] + (height / 2.))
+ start_ur = (start[0] + (width / 2.), start[1] + (height / 2.))
+ end_ll = (end[0] - (width / 2.), end[1] - (height / 2.))
+ end_lr = (end[0] + (width / 2.), end[1] - (height / 2.))
+ end_ul = (end[0] - (width / 2.), end[1] + (height / 2.))
+ end_ur = (end[0] + (width / 2.), end[1] + (height / 2.))
+ # 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):
+ 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))])
+
+ def equivalent(self, other, offset):
+
+ if not isinstance(other, Line):
+ return False
+
+ equiv_start = tuple(map(add, other.start, offset))
+ equiv_end = tuple(map(add, other.end, offset)) + +
+ return nearly_equal(self.start, equiv_start) and nearly_equal(self.end, equiv_end)
class Arc(Primitive):
"""
"""
- def __init__(self, start, end, center, direction, width, level_polarity='dark'):
- super(Arc, self).__init__(level_polarity)
- self.start = start
- self.end = end
- self.center = center
+ + def __init__(self, start, end, center, direction, aperture, quadrant_mode, **kwargs): + super(Arc, self).__init__(**kwargs)
+ self._start = start
+ self._end = end
+ self._center = center
self.direction = direction
- self.width = width
+ self.aperture = aperture
+ self._quadrant_mode = quadrant_mode
+ self._to_convert = ['start', 'end', 'center', 'aperture']
+
+ @property
+ def flashed(self):
+ return False
+
+ @property
+ def start(self):
+ return self._start
+
+ @start.setter
+ def start(self, value):
+ self._changed()
+ self._start = value
+
+ @property
+ def end(self):
+ return self._end
+
+ @end.setter
+ def end(self, value):
+ self._changed()
+ self._end = value
+
+ @property
+ def center(self):
+ return self._center
+
+ @center.setter
+ def center(self, value):
+ self._changed()
+ self._center = value
+
+ @property
+ def quadrant_mode(self):
+ return self._quadrant_mode
+
+ @quadrant_mode.setter
+ def quadrant_mode(self, quadrant_mode):
+ self._changed()
+ self._quadrant_mode = quadrant_mode
+
+ @property
+ def radius(self):
+ dy, dx = tuple([start - center for start, center
+ in zip(self.start, self.center)])
+ return math.sqrt(dy ** 2 + dx ** 2)
@property
def start_angle(self):
- dy, dx = map(sub, self.start, self.center)
- return math.atan2(dy, dx)
+ dy, dx = tuple([start - center for start, center
+ in zip(self.start, self.center)])
+ return math.atan2(dx, dy)
@property
def end_angle(self):
- dy, dx = map(sub, self.end, self.center)
- return math.atan2(dy, dx)
+ dy, dx = tuple([end - center for end, center
+ in zip(self.end, self.center)])
+ return math.atan2(dx, dy)
+
+ @property
+ def sweep_angle(self):
+ two_pi = 2 * math.pi
+ theta0 = (self.start_angle + two_pi) % two_pi
+ theta1 = (self.end_angle + two_pi) % two_pi
+ if self.direction == 'counterclockwise':
+ return abs(theta1 - theta0)
+ else:
+ theta0 += two_pi
+ return abs(theta0 - theta1) % two_pi
@property
def bounding_box(self):
- pass
+ if self._bounding_box is None:
+ two_pi = 2 * math.pi
+ theta0 = (self.start_angle + two_pi) % two_pi
+ theta1 = (self.end_angle + two_pi) % two_pi
+ points = [self.start, self.end]
+ if self.direction == 'counterclockwise':
+ # Passes through 0 degrees
+ if theta0 > theta1:
+ points.append((self.center[0] + self.radius, self.center[1]))
+ # Passes through 90 degrees
+ if theta0 <= math.pi / \
+ 2. and (theta1 >= math.pi / 2. or theta1 < theta0):
+ points.append((self.center[0], self.center[1] + self.radius))
+ # Passes through 180 degrees
+ if theta0 <= math.pi and (theta1 >= math.pi or theta1 < theta0):
+ points.append((self.center[0] - self.radius, self.center[1]))
+ # Passes through 270 degrees
+ if theta0 <= math.pi * \
+ 1.5 and (theta1 >= math.pi * 1.5 or theta1 < theta0):
+ points.append((self.center[0], self.center[1] - self.radius))
+ else:
+ # Passes through 0 degrees
+ if theta1 > theta0:
+ points.append((self.center[0] + self.radius, self.center[1]))
+ # Passes through 90 degrees
+ if theta1 <= math.pi / \
+ 2. and (theta0 >= math.pi / 2. or theta0 < theta1):
+ points.append((self.center[0], self.center[1] + self.radius))
+ # Passes through 180 degrees
+ if theta1 <= math.pi and (theta0 >= math.pi or theta0 < theta1):
+ points.append((self.center[0] - self.radius, self.center[1]))
+ # Passes through 270 degrees
+ if theta1 <= math.pi * \
+ 1.5 and (theta0 >= math.pi * 1.5 or theta0 < theta1):
+ points.append((self.center[0], self.center[1] - self.radius))
+ x, y = zip(*points)
+ min_x = min(x) - self.aperture.radius
+ max_x = max(x) + self.aperture.radius
+ 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 +
+ @property
+ def bounding_box_no_aperture(self):
+ '''Gets the bounding box without considering the aperture'''
+ two_pi = 2 * math.pi
+ theta0 = (self.start_angle + two_pi) % two_pi
+ theta1 = (self.end_angle + two_pi) % two_pi
+ points = [self.start, self.end]
+ if self.direction == 'counterclockwise':
+ # Passes through 0 degrees
+ if theta0 > theta1:
+ points.append((self.center[0] + self.radius, self.center[1]))
+ # Passes through 90 degrees
+ if theta0 <= math.pi / 2. and (theta1 >= math.pi / 2. or theta1 < theta0):
+ points.append((self.center[0], self.center[1] + self.radius))
+ # Passes through 180 degrees
+ if theta0 <= math.pi and (theta1 >= math.pi or theta1 < theta0):
+ points.append((self.center[0] - self.radius, self.center[1]))
+ # Passes through 270 degrees
+ if theta0 <= math.pi * 1.5 and (theta1 >= math.pi * 1.5 or theta1 < theta0):
+ points.append((self.center[0], self.center[1] - self.radius ))
+ else:
+ # Passes through 0 degrees
+ if theta1 > theta0:
+ points.append((self.center[0] + self.radius, self.center[1]))
+ # Passes through 90 degrees
+ if theta1 <= math.pi / 2. and (theta0 >= math.pi / 2. or theta0 < theta1):
+ points.append((self.center[0], self.center[1] + self.radius))
+ # Passes through 180 degrees
+ if theta1 <= math.pi and (theta0 >= math.pi or theta0 < theta1):
+ points.append((self.center[0] - self.radius, self.center[1]))
+ # Passes through 270 degrees
+ if theta1 <= math.pi * 1.5 and (theta0 >= math.pi * 1.5 or theta0 < theta1):
+ points.append((self.center[0], self.center[1] - self.radius ))
+ x, y = zip(*points)
+
+ min_x = min(x)
+ max_x = max(x)
+ min_y = min(y)
+ max_y = max(y)
+ return ((min_x, max_x), (min_y, max_y)) +
+ def offset(self, x_offset=0, y_offset=0):
+ self._changed()
+ self.start = tuple(map(add, self.start, (x_offset, y_offset)))
+ self.end = tuple(map(add, self.end, (x_offset, y_offset)))
+ self.center = tuple(map(add, self.center, (x_offset, y_offset)))
+
class Circle(Primitive):
"""
"""
- def __init__(self, position, diameter, level_polarity='dark'):
- super(Circle, self).__init__(level_polarity)
- self.position = position
- self.diameter = diameter
+ + def __init__(self, position, diameter, hole_diameter = None, **kwargs): + super(Circle, self).__init__(**kwargs)
+ validate_coordinates(position)
+ self._position = position
+ self._diameter = diameter + self.hole_diameter = hole_diameter
+ self._to_convert = ['position', 'diameter', 'hole_diameter'] +
+ @property
+ def flashed(self):
+ return True
+
+ @property
+ def position(self):
+ return self._position
+
+ @position.setter
+ def position(self, value):
+ self._changed()
+ self._position = value
+
+ @property
+ def diameter(self):
+ return self._diameter
+
+ @diameter.setter
+ def diameter(self, value):
+ self._changed()
+ self._diameter = value
@property
def radius(self):
return self.diameter / 2.
+
+ @property
+ def hole_radius(self):
+ if self.hole_diameter != None:
+ return self.hole_diameter / 2.
+ return None
@property
def bounding_box(self):
- min_x = self.position[0] - self.radius
- max_x = self.position[0] + self.radius
- min_y = self.position[1] - self.radius
- max_y = self.position[1] + self.radius
- return ((min_x, max_x), (min_y, max_y))
+ if self._bounding_box is None:
+ min_x = self.position[0] - self.radius
+ max_x = self.position[0] + self.radius
+ 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 +
+ def offset(self, x_offset=0, y_offset=0):
+ self.position = tuple(map(add, self.position, (x_offset, y_offset)))
+
+ def equivalent(self, other, offset):
+ '''Is this the same as the other circle, ignoring the offiset?'''
+
+ if not isinstance(other, Circle):
+ return False
+
+ if self.diameter != other.diameter or self.hole_diameter != other.hole_diameter:
+ return False
+
+ equiv_position = tuple(map(add, other.position, offset))
+
+ return nearly_equal(self.position, equiv_position) +
+
+class Ellipse(Primitive):
+ """
+ """
+
+ def __init__(self, position, width, height, **kwargs):
+ super(Ellipse, self).__init__(**kwargs)
+ validate_coordinates(position)
+ self._position = position
+ self._width = width
+ self._height = height
+ self._to_convert = ['position', 'width', 'height']
+ + @property
+ def flashed(self):
+ return True
+ + @property
+ def position(self):
+ return self._position
+ + @position.setter
+ def position(self, value):
+ self._changed()
+ self._position = value +
+ @property
+ def width(self):
+ return self._width
+
+ @width.setter
+ def width(self, value):
+ self._changed()
+ self._width = value
@property
- def stroke_width(self):
- return self.diameter
+ def height(self):
+ return self._height
+
+ @height.setter
+ def height(self, value):
+ self._changed()
+ self._height = value
+
+ @property
+ def bounding_box(self):
+ if self._bounding_box is None:
+ min_x = self.position[0] - (self.axis_aligned_width / 2.0)
+ max_x = self.position[0] + (self.axis_aligned_width / 2.0)
+ min_y = self.position[1] - (self.axis_aligned_height / 2.0)
+ max_y = self.position[1] + (self.axis_aligned_height / 2.0)
+ self._bounding_box = ((min_x, max_x), (min_y, max_y))
+ return self._bounding_box
+
+ @property
+ def axis_aligned_width(self):
+ ux = (self.width / 2.) * math.cos(math.radians(self.rotation))
+ vx = (self.height / 2.) * \
+ math.cos(math.radians(self.rotation) + (math.pi / 2.))
+ return 2 * math.sqrt((ux * ux) + (vx * vx))
+
+ @property
+ def axis_aligned_height(self):
+ uy = (self.width / 2.) * math.sin(math.radians(self.rotation))
+ vy = (self.height / 2.) * \
+ math.sin(math.radians(self.rotation) + (math.pi / 2.))
+ return 2 * math.sqrt((uy * uy) + (vy * vy))
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, level_polarity='dark'):
- super(Rectangle, self).__init__(level_polarity)
- self.position = position
- self.width = width
- self.height = height
+ + 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.hole_diameter = hole_diameter
+ self._to_convert = ['position', 'width', 'height', 'hole_diameter']
+ # TODO These are probably wrong when rotated
+ self._lower_left = None
+ self._upper_right = None
+
+ @property
+ def flashed(self):
+ return True
+ + @property
+ def position(self):
+ return self._position
+
+ @position.setter
+ def position(self, value):
+ self._changed()
+ self._position = value
+
+ @property + def width(self):
+ return self._width
+
+ @width.setter
+ def width(self, value):
+ self._changed()
+ self._width = value @property
- def lower_left(self):
- return (self.position[0] - (self.width / 2.),
- self.position[1] - (self.height / 2.))
+ def height(self):
+ return self._height
+ @height.setter
+ def height(self, value):
+ self._changed()
+ self._height = value
+
@property
+ def hole_radius(self):
+ """The radius of the hole. If there is no hole, returns None"""
+ if self.hole_diameter != None:
+ return self.hole_diameter / 2.
+ return None
+
+ @property def upper_right(self):
- return (self.position[0] + (self.width / 2.),
- self.position[1] + (self.height / 2.))
+ 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.),
+ self.position[1] - (self.axis_aligned_height / 2.))
@property
def bounding_box(self):
- min_x = self.lower_left[0]
- max_x = self.upper_right[0]
- min_y = self.lower_left[1]
- max_y = self.upper_right[1]
- return ((min_x, max_x), (min_y, max_y))
+ if self._bounding_box is None:
+ ll = (self.position[0] - (self.axis_aligned_width / 2.),
+ self.position[1] - (self.axis_aligned_height / 2.))
+ ur = (self.position[0] + (self.axis_aligned_width / 2.),
+ self.position[1] + (self.axis_aligned_height / 2.))
+ self._bounding_box = ((ll[0], ur[0]), (ll[1], ur[1]))
+ return self._bounding_box
+
+ @property
+ def vertices(self):
+ if self._vertices is None:
+ delta_w = self.width / 2.
+ delta_h = self.height / 2.
+ ll = ((self.position[0] - delta_w), (self.position[1] - delta_h))
+ ul = ((self.position[0] - delta_w), (self.position[1] + delta_h))
+ ur = ((self.position[0] + delta_w), (self.position[1] + delta_h))
+ lr = ((self.position[0] + delta_w), (self.position[1] - delta_h))
+ self._vertices = [((x * self._cos_theta - y * self._sin_theta),
+ (x * self._sin_theta + y * self._cos_theta))
+ for x, y in [ll, ul, ur, lr]]
+ return self._vertices
@property
- def stroke_width(self):
- return max((self.width, self.height))
+ def axis_aligned_width(self):
+ return (self._cos_theta * self.width + self._sin_theta * self.height)
+ @property + def _abs_height(self):
+ return (math.cos(math.radians(self.rotation)) * self.height +
+ math.sin(math.radians(self.rotation)) * self.width)
-class Obround(Primitive):
+ @property + def axis_aligned_height(self):
+ return (self._cos_theta * self.height + self._sin_theta * self.width)
+
+ def equivalent(self, other, offset):
+ """Is this the same as the other rect, ignoring the offset?"""
+
+ if not isinstance(other, Rectangle):
+ return False
+
+ if self.width != other.width or self.height != other.height or self.rotation != other.rotation or self.hole_diameter != other.hole_diameter:
+ return False
+
+ equiv_position = tuple(map(add, other.position, offset))
+
+ return nearly_equal(self.position, equiv_position) +
+
+class Diamond(Primitive):
"""
"""
- def __init__(self, position, width, height, level_polarity='dark'):
- super(Obround, self).__init__(level_polarity)
- self.position = position
- self.width = width
- self.height = height
+
+ def __init__(self, position, width, height, **kwargs):
+ super(Diamond, self).__init__(**kwargs)
+ validate_coordinates(position)
+ self._position = position
+ self._width = width
+ self._height = height
+ self._to_convert = ['position', 'width', 'height']
+
+ @property
+ def flashed(self):
+ return True
@property
- def orientation(self):
- return 'vertical' if self.height > self.width else 'horizontal'
+ def position(self):
+ return self._position
+
+ @position.setter
+ def position(self, value):
+ self._changed()
+ self._position = value @property
- def lower_left(self):
- return (self.position[0] - (self.width / 2.),
- self.position[1] - (self.height / 2.))
+ def width(self):
+ return self._width
+
+ @width.setter
+ def width(self, value):
+ self._changed()
+ self._width = value
+
+ @property + def height(self):
+ return self._height
+
+ @height.setter
+ def height(self, value):
+ self._changed()
+ self._height = value
+
+ @property
+ def bounding_box(self):
+ if self._bounding_box is None:
+ ll = (self.position[0] - (self.axis_aligned_width / 2.),
+ self.position[1] - (self.axis_aligned_height / 2.))
+ ur = (self.position[0] + (self.axis_aligned_width / 2.),
+ self.position[1] + (self.axis_aligned_height / 2.))
+ self._bounding_box = ((ll[0], ur[0]), (ll[1], ur[1]))
+ return self._bounding_box
+
+ @property
+ def vertices(self):
+ if self._vertices is None:
+ delta_w = self.width / 2.
+ delta_h = self.height / 2.
+ top = (self.position[0], (self.position[1] + delta_h))
+ right = ((self.position[0] + delta_w), self.position[1])
+ bottom = (self.position[0], (self.position[1] - delta_h))
+ left = ((self.position[0] - delta_w), self.position[1])
+ self._vertices = [(((x * self._cos_theta) - (y * self._sin_theta)),
+ ((x * self._sin_theta) + (y * self._cos_theta)))
+ for x, y in [top, right, bottom, left]]
+ return self._vertices
@property
+ def axis_aligned_width(self):
+ return (self._cos_theta * self.width + self._sin_theta * self.height)
+
+ @property
+ def axis_aligned_height(self):
+ return (self._cos_theta * self.height + self._sin_theta * self.width)
+
+
+class ChamferRectangle(Primitive):
+ """
+ """
+
+ def __init__(self, position, width, height, chamfer, corners, **kwargs):
+ super(ChamferRectangle, self).__init__(**kwargs)
+ validate_coordinates(position)
+ self._position = position
+ self._width = width
+ self._height = height
+ self._chamfer = chamfer
+ self._corners = corners
+ self._to_convert = ['position', 'width', 'height', 'chamfer']
+
+ @property
+ def flashed(self):
+ return True
+
+ @property
+ def position(self):
+ return self._position
+
+ @position.setter
+ def position(self, value):
+ self._changed()
+ self._position = value
+
+ @property
+ def width(self):
+ return self._width
+
+ @width.setter
+ def width(self, value):
+ self._changed()
+ self._width = value
+
+ @property
+ def height(self):
+ return self._height
+
+ @height.setter
+ def height(self, value):
+ self._changed()
+ self._height = value
+
+ @property
+ def chamfer(self):
+ return self._chamfer
+
+ @chamfer.setter
+ def chamfer(self, value):
+ self._changed()
+ self._chamfer = value
+
+ @property
+ def corners(self):
+ return self._corners
+
+ @corners.setter
+ def corners(self, value):
+ self._changed()
+ self._corners = value
+
+ @property
+ def bounding_box(self):
+ if self._bounding_box is None:
+ ll = (self.position[0] - (self.axis_aligned_width / 2.),
+ self.position[1] - (self.axis_aligned_height / 2.))
+ ur = (self.position[0] + (self.axis_aligned_width / 2.),
+ self.position[1] + (self.axis_aligned_height / 2.))
+ self._bounding_box = ((ll[0], ur[0]), (ll[1], ur[1]))
+ return self._bounding_box
+
+ @property
+ def vertices(self):
+ # TODO
+ return self._vertices
+
+ @property
+ def axis_aligned_width(self):
+ return (self._cos_theta * self.width +
+ self._sin_theta * self.height)
+
+ @property
+ def axis_aligned_height(self):
+ return (self._cos_theta * self.height +
+ self._sin_theta * self.width)
+
+
+class RoundRectangle(Primitive):
+ """
+ """
+
+ def __init__(self, position, width, height, radius, corners, **kwargs):
+ super(RoundRectangle, self).__init__(**kwargs)
+ validate_coordinates(position)
+ self._position = position
+ self._width = width
+ self._height = height
+ self._radius = radius
+ self._corners = corners
+ self._to_convert = ['position', 'width', 'height', 'radius']
+
+ @property
+ def flashed(self):
+ return True
+
+ @property
+ def position(self):
+ return self._position
+
+ @position.setter
+ def position(self, value):
+ self._changed()
+ self._position = value
+
+ @property
+ def width(self):
+ return self._width
+
+ @width.setter
+ def width(self, value):
+ self._changed()
+ self._width = value
+
+ @property
+ def height(self):
+ return self._height
+
+ @height.setter
+ def height(self, value):
+ self._changed()
+ self._height = value +
+ @property
+ def radius(self):
+ return self._radius
+
+ @radius.setter
+ def radius(self, value):
+ self._changed()
+ self._radius = value
+
+ @property
+ def corners(self):
+ return self._corners
+
+ @corners.setter
+ def corners(self, value):
+ self._changed()
+ self._corners = value
+
+ @property
+ def bounding_box(self):
+ if self._bounding_box is None:
+ ll = (self.position[0] - (self.axis_aligned_width / 2.),
+ self.position[1] - (self.axis_aligned_height / 2.))
+ ur = (self.position[0] + (self.axis_aligned_width / 2.),
+ self.position[1] + (self.axis_aligned_height / 2.))
+ self._bounding_box = ((ll[0], ur[0]), (ll[1], ur[1]))
+ return self._bounding_box
+
+ @property
+ def axis_aligned_width(self):
+ return (self._cos_theta * self.width +
+ self._sin_theta * self.height)
+
+ @property + def axis_aligned_height(self):
+ return (self._cos_theta * self.height +
+ self._sin_theta * self.width)
+
+
+class Obround(Primitive):
+ """ + """
+ + 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.hole_diameter = hole_diameter
+ self._to_convert = ['position', 'width', 'height', 'hole_diameter']
+
+ @property
+ def flashed(self):
+ return True +
+ @property
+ def position(self):
+ return self._position
+
+ @position.setter
+ def position(self, value):
+ self._changed()
+ self._position = value +
+ @property
+ def width(self):
+ return self._width
+
+ @width.setter
+ def width(self, value):
+ self._changed()
+ self._width = value
+
+ @property def upper_right(self):
- return (self.position[0] + (self.width / 2.),
- self.position[1] + (self.height / 2.))
+ return (self.position[0] + (self._abs_width / 2.),
+ self.position[1] + (self._abs_height / 2.))
+
+ @property
+ def height(self):
+ return self._height
+
+ @height.setter
+ def height(self, value):
+ self._changed()
+ self._height = value
+
+ @property
+ def hole_radius(self):
+ """The radius of the hole. If there is no hole, returns None"""
+ if self.hole_diameter != None:
+ return self.hole_diameter / 2.
+
+ return None +
+ @property
+ def orientation(self):
+ return 'vertical' if self.height > self.width else 'horizontal'
@property
def bounding_box(self):
- min_x = self.lower_left[0]
- max_x = self.upper_right[0]
- min_y = self.lower_left[1]
- max_y = self.upper_right[1]
- return ((min_x, max_x), (min_y, max_y))
+ if self._bounding_box is None:
+ ll = (self.position[0] - (self.axis_aligned_width / 2.),
+ self.position[1] - (self.axis_aligned_height / 2.))
+ ur = (self.position[0] + (self.axis_aligned_width / 2.),
+ self.position[1] + (self.axis_aligned_height / 2.))
+ self._bounding_box = ((ll[0], ur[0]), (ll[1], ur[1]))
+ return self._bounding_box
@property
def subshapes(self):
if self.orientation == 'vertical':
circle1 = Circle((self.position[0], self.position[1] +
- (self.height-self.width) / 2.), self.width)
+ (self.height - self.width) / 2.), self.width)
circle2 = Circle((self.position[0], self.position[1] -
- (self.height-self.width) / 2.), self.width)
+ (self.height - self.width) / 2.), self.width)
rect = Rectangle(self.position, self.width,
- (self.height - self.width))
+ (self.height - self.width))
else:
- circle1 = Circle((self.position[0] - (self.height - self.width) / 2.,
+ circle1 = Circle((self.position[0]
+ - (self.height - self.width) / 2.,
self.position[1]), self.height)
- circle2 = Circle((self.position[0] - (self.height - self.width) / 2.,
+ circle2 = Circle((self.position[0]
+ + (self.height - self.width) / 2.,
self.position[1]), self.height)
rect = Rectangle(self.position, (self.width - self.height),
- self.height)
+ self.height)
return {'circle1': circle1, 'circle2': circle2, 'rectangle': rect}
+ @property
+ def axis_aligned_width(self):
+ return (self._cos_theta * self.width +
+ self._sin_theta * self.height)
+
+ @property
+ def axis_aligned_height(self):
+ return (self._cos_theta * self.height +
+ self._sin_theta * self.width)
+
+
class Polygon(Primitive):
+ """ + Polygon flash defined by a set number of sides.
+ """ + def __init__(self, position, sides, radius, hole_diameter, **kwargs): + super(Polygon, self).__init__(**kwargs)
+ validate_coordinates(position)
+ self._position = position
+ self.sides = sides + self._radius = radius
+ self.hole_diameter = hole_diameter
+ self._to_convert = ['position', 'radius', 'hole_diameter']
+
+ @property
+ def flashed(self):
+ return True
+
+ @property
+ def diameter(self):
+ return self.radius * 2
+
+ @property
+ def hole_radius(self):
+ if self.hole_diameter != None:
+ return self.hole_diameter / 2.
+ return None +
+ @property
+ def position(self):
+ return self._position
+
+ @position.setter
+ def position(self, value):
+ self._changed()
+ self._position = value +
+ @property
+ def radius(self):
+ return self._radius
+
+ @radius.setter
+ def radius(self, value):
+ self._changed()
+ self._radius = value
+
+ @property
+ def bounding_box(self):
+ if self._bounding_box is None:
+ min_x = self.position[0] - self.radius
+ max_x = self.position[0] + self.radius
+ 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
+ + def offset(self, x_offset=0, y_offset=0):
+ self.position = tuple(map(add, self.position, (x_offset, y_offset)))
+
+ @property
+ def vertices(self):
+
+ offset = self.rotation
+ da = 360.0 / self.sides
+
+ points = []
+ for i in xrange(self.sides):
+ points.append(rotate_point((self.position[0] + self.radius, self.position[1]), offset + da * i, self.position))
+
+ return points
+
+ @property
+ def vertices(self):
+ if self._vertices is None:
+ 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?
+ """
+
+ # Quick check if it even makes sense to compare them
+ if type(self) != type(other) or self.sides != other.sides or self.radius != other.radius:
+ return False
+
+ equiv_pos = tuple(map(add, other.position, offset))
+
+ return nearly_equal(self.position, equiv_pos)
+
+
+class AMGroup(Primitive):
"""
- """
- def __init__(self, position, sides, radius, level_polarity='dark'):
- super(Polygon, self).__init__(level_polarity)
- self.position = position
- self.sides = sides
- self.radius = radius
+ """ + def __init__(self, amprimitives, stmt = None, **kwargs):
+ """
+
+ stmt : The original statment that generated this, since it is really hard to re-generate from primitives
+ """
+ super(AMGroup, self).__init__(**kwargs)
+
+ self.primitives = []
+ for amprim in amprimitives:
+ prim = amprim.to_primitive(self.units)
+ if isinstance(prim, list):
+ for p in prim:
+ self.primitives.append(p)
+ elif prim:
+ self.primitives.append(prim)
+ self._position = None
+ self._to_convert = ['_position', 'primitives']
+ self.stmt = stmt
+
+ def to_inch(self):
+ if self.units == 'metric':
+ super(AMGroup, self).to_inch()
+
+ # If we also have a stmt, convert that too
+ if self.stmt:
+ self.stmt.to_inch()
+
+ def to_metric(self):
+ if self.units == 'inch':
+ super(AMGroup, self).to_metric()
+
+ # If we also have a stmt, convert that too
+ if self.stmt:
+ self.stmt.to_metric()
+
+ @property
+ def flashed(self):
+ return True
+
@property
def bounding_box(self):
- min_x = self.position[0] - self.radius
- max_x = self.position[0] + self.radius
- min_y = self.position[1] - self.radius
- max_y = self.position[1] + self.radius
+ # TODO Make this cached like other items
+ xlims, ylims = zip(*[p.bounding_box for p in self.primitives])
+ minx, maxx = zip(*xlims)
+ miny, maxy = zip(*ylims)
+ min_x = min(minx)
+ max_x = max(maxx)
+ min_y = min(miny)
+ max_y = max(maxy)
return ((min_x, max_x), (min_y, max_y))
+
+ @property
+ def position(self):
+ return self._position
+
+ def offset(self, x_offset=0, y_offset=0):
+ self._position = tuple(map(add, self._position, (x_offset, y_offset)))
+
+ for primitive in self.primitives:
+ primitive.offset(x_offset, y_offset)
+
+ @position.setter
+ def position(self, new_pos):
+ '''
+ Sets the position of the AMGroup.
+ This offset all of the objects by the specified distance.
+ '''
+
+ if self._position:
+ dx = new_pos[0] - self._position[0]
+ dy = new_pos[1] - self._position[1]
+ else:
+ dx = new_pos[0]
+ dy = new_pos[1]
+
+ for primitive in self.primitives:
+ primitive.offset(dx, dy)
+
+ self._position = new_pos
+
+ def equivalent(self, other, offset):
+ '''
+ Is this the macro group the same as the other, ignoring the position offset?
+ '''
+
+ if len(self.primitives) != len(other.primitives):
+ return False
+
+ # We know they have the same number of primitives, so now check them all
+ for i in range(0, len(self.primitives)):
+ if not self.primitives[i].equivalent(other.primitives[i], offset):
+ return False
+
+ # If we didn't find any differences, then they are the same
+ return True
+class Outline(Primitive):
+ """
+ Outlines only exist as the rendering for a apeture macro outline.
+ They don't exist outside of AMGroup objects
+ """
+ + def __init__(self, primitives, **kwargs):
+ super(Outline, self).__init__(**kwargs)
+ self.primitives = primitives
+ self._to_convert = ['primitives']
+
+ if self.primitives[0].start != self.primitives[-1].end:
+ raise ValueError('Outline must be closed')
+
+ @property
+ def flashed(self):
+ return True
+
+ @property
+ def bounding_box(self):
+ if self._bounding_box is None:
+ xlims, ylims = zip(*[p.bounding_box for p in self.primitives])
+ minx, maxx = zip(*xlims)
+ miny, maxy = zip(*ylims)
+ min_x = min(minx)
+ max_x = max(maxx)
+ min_y = min(miny)
+ max_y = max(maxy)
+ self._bounding_box = ((min_x, max_x), (min_y, max_y))
+ return self._bounding_box
+
+ def offset(self, x_offset=0, y_offset=0):
+ self._changed()
+ for p in self.primitives:
+ p.offset(x_offset, y_offset)
+ + @property
+ def vertices(self):
+ if self._vertices is None:
+ theta = math.radians(360/self.sides)
+ vertices = [(self.position[0] + (math.cos(theta * side) * self.radius),
+ self.position[1] + (math.sin(theta * side) * self.radius))
+ for side in range(self.sides)]
+ self._vertices = [(((x * self._cos_theta) - (y * self._sin_theta)),
+ ((x * self._sin_theta) + (y * self._cos_theta)))
+ for x, y in vertices]
+ return self._vertices +
+ @property
+ def width(self):
+ bounding_box = self.bounding_box()
+ return bounding_box[0][1] - bounding_box[0][0]
+
+ def equivalent(self, other, offset):
+ '''
+ Is this the outline the same as the other, ignoring the position offset?
+ '''
+
+ # Quick check if it even makes sense to compare them
+ if type(self) != type(other) or len(self.primitives) != len(other.primitives):
+ return False
+
+ for i in range(0, len(self.primitives)):
+ if not self.primitives[i].equivalent(other.primitives[i], offset):
+ return False
+
+ return True
class Region(Primitive):
"""
"""
- def __init__(self, points, level_polarity='dark'):
- super(Region, self).__init__(level_polarity)
- self.points = points
+
+ def __init__(self, primitives, **kwargs):
+ super(Region, self).__init__(**kwargs)
+ self.primitives = primitives
+ self._to_convert = ['primitives']
+
+ @property
+ def flashed(self):
+ return False
+
+ @property
+ def bounding_box(self): + if self._bounding_box is None:
+ xlims, ylims = zip(*[p.bounding_box_no_aperture 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)
+
+
+class RoundButterfly(Primitive):
+ """ A circle with two diagonally-opposite quadrants removed
+ """
+
+ def __init__(self, position, diameter, **kwargs):
+ super(RoundButterfly, self).__init__(**kwargs)
+ validate_coordinates(position)
+ self.position = position
+ self.diameter = diameter
+ self._to_convert = ['position', 'diameter']
+
+ # TODO This does not reset bounding box correctly
+
+ @property
+ def flashed(self):
+ return True
+
+ @property
+ def radius(self):
+ return self.diameter / 2.
@property
def bounding_box(self):
- x_list, y_list = zip(*self.points)
- min_x = min(x_list)
- max_x = max(x_list)
- min_y = min(y_list)
- max_y = max(y_list)
- return ((min_x, max_x), (min_y, max_y))
+ if self._bounding_box is None:
+ min_x = self.position[0] - self.radius
+ max_x = self.position[0] + self.radius
+ 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
-class Drill(Primitive):
+class SquareButterfly(Primitive):
+ """ A square with two diagonally-opposite quadrants removed
"""
+
+ def __init__(self, position, side, **kwargs):
+ super(SquareButterfly, self).__init__(**kwargs)
+ validate_coordinates(position)
+ self.position = position
+ self.side = side
+ self._to_convert = ['position', 'side']
+
+ # TODO This does not reset bounding box correctly
+ + @property
+ def flashed(self):
+ return True + + @property
+ def bounding_box(self):
+ if self._bounding_box is None:
+ min_x = self.position[0] - (self.side / 2.)
+ max_x = self.position[0] + (self.side / 2.)
+ min_y = self.position[1] - (self.side / 2.)
+ max_y = self.position[1] + (self.side / 2.)
+ self._bounding_box = ((min_x, max_x), (min_y, max_y))
+ return self._bounding_box
+
+
+class Donut(Primitive):
+ """ A Shape with an identical concentric shape removed from its center
"""
- def __init__(self, position, diameter):
- super(Drill, self).__init__('dark')
+
+ def __init__(self, position, shape, inner_diameter,
+ outer_diameter, **kwargs):
+ super(Donut, self).__init__(**kwargs)
+ validate_coordinates(position)
self.position = position
- self.diameter = diameter
+ if shape not in ('round', 'square', 'hexagon', 'octagon'):
+ raise ValueError(
+ 'Valid shapes are round, square, hexagon or octagon')
+ self.shape = shape
+ if inner_diameter >= outer_diameter:
+ raise ValueError(
+ 'Outer diameter must be larger than inner diameter.')
+ self.inner_diameter = inner_diameter
+ self.outer_diameter = outer_diameter
+ if self.shape in ('round', 'square', 'octagon'):
+ self.width = outer_diameter
+ self.height = outer_diameter
+ else:
+ # Hexagon
+ self.width = 0.5 * math.sqrt(3.) * outer_diameter
+ self.height = outer_diameter + + self._to_convert = ['position', 'width',
+ 'height', 'inner_diameter', 'outer_diameter']
+
+ # TODO This does not reset bounding box correctly
+
+ @property
+ def flashed(self):
+ return True
+
+ @property
+ def lower_left(self):
+ return (self.position[0] - (self.width / 2.),
+ self.position[1] - (self.height / 2.))
+
+ @property
+ def upper_right(self):
+ return (self.position[0] + (self.width / 2.),
+ self.position[1] + (self.height / 2.) +
+ @property
+ def bounding_box(self):
+ if self._bounding_box is None:
+ ll = (self.position[0] - (self.width / 2.),
+ self.position[1] - (self.height / 2.))
+ ur = (self.position[0] + (self.width / 2.),
+ self.position[1] + (self.height / 2.))
+ self._bounding_box = ((ll[0], ur[0]), (ll[1], ur[1]))
+ return self._bounding_box
+
+
+class SquareRoundDonut(Primitive):
+ """ A Square with a circular cutout in the center
+ """
+
+ def __init__(self, position, inner_diameter, outer_diameter, **kwargs):
+ super(SquareRoundDonut, self).__init__(**kwargs)
+ validate_coordinates(position)
+ self.position = position
+ if inner_diameter >= outer_diameter:
+ raise ValueError(
+ 'Outer diameter must be larger than inner diameter.')
+ self.inner_diameter = inner_diameter
+ self.outer_diameter = outer_diameter
+ self._to_convert = ['position', 'inner_diameter', 'outer_diameter']
+
+ @property
+ def flashed(self):
+ return True
+
+ @property
+ def bounding_box(self):
+ if self._bounding_box is None:
+ 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
+
+
+class Drill(Primitive):
+ """ A drill hole
+ """ + def __init__(self, position, diameter, hit, **kwargs):
+ super(Drill, self).__init__('dark', **kwargs)
+ validate_coordinates(position)
+ self._position = position
+ self._diameter = diameter
+ self.hit = hit
+ self._to_convert = ['position', 'diameter', 'hit']
+
+ # TODO Ths won't handle the hit updates correctly
+
+ @property
+ def flashed(self):
+ return False +
+ @property + def position(self):
+ return self._position
+
+ @position.setter
+ def position(self, value):
+ self._changed()
+ self._position = value
+
+ @property
+ def diameter(self):
+ return self._diameter
+
+ @diameter.setter
+ def diameter(self, value):
+ self._changed()
+ self._diameter = value
@property
def radius(self):
@@ -238,8 +1576,60 @@ class Drill(Primitive): @property
def bounding_box(self):
- min_x = self.position[0] - self.radius
- max_x = self.position[0] + self.radius
- min_y = self.position[1] - self.radius
- max_y = self.position[1] + self.radius
- return ((min_x, max_x), (min_y, max_y))
+ if self._bounding_box is None:
+ min_x = self.position[0] - self.radius
+ max_x = self.position[0] + self.radius
+ 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
+ + def offset(self, x_offset=0, y_offset=0):
+ self._changed()
+ self.position = tuple(map(add, self.position, (x_offset, y_offset)))
+
+ def __str__(self):
+ return '<Drill %f (%f, %f) [%s]>' % (self.diameter, self.position[0], self.position[1], self.hit)
+
+
+class Slot(Primitive):
+ """ A drilled slot
+ """
+ def __init__(self, start, end, diameter, hit, **kwargs):
+ super(Slot, self).__init__('dark', **kwargs)
+ validate_coordinates(start)
+ validate_coordinates(end)
+ self.start = start
+ self.end = end
+ self.diameter = diameter
+ self.hit = hit
+ self._to_convert = ['start', 'end', 'diameter', 'hit']
+
+ # TODO this needs to use cached bounding box
+
+ @property
+ def flashed(self):
+ return False
+ + def bounding_box(self):
+ if self._bounding_box is None:
+ ll = tuple([c - self.outer_diameter / 2. for c in self.position])
+ ur = tuple([c + self.outer_diameter / 2. for c in self.position])
+ self._bounding_box = ((ll[0], ur[0]), (ll[1], ur[1]))
+ return self._bounding_box
+
+ def offset(self, x_offset=0, y_offset=0):
+ self.start = tuple(map(add, self.start, (x_offset, y_offset)))
+ self.end = tuple(map(add, self.end, (x_offset, y_offset)))
+ +
+class TestRecord(Primitive):
+ """ Netlist Test record
+ """
+
+ def __init__(self, position, net_name, layer, **kwargs):
+ super(TestRecord, self).__init__(**kwargs)
+ validate_coordinates(position)
+ self.position = position
+ self.net_name = net_name
+ self.layer = layer
diff --git a/gerber/render/__init__.py b/gerber/render/__init__.py index b4af4ad..f76d28f 100644 --- a/gerber/render/__init__.py +++ b/gerber/render/__init__.py @@ -24,5 +24,4 @@ SVG is the only supported format. """ -from svgwrite_backend import GerberSvgContext -from cairo_backend import GerberCairoContext +from .cairo_backend import GerberCairoContext diff --git a/gerber/render/apertures.py b/gerber/render/apertures.py deleted file mode 100644 index 52ae50c..0000000 --- a/gerber/render/apertures.py +++ /dev/null @@ -1,76 +0,0 @@ -#! /usr/bin/env python -# -*- coding: utf-8 -*- - -# Copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be> - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -gerber.render.apertures -============ -**Gerber Aperture base classes** - -This module provides base classes for gerber apertures. These are used by -the rendering engine to draw the gerber file. -""" -import math - -class Aperture(object): - """ Gerber Aperture base class - """ - def draw(self, ctx, x, y): - raise NotImplementedError('The draw method must be implemented \ - in an Aperture subclass.') - - def flash(self, ctx, x, y): - raise NotImplementedError('The flash method must be implemented \ - in an Aperture subclass.') - - def _arc_params(self, startx, starty, x, y, i, j): - center = (startx + i, starty + j) - radius = math.sqrt(math.pow(center[0] - x, 2) + - math.pow(center[1] - y, 2)) - delta_x0 = startx - center[0] - delta_y0 = center[1] - starty - delta_x1 = x - center[0] - delta_y1 = center[1] - y - start_angle = math.atan2(delta_y0, delta_x0) - end_angle = math.atan2(delta_y1, delta_x1) - return {'center': center, 'radius': radius, - 'start_angle': start_angle, 'end_angle': end_angle} - - -class Circle(Aperture): - """ Circular Aperture base class - """ - def __init__(self, diameter=0.0): - self.diameter = diameter - - -class Rect(Aperture): - """ Rectangular Aperture base class - """ - def __init__(self, size=(0, 0)): - self.size = size - - -class Obround(Aperture): - """ Obround Aperture base class - """ - def __init__(self, size=(0, 0)): - self.size = size - - -class Polygon(Aperture): - """ Polygon Aperture base class - """ - pass diff --git a/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py index df513bb..8c7232f 100644 --- a/gerber/render/cairo_backend.py +++ b/gerber/render/cairo_backend.py @@ -12,80 +12,517 @@ # 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.
+ +# See the License for the specific language governing permissions and +# limitations under the License. + +try:
+ import cairo
+except ImportError:
+ import cairocffi as cairo
+ +import math
+from operator import mul, div
+import tempfile
-from .render import GerberContext
-from operator import mul
import cairocffi as cairo
-import math
-SCALE = 300.
+from ..primitives import *
+from .render import GerberContext, RenderSettings
+from .theme import THEMES
+
+try:
+ from cStringIO import StringIO
+except(ImportError):
+ from io import StringIO
class GerberCairoContext(GerberContext):
- def __init__(self, surface=None, size=(1000, 1000)):
- GerberContext.__init__(self)
- if surface is None:
- self.surface = cairo.ImageSurface(cairo.FORMAT_ARGB32,
- size[0], size[1])
+
+ def __init__(self, scale=300):
+ super(GerberCairoContext, self).__init__()
+ self.scale = (scale, scale)
+ self.surface = None
+ self.ctx = None
+ self.active_layer = None
+ self.output_ctx = None
+ self.bg = False
+ self.mask = None
+ self.mask_ctx = None
+ self.origin_in_inch = None
+ self.size_in_inch = None
+ self._xform_matrix = None
+
+ @property
+ def origin_in_pixels(self):
+ return (self.scale_point(self.origin_in_inch)
+ if self.origin_in_inch is not None else (0.0, 0.0))
+
+ @property
+ def size_in_pixels(self):
+ return (self.scale_point(self.size_in_inch)
+ if self.size_in_inch is not None else (0.0, 0.0))
+
+ def set_bounds(self, bounds, new_surface=False):
+ origin_in_inch = (bounds[0][0], bounds[1][0])
+ size_in_inch = (abs(bounds[0][1] - bounds[0][0]),
+ abs(bounds[1][1] - bounds[1][0]))
+ size_in_pixels = self.scale_point(size_in_inch)
+ self.origin_in_inch = origin_in_inch if self.origin_in_inch is None else self.origin_in_inch
+ self.size_in_inch = size_in_inch if self.size_in_inch is None else self.size_in_inch
+ if (self.surface is None) or new_surface:
+ self.surface_buffer = tempfile.NamedTemporaryFile()
+ self.surface = cairo.SVGSurface(
+ self.surface_buffer, size_in_pixels[0], size_in_pixels[1])
+ self.output_ctx = cairo.Context(self.surface)
+ self.output_ctx.set_fill_rule(cairo.FILL_RULE_EVEN_ODD)
+ self.output_ctx.scale(1, -1)
+ self.output_ctx.translate(-(origin_in_inch[0] * self.scale[0]),
+ (-origin_in_inch[1] * self.scale[0]) - size_in_pixels[1])
+ self._xform_matrix = cairo.Matrix(xx=1.0, yy=-1.0,
+ x0=-self.origin_in_pixels[0],
+ y0=self.size_in_pixels[1] + self.origin_in_pixels[1])
+
+ def render_layers(self, layers, filename, theme=THEMES['default']):
+ """ Render a set of layers
+ """
+ self.set_bounds(layers[0].bounds, True)
+ self._paint_background(True)
+
+ for layer in layers:
+ self._render_layer(layer, theme)
+ self.dump(filename)
+
+ def dump(self, filename):
+ """ Save image as `filename`
+ """
+ if filename and filename.lower().endswith(".svg"):
+ self.surface.finish()
+ self.surface_buffer.flush()
+ with open(filename, "w") as f:
+ self.surface_buffer.seek(0)
+ f.write(self.surface_buffer.read())
+ f.flush()
else:
- self.surface = surface
- self.ctx = cairo.Context(self.surface)
- self.size = size
- self.ctx.translate(0, self.size[1])
- self.scale = (SCALE,SCALE)
- self.ctx.scale(1, -1)
- self.apertures = {}
- self.background = False
+ return self.surface.write_to_png(filename)
- def set_bounds(self, bounds):
- xbounds, ybounds = bounds
- self.ctx.rectangle(SCALE * xbounds[0], SCALE * ybounds[0], SCALE * (xbounds[1]- xbounds[0]), SCALE * (ybounds[1] - ybounds[0]))
- self.ctx.set_source_rgb(0,0,0)
- self.ctx.fill()
+ def dump_str(self):
+ """ Return a string containing the rendered image.
+ """
+ fobj = StringIO()
+ self.surface.write_to_png(fobj)
+ return fobj.getvalue()
+
+ def dump_svg_str(self):
+ """ Return a string containg the rendered SVG.
+ """
+ self.surface.finish()
+ self.surface_buffer.flush()
+ return self.surface_buffer.read()
+
+ def _render_layer(self, layer, theme=THEMES['default']):
+ settings = theme.get(layer.layer_class, RenderSettings())
+ self.color = settings.color
+ self.alpha = settings.alpha
+ self.invert = settings.invert
+
+ # Get a new clean layer to render on
+ self._new_render_layer()
+ if settings.mirror:
+ raise Warning('mirrored layers aren\'t supported yet...')
+ for prim in layer.primitives:
+ self.render(prim)
+ # Add layer to image
+ self._flatten()
def _render_line(self, line, color):
- start = map(mul, line.start, self.scale)
- end = map(mul, line.end, self.scale)
- self.ctx.set_source_rgb (*color)
- self.ctx.set_line_width(line.width * SCALE)
+ 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)
+ self.ctx.move_to(*points[0])
+ for point in points[1:]:
+ self.ctx.line_to(*point)
+ self.ctx.fill()
+
+ def _render_arc(self, arc, color):
+ center = self.scale_point(arc.center)
+ start = self.scale_point(arc.start)
+ end = self.scale_point(arc.end)
+ radius = self.scale[0] * arc.radius
+ angle1 = arc.start_angle
+ angle2 = arc.end_angle
+ if angle1 == angle2 and arc.quadrant_mode != 'single-quadrant':
+ # Make the angles slightly different otherwise Cario will draw nothing
+ angle2 -= 0.000000001
+ if isinstance(arc.aperture, Circle):
+ width = arc.aperture.diameter if arc.aperture.diameter != 0 else 0.001
+ else:
+ width = max(arc.aperture.width, arc.aperture.height, 0.001)
+
+ 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"\
+ 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_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)
- self.ctx.line_to(*end)
- self.ctx.stroke()
+ 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):
- points = [tuple(map(mul, point, self.scale)) for point in region.points]
- self.ctx.set_source_rgb (*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))
+ for prim in region.primitives:
+ if isinstance(prim, Line):
+ self.ctx.line_to(*self.scale_point(prim.end))
+ else:
+ center = self.scale_point(prim.center)
+ radius = self.scale[0] * prim.radius
+ angle1 = prim.start_angle
+ angle2 = prim.end_angle
+ if prim.direction == 'counterclockwise':
+ self.ctx.arc(*center, radius=radius,
+ angle1=angle1, angle2=angle2)
+ 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)
+ if not self.invert:
+ self.ctx.set_source_rgba(color[0], color[1], color[2], 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)
+
+ if circle.hole_diameter > 0:
+ self.ctx.push_group()
+
self.ctx.set_line_width(0)
- self.ctx.move_to(*points[0])
- for point in points[1:]:
- self.ctx.move_to(*point)
+ self.ctx.arc(center[0], center[1], radius=circle.radius * self.scale[0], angle1=0, angle2=2 * math.pi)
+ self.ctx.fill()
+
+ if circle.hole_diameter > 0:
+ # Render the center clear
+
+ self.ctx.set_source_rgba(color[0], color[1], color[2], self.alpha)
+ self.ctx.set_operator(cairo.OPERATOR_CLEAR)
+ self.ctx.arc(center[0], center[1], radius=circle.hole_radius * self.scale[0], angle1=0, angle2=2 * math.pi)
+ self.ctx.fill()
+
+ self.ctx.pop_group_to_source()
+ self.ctx.paint_with_alpha(1) +======= self.ctx.fill()
def _render_circle(self, circle, color):
- center = map(mul, circle.position, self.scale)
- self.ctx.set_source_rgb (*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 * SCALE, angle1=0, angle2=2 * math.pi)
+ 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):
- ll = map(mul, rectangle.lower_left, self.scale)
- width, height = tuple(map(mul, (rectangle.width, rectangle.height), map(abs, self.scale)))
- self.ctx.set_source_rgb (*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)
+ 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)
+
+ if rectangle.rotation != 0:
+ self.ctx.save()
+
+ center = map(mul, rectangle.position, self.scale)
+ matrix = cairo.Matrix()
+ matrix.translate(center[0], center[1])
+ # For drawing, we already handles the translation
+ lower_left[0] = lower_left[0] - center[0]
+ lower_left[1] = lower_left[1] - center[1]
+ matrix.rotate(rectangle.rotation)
+ self.ctx.transform(matrix)
+
+ if rectangle.hole_diameter > 0:
+ self.ctx.push_group()
+
+ self.ctx.set_line_width(0)
+ self.ctx.rectangle(lower_left[0], lower_left[1], width, height)
+ self.ctx.fill()
+
+ if rectangle.hole_diameter > 0:
+ # Render the center clear
+ self.ctx.set_source_rgba(color[0], color[1], color[2], self.alpha)
+ self.ctx.set_operator(cairo.OPERATOR_CLEAR)
+ center = map(mul, rectangle.position, self.scale)
+ self.ctx.arc(center[0], center[1], radius=rectangle.hole_radius * self.scale[0], angle1=0, angle2=2 * math.pi)
+ self.ctx.fill()
+
+ self.ctx.pop_group_to_source()
+ self.ctx.paint_with_alpha(1)
+
+ 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(*ll,width=width, height=height)
+ 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):
+
+ if not self.invert:
+ self.ctx.set_source_rgba(color[0], color[1], color[2], alpha=self.alpha)
+ self.ctx.set_operator(cairo.OPERATOR_OVER if obround.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)
+
+ if obround.hole_diameter > 0:
+ self.ctx.push_group()
+
self._render_circle(obround.subshapes['circle1'], color)
self._render_circle(obround.subshapes['circle2'], color)
self._render_rectangle(obround.subshapes['rectangle'], color)
+
+ if obround.hole_diameter > 0:
+ # Render the center clear
+ self.ctx.set_source_rgba(color[0], color[1], color[2], self.alpha)
+ self.ctx.set_operator(cairo.OPERATOR_CLEAR)
+ center = map(mul, obround.position, self.scale)
+ self.ctx.arc(center[0], center[1], radius=obround.hole_radius * self.scale[0], angle1=0, angle2=2 * math.pi)
+ self.ctx.fill()
+
+ self.ctx.pop_group_to_source()
+ self.ctx.paint_with_alpha(1)
+
+ def _render_polygon(self, polygon, color):
+
+ # TODO Ths does not handle rotation of a polygon
+ if not self.invert:
+ self.ctx.set_source_rgba(color[0], color[1], color[2], alpha=self.alpha)
+ self.ctx.set_operator(cairo.OPERATOR_OVER if polygon.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)
+
+ if polygon.hole_radius > 0:
+ self.ctx.push_group()
+
+ vertices = polygon.vertices
+
+ self.ctx.set_line_width(0)
+ self.ctx.set_line_cap(cairo.LINE_CAP_ROUND)
+
+ # Start from before the end so it is easy to iterate and make sure it is closed
+ self.ctx.move_to(*map(mul, vertices[-1], self.scale))
+ for v in vertices:
+ self.ctx.line_to(*map(mul, v, self.scale))
+
+ self.ctx.fill()
+
+ if polygon.hole_radius > 0:
+ # Render the center clear
+ center = tuple(map(mul, polygon.position, self.scale))
+ self.ctx.set_source_rgba(color[0], color[1], color[2], self.alpha)
+ self.ctx.set_operator(cairo.OPERATOR_CLEAR)
+ 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()
+
+ self.ctx.pop_group_to_source()
+ self.ctx.paint_with_alpha(1)
- def _render_drill(self, circle, color):
+ def _render_drill(self, circle, color=None):
+ color = color if color is not None else self.drill_color
self._render_circle(circle, color)
+
+ def _render_slot(self, slot, color):
+ start = map(mul, slot.start, self.scale)
+ end = map(mul, slot.end, self.scale)
+
+ width = slot.diameter
+
+ if not self.invert:
+ self.ctx.set_source_rgba(color[0], color[1], color[2], alpha=self.alpha)
+ self.ctx.set_operator(cairo.OPERATOR_OVER if slot.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)
- def dump(self, filename):
- self.surface.write_to_png(filename)
+ self.ctx.set_line_width(width * self.scale[0])
+ self.ctx.set_line_cap(cairo.LINE_CAP_ROUND)
+ self.ctx.move_to(*start)
+ self.ctx.line_to(*end)
+ self.ctx.stroke()
+
+ def _render_amgroup(self, amgroup, color):
+ self.ctx.push_group()
+ for primitive in amgroup.primitives:
+ self.render(primitive)
+ self.ctx.pop_group_to_source()
+ self.ctx.paint_with_alpha(1)
+
+ def _render_test_record(self, primitive, color):
+ position = [pos + origin for pos, origin in zip(primitive.position, self.origin_in_inch)]
+ self.ctx.set_operator(cairo.OPERATOR_OVER)
+ self.ctx.select_font_face(
+ 'monospace', cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_BOLD)
+ self.ctx.set_font_size(13)
+ self._render_circle(Circle(position, 0.015), color)
+ self.ctx.set_source_rgba(*color, alpha=self.alpha)
+ self.ctx.set_operator(
+ cairo.OPERATOR_OVER if primitive.level_polarity == 'dark' else cairo.OPERATOR_CLEAR)
+ self.ctx.move_to(*[self.scale[0] * (coord + 0.015)
+ for coord in position])
+ self.ctx.scale(1, -1)
+ self.ctx.show_text(primitive.net_name)
+ self.ctx.scale(1, -1) +
+ def _new_render_layer(self, color=None):
+ size_in_pixels = self.scale_point(self.size_in_inch)
+ layer = cairo.SVGSurface(None, size_in_pixels[0], size_in_pixels[1])
+ ctx = cairo.Context(layer)
+ ctx.set_fill_rule(cairo.FILL_RULE_EVEN_ODD)
+ ctx.scale(1, -1)
+ ctx.translate(-(self.origin_in_inch[0] * self.scale[0]),
+ (-self.origin_in_inch[1] * self.scale[0])
+ - size_in_pixels[1])
+ if self.invert:
+ ctx.set_operator(cairo.OPERATOR_OVER)
+ ctx.set_source_rgba(*self.color, alpha=self.alpha)
+ ctx.paint()
+ self.ctx = ctx
+ self.active_layer = layer
+
+ 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()
+ self.ctx = None
+ self.active_layer = None
+
+ def _paint_background(self, force=False):
+ 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)])
+======= + 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/excellon_backend.py b/gerber/render/excellon_backend.py new file mode 100644 index 0000000..da5b22b --- /dev/null +++ b/gerber/render/excellon_backend.py @@ -0,0 +1,189 @@ + +from .render import GerberContext +from ..excellon import DrillSlot +from ..excellon_statements import * + +class ExcellonContext(GerberContext): + + MODE_DRILL = 1 + MODE_SLOT =2 + + def __init__(self, settings): + GerberContext.__init__(self) + + # Statements that we write + self.comments = [] + self.header = [] + self.tool_def = [] + self.body_start = [RewindStopStmt()] + self.body = [] + self.start = [HeaderBeginStmt()] + + # Current tool and position + self.handled_tools = set() + self.cur_tool = None + self.drill_mode = ExcellonContext.MODE_DRILL + self.drill_down = False + self._pos = (None, None) + + self.settings = settings + + self._start_header() + self._start_comments() + + def _start_header(self): + """Create the header from the settings""" + + self.header.append(UnitStmt.from_settings(self.settings)) + + if self.settings.notation == 'incremental': + raise NotImplementedError('Incremental mode is not implemented') + else: + self.body.append(AbsoluteModeStmt()) + + def _start_comments(self): + + # Write the digits used - this isn't valid Excellon statement, so we write as a comment + self.comments.append(CommentStmt('FILE_FORMAT=%d:%d' % (self.settings.format[0], self.settings.format[1]))) + + def _get_end(self): + """How we end depends on our mode""" + + end = [] + + if self.drill_down: + end.append(RetractWithClampingStmt()) + end.append(RetractWithoutClampingStmt()) + + end.append(EndOfProgramStmt()) + + return end + + @property + def statements(self): + return self.start + self.comments + self.header + self.body_start + self.body + self._get_end() + + def set_bounds(self, bounds): + pass + + def _paint_background(self): + pass + + def _render_line(self, line, color): + raise ValueError('Invalid Excellon object') + def _render_arc(self, arc, color): + raise ValueError('Invalid Excellon object') + + def _render_region(self, region, color): + raise ValueError('Invalid Excellon object') + + def _render_level_polarity(self, region): + raise ValueError('Invalid Excellon object') + + def _render_circle(self, circle, color): + raise ValueError('Invalid Excellon object') + + def _render_rectangle(self, rectangle, color): + raise ValueError('Invalid Excellon object') + + def _render_obround(self, obround, color): + raise ValueError('Invalid Excellon object') + + def _render_polygon(self, polygon, color): + raise ValueError('Invalid Excellon object') + + def _simplify_point(self, point): + return (point[0] if point[0] != self._pos[0] else None, point[1] if point[1] != self._pos[1] else None) + + def _render_drill(self, drill, color): + + if self.drill_mode != ExcellonContext.MODE_DRILL: + self._start_drill_mode() + + tool = drill.hit.tool + if not tool in self.handled_tools: + self.handled_tools.add(tool) + self.header.append(ExcellonTool.from_tool(tool)) + + if tool != self.cur_tool: + self.body.append(ToolSelectionStmt(tool.number)) + self.cur_tool = tool + + point = self._simplify_point(drill.position) + self._pos = drill.position + self.body.append(CoordinateStmt.from_point(point)) + + def _start_drill_mode(self): + """ + If we are not in drill mode, then end the ROUT so we can do basic drilling + """ + + if self.drill_mode == ExcellonContext.MODE_SLOT: + + # Make sure we are retracted before changing modes + last_cmd = self.body[-1] + if self.drill_down: + self.body.append(RetractWithClampingStmt()) + self.body.append(RetractWithoutClampingStmt()) + self.drill_down = False + + # Switch to drill mode + self.body.append(DrillModeStmt()) + self.drill_mode = ExcellonContext.MODE_DRILL + + else: + raise ValueError('Should be in slot mode') + + def _render_slot(self, slot, color): + + # Set the tool first, before we might go into drill mode + tool = slot.hit.tool + if not tool in self.handled_tools: + self.handled_tools.add(tool) + self.header.append(ExcellonTool.from_tool(tool)) + + if tool != self.cur_tool: + self.body.append(ToolSelectionStmt(tool.number)) + self.cur_tool = tool + + # Two types of drilling - normal drill and slots + if slot.hit.slot_type == DrillSlot.TYPE_ROUT: + + # For ROUT, setting the mode is part of the actual command. + + # Are we in the right position? + if slot.start != self._pos: + if self.drill_down: + # We need to move into the right position, so retract + self.body.append(RetractWithClampingStmt()) + self.drill_down = False + + # Move to the right spot + point = self._simplify_point(slot.start) + self._pos = slot.start + self.body.append(CoordinateStmt.from_point(point, mode="ROUT")) + + # Now we are in the right spot, so drill down + if not self.drill_down: + self.body.append(ZAxisRoutPositionStmt()) + self.drill_down = True + + # Do a linear move from our current position to the end position + point = self._simplify_point(slot.end) + self._pos = slot.end + self.body.append(CoordinateStmt.from_point(point, mode="LINEAR")) + + self.drill_mode = ExcellonContext.MODE_SLOT + + else: + # This is a G85 slot, so do this in normally drilling mode + if self.drill_mode != ExcellonContext.MODE_DRILL: + self._start_drill_mode() + + # Slots don't use simplified points + self._pos = slot.end + self.body.append(SlotStmt.from_points(slot.start, slot.end)) + + def _render_inverted_layer(self): + pass +
\ No newline at end of file diff --git a/gerber/render/render.py b/gerber/render/render.py index f5c58d8..b319648 100644 --- a/gerber/render/render.py +++ b/gerber/render/render.py @@ -23,12 +23,13 @@ Rendering Render Gerber and Excellon files to a variety of formats. The render module currently supports SVG rendering using the `svgwrite` library. """ + + +from ..primitives import * from ..gerber_statements import (CommentStmt, UnknownStmt, EofStmt, ParamStmt, CoordStmt, ApertureStmt, RegionModeStmt, - QuadrantModeStmt, -) + QuadrantModeStmt,) -from ..primitives import * class GerberContext(object): """ Gerber rendering context base class @@ -41,7 +42,7 @@ class GerberContext(object): Attributes ---------- units : string - Measurement units + Measurement units. 'inch' or 'metric' color : tuple (<float>, <float>, <float>) Color used for rendering as a tuple of normalized (red, green, blue) values. @@ -56,79 +57,89 @@ class GerberContext(object): alpha : float Rendering opacity. Between 0.0 (transparent) and 1.0 (opaque.) """ + def __init__(self, units='inch'): - self.units = units - self.color = (0.7215, 0.451, 0.200) - self.drill_color = (0.25, 0.25, 0.25) - self.background_color = (0.0, 0.0, 0.0) - self.alpha = 1.0 - - def set_units(self, units): - """ Set context measurement units - - Parameters - ---------- - unit : string - Measurement units. may be 'inch' or 'metric' - - Raises - ------ - ValueError - If `unit` is not 'inch' or 'metric' - """ + self._units = units + self._color = (0.7215, 0.451, 0.200) + self._background_color = (0.0, 0.0, 0.0) + self._alpha = 1.0 + self._invert = False + self.ctx = None + + @property + def units(self): + return self._units + + @units.setter + def units(self, units): if units not in ('inch', 'metric'): raise ValueError('Units may be "inch" or "metric"') - self.units = units - - def set_color(self, color): - """ Set rendering color. - - Parameters - ---------- - color : tuple (<float>, <float>, <float>) - Color as a tuple of (red, green, blue) values. Each channel is - represented as a float value in (0, 1) - """ - self.color = color - - def set_drill_color(self, color): - """ Set color used for rendering drill hits. - - Parameters - ---------- - color : tuple (<float>, <float>, <float>) - Color as a tuple of (red, green, blue) values. Each channel is - represented as a float value in (0, 1) - """ - self.drill_color = color - - def set_background_color(self, color): - """ Set rendering background color - - Parameters - ---------- - color : tuple (<float>, <float>, <float>) - Color as a tuple of (red, green, blue) values. Each channel is - represented as a float value in (0, 1) - """ - self.background_color = color - - def set_alpha(self, alpha): - """ Set layer rendering opacity - - .. note:: - Not all backends/rendering devices support this parameter. - - Parameters - ---------- - alpha : float - Rendering opacity. must be between 0.0 (transparent) and 1.0 (opaque) - """ - self.alpha = alpha + self._units = units + + @property + def color(self): + return self._color + + @color.setter + def color(self, color): + if len(color) != 3: + raise TypeError('Color must be a tuple of R, G, and B values') + for c in color: + if c < 0 or c > 1: + raise ValueError('Channel values must be between 0.0 and 1.0') + self._color = color + + @property + def drill_color(self): + return self._drill_color + + @drill_color.setter + def drill_color(self, color): + if len(color) != 3: + raise TypeError('Drill color must be a tuple of R, G, and B values') + for c in color: + if c < 0 or c > 1: + raise ValueError('Channel values must be between 0.0 and 1.0') + self._drill_color = color + + @property + def background_color(self): + return self._background_color + + @background_color.setter + def background_color(self, color): + if len(color) != 3: + raise TypeError('Background color must be a tuple of R, G, and B values') + for c in color: + if c < 0 or c > 1: + raise ValueError('Channel values must be between 0.0 and 1.0') + self._background_color = color + + @property + def alpha(self): + return self._alpha + + @alpha.setter + def alpha(self, alpha): + if alpha < 0 or alpha > 1: + raise ValueError('Alpha must be between 0.0 and 1.0') + self._alpha = alpha + + @property + def invert(self): + return self._invert + + @invert.setter + def invert(self, invert): + self._invert = invert def render(self, primitive): - color = (self.color if primitive.level_polarity == 'dark' - else self.background_color) + if not primitive: + return + + self._pre_render_primitive(primitive) + + color = self.color if isinstance(primitive, Line): self._render_line(primitive, color) elif isinstance(primitive, Arc): @@ -142,11 +153,35 @@ class GerberContext(object): elif isinstance(primitive, Obround): self._render_obround(primitive, color) elif isinstance(primitive, Polygon): - self._render_polygon(Polygon, color) + self._render_polygon(primitive, color) elif isinstance(primitive, Drill): - self._render_drill(primitive, self.drill_color) - else: - return + self._render_drill(primitive, self.color) + elif isinstance(primitive, Slot): + self._render_slot(primitive, self.color) + elif isinstance(primitive, AMGroup): + self._render_amgroup(primitive, color) + elif isinstance(primitive, Outline): + self._render_region(primitive, color) + elif isinstance(primitive, TestRecord): + self._render_test_record(primitive, color) + + self._post_render_primitive(primitive) + + def _pre_render_primitive(self, primitive): + """ + Called before rendering a primitive. Use the callback to perform some action before rendering + a primitive, for example adding a comment. + """ + return + + def _post_render_primitive(self, primitive): + """ + Called after rendering a primitive. Use the callback to perform some action after rendering + a primitive + """ + return + + def _render_line(self, primitive, color): pass @@ -171,4 +206,21 @@ class GerberContext(object): def _render_drill(self, primitive, color): pass + + def _render_slot(self, primitive, color): + pass + + def _render_amgroup(self, primitive, color): + pass + + def _render_test_record(self, primitive, color): + pass + +class RenderSettings(object): + + def __init__(self, color=(0.0, 0.0, 0.0), alpha=1.0, invert=False, mirror=False): + self.color = color + self.alpha = alpha + self.invert = invert + self.mirror = mirror diff --git a/gerber/render/rs274x_backend.py b/gerber/render/rs274x_backend.py new file mode 100644 index 0000000..b4b4612 --- /dev/null +++ b/gerber/render/rs274x_backend.py @@ -0,0 +1,495 @@ +"""Renders an in-memory Gerber file to statements which can be written to a string +""" +from copy import deepcopy + +try: + from cStringIO import StringIO +except(ImportError): + from io import StringIO + +from .render import GerberContext +from ..am_statements import * +from ..gerber_statements import * +from ..primitives import AMGroup, Arc, Circle, Line, Obround, Outline, Polygon, Rectangle + + +class AMGroupContext(object): + '''A special renderer to generate aperature macros from an AMGroup''' + + def __init__(self): + self.statements = [] + + def render(self, amgroup, name): + + if amgroup.stmt: + # We know the statement it was generated from, so use that to create the AMParamStmt + # It will give a much better result + + stmt = deepcopy(amgroup.stmt) + stmt.name = name + + return stmt + + else: + # Clone ourselves, then offset by the psotion so that + # our render doesn't have to consider offset. Just makes things simpler + nooffset_group = deepcopy(amgroup) + nooffset_group.position = (0, 0) + + # Now draw the shapes + for primitive in nooffset_group.primitives: + if isinstance(primitive, Outline): + self._render_outline(primitive) + elif isinstance(primitive, Circle): + self._render_circle(primitive) + elif isinstance(primitive, Rectangle): + self._render_rectangle(primitive) + elif isinstance(primitive, Line): + self._render_line(primitive) + elif isinstance(primitive, Polygon): + self._render_polygon(primitive) + else: + raise ValueError('amgroup') + + statement = AMParamStmt('AM', name, self._statements_to_string()) + return statement + + def _statements_to_string(self): + macro = '' + + for statement in self.statements: + macro += statement.to_gerber() + + return macro + + def _render_circle(self, circle): + self.statements.append(AMCirclePrimitive.from_primitive(circle)) + + def _render_rectangle(self, rectangle): + self.statements.append(AMCenterLinePrimitive.from_primitive(rectangle)) + + def _render_line(self, line): + self.statements.append(AMVectorLinePrimitive.from_primitive(line)) + + def _render_outline(self, outline): + self.statements.append(AMOutlinePrimitive.from_primitive(outline)) + + def _render_polygon(self, polygon): + self.statements.append(AMPolygonPrimitive.from_primitive(polygon)) + + def _render_thermal(self, thermal): + pass + + +class Rs274xContext(GerberContext): + + def __init__(self, settings): + GerberContext.__init__(self) + self.comments = [] + self.header = [] + self.body = [] + self.end = [EofStmt()] + + # Current values so we know if we have to execute + # moves, levey changes before anything else + self._level_polarity = None + self._pos = (None, None) + self._func = None + self._quadrant_mode = None + self._dcode = None + + # Primarily for testing and comarison to files, should we write + # flashes as a single statement or a move plus flash? Set to true + # to do in a single statement. Normally this can be false + self.condensed_flash = True + + # When closing a region, force a D02 staement to close a region. + # This is normally not necessary because regions are closed with a G37 + # staement, but this will add an extra statement for doubly close + # the region + self.explicit_region_move_end = False + + self._next_dcode = 10 + self._rects = {} + self._circles = {} + self._obrounds = {} + self._polygons = {} + self._macros = {} + + self._i_none = 0 + self._j_none = 0 + + self.settings = settings + + self._start_header(settings) + + def _start_header(self, settings): + self.header.append(FSParamStmt.from_settings(settings)) + self.header.append(MOParamStmt.from_units(settings.units)) + + def _simplify_point(self, point): + return (point[0] if point[0] != self._pos[0] else None, point[1] if point[1] != self._pos[1] else None) + + def _simplify_offset(self, point, offset): + + if point[0] != offset[0]: + xoffset = point[0] - offset[0] + else: + xoffset = self._i_none + + if point[1] != offset[1]: + yoffset = point[1] - offset[1] + else: + yoffset = self._j_none + + return (xoffset, yoffset) + + @property + def statements(self): + return self.comments + self.header + self.body + self.end + + def set_bounds(self, bounds): + pass + + def _paint_background(self): + pass + + def _select_aperture(self, aperture): + + # Select the right aperture if not already selected + if aperture: + if isinstance(aperture, Circle): + aper = self._get_circle(aperture.diameter, aperture.hole_diameter) + elif isinstance(aperture, Rectangle): + aper = self._get_rectangle(aperture.width, aperture.height) + elif isinstance(aperture, Obround): + aper = self._get_obround(aperture.width, aperture.height) + elif isinstance(aperture, AMGroup): + aper = self._get_amacro(aperture) + else: + raise NotImplementedError('Line with invalid aperture type') + + if aper.d != self._dcode: + self.body.append(ApertureStmt(aper.d)) + self._dcode = aper.d + + def _pre_render_primitive(self, primitive): + + if hasattr(primitive, 'comment'): + self.body.append(CommentStmt(primitive.comment)) + + def _render_line(self, line, color): + + self._select_aperture(line.aperture) + + self._render_level_polarity(line) + + # Get the right function + if self._func != CoordStmt.FUNC_LINEAR: + func = CoordStmt.FUNC_LINEAR + else: + func = None + self._func = CoordStmt.FUNC_LINEAR + + if self._pos != line.start: + self.body.append(CoordStmt.move(func, self._simplify_point(line.start))) + self._pos = line.start + # We already set the function, so the next command doesn't require that + func = None + + point = self._simplify_point(line.end) + + # In some files, we see a lot of duplicated ponts, so omit those + if point[0] != None or point[1] != None: + self.body.append(CoordStmt.line(func, self._simplify_point(line.end))) + self._pos = line.end + elif func: + self.body.append(CoordStmt.mode(func)) + + def _render_arc(self, arc, color): + + # Optionally set the quadrant mode if it has changed: + if arc.quadrant_mode != self._quadrant_mode: + + if arc.quadrant_mode != 'multi-quadrant': + self.body.append(QuadrantModeStmt.single()) + else: + self.body.append(QuadrantModeStmt.multi()) + + self._quadrant_mode = arc.quadrant_mode + + # Select the right aperture if not already selected + self._select_aperture(arc.aperture) + + self._render_level_polarity(arc) + + # Find the right movement mode. Always set to be sure it is really right + dir = arc.direction + if dir == 'clockwise': + func = CoordStmt.FUNC_ARC_CW + self._func = CoordStmt.FUNC_ARC_CW + elif dir == 'counterclockwise': + func = CoordStmt.FUNC_ARC_CCW + self._func = CoordStmt.FUNC_ARC_CCW + else: + raise ValueError('Invalid circular interpolation mode') + + if self._pos != arc.start: + # TODO I'm not sure if this is right + self.body.append(CoordStmt.move(CoordStmt.FUNC_LINEAR, self._simplify_point(arc.start))) + self._pos = arc.start + + center = self._simplify_offset(arc.center, arc.start) + end = self._simplify_point(arc.end) + self.body.append(CoordStmt.arc(func, end, center)) + self._pos = arc.end + + def _render_region(self, region, color): + + self._render_level_polarity(region) + + self.body.append(RegionModeStmt.on()) + + for p in region.primitives: + + if isinstance(p, Line): + self._render_line(p, color) + else: + self._render_arc(p, color) + + if self.explicit_region_move_end: + self.body.append(CoordStmt.move(None, None)) + + self.body.append(RegionModeStmt.off()) + + def _render_level_polarity(self, region): + if region.level_polarity != self._level_polarity: + self._level_polarity = region.level_polarity + self.body.append(LPParamStmt.from_region(region)) + + def _render_flash(self, primitive, aperture): + + self._render_level_polarity(primitive) + + if aperture.d != self._dcode: + self.body.append(ApertureStmt(aperture.d)) + self._dcode = aperture.d + + if self.condensed_flash: + self.body.append(CoordStmt.flash(self._simplify_point(primitive.position))) + else: + self.body.append(CoordStmt.move(None, self._simplify_point(primitive.position))) + self.body.append(CoordStmt.flash(None)) + + self._pos = primitive.position + + def _get_circle(self, diameter, hole_diameter, dcode = None): + '''Define a circlar aperture''' + + aper = self._circles.get((diameter, hole_diameter), None) + + if not aper: + if not dcode: + dcode = self._next_dcode + self._next_dcode += 1 + else: + self._next_dcode = max(dcode + 1, self._next_dcode) + + aper = ADParamStmt.circle(dcode, diameter, hole_diameter) + self._circles[(diameter, hole_diameter)] = aper + self.header.append(aper) + + return aper + + def _render_circle(self, circle, color): + + aper = self._get_circle(circle.diameter, circle.hole_diameter) + self._render_flash(circle, aper) + + def _get_rectangle(self, width, height, dcode = None): + '''Get a rectanglar aperture. If it isn't defined, create it''' + + key = (width, height) + aper = self._rects.get(key, None) + + if not aper: + if not dcode: + dcode = self._next_dcode + self._next_dcode += 1 + else: + self._next_dcode = max(dcode + 1, self._next_dcode) + + aper = ADParamStmt.rect(dcode, width, height) + self._rects[(width, height)] = aper + self.header.append(aper) + + return aper + + def _render_rectangle(self, rectangle, color): + + aper = self._get_rectangle(rectangle.width, rectangle.height) + self._render_flash(rectangle, aper) + + def _get_obround(self, width, height, dcode = None): + + key = (width, height) + aper = self._obrounds.get(key, None) + + if not aper: + if not dcode: + dcode = self._next_dcode + self._next_dcode += 1 + else: + self._next_dcode = max(dcode + 1, self._next_dcode) + + aper = ADParamStmt.obround(dcode, width, height) + self._obrounds[key] = aper + self.header.append(aper) + + return aper + + def _render_obround(self, obround, color): + + aper = self._get_obround(obround.width, obround.height) + self._render_flash(obround, aper) + + def _render_polygon(self, polygon, color): + + aper = self._get_polygon(polygon.radius, polygon.sides, polygon.rotation, polygon.hole_radius) + self._render_flash(polygon, aper) + + def _get_polygon(self, radius, num_vertices, rotation, hole_radius, dcode = None): + + key = (radius, num_vertices, rotation, hole_radius) + aper = self._polygons.get(key, None) + + if not aper: + if not dcode: + dcode = self._next_dcode + self._next_dcode += 1 + else: + self._next_dcode = max(dcode + 1, self._next_dcode) + + aper = ADParamStmt.polygon(dcode, radius * 2, num_vertices, rotation, hole_radius * 2) + self._polygons[key] = aper + self.header.append(aper) + + return aper + + def _render_drill(self, drill, color): + raise ValueError('Drills are not valid in RS274X files') + + def _hash_amacro(self, amgroup): + '''Calculate a very quick hash code for deciding if we should even check AM groups for comparision''' + + # We always start with an X because this forms part of the name + # Basically, in some cases, the name might start with a C, R, etc. That can appear + # to conflict with normal aperture definitions. Technically, it shouldn't because normal + # aperture definitions should have a comma, but in some cases the commit is omitted + hash = 'X' + for primitive in amgroup.primitives: + + hash += primitive.__class__.__name__[0] + + bbox = primitive.bounding_box + hash += str((bbox[0][1] - bbox[0][0]) * 100000)[0:2] + hash += str((bbox[1][1] - bbox[1][0]) * 100000)[0:2] + + if hasattr(primitive, 'primitives'): + hash += str(len(primitive.primitives)) + + if isinstance(primitive, Rectangle): + hash += str(primitive.width * 1000000)[0:2] + hash += str(primitive.height * 1000000)[0:2] + elif isinstance(primitive, Circle): + hash += str(primitive.diameter * 1000000)[0:2] + + if len(hash) > 20: + # The hash might actually get quite complex, so stop before + # it gets too long + break + + return hash + + def _get_amacro(self, amgroup, dcode = None): + # Macros are a little special since we don't have a good way to compare them quickly + # but in most cases, this should work + + hash = self._hash_amacro(amgroup) + macro = None + macroinfo = self._macros.get(hash, None) + + if macroinfo: + + # We have a definition, but check that the groups actually are the same + for macro in macroinfo: + + # Macros should have positions, right? But if the macro is selected for non-flashes + # then it won't have a position. This is of course a bad gerber, but they do exist + if amgroup.position: + position = amgroup.position + else: + position = (0, 0) + + offset = (position[0] - macro[1].position[0], position[1] - macro[1].position[1]) + if amgroup.equivalent(macro[1], offset): + break + macro = None + + # Did we find one in the group0 + if not macro: + # This is a new macro, so define it + if not dcode: + dcode = self._next_dcode + self._next_dcode += 1 + else: + self._next_dcode = max(dcode + 1, self._next_dcode) + + # Create the statements + # TODO + amrenderer = AMGroupContext() + statement = amrenderer.render(amgroup, hash) + + self.header.append(statement) + + aperdef = ADParamStmt.macro(dcode, hash) + self.header.append(aperdef) + + # Store the dcode and the original so we can check if it really is the same + # If it didn't have a postition, set it to 0, 0 + if amgroup.position == None: + amgroup.position = (0, 0) + macro = (aperdef, amgroup) + + if macroinfo: + macroinfo.append(macro) + else: + self._macros[hash] = [macro] + + return macro[0] + + def _render_amgroup(self, amgroup, color): + + aper = self._get_amacro(amgroup) + self._render_flash(amgroup, aper) + + def _render_inverted_layer(self): + pass + + def _new_render_layer(self): + # TODO Might need to implement this + pass + + def _flatten(self): + # TODO Might need to implement this + pass + + def dump(self): + """Write the rendered file to a StringIO steam""" + statements = map(lambda stmt: stmt.to_gerber(self.settings), self.statements) + stream = StringIO() + for statement in statements: + stream.write(statement + '\n') + + return stream +
\ No newline at end of file diff --git a/gerber/render/svgwrite_backend.py b/gerber/render/svgwrite_backend.py deleted file mode 100644 index 2df87b3..0000000 --- a/gerber/render/svgwrite_backend.py +++ /dev/null @@ -1,155 +0,0 @@ -#! /usr/bin/env python -# -*- coding: utf-8 -*- - -# Copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be> -# Based on render_svg.py by Paulo Henrique Silva <ph.silva@gmail.com> - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from .render import GerberContext -from operator import mul -import svgwrite - -SCALE = 300 - - -def svg_color(color): - color = tuple([int(ch * 255) for ch in color]) - return 'rgb(%d, %d, %d)' % color - - -class GerberSvgContext(GerberContext): - def __init__(self): - GerberContext.__init__(self) - self.scale = (SCALE, -SCALE) - self.dwg = svgwrite.Drawing() - self.background = False - - def dump(self, filename): - self.dwg.saveas(filename) - - def set_bounds(self, bounds): - xbounds, ybounds = bounds - size = (SCALE * (xbounds[1] - xbounds[0]), - SCALE * (ybounds[1] - ybounds[0])) - if not self.background: - vbox = '%f, %f, %f, %f' % (SCALE * xbounds[0], -SCALE * ybounds[1], - size[0], size[1]) - self.dwg = svgwrite.Drawing(viewBox=vbox) - rect = self.dwg.rect(insert=(SCALE * xbounds[0], - -SCALE * ybounds[1]), - size=size, - fill=svg_color(self.background_color)) - self.dwg.add(rect) - self.background = True - - def _render_line(self, line, color): - start = map(mul, line.start, self.scale) - end = map(mul, line.end, self.scale) - aline = self.dwg.line(start=start, end=end, - stroke=svg_color(color), - stroke_width=SCALE * line.width, - stroke_linecap='round') - aline.stroke(opacity=self.alpha) - self.dwg.add(aline) - - def _render_region(self, region, color): - points = [tuple(map(mul, point, self.scale)) for point in region.points] - region_path = self.dwg.path(d='M %f, %f' % points[0], - fill=svg_color(color), - stroke='none') - region_path.fill(opacity=self.alpha) - for point in points[1:]: - region_path.push('L %f, %f' % point) - self.dwg.add(region_path) - - def _render_circle(self, circle, color): - center = map(mul, circle.position, self.scale) - acircle = self.dwg.circle(center=center, - r = SCALE * circle.radius, - fill=svg_color(color)) - acircle.fill(opacity=self.alpha) - self.dwg.add(acircle) - - def _render_rectangle(self, rectangle, color): - center = map(mul, rectangle.position, self.scale) - size = tuple(map(mul, (rectangle.width, rectangle.height), map(abs, self.scale))) - insert = center[0] - size[0] / 2., center[1] - size[1] / 2. - arect = self.dwg.rect(insert=insert, size=size, - fill=svg_color(color)) - arect.fill(opacity=self.alpha) - self.dwg.add(arect) - - def _render_obround(self, obround, color): - x, y = tuple(map(mul, obround.position, self.scale)) - xsize, ysize = tuple(map(mul, (obround.width, obround.height), - self.scale)) - xscale, yscale = self.scale - - # Corner case... - if xsize == ysize: - circle = self.dwg.circle(center=(x, y), - r = (xsize / 2.0), - fill=svg_color(color)) - circle.fill(opacity=self.alpha) - self.dwg.add(circle) - - # Horizontal obround - elif xsize > ysize: - rectx = xsize - ysize - recty = ysize - c1 = self.dwg.circle(center=(x - (rectx / 2.0), y), - r = (ysize / 2.0), - fill=svg_color(color)) - - c2 = self.dwg.circle(center=(x + (rectx / 2.0), y), - r = (ysize / 2.0), - fill=svg_color(color)) - - rect = self.dwg.rect(insert=(x, y), - size=(xsize, ysize), - fill=svg_color(color)) - c1.fill(opacity=self.alpha) - c2.fill(opacity=self.alpha) - rect.fill(opacity=self.alpha) - self.dwg.add(c1) - self.dwg.add(c2) - self.dwg.add(rect) - - # Vertical obround - else: - rectx = xsize - recty = ysize - xsize - c1 = self.dwg.circle(center=(x, y - (recty / 2.)), - r = (xsize / 2.), - fill=svg_color(color)) - - c2 = self.dwg.circle(center=(x, y + (recty / 2.)), - r = (xsize / 2.), - fill=svg_color(color)) - - rect = self.dwg.rect(insert=(x, y), - size=(xsize, ysize), - fill=svg_color(color)) - c1.fill(opacity=self.alpha) - c2.fill(opacity=self.alpha) - rect.fill(opacity=self.alpha) - self.dwg.add(c1) - self.dwg.add(c2) - self.dwg.add(rect) - - def _render_drill(self, circle, color): - center = map(mul, circle.position, self.scale) - hit = self.dwg.circle(center=center, r=SCALE * circle.radius, - fill=svg_color(color)) - self.dwg.add(hit) diff --git a/gerber/render/theme.py b/gerber/render/theme.py new file mode 100644 index 0000000..6135ccb --- /dev/null +++ b/gerber/render/theme.py @@ -0,0 +1,70 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright 2013-2014 Paulo Henrique Silva <ph.silva@gmail.com> + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from .render import RenderSettings + +COLORS = { + 'black': (0.0, 0.0, 0.0), + 'white': (1.0, 1.0, 1.0), + 'red': (1.0, 0.0, 0.0), + 'green': (0.0, 1.0, 0.0), + 'blue': (0.0, 0.0, 1.0), + 'fr-4': (0.290, 0.345, 0.0), + 'green soldermask': (0.0, 0.612, 0.396), + 'blue soldermask': (0.059, 0.478, 0.651), + 'red soldermask': (0.968, 0.169, 0.165), + 'black soldermask': (0.298, 0.275, 0.282), + 'purple soldermask': (0.2, 0.0, 0.334), + 'enig copper': (0.686, 0.525, 0.510), + 'hasl copper': (0.871, 0.851, 0.839) +} + + +class Theme(object): + + def __init__(self, name=None, **kwargs): + self.name = 'Default' if name is None else name + self.background = kwargs.get('background', RenderSettings(COLORS['black'], alpha=0.0)) + self.topsilk = kwargs.get('topsilk', RenderSettings(COLORS['white'])) + self.bottomsilk = kwargs.get('bottomsilk', RenderSettings(COLORS['white'])) + self.topmask = kwargs.get('topmask', RenderSettings(COLORS['green soldermask'], alpha=0.8, invert=True)) + self.bottommask = kwargs.get('bottommask', RenderSettings(COLORS['green soldermask'], alpha=0.8, invert=True)) + self.top = kwargs.get('top', RenderSettings(COLORS['hasl copper'])) + self.bottom = kwargs.get('top', RenderSettings(COLORS['hasl copper'])) + self.drill = kwargs.get('drill', RenderSettings(COLORS['black'])) + self.ipc_netlist = kwargs.get('ipc_netlist', RenderSettings(COLORS['red'])) + + def __getitem__(self, key): + return getattr(self, key) + + def get(self, key, noneval=None): + val = getattr(self, key) + return val if val is not None else noneval + + +THEMES = { + 'default': Theme(), + 'OSH Park': Theme(name='OSH Park', + top=RenderSettings(COLORS['enig copper']), + bottom=RenderSettings(COLORS['enig copper']), + topmask=RenderSettings(COLORS['purple soldermask'], alpha=0.8, invert=True), + bottommask=RenderSettings(COLORS['purple soldermask'], alpha=0.8, invert=True)), + 'Blue': Theme(name='Blue', + topmask=RenderSettings(COLORS['blue soldermask'], alpha=0.8, invert=True), + bottommask=RenderSettings(COLORS['blue soldermask'], alpha=0.8, invert=True)), +} diff --git a/gerber/rs274x.py b/gerber/rs274x.py index f7be44d..e84c161 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -18,13 +18,21 @@ """ This module provides an RS-274-X class and parser. """ - import copy import json import re +import sys + +try: + from cStringIO import StringIO +except(ImportError): + from io import StringIO + from .gerber_statements import * from .primitives import * from .cam import CamFile, FileSettings +from .utils import sq_distance + def read(filename): """ Read data from filename and return a GerberFile @@ -42,6 +50,22 @@ def read(filename): return GerberParser().parse(filename) +def loads(data): + """ Generate a GerberFile object from rs274x data in memory + + Parameters + ---------- + data : string + string containing gerber file contents + + Returns + ------- + file : :class:`gerber.rs274x.GerberFile` + A GerberFile created from the specified file. + """ + return GerberParser().parse_raw(data) + + class GerberFile(CamFile): """ A class representing a single gerber file @@ -71,9 +95,11 @@ class GerberFile(CamFile): `bounds` is stored as ((min x, max x), (min y, max y)) """ - def __init__(self, statements, settings, primitives, filename=None): - super(GerberFile, self).__init__(statements, settings, primitives, filename) + def __init__(self, statements, settings, primitives, apertures, filename=None): + super(GerberFile, self).__init__(statements, settings, primitives, filename) + + self.apertures = apertures @property def comments(self): @@ -87,29 +113,61 @@ class GerberFile(CamFile): @property def bounds(self): - xbounds = [0.0, 0.0] - ybounds = [0.0, 0.0] - for stmt in [stmt for stmt in self.statements - if isinstance(stmt, CoordStmt)]: + min_x = min_y = 1000000 + max_x = max_y = -1000000 + for stmt in [stmt for stmt in self.statements if isinstance(stmt, CoordStmt)]: if stmt.x is not None: - if stmt.x < xbounds[0]: - xbounds[0] = stmt.x - elif stmt.x > xbounds[1]: - xbounds[1] = stmt.x + min_x = min(stmt.x, min_x) + max_x = max(stmt.x, max_x) if stmt.y is not None: - if stmt.y < ybounds[0]: - ybounds[0] = stmt.y - elif stmt.y > ybounds[1]: - ybounds[1] = stmt.y - return (xbounds, ybounds) + min_y = min(stmt.y, min_y) + max_y = max(stmt.y, max_y) + return ((min_x, max_x), (min_y, max_y)) + + @property + def bounding_box(self): + min_x = min_y = 1000000 + max_x = max_y = -1000000 + + for prim in self.primitives: + bounds = prim.bounding_box + min_x = min(bounds[0][0], min_x) + max_x = max(bounds[0][1], max_x) + + min_y = min(bounds[1][0], min_y) + max_y = max(bounds[1][1], max_y) + return ((min_x, max_x), (min_y, max_y)) - def write(self, filename): + def write(self, filename, settings=None): """ Write data out to a gerber file """ with open(filename, 'w') as f: for statement in self.statements: - f.write(statement.to_gerber()) + f.write(statement.to_gerber(settings or self.settings)) + f.write("\n") + + def to_inch(self): + if self.units != 'inch': + self.units = 'inch' + for statement in self.statements: + statement.to_inch() + for primitive in self.primitives: + primitive.to_inch() + + def to_metric(self): + if self.units != 'metric': + self.units = 'metric' + for statement in self.statements: + statement.to_metric() + for primitive in self.primitives: + primitive.to_metric() + + def offset(self, x_offset=0, y_offset=0): + for statement in self.statements: + statement.offset(x_offset, y_offset) + for primitive in self.primitives: + primitive.offset(x_offset, y_offset) class GerberParser(object): @@ -118,42 +176,50 @@ class GerberParser(object): NUMBER = r"[\+-]?\d+" DECIMAL = r"[\+-]?\d+([.]?\d+)?" STRING = r"[a-zA-Z0-9_+\-/!?<>”’(){}.\|&@# :]+" - NAME = r"[a-zA-Z_$][a-zA-Z_$0-9]+" - FUNCTION = r"G\d{2}" - - COORD_OP = r"D[0]?[123]" + NAME = r"[a-zA-Z_$\.][a-zA-Z_$\.0-9+\-]+" - FS = r"(?P<param>FS)(?P<zero>(L|T))?(?P<notation>(A|I))X(?P<x>[0-7][0-7])Y(?P<y>[0-7][0-7])" + FS = r"(?P<param>FS)(?P<zero>(L|T|D))?(?P<notation>(A|I))[NG0-9]*X(?P<x>[0-7][0-7])Y(?P<y>[0-7][0-7])[DM0-9]*" MO = r"(?P<param>MO)(?P<mo>(MM|IN))" - IP = r"(?P<param>IP)(?P<ip>(POS|NEG))" LP = r"(?P<param>LP)(?P<lp>(D|C))" - AD_CIRCLE = r"(?P<param>AD)D(?P<d>\d+)(?P<shape>C)[,]?(?P<modifiers>[^,]*)?" - AD_RECT = r"(?P<param>AD)D(?P<d>\d+)(?P<shape>R)[,](?P<modifiers>[^,]*)" - AD_OBROUND = r"(?P<param>AD)D(?P<d>\d+)(?P<shape>O)[,](?P<modifiers>[^,]*)" - AD_POLY = r"(?P<param>AD)D(?P<d>\d+)(?P<shape>P)[,](?P<modifiers>[^,]*)" - AD_MACRO = r"(?P<param>AD)D(?P<d>\d+)(?P<shape>{name})[,]?(?P<modifiers>[^,]*)?".format(name=NAME) - AM = r"(?P<param>AM)(?P<name>{name})\*(?P<macro>.*)".format(name=NAME) + AD_CIRCLE = r"(?P<param>AD)D(?P<d>\d+)(?P<shape>C)[,]?(?P<modifiers>[^,%]*)" + AD_RECT = r"(?P<param>AD)D(?P<d>\d+)(?P<shape>R)[,](?P<modifiers>[^,%]*)" + AD_OBROUND = r"(?P<param>AD)D(?P<d>\d+)(?P<shape>O)[,](?P<modifiers>[^,%]*)" + AD_POLY = r"(?P<param>AD)D(?P<d>\d+)(?P<shape>P)[,](?P<modifiers>[^,%]*)" + AD_MACRO = r"(?P<param>AD)D(?P<d>\d+)(?P<shape>{name})[,]?(?P<modifiers>[^,%]*)".format(name=NAME) + AM = r"(?P<param>AM)(?P<name>{name})\*(?P<macro>[^%]*)".format(name=NAME) # begin deprecated - OF = r"(?P<param>OF)(A(?P<a>{decimal}))?(B(?P<b>{decimal}))?".format(decimal=DECIMAL) + AS = r"(?P<param>AS)(?P<mode>(AXBY)|(AYBX))" IN = r"(?P<param>IN)(?P<name>.*)" + IP = r"(?P<param>IP)(?P<ip>(POS|NEG))" + IR = r"(?P<param>IR)(?P<angle>{number})".format(number=NUMBER) + MI = r"(?P<param>MI)(A(?P<a>0|1))?(B(?P<b>0|1))?" + OF = r"(?P<param>OF)(A(?P<a>{decimal}))?(B(?P<b>{decimal}))?".format(decimal=DECIMAL) + SF = r"(?P<param>SF)(?P<discarded>.*)" LN = r"(?P<param>LN)(?P<name>.*)" + DEPRECATED_UNIT = re.compile(r'(?P<mode>G7[01])\*') + DEPRECATED_FORMAT = re.compile(r'(?P<format>G9[01])\*') # end deprecated - PARAMS = (FS, MO, IP, LP, AD_CIRCLE, AD_RECT, AD_OBROUND, AD_POLY, AD_MACRO, AM, OF, IN, LN) - PARAM_STMT = [re.compile(r"%{0}\*%".format(p)) for p in PARAMS] + PARAMS = (FS, MO, LP, AD_CIRCLE, AD_RECT, AD_OBROUND, AD_POLY, + AD_MACRO, AM, AS, IN, IP, IR, MI, OF, SF, LN) + + PARAM_STMT = [re.compile(r"%?{0}\*%?".format(p)) for p in PARAMS] + + COORD_FUNCTION = r"G0?[123]" + COORD_OP = r"D0?[123]" COORD_STMT = re.compile(( r"(?P<function>{function})?" r"(X(?P<x>{number}))?(Y(?P<y>{number}))?" r"(I(?P<i>{number}))?(J(?P<j>{number}))?" - r"(?P<op>{op})?\*".format(number=NUMBER, function=FUNCTION, op=COORD_OP))) + r"(?P<op>{op})?\*".format(number=NUMBER, function=COORD_FUNCTION, op=COORD_OP))) - APERTURE_STMT = re.compile(r"(G54)?D(?P<d>\d+)\*") + APERTURE_STMT = re.compile(r"(?P<deprecated>(G54)|(G55))?D(?P<d>\d+)\*") - COMMENT_STMT = re.compile(r"G04(?P<comment>[^*]*)(\*)?") + COMMENT_STMT = re.compile(r"G0?4(?P<comment>[^*]*)(\*)?") - EOF_STMT = re.compile(r"(?P<eof>M02)\*") + EOF_STMT = re.compile(r"(?P<eof>M[0]?[012])\*") REGION_MODE_STMT = re.compile(r'(?P<mode>G3[67])\*') QUAD_MODE_STMT = re.compile(r'(?P<mode>G7[45])\*') @@ -163,10 +229,11 @@ class GerberParser(object): self.statements = [] self.primitives = [] self.apertures = {} + self.macros = {} self.current_region = None self.x = 0 self.y = 0 - + self.op = "D02" self.aperture = 0 self.interpolation = 'linear' self.direction = 'clockwise' @@ -176,31 +243,67 @@ class GerberParser(object): self.quadrant_mode = 'multi-quadrant' self.step_and_repeat = (1, 1, 0, 0) - def parse(self, filename): - fp = open(filename, "r") - data = fp.readlines() + with open(filename, "rU") as fp: + data = fp.read() + return self.parse_raw(data, filename) - for stmt in self._parse(data): + def parse_raw(self, data, filename=None): + for stmt in self._parse(self._split_commands(data)): self.evaluate(stmt) self.statements.append(stmt) - return GerberFile(self.statements, self.settings, self.primitives, filename) + # Initialize statement units + for stmt in self.statements: + stmt.units = self.settings.units + + return GerberFile(self.statements, self.settings, self.primitives, self.apertures.values(), filename) + + def _split_commands(self, data): + """ + Split the data into commands. Commands end with * (and also newline to help with some badly formatted files) + """ + + length = len(data) + start = 0 + in_header = True + + for cur in range(0, length): + + val = data[cur] + + if val == '%' and start == cur: + in_header = True + continue + + if val == '\r' or val == '\n': + if start != cur: + yield data[start:cur] + start = cur + 1 + + elif not in_header and val == '*': + yield data[start:cur + 1] + start = cur + 1 + + elif in_header and val == '%': + yield data[start:cur + 1] + start = cur + 1 + in_header = False def dump_json(self): stmts = {"statements": [stmt.__dict__ for stmt in self.statements]} return json.dumps(stmts) def dump_str(self): - s = "" + string = "" for stmt in self.statements: - s += str(stmt) + "\n" - return s + string += str(stmt) + "\n" + return string def _parse(self, data): oldline = '' - for i, line in enumerate(data): + for line in data: line = oldline + line.strip() # skip empty lines @@ -208,30 +311,20 @@ class GerberParser(object): continue # deal with multi-line parameters - if line.startswith("%") and not line.endswith("%"): + if line.startswith("%") and not line.endswith("%") and not "%" in line[1:]: oldline = line continue did_something = True # make sure we do at least one loop while did_something and len(line) > 0: did_something = False - - # Region Mode - (mode, r) = _match_one(self.REGION_MODE_STMT, line) - if mode: - yield RegionModeStmt.from_gerber(line) - line = r - did_something = True - continue - - # Quadrant Mode - (mode, r) = _match_one(self.QUAD_MODE_STMT, line) - if mode: - yield QuadrantModeStmt.from_gerber(line) - line = r + + # consume empty data blocks + if line[0] == '*': + line = line[1:] did_something = True continue - + # coord (coord, r) = _match_one(self.COORD_STMT, line) if coord: @@ -239,26 +332,18 @@ class GerberParser(object): line = r did_something = True continue - + # aperture selection (aperture, r) = _match_one(self.APERTURE_STMT, line) if aperture: yield ApertureStmt(**aperture) - - did_something = True - line = r - continue - - # comment - (comment, r) = _match_one(self.COMMENT_STMT, line) - if comment: - yield CommentStmt(comment["comment"]) did_something = True line = r continue # parameter (param, r) = _match_one_from_many(self.PARAM_STMT, line) + if param: if param["param"] == "FS": stmt = FSParamStmt.from_dict(param) @@ -270,26 +355,86 @@ class GerberParser(object): stmt = MOParamStmt.from_dict(param) self.settings.units = stmt.mode yield stmt - elif param["param"] == "IP": - yield IPParamStmt.from_dict(param) elif param["param"] == "LP": yield LPParamStmt.from_dict(param) elif param["param"] == "AD": yield ADParamStmt.from_dict(param) elif param["param"] == "AM": - yield AMParamStmt.from_dict(param) + stmt = AMParamStmt.from_dict(param) + stmt.units = self.settings.units + yield stmt elif param["param"] == "OF": yield OFParamStmt.from_dict(param) elif param["param"] == "IN": yield INParamStmt.from_dict(param) elif param["param"] == "LN": yield LNParamStmt.from_dict(param) + # deprecated commands AS, IN, IP, IR, MI, OF, SF, LN + elif param["param"] == "AS": + yield ASParamStmt.from_dict(param) + elif param["param"] == "IN": + yield INParamStmt.from_dict(param) + elif param["param"] == "IP": + yield IPParamStmt.from_dict(param) + elif param["param"] == "IR": + yield IRParamStmt.from_dict(param) + elif param["param"] == "MI": + yield MIParamStmt.from_dict(param) + elif param["param"] == "OF": + yield OFParamStmt.from_dict(param) + elif param["param"] == "SF": + yield SFParamStmt.from_dict(param) + elif param["param"] == "LN": + yield LNParamStmt.from_dict(param) else: yield UnknownStmt(line) + + did_something = True + line = r + continue + + # Region Mode + (mode, r) = _match_one(self.REGION_MODE_STMT, line) + if mode: + yield RegionModeStmt.from_gerber(line) + line = r + did_something = True + continue + + # Quadrant Mode + (mode, r) = _match_one(self.QUAD_MODE_STMT, line) + if mode: + yield QuadrantModeStmt.from_gerber(line) + line = r + did_something = True + continue + + # comment + (comment, r) = _match_one(self.COMMENT_STMT, line) + if comment: + yield CommentStmt(comment["comment"]) did_something = True line = r continue + # deprecated codes + (deprecated_unit, r) = _match_one(self.DEPRECATED_UNIT, line) + if deprecated_unit: + stmt = MOParamStmt(param="MO", mo="inch" if "G70" in + deprecated_unit["mode"] else "metric") + self.settings.units = stmt.mode + yield stmt + line = r + did_something = True + continue + + (deprecated_format, r) = _match_one(self.DEPRECATED_FORMAT, line) + if deprecated_format: + yield DeprecatedStmt.from_gerber(line) + line = r + did_something = True + continue + # eof (eof, r) = _match_one(self.EOF_STMT, line) if eof: @@ -298,14 +443,6 @@ class GerberParser(object): line = r continue - if False: - print self.COORD_STMT.pattern - print self.APERTURE_STMT.pattern - print self.COMMENT_STMT.pattern - print self.EOF_STMT.pattern - for i in self.PARAM_STMT: - print i.pattern - if line.find('*') > 0: yield UnknownStmt(line) did_something = True @@ -338,32 +475,69 @@ class GerberParser(object): elif isinstance(stmt, (RegionModeStmt, QuadrantModeStmt)): self._evaluate_mode(stmt) - elif isinstance(stmt, (CommentStmt, UnknownStmt, EofStmt)): + elif isinstance(stmt, (CommentStmt, UnknownStmt, DeprecatedStmt, EofStmt)): return else: raise Exception("Invalid statement to evaluate") - def _define_aperture(self, d, shape, modifiers): aperture = None if shape == 'C': - diameter = float(modifiers[0][0]) - aperture = Circle(position=None, diameter=diameter) + diameter = modifiers[0][0] + + if len(modifiers[0]) >= 2: + hole_diameter = modifiers[0][1] + else: + hole_diameter = None + + aperture = Circle(position=None, diameter=diameter, hole_diameter=hole_diameter, units=self.settings.units) elif shape == 'R': - width = float(modifiers[0][0]) - height = float(modifiers[0][1]) - aperture = Rectangle(position=None, width=width, height=height) + width = modifiers[0][0] + height = modifiers[0][1] + + if len(modifiers[0]) >= 3: + hole_diameter = modifiers[0][2] + else: + hole_diameter = None + + aperture = Rectangle(position=None, width=width, height=height, hole_diameter=hole_diameter, units=self.settings.units) elif shape == 'O': - width = float(modifiers[0][0]) - height = float(modifiers[0][1]) - aperture = Obround(position=None, width=width, height=height) + width = modifiers[0][0] + height = modifiers[0][1] + + if len(modifiers[0]) >= 3: + hole_diameter = modifiers[0][2] + else: + hole_diameter = None + + aperture = Obround(position=None, width=width, height=height, hole_diameter=hole_diameter, units=self.settings.units) + elif shape == 'P': + outer_diameter = modifiers[0][0] + number_vertices = int(modifiers[0][1]) + if len(modifiers[0]) > 2: + rotation = modifiers[0][2] + else: + rotation = 0 + + if len(modifiers[0]) > 3: + hole_diameter = modifiers[0][3] + else: + hole_diameter = None + aperture = Polygon(position=None, sides=number_vertices, radius=outer_diameter/2.0, hole_diameter=hole_diameter, rotation=rotation) + else: + aperture = self.macros[shape].build(modifiers) + self.apertures[d] = aperture def _evaluate_mode(self, stmt): if stmt.type == 'RegionMode': if self.region_mode == 'on' and stmt.mode == 'off': - self.primitives.append(Region(self.current_region, self.level_polarity)) + # Sometimes we have regions that have no points. Skip those + if self.current_region: + self.primitives.append(Region(self.current_region, + level_polarity=self.level_polarity, units=self.settings.units)) + self.current_region = None self.region_mode = stmt.mode elif stmt.type == 'QuadrantMode': @@ -380,6 +554,8 @@ class GerberParser(object): self.image_polarity = stmt.ip elif stmt.param == "LP": self.level_polarity = stmt.lp + elif stmt.param == "AM": + self.macros[stmt.name] = stmt elif stmt.param == "AD": self._define_aperture(stmt.d, stmt.shape, stmt.modifiers) @@ -391,34 +567,131 @@ class GerberParser(object): self.interpolation = 'linear' elif stmt.function in ('G02', 'G2', 'G03', 'G3'): self.interpolation = 'arc' - self.direction = ('clockwise' if stmt.function in ('G02', 'G2') else 'counterclockwise') + self.direction = ('clockwise' if stmt.function in + ('G02', 'G2') else 'counterclockwise') - if stmt.op == "D01": - if self.region_mode == 'on': - if self.current_region is None: - self.current_region = [(self.x, self.y), ] - self.current_region.append((x, y,)) + if stmt.only_function: + # Sometimes we get a coordinate statement + # that only sets the function. If so, don't + # try futher otherwise that might draw/flash something + return + + if stmt.op: + self.op = stmt.op + else: + # no implicit op allowed, force here if coord block doesn't have it + stmt.op = self.op + + if self.op == "D01" or self.op == "D1": + start = (self.x, self.y) + end = (x, y) + + if self.interpolation == 'linear': + if self.region_mode == 'off': + self.primitives.append(Line(start, end, + self.apertures[self.aperture], + level_polarity=self.level_polarity, + units=self.settings.units)) + 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. + + if self.current_region is None: + self.current_region = [Line(start, end, + self.apertures.get(self.aperture, + Circle((0, 0), 0)), + level_polarity=self.level_polarity, + units=self.settings.units), ] + else: + self.current_region.append(Line(start, end, + self.apertures.get(self.aperture, + Circle((0, 0), 0)), + level_polarity=self.level_polarity, + units=self.settings.units)) else: - start = (self.x, self.y) - end = (x, y) - width = self.apertures[self.aperture].stroke_width - if self.interpolation == 'linear': - self.primitives.append(Line(start, end, width, self.level_polarity)) + i = 0 if stmt.i is None else stmt.i + 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)) else: - center = (start[0] + stmt.i, start[1] + stmt.j) - self.primitives.append(Arc(start, end, center, self.direction, width, self.level_polarity)) - - elif stmt.op == "D02": - pass + 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),] + 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)) + + elif self.op == "D02" or self.op == "D2": + + if self.region_mode == "on": + # D02 in the middle of a region finishes that region and starts a new one + if self.current_region and len(self.current_region) > 1: + self.primitives.append(Region(self.current_region, level_polarity=self.level_polarity, units=self.settings.units)) + self.current_region = None - elif stmt.op == "D03": + elif self.op == "D03" or self.op == "D3": primitive = copy.deepcopy(self.apertures[self.aperture]) - # XXX: temporary fix because there are no primitives for Macros and Polygon + + if primitive is not None: - primitive.position = (x, y) - primitive.level_polarity = self.level_polarity - self.primitives.append(primitive) + + if not isinstance(primitive, AMParamStmt): + primitive.position = (x, y) + primitive.level_polarity = self.level_polarity + primitive.units = self.settings.units + self.primitives.append(primitive) + else: + # Aperture Macro + for am_prim in primitive.primitives: + renderable = am_prim.to_primitive((x, y), + self.level_polarity, + self.settings.units) + if renderable is not None: + self.primitives.append(renderable) self.x, self.y = x, y + + def _find_center(self, start, end, offsets): + """ + In single quadrant mode, the offsets are always positive, which means there are 4 possible centers. + The correct center is the only one that results in an arc with sweep angle of less than or equal to 90 degrees + """ + + if self.quadrant_mode == 'single-quadrant': + + # The Gerber spec says single quadrant only has one possible center, and you can detect + # based on the angle. But for real files, this seems to work better - there is usually + # only one option that makes sense for the center (since the distance should be the same + # from start and end). Find the center that makes the most sense + sqdist_diff_min = sys.maxint + center = None + for factors in [(1, 1), (1, -1), (-1, 1), (-1, -1)]: + + test_center = (start[0] + offsets[0] * factors[0], start[1] + offsets[1] * factors[1]) + + sqdist_start = sq_distance(start, test_center) + sqdist_end = sq_distance(end, test_center) + + if abs(sqdist_start - sqdist_end) < sqdist_diff_min: + center = test_center + sqdist_diff_min = abs(sqdist_start - sqdist_end) + + return center + else: + return (start[0] + offsets[0], start[1] + offsets[1]) def _evaluate_aperture(self, stmt): self.aperture = stmt.d diff --git a/gerber/tests/golden/example_am_exposure_modifier.png b/gerber/tests/golden/example_am_exposure_modifier.png Binary files differnew file mode 100644 index 0000000..dac951f --- /dev/null +++ b/gerber/tests/golden/example_am_exposure_modifier.png 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_holes_dont_clear.png b/gerber/tests/golden/example_holes_dont_clear.png Binary files differnew file mode 100644 index 0000000..7efb67b --- /dev/null +++ b/gerber/tests/golden/example_holes_dont_clear.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.gbr b/gerber/tests/golden/example_single_quadrant.gbr new file mode 100644 index 0000000..b0a3166 --- /dev/null +++ b/gerber/tests/golden/example_single_quadrant.gbr @@ -0,0 +1,16 @@ +%FSLAX23Y23*% +%MOIN*% +%ADD10C,0.01*% +G74* +D10* +%LPD*% +G01X1100Y600D02* +G03X700Y1000I-400J0D01* +G03X300Y600I0J-400D01* +G03X700Y200I400J0D01* +G03X1100Y600I0J400D01* +G01X300D02* +X1100D01* +X700Y200D02* +Y1000D01* +M02* 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.gbr b/gerber/tests/golden/example_two_square_boxes.gbr new file mode 100644 index 0000000..b5c60d1 --- /dev/null +++ b/gerber/tests/golden/example_two_square_boxes.gbr @@ -0,0 +1,16 @@ +%FSLAX25Y25*% +%MOMM*% +%ADD10C,0.01*% +D10* +%LPD*% +G01X0Y0D02* +X500000D01* +Y500000D01* +X0D01* +Y0D01* +X600000D02* +X1100000D01* +Y500000D01* +X600000D01* +Y0D01* +M02* diff --git a/gerber/tests/golden/example_two_square_boxes.png b/gerber/tests/golden/example_two_square_boxes.png Binary files differnew file mode 100644 index 0000000..98d0518 --- /dev/null +++ b/gerber/tests/golden/example_two_square_boxes.png diff --git a/gerber/tests/resources/example_am_exposure_modifier.gbr b/gerber/tests/resources/example_am_exposure_modifier.gbr new file mode 100644 index 0000000..5f3f3dd --- /dev/null +++ b/gerber/tests/resources/example_am_exposure_modifier.gbr @@ -0,0 +1,16 @@ +G04 Umaco example for exposure modifier and clearing area* +%FSLAX26Y26*% +%MOIN*% +%AMSQUAREWITHHOLE* +21,0.1,1,1,0,0,0* +1,0,0.5,0,0*% +%ADD10SQUAREWITHHOLE*% +%ADD11C,1*% +G01* +%LPD*% +D11* +X-1000000Y-250000D02* +X1000000Y250000D01* +D10* +X0Y0D03* +M02*
\ No newline at end of file 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_holes_dont_clear.gbr b/gerber/tests/resources/example_holes_dont_clear.gbr new file mode 100644 index 0000000..deeebd0 --- /dev/null +++ b/gerber/tests/resources/example_holes_dont_clear.gbr @@ -0,0 +1,13 @@ +G04 Demonstrates that apertures with holes do not clear the area - only the aperture hole* +%FSLAX26Y26*% +%MOIN*% +%ADD10C,1X0.5*% +%ADD11C,0.1*% +G01* +%LPD*% +D11* +X-1000000Y-250000D02* +X1000000Y250000D01* +D10* +X0Y0D03* +M02*
\ 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/resources/example_two_square_boxes.gbr b/gerber/tests/resources/example_two_square_boxes.gbr new file mode 100644 index 0000000..54a8ac1 --- /dev/null +++ b/gerber/tests/resources/example_two_square_boxes.gbr @@ -0,0 +1,19 @@ +G04 Ucamco ex. 1: Two square boxes* +%FSLAX25Y25*% +%MOMM*% +%TF.Part,Other*% +%LPD*% +%ADD10C,0.010*% +D10* +X0Y0D02* +G01* +X500000Y0D01* +Y500000D01* +X0D01* +Y0D01* +X600000D02* +X1100000D01* +Y500000D01* +X600000D01* +Y0D01* +M02*
\ No newline at end of file diff --git a/gerber/tests/resources/ipc-d-356.ipc b/gerber/tests/resources/ipc-d-356.ipc new file mode 100644 index 0000000..2ed3f49 --- /dev/null +++ b/gerber/tests/resources/ipc-d-356.ipc @@ -0,0 +1,115 @@ +C IPC-D-356 generated by EAGLE Version 7.1.0 Copyright (c) 1988-2014 CadSoft +C Database /Some/Path/To/File +C +P JOB EAGLE 7.1 NETLIST, DATE: 2/20/15 12:00 AM +P UNITS CUST 0 +P DIM N +P NNAME1 A_REALLY_LONG_NET_NAME +317GND VIA D 24PA00X 14900Y 1450X 396Y 396 +317GND VIA D 24PA00X 3850Y 8500X 396Y 396 +317GND VIA D 24PA00X 6200Y 10650X 396Y 396 +317GND VIA D 24PA00X 8950Y 1000X 396Y 396 +317GND VIA D 24PA00X 11800Y 2250X 396Y 396 +317GND VIA D 24PA00X 15350Y 3200X 396Y 396 +317GND VIA D 24PA00X 13200Y 3800X 396Y 396 +317GND VIA D 24PA00X 9700Y 12050X 396Y 396 +317GND VIA D 24PA00X 13950Y 11900X 396Y 396 +317GND VIA D 24PA00X 13050Y 7050X 396Y 396 +317GND VIA D 24PA00X 13000Y 8400X 396Y 396 +317N$3 VIA D 24PA00X 11350Y 10100X 396Y 396 +317N$3 VIA D 24PA00X 13250Y 5700X 396Y 396 +317VCC VIA D 24PA00X 15550Y 6850X 396Y 396 +327N$3 C1 -+ A01X 9700Y 10402X1575Y 630R270 +327GND C1 -- A01X 9700Y 13198X1575Y 630R270 +327VCC C2 -+ A01X 13950Y 9677X1535Y 630R270 +327GND C2 -- A01X 13950Y 13023X1535Y 630R270 +327VCC C3 -1 A01X 3850Y 9924X 512Y 591R270 +327GND C3 -2 A01X 3850Y 9176X 512Y 591R270 +327VCC C4 -1 A01X 10374Y 1000X 512Y 591R180 +327GND C4 -2 A01X 9626Y 1000X 512Y 591R180 +327VCC C5 -1 A01X 14700Y 3924X 512Y 591R270 +327GND C5 -2 A01X 14700Y 3176X 512Y 591R270 +317DMX+ DMX -1 D 40PA00X 5050Y 13900X 600Y1200R 90 +317DMX- DMX -2 D 40PA00X 6050Y 13900X 600Y1200R 90 +317GND DMX -3 D 40PA00X 7050Y 13900X 600Y1200R 90 +317PIC_MCLR J1 -1 D 35PA00X 16900Y 6400X 554Y 554R 90 +317VCC J1 -2 D 35PA00X 17900Y 6900X 554Y 554R 90 +317GND J1 -3 D 35PA00X 16900Y 7400X 554Y 554R 90 +317PIC_PGD J1 -4 D 35PA00X 17900Y 7900X 554Y 554R 90 +317PIC_PGC J1 -5 D 35PA00X 16900Y 8400X 554Y 554R 90 +317 J1 -6 D 35PA00X 17900Y 8900X 554Y 554R 90 +327N$4 L1 -1 A01X 13950Y 6382X 748Y1339R 90 +327VCC L1 -2 A01X 13950Y 7918X 748Y1339R 90 +327N$5 LED1 -A A01X 16313Y 1450X 472Y 472R 0 +327GND LED1 -C A01X 15487Y 1450X 472Y 472R 0 +317 MIDI -1 D 40PA00X 1200Y 9500X 600Y1200R 0 +317 MIDI -2 D 40PA00X 1200Y 8500X 600Y1200R 0 +317 MIDI -3 D 40PA00X 1200Y 7500X 600Y1200R 0 +317N$9 MIDI -4 D 40PA00X 1200Y 6500X 600Y1200R 0 +317N$10 MIDI -5 D 40PA00X 1200Y 5500X 600Y1200R 0 +317N$3 PWR -1 D 40PA00X 17050Y 13750X 600Y1200R 90 +317GND PWR -2 D 40PA00X 18050Y 13750X 600Y1200R 90 +327DMX+ R1 -1 A01X 5076Y 11500X 512Y 591R 0 +327DMX- R1 -2 A01X 5824Y 11500X 512Y 591R 0 +327VCC R2 -1 A01X 14376Y 5300X 512Y 591R 0 +327PIC_MCLR R2 -2 A01X 15124Y 5300X 512Y 591R 0 +327N$9 R3 -1 A01X 3126Y 6500X 512Y 591R 0 +327N$6 R3 -2 A01X 3874Y 6500X 512Y 591R 0 +327PIC_RX R4 -1 A01X 9600Y 2624X 512Y 591R270 +327VCC R4 -2 A01X 9600Y 1876X 512Y 591R270 +327VCC R5 -1 A01X 17974Y 1450X 512Y 591R180 +327N$5 R5 -2 A01X 17226Y 1450X 512Y 591R180 +327N$3 U1 -1 A01X 12330Y 5710X 420Y 850R 90 +327N$4 U1 -2 A01X 12330Y 6380X 420Y 850R 90 +327GND U1 -3 A01X 12330Y 7050X 420Y 850R 90 +327VCC U1 -4 A01X 12330Y 7720X 420Y 850R 90 +327GND U1 -5 A01X 12330Y 8390X 420Y 850R 90 +327 U1 -6 A01X 9050Y 7050X4252Y4098R 90 +327PIC_MCLR U2 -1 A01X 11123Y 4063X 157Y 591R270 +327 U2 -2 A01X 11123Y 3807X 157Y 591R270 +327 U2 -3 A01X 11123Y 3552X 157Y 591R270 +327N$1 U2 -4 A01X 11123Y 3296X 157Y 591R270 +327N$2 U2 -5 A01X 11123Y 3040X 157Y 591R270 +327PIC_RX U2 -6 A01X 11123Y 2784X 157Y 591R270 +327 U2 -7 A01X 11123Y 2528X 157Y 591R270 +327GND U2 -8 A01X 11123Y 2272X 157Y 591R270 +327 U2 -9 A01X 11123Y 2016X 157Y 591R270 +327 U2 -10 A01X 11123Y 1760X 157Y 591R270 +327 U2 -11 A01X 11123Y 1504X 157Y 591R270 +327 U2 -12 A01X 11123Y 1248X 157Y 591R270 +327VCC U2 -13 A01X 11123Y 993X 157Y 591R270 +327 U2 -14 A01X 11123Y 737X 157Y 591R270 +327 U2 -15 A01X 13977Y 737X 157Y 591R270 +327 U2 -16 A01X 13977Y 993X 157Y 591R270 +327 U2 -17 A01X 13977Y 1248X 157Y 591R270 +327 U2 -18 A01X 13977Y 1504X 157Y 591R270 +327 U2 -19 A01X 13977Y 1760X 157Y 591R270 +327 U2 -20 A01X 13977Y 2016X 157Y 591R270 +327PIC_PGD U2 -21 A01X 13977Y 2272X 157Y 591R270 +327PIC_PGC U2 -22 A01X 13977Y 2528X 157Y 591R270 +327 U2 -23 A01X 13977Y 2784X 157Y 591R270 +327 U2 -24 A01X 13977Y 3040X 157Y 591R270 +327 U2 -25 A01X 13977Y 3296X 157Y 591R270 +327 U2 -26 A01X 13977Y 3552X 157Y 591R270 +327GND U2 -27 A01X 13977Y 3807X 157Y 591R270 +327VCC U2 -28 A01X 13977Y 4063X 157Y 591R270 +327N$2 U3 -1 A01X 4700Y 7540X 260Y 800R 0 +327VCC U3 -2 A01X 5200Y 7540X 260Y 800R 0 +327VCC U3 -3 A01X 5700Y 7540X 260Y 800R 0 +327N$1 U3 -4 A01X 6200Y 7540X 260Y 800R 0 +327GND U3 -5 A01X 6200Y 9960X 260Y 800R 0 +327DMX- U3 -6 A01X 5700Y 9960X 260Y 800R 0 +327DMX+ U3 -7 A01X 5200Y 9960X 260Y 800R 0 +327VCC U3 -8 A01X 4700Y 9960X 260Y 800R 0 +327 U4 -1 A01X 4704Y 3850X 394Y 500R 0 +327N$6 U4 -2 A01X 4704Y 2800X 394Y 500R 0 +327N$10 U4 -3 A01X 4704Y 1800X 394Y 500R 0 +327 U4 -4 A01X 4704Y 750X 394Y 500R 0 +327GND U4 -5 A01X 8396Y 750X 394Y 500R 0 +327PIC_RX U4 -6 A01X 8396Y 1800X 394Y 500R 0 +327 U4 -7 A01X 8396Y 2800X 394Y 500R 0 +327VCC U4 -8 A01X 8396Y 3850X 394Y 500R 0 +327NNAME1 NA -69 A01X 8396Y 3850X 394Y 500R 0 +389BOARD_EDGE X0Y0 X22500 Y15000 X0 +089 X1300Y240 +999 diff --git a/gerber/tests/resources/multiline_read.ger b/gerber/tests/resources/multiline_read.ger new file mode 100644 index 0000000..02242e4 --- /dev/null +++ b/gerber/tests/resources/multiline_read.ger @@ -0,0 +1,9 @@ +G75* +G71* +%OFA0B0*% +%FSLAX23Y23*% +%IPPOS*% +%LPD*% +%ADD10C,0.1*% +%LPD*%D10* +M02*
\ No newline at end of file diff --git a/gerber/tests/resources/top_copper.GTL b/gerber/tests/resources/top_copper.GTL index cedd2fd..d53f5ec 100644 --- a/gerber/tests/resources/top_copper.GTL +++ b/gerber/tests/resources/top_copper.GTL @@ -1,3457 +1 @@ -G75* -%MOIN*% -%OFA0B0*% -%FSLAX24Y24*% -%IPPOS*% -%LPD*% -%AMOC8* -5,1,8,0,0,1.08239X$1,22.5* -% -%ADD10C,0.0000*% -%ADD11R,0.0260X0.0800*% -%ADD12R,0.0591X0.0157*% -%ADD13R,0.4098X0.4252*% -%ADD14R,0.0850X0.0420*% -%ADD15R,0.0630X0.1575*% -%ADD16R,0.0591X0.0512*% -%ADD17R,0.0512X0.0591*% -%ADD18R,0.0630X0.1535*% -%ADD19R,0.1339X0.0748*% -%ADD20C,0.0004*% -%ADD21C,0.0554*% -%ADD22R,0.0394X0.0500*% -%ADD23C,0.0600*% -%ADD24R,0.0472X0.0472*% -%ADD25C,0.0160*% -%ADD26C,0.0396*% -%ADD27C,0.0240*% -D10* -X000300Y003064D02* -X000300Y018064D01* -X022800Y018064D01* -X022800Y003064D01* -X000300Y003064D01* -X001720Y005114D02* -X001722Y005164D01* -X001728Y005214D01* -X001738Y005263D01* -X001752Y005311D01* -X001769Y005358D01* -X001790Y005403D01* -X001815Y005447D01* -X001843Y005488D01* -X001875Y005527D01* -X001909Y005564D01* -X001946Y005598D01* -X001986Y005628D01* -X002028Y005655D01* -X002072Y005679D01* -X002118Y005700D01* -X002165Y005716D01* -X002213Y005729D01* -X002263Y005738D01* -X002312Y005743D01* -X002363Y005744D01* -X002413Y005741D01* -X002462Y005734D01* -X002511Y005723D01* -X002559Y005708D01* -X002605Y005690D01* -X002650Y005668D01* -X002693Y005642D01* -X002734Y005613D01* -X002773Y005581D01* -X002809Y005546D01* -X002841Y005508D01* -X002871Y005468D01* -X002898Y005425D01* -X002921Y005381D01* -X002940Y005335D01* -X002956Y005287D01* -X002968Y005238D01* -X002976Y005189D01* -X002980Y005139D01* -X002980Y005089D01* -X002976Y005039D01* -X002968Y004990D01* -X002956Y004941D01* -X002940Y004893D01* -X002921Y004847D01* -X002898Y004803D01* -X002871Y004760D01* -X002841Y004720D01* -X002809Y004682D01* -X002773Y004647D01* -X002734Y004615D01* -X002693Y004586D01* -X002650Y004560D01* -X002605Y004538D01* -X002559Y004520D01* -X002511Y004505D01* -X002462Y004494D01* -X002413Y004487D01* -X002363Y004484D01* -X002312Y004485D01* -X002263Y004490D01* -X002213Y004499D01* -X002165Y004512D01* -X002118Y004528D01* -X002072Y004549D01* -X002028Y004573D01* -X001986Y004600D01* -X001946Y004630D01* -X001909Y004664D01* -X001875Y004701D01* -X001843Y004740D01* -X001815Y004781D01* -X001790Y004825D01* -X001769Y004870D01* -X001752Y004917D01* -X001738Y004965D01* -X001728Y005014D01* -X001722Y005064D01* -X001720Y005114D01* -X001670Y016064D02* -X001672Y016114D01* -X001678Y016164D01* -X001688Y016213D01* -X001702Y016261D01* -X001719Y016308D01* -X001740Y016353D01* -X001765Y016397D01* -X001793Y016438D01* -X001825Y016477D01* -X001859Y016514D01* -X001896Y016548D01* -X001936Y016578D01* -X001978Y016605D01* -X002022Y016629D01* -X002068Y016650D01* -X002115Y016666D01* -X002163Y016679D01* -X002213Y016688D01* -X002262Y016693D01* -X002313Y016694D01* -X002363Y016691D01* -X002412Y016684D01* -X002461Y016673D01* -X002509Y016658D01* -X002555Y016640D01* -X002600Y016618D01* -X002643Y016592D01* -X002684Y016563D01* -X002723Y016531D01* -X002759Y016496D01* -X002791Y016458D01* -X002821Y016418D01* -X002848Y016375D01* -X002871Y016331D01* -X002890Y016285D01* -X002906Y016237D01* -X002918Y016188D01* -X002926Y016139D01* -X002930Y016089D01* -X002930Y016039D01* -X002926Y015989D01* -X002918Y015940D01* -X002906Y015891D01* -X002890Y015843D01* -X002871Y015797D01* -X002848Y015753D01* -X002821Y015710D01* -X002791Y015670D01* -X002759Y015632D01* -X002723Y015597D01* -X002684Y015565D01* -X002643Y015536D01* -X002600Y015510D01* -X002555Y015488D01* -X002509Y015470D01* -X002461Y015455D01* -X002412Y015444D01* -X002363Y015437D01* -X002313Y015434D01* -X002262Y015435D01* -X002213Y015440D01* -X002163Y015449D01* -X002115Y015462D01* -X002068Y015478D01* -X002022Y015499D01* -X001978Y015523D01* -X001936Y015550D01* -X001896Y015580D01* -X001859Y015614D01* -X001825Y015651D01* -X001793Y015690D01* -X001765Y015731D01* -X001740Y015775D01* -X001719Y015820D01* -X001702Y015867D01* -X001688Y015915D01* -X001678Y015964D01* -X001672Y016014D01* -X001670Y016064D01* -X020060Y012714D02* -X020062Y012764D01* -X020068Y012814D01* -X020078Y012863D01* -X020091Y012912D01* -X020109Y012959D01* -X020130Y013005D01* -X020154Y013048D01* -X020182Y013090D01* -X020213Y013130D01* -X020247Y013167D01* -X020284Y013201D01* -X020324Y013232D01* -X020366Y013260D01* -X020409Y013284D01* -X020455Y013305D01* -X020502Y013323D01* -X020551Y013336D01* -X020600Y013346D01* -X020650Y013352D01* -X020700Y013354D01* -X020750Y013352D01* -X020800Y013346D01* -X020849Y013336D01* -X020898Y013323D01* -X020945Y013305D01* -X020991Y013284D01* -X021034Y013260D01* -X021076Y013232D01* -X021116Y013201D01* -X021153Y013167D01* -X021187Y013130D01* -X021218Y013090D01* -X021246Y013048D01* -X021270Y013005D01* -X021291Y012959D01* -X021309Y012912D01* -X021322Y012863D01* -X021332Y012814D01* -X021338Y012764D01* -X021340Y012714D01* -X021338Y012664D01* -X021332Y012614D01* -X021322Y012565D01* -X021309Y012516D01* -X021291Y012469D01* -X021270Y012423D01* -X021246Y012380D01* -X021218Y012338D01* -X021187Y012298D01* -X021153Y012261D01* -X021116Y012227D01* -X021076Y012196D01* -X021034Y012168D01* -X020991Y012144D01* -X020945Y012123D01* -X020898Y012105D01* -X020849Y012092D01* -X020800Y012082D01* -X020750Y012076D01* -X020700Y012074D01* -X020650Y012076D01* -X020600Y012082D01* -X020551Y012092D01* -X020502Y012105D01* -X020455Y012123D01* -X020409Y012144D01* -X020366Y012168D01* -X020324Y012196D01* -X020284Y012227D01* -X020247Y012261D01* -X020213Y012298D01* -X020182Y012338D01* -X020154Y012380D01* -X020130Y012423D01* -X020109Y012469D01* -X020091Y012516D01* -X020078Y012565D01* -X020068Y012614D01* -X020062Y012664D01* -X020060Y012714D01* -X020170Y016064D02* -X020172Y016114D01* -X020178Y016164D01* -X020188Y016213D01* -X020202Y016261D01* -X020219Y016308D01* -X020240Y016353D01* -X020265Y016397D01* -X020293Y016438D01* -X020325Y016477D01* -X020359Y016514D01* -X020396Y016548D01* -X020436Y016578D01* -X020478Y016605D01* -X020522Y016629D01* -X020568Y016650D01* -X020615Y016666D01* -X020663Y016679D01* -X020713Y016688D01* -X020762Y016693D01* -X020813Y016694D01* -X020863Y016691D01* -X020912Y016684D01* -X020961Y016673D01* -X021009Y016658D01* -X021055Y016640D01* -X021100Y016618D01* -X021143Y016592D01* -X021184Y016563D01* -X021223Y016531D01* -X021259Y016496D01* -X021291Y016458D01* -X021321Y016418D01* -X021348Y016375D01* -X021371Y016331D01* -X021390Y016285D01* -X021406Y016237D01* -X021418Y016188D01* -X021426Y016139D01* -X021430Y016089D01* -X021430Y016039D01* -X021426Y015989D01* -X021418Y015940D01* -X021406Y015891D01* -X021390Y015843D01* -X021371Y015797D01* -X021348Y015753D01* -X021321Y015710D01* -X021291Y015670D01* -X021259Y015632D01* -X021223Y015597D01* -X021184Y015565D01* -X021143Y015536D01* -X021100Y015510D01* -X021055Y015488D01* -X021009Y015470D01* -X020961Y015455D01* -X020912Y015444D01* -X020863Y015437D01* -X020813Y015434D01* -X020762Y015435D01* -X020713Y015440D01* -X020663Y015449D01* -X020615Y015462D01* -X020568Y015478D01* -X020522Y015499D01* -X020478Y015523D01* -X020436Y015550D01* -X020396Y015580D01* -X020359Y015614D01* -X020325Y015651D01* -X020293Y015690D01* -X020265Y015731D01* -X020240Y015775D01* -X020219Y015820D01* -X020202Y015867D01* -X020188Y015915D01* -X020178Y015964D01* -X020172Y016014D01* -X020170Y016064D01* -X020060Y008714D02* -X020062Y008764D01* -X020068Y008814D01* -X020078Y008863D01* -X020091Y008912D01* -X020109Y008959D01* -X020130Y009005D01* -X020154Y009048D01* -X020182Y009090D01* -X020213Y009130D01* -X020247Y009167D01* -X020284Y009201D01* -X020324Y009232D01* -X020366Y009260D01* -X020409Y009284D01* -X020455Y009305D01* -X020502Y009323D01* -X020551Y009336D01* -X020600Y009346D01* -X020650Y009352D01* -X020700Y009354D01* -X020750Y009352D01* -X020800Y009346D01* -X020849Y009336D01* -X020898Y009323D01* -X020945Y009305D01* -X020991Y009284D01* -X021034Y009260D01* -X021076Y009232D01* -X021116Y009201D01* -X021153Y009167D01* -X021187Y009130D01* -X021218Y009090D01* -X021246Y009048D01* -X021270Y009005D01* -X021291Y008959D01* -X021309Y008912D01* -X021322Y008863D01* -X021332Y008814D01* -X021338Y008764D01* -X021340Y008714D01* -X021338Y008664D01* -X021332Y008614D01* -X021322Y008565D01* -X021309Y008516D01* -X021291Y008469D01* -X021270Y008423D01* -X021246Y008380D01* -X021218Y008338D01* -X021187Y008298D01* -X021153Y008261D01* -X021116Y008227D01* -X021076Y008196D01* -X021034Y008168D01* -X020991Y008144D01* -X020945Y008123D01* -X020898Y008105D01* -X020849Y008092D01* -X020800Y008082D01* -X020750Y008076D01* -X020700Y008074D01* -X020650Y008076D01* -X020600Y008082D01* -X020551Y008092D01* -X020502Y008105D01* -X020455Y008123D01* -X020409Y008144D01* -X020366Y008168D01* -X020324Y008196D01* -X020284Y008227D01* -X020247Y008261D01* -X020213Y008298D01* -X020182Y008338D01* -X020154Y008380D01* -X020130Y008423D01* -X020109Y008469D01* -X020091Y008516D01* -X020078Y008565D01* -X020068Y008614D01* -X020062Y008664D01* -X020060Y008714D01* -X020170Y005064D02* -X020172Y005114D01* -X020178Y005164D01* -X020188Y005213D01* -X020202Y005261D01* -X020219Y005308D01* -X020240Y005353D01* -X020265Y005397D01* -X020293Y005438D01* -X020325Y005477D01* -X020359Y005514D01* -X020396Y005548D01* -X020436Y005578D01* -X020478Y005605D01* -X020522Y005629D01* -X020568Y005650D01* -X020615Y005666D01* -X020663Y005679D01* -X020713Y005688D01* -X020762Y005693D01* -X020813Y005694D01* -X020863Y005691D01* -X020912Y005684D01* -X020961Y005673D01* -X021009Y005658D01* -X021055Y005640D01* -X021100Y005618D01* -X021143Y005592D01* -X021184Y005563D01* -X021223Y005531D01* -X021259Y005496D01* -X021291Y005458D01* -X021321Y005418D01* -X021348Y005375D01* -X021371Y005331D01* -X021390Y005285D01* -X021406Y005237D01* -X021418Y005188D01* -X021426Y005139D01* -X021430Y005089D01* -X021430Y005039D01* -X021426Y004989D01* -X021418Y004940D01* -X021406Y004891D01* -X021390Y004843D01* -X021371Y004797D01* -X021348Y004753D01* -X021321Y004710D01* -X021291Y004670D01* -X021259Y004632D01* -X021223Y004597D01* -X021184Y004565D01* -X021143Y004536D01* -X021100Y004510D01* -X021055Y004488D01* -X021009Y004470D01* -X020961Y004455D01* -X020912Y004444D01* -X020863Y004437D01* -X020813Y004434D01* -X020762Y004435D01* -X020713Y004440D01* -X020663Y004449D01* -X020615Y004462D01* -X020568Y004478D01* -X020522Y004499D01* -X020478Y004523D01* -X020436Y004550D01* -X020396Y004580D01* -X020359Y004614D01* -X020325Y004651D01* -X020293Y004690D01* -X020265Y004731D01* -X020240Y004775D01* -X020219Y004820D01* -X020202Y004867D01* -X020188Y004915D01* -X020178Y004964D01* -X020172Y005014D01* -X020170Y005064D01* -D11* -X006500Y010604D03* -X006000Y010604D03* -X005500Y010604D03* -X005000Y010604D03* -X005000Y013024D03* -X005500Y013024D03* -X006000Y013024D03* -X006500Y013024D03* -D12* -X011423Y007128D03* -X011423Y006872D03* -X011423Y006616D03* -X011423Y006360D03* -X011423Y006104D03* -X011423Y005848D03* -X011423Y005592D03* -X011423Y005336D03* -X011423Y005080D03* -X011423Y004825D03* -X011423Y004569D03* -X011423Y004313D03* -X011423Y004057D03* -X011423Y003801D03* -X014277Y003801D03* -X014277Y004057D03* -X014277Y004313D03* -X014277Y004569D03* -X014277Y004825D03* -X014277Y005080D03* -X014277Y005336D03* -X014277Y005592D03* -X014277Y005848D03* -X014277Y006104D03* -X014277Y006360D03* -X014277Y006616D03* -X014277Y006872D03* -X014277Y007128D03* -D13* -X009350Y010114D03* -D14* -X012630Y010114D03* -X012630Y010784D03* -X012630Y011454D03* -X012630Y009444D03* -X012630Y008774D03* -D15* -X010000Y013467D03* -X010000Y016262D03* -D16* -X004150Y012988D03* -X004150Y012240D03* -X009900Y005688D03* -X009900Y004940D03* -X015000Y006240D03* -X015000Y006988D03* -D17* -X014676Y008364D03* -X015424Y008364D03* -X017526Y004514D03* -X018274Y004514D03* -X010674Y004064D03* -X009926Y004064D03* -X004174Y009564D03* -X003426Y009564D03* -X005376Y014564D03* -X006124Y014564D03* -D18* -X014250Y016088D03* -X014250Y012741D03* -D19* -X014250Y010982D03* -X014250Y009447D03* -D20* -X022869Y007639D02* -X022869Y013789D01* -D21* -X018200Y011964D03* -X017200Y011464D03* -X017200Y010464D03* -X018200Y009964D03* -X018200Y010964D03* -X017200Y009464D03* -D22* -X008696Y006914D03* -X008696Y005864D03* -X008696Y004864D03* -X008696Y003814D03* -X005004Y003814D03* -X005004Y004864D03* -X005004Y005864D03* -X005004Y006914D03* -D23* -X001800Y008564D02* -X001200Y008564D01* -X001200Y009564D02* -X001800Y009564D01* -X001800Y010564D02* -X001200Y010564D01* -X001200Y011564D02* -X001800Y011564D01* -X001800Y012564D02* -X001200Y012564D01* -X005350Y016664D02* -X005350Y017264D01* -X006350Y017264D02* -X006350Y016664D01* -X007350Y016664D02* -X007350Y017264D01* -X017350Y017114D02* -X017350Y016514D01* -X018350Y016514D02* -X018350Y017114D01* -D24* -X016613Y004514D03* -X015787Y004514D03* -D25* -X015200Y004514D01* -X014868Y004649D02* -X014732Y004649D01* -X014842Y004586D02* -X014842Y004443D01* -X014896Y004311D01* -X014997Y004211D01* -X015129Y004156D01* -X015271Y004156D01* -X015395Y004207D01* -X015484Y004118D01* -X016089Y004118D01* -X016183Y004212D01* -X016183Y004817D01* -X016089Y004911D01* -X015484Y004911D01* -X015395Y004821D01* -X015271Y004872D01* -X015129Y004872D01* -X014997Y004818D01* -X014896Y004717D01* -X014842Y004586D01* -X014842Y004491D02* -X014732Y004491D01* -X014732Y004332D02* -X014888Y004332D01* -X014732Y004174D02* -X015086Y004174D01* -X015314Y004174D02* -X015428Y004174D01* -X014732Y004015D02* -X019505Y004015D01* -X019568Y003922D02* -X019568Y003922D01* -X019568Y003922D01* -X019286Y004335D01* -X019286Y004335D01* -X019139Y004814D01* -X019139Y005315D01* -X019286Y005793D01* -X019286Y005793D01* -X019568Y006207D01* -X019568Y006207D01* -X019960Y006519D01* -X019960Y006519D01* -X020426Y006702D01* -X020926Y006740D01* -X020926Y006740D01* -X021414Y006628D01* -X021414Y006628D01* -X021847Y006378D01* -X021847Y006378D01* -X022188Y006011D01* -X022188Y006011D01* -X022320Y005737D01* -X022320Y015392D01* -X022188Y015118D01* -X022188Y015118D01* -X021847Y014751D01* -X021847Y014751D01* -X021414Y014500D01* -X021414Y014500D01* -X020926Y014389D01* -X020926Y014389D01* -X020426Y014426D01* -X020426Y014426D01* -X019960Y014609D01* -X019960Y014609D01* -X019568Y014922D01* -X019568Y014922D01* -X019568Y014922D01* -X019286Y015335D01* -X019286Y015335D01* -X019139Y015814D01* -X019139Y016315D01* -X019286Y016793D01* -X019286Y016793D01* -X019568Y017207D01* -X019568Y017207D01* -X019568Y017207D01* -X019960Y017519D01* -X019960Y017519D01* -X020126Y017584D01* -X016626Y017584D01* -X016637Y017573D01* -X016924Y017287D01* -X016960Y017375D01* -X017089Y017504D01* -X017258Y017574D01* -X017441Y017574D01* -X017611Y017504D01* -X017740Y017375D01* -X017810Y017206D01* -X017810Y016423D01* -X017740Y016254D01* -X017611Y016124D01* -X017441Y016054D01* -X017258Y016054D01* -X017089Y016124D01* -X016960Y016254D01* -X016890Y016423D01* -X016890Y016557D01* -X016841Y016577D01* -X016284Y017134D01* -X010456Y017134D01* -X010475Y017116D01* -X010475Y016310D01* -X010475Y016310D01* -X010495Y016216D01* -X010477Y016123D01* -X010475Y016120D01* -X010475Y015408D01* -X010381Y015315D01* -X010305Y015315D01* -X010358Y015186D01* -X010358Y015043D01* -X010304Y014911D01* -X010203Y014811D01* -X010071Y014756D01* -X009929Y014756D01* -X009797Y014811D01* -X009696Y014911D01* -X009642Y015043D01* -X009642Y015186D01* -X009695Y015315D01* -X009619Y015315D01* -X009525Y015408D01* -X009525Y017116D01* -X009544Y017134D01* -X009416Y017134D01* -X009330Y017048D01* -X009330Y014080D01* -X009525Y013885D01* -X009525Y014320D01* -X009619Y014414D01* -X010381Y014414D01* -X010475Y014320D01* -X010475Y013747D01* -X011403Y013747D01* -X011506Y013704D01* -X011688Y013522D01* -X011721Y013522D01* -X011853Y013468D01* -X011954Y013367D01* -X013755Y013367D01* -X013755Y013525D02* -X011685Y013525D01* -X011526Y013684D02* -X013893Y013684D01* -X013911Y013689D02* -X013866Y013677D01* -X013825Y013653D01* -X013791Y013619D01* -X013767Y013578D01* -X013755Y013533D01* -X013755Y012819D01* -X014173Y012819D01* -X014173Y013689D01* -X013911Y013689D01* -X014173Y013684D02* -X014327Y013684D01* -X014327Y013689D02* -X014327Y012819D01* -X014173Y012819D01* -X014173Y012664D01* -X014327Y012664D01* -X014327Y011793D01* -X014589Y011793D01* -X014634Y011806D01* -X014675Y011829D01* -X014709Y011863D01* -X014733Y011904D01* -X014745Y011950D01* -X014745Y012664D01* -X014327Y012664D01* -X014327Y012819D01* -X014745Y012819D01* -X014745Y013533D01* -X014733Y013578D01* -X014709Y013619D01* -X014675Y013653D01* -X014634Y013677D01* -X014589Y013689D01* -X014327Y013689D01* -X014327Y013525D02* -X014173Y013525D01* -X014173Y013367D02* -X014327Y013367D01* -X014327Y013208D02* -X014173Y013208D01* -X014173Y013050D02* -X014327Y013050D01* -X014327Y012891D02* -X014173Y012891D01* -X014173Y012733D02* -X010475Y012733D01* -X010475Y012613D02* -X010475Y013187D01* -X011232Y013187D01* -X011292Y013126D01* -X011292Y013093D01* -X011346Y012961D01* -X011447Y012861D01* -X011579Y012806D01* -X011721Y012806D01* -X011853Y012861D01* -X011954Y012961D01* -X012008Y013093D01* -X012008Y013236D01* -X011954Y013367D01* -X012008Y013208D02* -X013755Y013208D01* -X013755Y013050D02* -X011990Y013050D01* -X011883Y012891D02* -X013755Y012891D01* -X013755Y012664D02* -X013755Y011950D01* -X013767Y011904D01* -X013791Y011863D01* -X013825Y011829D01* -X013866Y011806D01* -X013911Y011793D01* -X014173Y011793D01* -X014173Y012664D01* -X013755Y012664D01* -X013755Y012574D02* -X010436Y012574D01* -X010475Y012613D02* -X010381Y012519D01* -X009619Y012519D01* -X009525Y012613D01* -X009525Y013234D01* -X009444Y013234D01* -X009341Y013277D01* -X009263Y013356D01* -X009263Y013356D01* -X008813Y013806D01* -X008770Y013909D01* -X008770Y017220D01* -X008813Y017323D01* -X009074Y017584D01* -X007681Y017584D01* -X007740Y017525D01* -X007810Y017356D01* -X007810Y016573D01* -X007740Y016404D01* -X007611Y016274D01* -X007441Y016204D01* -X007258Y016204D01* -X007089Y016274D01* -X006960Y016404D01* -X006890Y016573D01* -X006890Y017356D01* -X006960Y017525D01* -X007019Y017584D01* -X006681Y017584D01* -X006740Y017525D01* -X006810Y017356D01* -X006810Y016573D01* -X006740Y016404D01* -X006611Y016274D01* -X006590Y016266D01* -X006590Y015367D01* -X006553Y015278D01* -X006340Y015065D01* -X006340Y015020D01* -X006446Y015020D01* -X006540Y014926D01* -X006540Y014203D01* -X006446Y014109D01* -X006240Y014109D01* -X006240Y013961D01* -X006297Y014018D01* -X006429Y014072D01* -X006571Y014072D01* -X006703Y014018D01* -X006804Y013917D01* -X006858Y013786D01* -X006858Y013643D01* -X006804Y013511D01* -X006786Y013494D01* -X006790Y013491D01* -X006790Y012558D01* -X006696Y012464D01* -X006304Y012464D01* -X006250Y012518D01* -X006196Y012464D01* -X005804Y012464D01* -X005750Y012518D01* -X005696Y012464D01* -X005304Y012464D01* -X005264Y012504D01* -X005241Y012480D01* -X005199Y012457D01* -X005154Y012444D01* -X005000Y012444D01* -X005000Y013024D01* -X005000Y013024D01* -X005000Y012444D01* -X004846Y012444D01* -X004801Y012457D01* -X004759Y012480D01* -X004726Y012514D01* -X004702Y012555D01* -X004690Y012601D01* -X004690Y013024D01* -X005000Y013024D01* -X005000Y013024D01* -X004964Y012988D01* -X004150Y012988D01* -X004198Y012940D02* -X004198Y013036D01* -X004625Y013036D01* -X004625Y013268D01* -X004613Y013314D01* -X004589Y013355D01* -X004556Y013388D01* -X004515Y013412D01* -X004469Y013424D01* -X004198Y013424D01* -X004198Y013036D01* -X004102Y013036D01* -X004102Y012940D01* -X003675Y012940D01* -X003675Y012709D01* -X003687Y012663D01* -X003711Y012622D01* -X003732Y012600D01* -X003695Y012562D01* -X003695Y011918D01* -X003788Y011824D01* -X003904Y011824D01* -X003846Y011767D01* -X003792Y011636D01* -X003792Y011493D01* -X003846Y011361D01* -X003947Y011261D01* -X004079Y011206D01* -X004221Y011206D01* -X004353Y011261D01* -X004454Y011361D01* -X004508Y011493D01* -X004508Y011636D01* -X004454Y011767D01* -X004396Y011824D01* -X004512Y011824D01* -X004605Y011918D01* -X004605Y012562D01* -X004568Y012600D01* -X004589Y012622D01* -X004613Y012663D01* -X004625Y012709D01* -X004625Y012940D01* -X004198Y012940D01* -X004198Y013050D02* -X004102Y013050D01* -X004102Y013036D02* -X004102Y013424D01* -X003831Y013424D01* -X003785Y013412D01* -X003744Y013388D01* -X003711Y013355D01* -X003687Y013314D01* -X003675Y013268D01* -X003675Y013036D01* -X004102Y013036D01* -X004102Y013208D02* -X004198Y013208D01* -X004198Y013367D02* -X004102Y013367D01* -X003723Y013367D02* -X000780Y013367D01* -X000780Y013525D02* -X004720Y013525D01* -X004726Y013535D02* -X004702Y013494D01* -X004690Y013448D01* -X004690Y013024D01* -X005000Y013024D01* -X005000Y012264D01* -X005750Y011514D01* -X005750Y010604D01* -X005500Y010604D01* -X005500Y010024D01* -X005654Y010024D01* -X005699Y010037D01* -X005741Y010060D01* -X005750Y010070D01* -X005759Y010060D01* -X005801Y010037D01* -X005846Y010024D01* -X006000Y010024D01* -X006154Y010024D01* -X006199Y010037D01* -X006241Y010060D01* -X006260Y010080D01* -X006260Y008267D01* -X006297Y008178D01* -X006364Y008111D01* -X006364Y008111D01* -X006821Y007654D01* -X006149Y007654D01* -X005240Y008564D01* -X005240Y010080D01* -X005259Y010060D01* -X005301Y010037D01* -X005346Y010024D01* -X005500Y010024D01* -X005500Y010604D01* -X005500Y010604D01* -X005500Y010604D01* -X005690Y010604D01* -X006000Y010604D01* -X006000Y010024D01* -X006000Y010604D01* -X006000Y010604D01* -X006000Y010604D01* -X005750Y010604D01* -X005500Y010604D02* -X006000Y010604D01* -X006000Y011184D01* -X005846Y011184D01* -X005801Y011172D01* -X005759Y011148D01* -X005741Y011148D01* -X005699Y011172D01* -X005654Y011184D01* -X005500Y011184D01* -X005346Y011184D01* -X005301Y011172D01* -X005259Y011148D01* -X005213Y011148D01* -X005196Y011164D02* -X005236Y011125D01* -X005259Y011148D01* -X005196Y011164D02* -X004804Y011164D01* -X004710Y011071D01* -X004710Y010138D01* -X004760Y010088D01* -X004760Y009309D01* -X004753Y009324D01* -X004590Y009488D01* -X004590Y009926D01* -X004496Y010020D01* -X003852Y010020D01* -X003800Y009968D01* -X003748Y010020D01* -X003104Y010020D01* -X003010Y009926D01* -X003010Y009804D01* -X002198Y009804D01* -X002190Y009825D01* -X002061Y009954D01* -X001891Y010024D01* -X001108Y010024D01* -X000939Y009954D01* -X000810Y009825D01* -X000780Y009752D01* -X000780Y010376D01* -X000810Y010304D01* -X000939Y010174D01* -X001108Y010104D01* -X001891Y010104D01* -X002061Y010174D01* -X002190Y010304D01* -X002260Y010473D01* -X002260Y010656D01* -X002190Y010825D01* -X002061Y010954D01* -X001891Y011024D01* -X001108Y011024D01* -X000939Y010954D01* -X000810Y010825D01* -X000780Y010752D01* -X000780Y011376D01* -X000810Y011304D01* -X000939Y011174D01* -X001108Y011104D01* -X001891Y011104D01* -X002061Y011174D01* -X002190Y011304D01* -X002260Y011473D01* -X002260Y011656D01* -X002190Y011825D01* -X002061Y011954D01* -X001891Y012024D01* -X001108Y012024D01* -X000939Y011954D01* -X000810Y011825D01* -X000780Y011752D01* -X000780Y012376D01* -X000810Y012304D01* -X000939Y012174D01* -X001108Y012104D01* -X001891Y012104D01* -X002061Y012174D01* -X002190Y012304D01* -X002260Y012473D01* -X002260Y012656D01* -X002190Y012825D01* -X002061Y012954D01* -X001891Y013024D01* -X001108Y013024D01* -X000939Y012954D01* -X000810Y012825D01* -X000780Y012752D01* -X000780Y015356D01* -X000786Y015335D01* -X001068Y014922D01* -X001068Y014922D01* -X001068Y014922D01* -X001460Y014609D01* -X001926Y014426D01* -X002426Y014389D01* -X002914Y014500D01* -X003347Y014751D01* -X003347Y014751D01* -X003688Y015118D01* -X003905Y015569D01* -X003980Y016064D01* -X003905Y016560D01* -X003688Y017011D01* -X003347Y017378D01* -X002990Y017584D01* -X005019Y017584D01* -X004960Y017525D01* -X004890Y017356D01* -X004890Y016573D01* -X004960Y016404D01* -X005089Y016274D01* -X005110Y016266D01* -X005110Y015020D01* -X005054Y015020D01* -X004960Y014926D01* -X004960Y014203D01* -X005054Y014109D01* -X005260Y014109D01* -X005260Y013549D01* -X005241Y013568D01* -X005199Y013592D01* -X005154Y013604D01* -X005000Y013604D01* -X004846Y013604D01* -X004801Y013592D01* -X004759Y013568D01* -X004726Y013535D01* -X004690Y013367D02* -X004577Y013367D01* -X004625Y013208D02* -X004690Y013208D01* -X004690Y013050D02* -X004625Y013050D01* -X004625Y012891D02* -X004690Y012891D01* -X004690Y012733D02* -X004625Y012733D01* -X004593Y012574D02* -X004697Y012574D01* -X004605Y012416D02* -X013755Y012416D01* -X013755Y012257D02* -X011559Y012257D01* -X011559Y012307D02* -X011465Y012400D01* -X007235Y012400D01* -X007141Y012307D01* -X007141Y008013D01* -X006740Y008414D01* -X006740Y010088D01* -X006790Y010138D01* -X006790Y011071D01* -X006696Y011164D01* -X006304Y011164D01* -X006264Y011125D01* -X006241Y011148D01* -X006287Y011148D01* -X006241Y011148D02* -X006199Y011172D01* -X006154Y011184D01* -X006000Y011184D01* -X006000Y010604D01* -X006000Y010604D01* -X006000Y010672D02* -X006000Y010672D01* -X006000Y010514D02* -X006000Y010514D01* -X006000Y010355D02* -X006000Y010355D01* -X006000Y010197D02* -X006000Y010197D01* -X006000Y010038D02* -X006000Y010038D01* -X006202Y010038D02* -X006260Y010038D01* -X006260Y009880D02* -X005240Y009880D01* -X005240Y010038D02* -X005297Y010038D01* -X005500Y010038D02* -X005500Y010038D01* -X005500Y010197D02* -X005500Y010197D01* -X005500Y010355D02* -X005500Y010355D01* -X005500Y010514D02* -X005500Y010514D01* -X005500Y010604D02* -X005500Y011184D01* -X005500Y010604D01* -X005500Y010604D01* -X005500Y010672D02* -X005500Y010672D01* -X005500Y010831D02* -X005500Y010831D01* -X005500Y010989D02* -X005500Y010989D01* -X005500Y011148D02* -X005500Y011148D01* -X005741Y011148D02* -X005750Y011139D01* -X005759Y011148D01* -X006000Y011148D02* -X006000Y011148D01* -X006000Y010989D02* -X006000Y010989D01* -X006000Y010831D02* -X006000Y010831D01* -X006500Y010604D02* -X006500Y008314D01* -X007150Y007664D01* -X009450Y007664D01* -X010750Y006364D01* -X011419Y006364D01* -X011423Y006360D01* -X011377Y006364D01* -X011423Y006104D02* -X010660Y006104D01* -X009350Y007414D01* -X006050Y007414D01* -X005000Y008464D01* -X005000Y010604D01* -X004710Y010672D02* -X002253Y010672D01* -X002260Y010514D02* -X004710Y010514D01* -X004710Y010355D02* -X002211Y010355D01* -X002083Y010197D02* -X004710Y010197D01* -X004760Y010038D02* -X000780Y010038D01* -X000780Y009880D02* -X000865Y009880D01* -X000917Y010197D02* -X000780Y010197D01* -X000780Y010355D02* -X000789Y010355D01* -X000780Y010831D02* -X000816Y010831D01* -X000780Y010989D02* -X001024Y010989D01* -X001003Y011148D02* -X000780Y011148D01* -X000780Y011306D02* -X000809Y011306D01* -X000780Y011782D02* -X000792Y011782D01* -X000780Y011940D02* -X000925Y011940D01* -X000780Y012099D02* -X003695Y012099D01* -X003695Y012257D02* -X002144Y012257D01* -X002236Y012416D02* -X003695Y012416D01* -X003707Y012574D02* -X002260Y012574D01* -X002228Y012733D02* -X003675Y012733D01* -X003675Y012891D02* -X002124Y012891D01* -X002075Y011940D02* -X003695Y011940D01* -X003861Y011782D02* -X002208Y011782D01* -X002260Y011623D02* -X003792Y011623D01* -X003804Y011465D02* -X002257Y011465D01* -X002191Y011306D02* -X003902Y011306D01* -X004150Y011564D02* -X004150Y012240D01* -X004605Y012257D02* -X007141Y012257D01* -X007141Y012099D02* -X004605Y012099D01* -X004605Y011940D02* -X007141Y011940D01* -X007141Y011782D02* -X004439Y011782D01* -X004508Y011623D02* -X007141Y011623D01* -X007141Y011465D02* -X004496Y011465D01* -X004398Y011306D02* -X007141Y011306D01* -X007141Y011148D02* -X006713Y011148D01* -X006790Y010989D02* -X007141Y010989D01* -X007141Y010831D02* -X006790Y010831D01* -X006790Y010672D02* -X007141Y010672D01* -X007141Y010514D02* -X006790Y010514D01* -X006790Y010355D02* -X007141Y010355D01* -X007141Y010197D02* -X006790Y010197D01* -X006740Y010038D02* -X007141Y010038D01* -X007141Y009880D02* -X006740Y009880D01* -X006740Y009721D02* -X007141Y009721D01* -X007141Y009563D02* -X006740Y009563D01* -X006740Y009404D02* -X007141Y009404D01* -X007141Y009246D02* -X006740Y009246D01* -X006740Y009087D02* -X007141Y009087D01* -X007141Y008929D02* -X006740Y008929D01* -X006740Y008770D02* -X007141Y008770D01* -X007141Y008612D02* -X006740Y008612D01* -X006740Y008453D02* -X007141Y008453D01* -X007141Y008295D02* -X006859Y008295D01* -X007017Y008136D02* -X007141Y008136D01* -X006656Y007819D02* -X005984Y007819D01* -X005826Y007978D02* -X006497Y007978D01* -X006339Y008136D02* -X005667Y008136D01* -X005509Y008295D02* -X006260Y008295D01* -X006260Y008453D02* -X005350Y008453D01* -X005240Y008612D02* -X006260Y008612D01* -X006260Y008770D02* -X005240Y008770D01* -X005240Y008929D02* -X006260Y008929D01* -X006260Y009087D02* -X005240Y009087D01* -X005240Y009246D02* -X006260Y009246D01* -X006260Y009404D02* -X005240Y009404D01* -X005240Y009563D02* -X006260Y009563D01* -X006260Y009721D02* -X005240Y009721D01* -X004760Y009721D02* -X004590Y009721D01* -X004590Y009563D02* -X004760Y009563D01* -X004760Y009404D02* -X004673Y009404D01* -X004550Y009188D02* -X004174Y009564D01* -X004590Y009880D02* -X004760Y009880D01* -X004550Y009188D02* -X004550Y006114D01* -X004800Y005864D01* -X005004Y005864D01* -X004647Y005678D02* -X004647Y005548D01* -X004740Y005454D01* -X005267Y005454D01* -X005360Y005548D01* -X005360Y006181D01* -X005267Y006274D01* -X004790Y006274D01* -X004790Y006504D01* -X005267Y006504D01* -X005360Y006598D01* -X005360Y007231D01* -X005267Y007324D01* -X004790Y007324D01* -X004790Y008344D01* -X004797Y008328D01* -X005847Y007278D01* -X005914Y007211D01* -X006002Y007174D01* -X008320Y007174D01* -X008320Y006933D01* -X008678Y006933D01* -X008678Y006896D01* -X008320Y006896D01* -X008320Y006641D01* -X008332Y006595D01* -X008356Y006554D01* -X008389Y006520D01* -X008430Y006497D01* -X008476Y006484D01* -X008678Y006484D01* -X008678Y006896D01* -X008715Y006896D01* -X008715Y006933D01* -X009073Y006933D01* -X009073Y007174D01* -X009251Y007174D01* -X010337Y006088D01* -X010278Y006088D01* -X010262Y006104D01* -X009538Y006104D01* -X009445Y006011D01* -X009445Y005928D01* -X009276Y005928D01* -X009188Y005892D01* -X009064Y005768D01* -X009053Y005757D01* -X009053Y006181D01* -X008960Y006274D01* -X008433Y006274D01* -X008340Y006181D01* -X008340Y005548D01* -X008433Y005454D01* -X008960Y005454D01* -X008960Y005455D01* -X008960Y005274D01* -X008960Y005274D01* -X008433Y005274D01* -X008340Y005181D01* -X008340Y004548D01* -X008433Y004454D01* -X008960Y004454D01* -X009053Y004548D01* -X009053Y004627D01* -X009136Y004661D01* -X009203Y004728D01* -X009403Y004928D01* -X009428Y004988D01* -X009852Y004988D01* -X009852Y004892D01* -X009425Y004892D01* -X009425Y004661D01* -X009437Y004615D01* -X009461Y004574D01* -X009494Y004540D01* -X009535Y004517D01* -X009581Y004504D01* -X009589Y004504D01* -X009510Y004426D01* -X009510Y004311D01* -X009453Y004368D01* -X009321Y004422D01* -X009179Y004422D01* -X009047Y004368D01* -X008984Y004304D01* -X008899Y004304D01* -X008811Y004268D01* -X008767Y004224D01* -X008433Y004224D01* -X008340Y004131D01* -X008340Y003544D01* -X005360Y003544D01* -X005360Y004131D01* -X005267Y004224D01* -X004740Y004224D01* -X004647Y004131D01* -X004647Y003544D01* -X002937Y003544D01* -X002964Y003550D01* -X003397Y003801D01* -X003397Y003801D01* -X003738Y004168D01* -X003955Y004619D01* -X004030Y005114D01* -X003955Y005610D01* -X003738Y006061D01* -X003397Y006428D01* -X002964Y006678D01* -X002964Y006678D01* -X002476Y006790D01* -X002476Y006790D01* -X001976Y006752D01* -X001510Y006569D01* -X001118Y006257D01* -X000836Y005843D01* -X000780Y005660D01* -X000780Y008376D01* -X000810Y008304D01* -X000939Y008174D01* -X001108Y008104D01* -X001891Y008104D01* -X002061Y008174D01* -X002190Y008304D01* -X002198Y008324D01* -X003701Y008324D01* -X004060Y007965D01* -X004060Y005267D01* -X004097Y005178D01* -X004164Y005111D01* -X004497Y004778D01* -X004564Y004711D01* -X004647Y004677D01* -X004647Y004548D01* -X004740Y004454D01* -X005267Y004454D01* -X005360Y004548D01* -X005360Y005181D01* -X005267Y005274D01* -X004740Y005274D01* -X004710Y005244D01* -X004540Y005414D01* -X004540Y005785D01* -X004647Y005678D01* -X004647Y005600D02* -X004540Y005600D01* -X004540Y005442D02* -X008960Y005442D01* -X008960Y005283D02* -X004670Y005283D01* -X004309Y004966D02* -X004008Y004966D01* -X004030Y005114D02* -X004030Y005114D01* -X004028Y005125D02* -X004150Y005125D01* -X004060Y005283D02* -X004005Y005283D01* -X003981Y005442D02* -X004060Y005442D01* -X004060Y005600D02* -X003957Y005600D01* -X003883Y005759D02* -X004060Y005759D01* -X004060Y005917D02* -X003807Y005917D01* -X003738Y006061D02* -X003738Y006061D01* -X003724Y006076D02* -X004060Y006076D01* -X004060Y006234D02* -X003577Y006234D01* -X003430Y006393D02* -X004060Y006393D01* -X004060Y006551D02* -X003184Y006551D01* -X003397Y006428D02* -X003397Y006428D01* -X002825Y006710D02* -X004060Y006710D01* -X004060Y006868D02* -X000780Y006868D01* -X000780Y006710D02* -X001868Y006710D01* -X001976Y006752D02* -X001976Y006752D01* -X001510Y006569D02* -X001510Y006569D01* -X001488Y006551D02* -X000780Y006551D01* -X000780Y006393D02* -X001289Y006393D01* -X001118Y006257D02* -X001118Y006257D01* -X001118Y006257D01* -X001103Y006234D02* -X000780Y006234D01* -X000780Y006076D02* -X000995Y006076D01* -X000887Y005917D02* -X000780Y005917D01* -X000836Y005843D02* -X000836Y005843D01* -X000810Y005759D02* -X000780Y005759D01* -X000780Y007027D02* -X004060Y007027D01* -X004060Y007185D02* -X000780Y007185D01* -X000780Y007344D02* -X004060Y007344D01* -X004060Y007502D02* -X000780Y007502D01* -X000780Y007661D02* -X004060Y007661D01* -X004060Y007819D02* -X000780Y007819D01* -X000780Y007978D02* -X004047Y007978D01* -X003889Y008136D02* -X001969Y008136D01* -X002181Y008295D02* -X003730Y008295D01* -X003800Y008564D02* -X001500Y008564D01* -X001031Y008136D02* -X000780Y008136D01* -X000780Y008295D02* -X000819Y008295D01* -X001500Y009564D02* -X003426Y009564D01* -X003010Y009880D02* -X002135Y009880D01* -X002184Y010831D02* -X004710Y010831D01* -X004710Y010989D02* -X001976Y010989D01* -X001997Y011148D02* -X004787Y011148D01* -X005702Y010038D02* -X005797Y010038D01* -X004830Y008295D02* -X004790Y008295D01* -X004790Y008136D02* -X004989Y008136D01* -X005147Y007978D02* -X004790Y007978D01* -X004790Y007819D02* -X005306Y007819D01* -X005464Y007661D02* -X004790Y007661D01* -X004790Y007502D02* -X005623Y007502D01* -X005781Y007344D02* -X004790Y007344D01* -X005360Y007185D02* -X005976Y007185D01* -X006143Y007661D02* -X006814Y007661D01* -X005360Y007027D02* -X008320Y007027D01* -X008320Y006868D02* -X005360Y006868D01* -X005360Y006710D02* -X008320Y006710D01* -X008358Y006551D02* -X005314Y006551D01* -X005307Y006234D02* -X008393Y006234D01* -X008340Y006076D02* -X005360Y006076D01* -X005360Y005917D02* -X008340Y005917D01* -X008340Y005759D02* -X005360Y005759D01* -X005360Y005600D02* -X008340Y005600D01* -X008340Y005125D02* -X005360Y005125D01* -X005360Y004966D02* -X008340Y004966D01* -X008340Y004808D02* -X005360Y004808D01* -X005360Y004649D02* -X008340Y004649D01* -X008397Y004491D02* -X005303Y004491D01* -X005317Y004174D02* -X008383Y004174D01* -X008340Y004015D02* -X005360Y004015D01* -X005360Y003857D02* -X008340Y003857D01* -X008340Y003698D02* -X005360Y003698D01* -X004647Y003698D02* -X003220Y003698D01* -X003449Y003857D02* -X004647Y003857D01* -X004647Y004015D02* -X003596Y004015D01* -X003738Y004168D02* -X003738Y004168D01* -X003741Y004174D02* -X004690Y004174D01* -X004704Y004491D02* -X003894Y004491D01* -X003955Y004619D02* -X003955Y004619D01* -X003960Y004649D02* -X004647Y004649D01* -X004467Y004808D02* -X003984Y004808D01* -X003817Y004332D02* -X009012Y004332D01* -X008996Y004491D02* -X009575Y004491D01* -X009510Y004332D02* -X009488Y004332D01* -X009250Y004064D02* -X008946Y004064D01* -X008696Y003814D01* -X009053Y003758D02* -X009053Y003544D01* -X020126Y003544D01* -X019960Y003609D01* -X019960Y003609D01* -X019568Y003922D01* -X019650Y003857D02* -X014732Y003857D01* -X014732Y003698D02* -X019848Y003698D01* -X019397Y004174D02* -X018704Y004174D01* -X018710Y004195D02* -X018710Y004466D01* -X018322Y004466D01* -X018322Y004039D01* -X018554Y004039D01* -X018599Y004051D01* -X018640Y004075D01* -X018674Y004109D01* -X018698Y004150D01* -X018710Y004195D01* -X018710Y004332D02* -X019288Y004332D01* -X019238Y004491D02* -X018322Y004491D01* -X018322Y004466D02* -X018322Y004562D01* -X018710Y004562D01* -X018710Y004833D01* -X018698Y004879D01* -X018674Y004920D01* -X018640Y004954D01* -X018599Y004977D01* -X018554Y004990D01* -X018322Y004990D01* -X018322Y004562D01* -X018226Y004562D01* -X018226Y004990D01* -X017994Y004990D01* -X017949Y004977D01* -X017908Y004954D01* -X017886Y004932D01* -X017848Y004970D01* -X017204Y004970D01* -X017110Y004876D01* -X017110Y004754D01* -X017010Y004754D01* -X017010Y004817D01* -X016916Y004911D01* -X016311Y004911D01* -X016217Y004817D01* -X016217Y004212D01* -X016311Y004118D01* -X016916Y004118D01* -X017010Y004212D01* -X017010Y004274D01* -X017110Y004274D01* -X017110Y004153D01* -X017204Y004059D01* -X017848Y004059D01* -X017886Y004097D01* -X017908Y004075D01* -X017949Y004051D01* -X017994Y004039D01* -X018226Y004039D01* -X018226Y004466D01* -X018322Y004466D01* -X018322Y004332D02* -X018226Y004332D01* -X018226Y004174D02* -X018322Y004174D01* -X018322Y004649D02* -X018226Y004649D01* -X018226Y004808D02* -X018322Y004808D01* -X018322Y004966D02* -X018226Y004966D01* -X017930Y004966D02* -X017851Y004966D01* -X017526Y004514D02* -X016613Y004514D01* -X016217Y004491D02* -X016183Y004491D01* -X016183Y004649D02* -X016217Y004649D01* -X016217Y004808D02* -X016183Y004808D01* -X016670Y005096D02* -X016758Y005133D01* -X018836Y007211D01* -X018903Y007278D01* -X018940Y007367D01* -X018940Y010512D01* -X018903Y010600D01* -X018634Y010870D01* -X018637Y010877D01* -X018637Y011051D01* -X018571Y011212D01* -X018448Y011335D01* -X018287Y011401D01* -X018113Y011401D01* -X017952Y011335D01* -X017829Y011212D01* -X017818Y011185D01* -X017634Y011370D01* -X017637Y011377D01* -X017637Y011551D01* -X017571Y011712D01* -X017448Y011835D01* -X017287Y011901D01* -X017113Y011901D01* -X016952Y011835D01* -X016829Y011712D01* -X016763Y011551D01* -X016763Y011377D01* -X016829Y011217D01* -X016952Y011094D01* -X017113Y011027D01* -X017287Y011027D01* -X017295Y011030D01* -X017460Y010865D01* -X017460Y010823D01* -X017448Y010835D01* -X017287Y010901D01* -X017113Y010901D01* -X016952Y010835D01* -X016829Y010712D01* -X016763Y010551D01* -X016763Y010377D01* -X016829Y010217D01* -X016952Y010094D01* -X017113Y010027D01* -X017287Y010027D01* -X017448Y010094D01* -X017460Y010106D01* -X017460Y009823D01* -X017448Y009835D01* -X017287Y009901D01* -X017113Y009901D01* -X016952Y009835D01* -X016829Y009712D01* -X016763Y009551D01* -X016763Y009377D01* -X016829Y009217D01* -X016952Y009094D01* -X016960Y009091D01* -X016960Y008914D01* -X016651Y008604D01* -X015840Y008604D01* -X015840Y008726D01* -X015746Y008820D01* -X015102Y008820D01* -X015064Y008782D01* -X015042Y008804D01* -X015001Y008827D01* -X014956Y008840D01* -X014724Y008840D01* -X014724Y008412D01* -X014628Y008412D01* -X014628Y008316D01* -X014240Y008316D01* -X014240Y008045D01* -X014252Y008000D01* -X014276Y007959D01* -X014310Y007925D01* -X014345Y007904D01* -X013152Y007904D01* -X013064Y007868D01* -X012997Y007800D01* -X012564Y007368D01* -X011375Y007368D01* -X011372Y007366D01* -X011061Y007366D01* -X010968Y007273D01* -X010968Y006604D01* -X010849Y006604D01* -X009625Y007828D01* -X011465Y007828D01* -X011559Y007922D01* -X011559Y012307D01* -X011559Y012099D02* -X013755Y012099D01* -X013758Y011940D02* -X011559Y011940D01* -X011559Y011782D02* -X012096Y011782D01* -X012139Y011824D02* -X012045Y011731D01* -X012045Y011178D01* -X012090Y011133D01* -X012061Y011105D01* -X012037Y011064D01* -X012025Y011018D01* -X012025Y010809D01* -X012605Y010809D01* -X012605Y010759D01* -X012025Y010759D01* -X012025Y010551D01* -X012037Y010505D01* -X012061Y010464D01* -X012090Y010435D01* -X012045Y010391D01* -X012045Y009838D01* -X012104Y009779D01* -X012045Y009721D01* -X012045Y009168D01* -X012104Y009109D01* -X012045Y009051D01* -X012045Y008498D01* -X012139Y008404D01* -X013121Y008404D01* -X013201Y008484D01* -X013324Y008484D01* -X013347Y008461D01* -X013479Y008406D01* -X013621Y008406D01* -X013753Y008461D01* -X013854Y008561D01* -X013908Y008693D01* -X013908Y008836D01* -X013876Y008913D01* -X014986Y008913D01* -X015079Y009006D01* -X015079Y009887D01* -X014986Y009981D01* -X013682Y009981D01* -X013708Y010043D01* -X013708Y010186D01* -X013654Y010317D01* -X013553Y010418D01* -X013421Y010472D01* -X013279Y010472D01* -X013176Y010430D01* -X013170Y010435D01* -X013199Y010464D01* -X013223Y010505D01* -X013235Y010551D01* -X013235Y010759D01* -X012655Y010759D01* -X012655Y010809D01* -X013235Y010809D01* -X013235Y011018D01* -X013223Y011064D01* -X013199Y011105D01* -X013176Y011128D01* -X013229Y011106D01* -X013371Y011106D01* -X013401Y011118D01* -X013401Y011062D01* -X014170Y011062D01* -X014170Y010902D01* -X014330Y010902D01* -X014330Y010428D01* -X014943Y010428D01* -X014989Y010440D01* -X015030Y010464D01* -X015063Y010498D01* -X015087Y010539D01* -X015099Y010584D01* -X015099Y010902D01* -X014330Y010902D01* -X014330Y011062D01* -X015099Y011062D01* -X015099Y011380D01* -X015087Y011426D01* -X015063Y011467D01* -X015030Y011500D01* -X014989Y011524D01* -X014943Y011536D01* -X014330Y011536D01* -X014330Y011062D01* -X014170Y011062D01* -X014170Y011536D01* -X013658Y011536D01* -X013604Y011667D01* -X013503Y011768D01* -X013371Y011822D01* -X013229Y011822D01* -X013154Y011792D01* -X013121Y011824D01* -X012139Y011824D01* -X012045Y011623D02* -X011559Y011623D01* -X011559Y011465D02* -X012045Y011465D01* -X012045Y011306D02* -X011559Y011306D01* -X011559Y011148D02* -X012075Y011148D01* -X012025Y010989D02* -X011559Y010989D01* -X011559Y010831D02* -X012025Y010831D01* -X012025Y010672D02* -X011559Y010672D01* -X011559Y010514D02* -X012035Y010514D01* -X012045Y010355D02* -X011559Y010355D01* -X011559Y010197D02* -X012045Y010197D01* -X012045Y010038D02* -X011559Y010038D01* -X011559Y009880D02* -X012045Y009880D01* -X012046Y009721D02* -X011559Y009721D01* -X011559Y009563D02* -X012045Y009563D01* -X012045Y009404D02* -X011559Y009404D01* -X011559Y009246D02* -X012045Y009246D01* -X012082Y009087D02* -X011559Y009087D01* -X011559Y008929D02* -X012045Y008929D01* -X012045Y008770D02* -X011559Y008770D01* -X011559Y008612D02* -X012045Y008612D01* -X012090Y008453D02* -X011559Y008453D01* -X011559Y008295D02* -X014240Y008295D01* -X014240Y008412D02* -X014628Y008412D01* -X014628Y008840D01* -X014396Y008840D01* -X014351Y008827D01* -X014310Y008804D01* -X014276Y008770D01* -X014252Y008729D01* -X014240Y008683D01* -X014240Y008412D01* -X014240Y008453D02* -X013735Y008453D01* -X013874Y008612D02* -X014240Y008612D01* -X014276Y008770D02* -X013908Y008770D01* -X013365Y008453D02* -X013170Y008453D01* -X013016Y007819D02* -X009634Y007819D01* -X009793Y007661D02* -X012857Y007661D01* -X012699Y007502D02* -X009951Y007502D01* -X010110Y007344D02* -X011039Y007344D01* -X010968Y007185D02* -X010268Y007185D01* -X010427Y007027D02* -X010968Y007027D01* -X010968Y006868D02* -X010585Y006868D01* -X010744Y006710D02* -X010968Y006710D01* -X011423Y007128D02* -X012663Y007128D01* -X013200Y007664D01* -X015250Y007664D01* -X015424Y007838D01* -X015424Y008364D01* -X016750Y008364D01* -X017200Y008814D01* -X017200Y009464D01* -X016817Y009246D02* -X015079Y009246D01* -X015079Y009404D02* -X016763Y009404D01* -X016768Y009563D02* -X015079Y009563D01* -X015079Y009721D02* -X016839Y009721D01* -X017061Y009880D02* -X015079Y009880D01* -X015073Y010514D02* -X016763Y010514D01* -X016772Y010355D02* -X013615Y010355D01* -X013557Y010428D02* -X014170Y010428D01* -X014170Y010902D01* -X013401Y010902D01* -X013401Y010584D01* -X013413Y010539D01* -X013437Y010498D01* -X013470Y010464D01* -X013511Y010440D01* -X013557Y010428D01* -X013427Y010514D02* -X013225Y010514D01* -X013235Y010672D02* -X013401Y010672D01* -X013401Y010831D02* -X013235Y010831D01* -X013235Y010989D02* -X014170Y010989D01* -X014170Y010831D02* -X014330Y010831D01* -X014330Y010989D02* -X017336Y010989D01* -X017452Y010831D02* -X017460Y010831D01* -X017700Y010964D02* -X017200Y011464D01* -X016792Y011306D02* -X015099Y011306D01* -X015099Y011148D02* -X016898Y011148D01* -X016948Y010831D02* -X015099Y010831D01* -X015099Y010672D02* -X016813Y010672D01* -X016849Y010197D02* -X013703Y010197D01* -X013706Y010038D02* -X017086Y010038D01* -X017314Y010038D02* -X017460Y010038D01* -X017460Y009880D02* -X017339Y009880D01* -X017940Y009588D02* -X017960Y009573D01* -X018025Y009541D01* -X018093Y009518D01* -X018164Y009507D01* -X018191Y009507D01* -X018191Y009956D01* -X018209Y009956D01* -X018209Y009507D01* -X018236Y009507D01* -X018307Y009518D01* -X018375Y009541D01* -X018440Y009573D01* -X018460Y009588D01* -X018460Y007514D01* -X017940Y006994D01* -X017940Y009588D01* -X017940Y009563D02* -X017981Y009563D01* -X017940Y009404D02* -X018460Y009404D01* -X018460Y009246D02* -X017940Y009246D01* -X017940Y009087D02* -X018460Y009087D01* -X018460Y008929D02* -X017940Y008929D01* -X017940Y008770D02* -X018460Y008770D01* -X018460Y008612D02* -X017940Y008612D01* -X017940Y008453D02* -X018460Y008453D01* -X018460Y008295D02* -X017940Y008295D01* -X017940Y008136D02* -X018460Y008136D01* -X018460Y007978D02* -X017940Y007978D01* -X017940Y007819D02* -X018460Y007819D01* -X018460Y007661D02* -X017940Y007661D01* -X017940Y007502D02* -X018449Y007502D01* -X018290Y007344D02* -X017940Y007344D01* -X017940Y007185D02* -X018132Y007185D01* -X017973Y007027D02* -X017940Y007027D01* -X017700Y006814D02* -X017700Y010964D01* -X017697Y011306D02* -X017924Y011306D01* -X017952Y011594D02* -X018113Y011527D01* -X018287Y011527D01* -X018448Y011594D01* -X018571Y011717D01* -X018637Y011877D01* -X018637Y012051D01* -X018571Y012212D01* -X018448Y012335D01* -X018287Y012401D01* -X018113Y012401D01* -X017952Y012335D01* -X017829Y012212D01* -X017763Y012051D01* -X017763Y011877D01* -X017829Y011717D01* -X017952Y011594D01* -X017923Y011623D02* -X017607Y011623D01* -X017637Y011465D02* -X022320Y011465D01* -X022320Y011623D02* -X020956Y011623D01* -X020847Y011594D02* -X021132Y011671D01* -X021388Y011818D01* -X021596Y012027D01* -X021744Y012282D01* -X021820Y012567D01* -X021820Y012862D01* -X021744Y013147D01* -X021596Y013402D01* -X021388Y013611D01* -X021132Y013758D01* -X020847Y013834D01* -X020553Y013834D01* -X020268Y013758D01* -X020012Y013611D01* -X019804Y013402D01* -X019656Y013147D01* -X019580Y012862D01* -X019580Y012567D01* -X019656Y012282D01* -X019804Y012027D01* -X020012Y011818D01* -X020268Y011671D01* -X020553Y011594D01* -X020847Y011594D01* -X020444Y011623D02* -X018477Y011623D01* -X018598Y011782D02* -X020075Y011782D01* -X019890Y011940D02* -X018637Y011940D01* -X018617Y012099D02* -X019762Y012099D01* -X019671Y012257D02* -X018525Y012257D01* -X017875Y012257D02* -X014745Y012257D01* -X014745Y012099D02* -X017783Y012099D01* -X017763Y011940D02* -X014742Y011940D01* -X014327Y011940D02* -X014173Y011940D01* -X014173Y012099D02* -X014327Y012099D01* -X014327Y012257D02* -X014173Y012257D01* -X014173Y012416D02* -X014327Y012416D01* -X014327Y012574D02* -X014173Y012574D01* -X014327Y012733D02* -X019580Y012733D01* -X019588Y012891D02* -X014745Y012891D01* -X014745Y013050D02* -X019630Y013050D01* -X019692Y013208D02* -X014745Y013208D01* -X014745Y013367D02* -X019783Y013367D01* -X019927Y013525D02* -X014745Y013525D01* -X014607Y013684D02* -X020139Y013684D01* -X021261Y013684D02* -X022320Y013684D01* -X022320Y013842D02* -X010475Y013842D01* -X010475Y014001D02* -X022320Y014001D01* -X022320Y014159D02* -X010475Y014159D01* -X010475Y014318D02* -X022320Y014318D01* -X022320Y014476D02* -X021308Y014476D01* -X021647Y014635D02* -X022320Y014635D01* -X022320Y014793D02* -X021887Y014793D01* -X021847Y014751D02* -X021847Y014751D01* -X022034Y014952D02* -X022320Y014952D01* -X022320Y015110D02* -X022181Y015110D01* -X022261Y015269D02* -X022320Y015269D01* -X020299Y014476D02* -X009330Y014476D01* -X009330Y014318D02* -X009525Y014318D01* -X009525Y014159D02* -X009330Y014159D01* -X009409Y014001D02* -X009525Y014001D01* -X008935Y013684D02* -X006858Y013684D01* -X006835Y013842D02* -X008797Y013842D01* -X008770Y014001D02* -X006720Y014001D01* -X006496Y014159D02* -X008770Y014159D01* -X008770Y014318D02* -X006540Y014318D01* -X006540Y014476D02* -X008770Y014476D01* -X008770Y014635D02* -X006540Y014635D01* -X006540Y014793D02* -X008770Y014793D01* -X008770Y014952D02* -X006514Y014952D01* -X006385Y015110D02* -X008770Y015110D01* -X008770Y015269D02* -X006544Y015269D01* -X006590Y015427D02* -X008770Y015427D01* -X008770Y015586D02* -X006590Y015586D01* -X006590Y015744D02* -X008770Y015744D01* -X008770Y015903D02* -X006590Y015903D01* -X006590Y016061D02* -X008770Y016061D01* -X008770Y016220D02* -X007479Y016220D01* -X007221Y016220D02* -X006590Y016220D01* -X006715Y016378D02* -X006985Y016378D01* -X006905Y016537D02* -X006795Y016537D01* -X006810Y016695D02* -X006890Y016695D01* -X006890Y016854D02* -X006810Y016854D01* -X006810Y017012D02* -X006890Y017012D01* -X006890Y017171D02* -X006810Y017171D01* -X006810Y017329D02* -X006890Y017329D01* -X006945Y017488D02* -X006755Y017488D01* -X006350Y016964D02* -X006350Y015414D01* -X006100Y015164D01* -X006100Y014588D01* -X006124Y014564D01* -X006000Y014490D01* -X006000Y013024D01* -X005500Y013024D02* -X005500Y014440D01* -X005376Y014564D01* -X005350Y014590D01* -X005350Y016964D01* -X004890Y017012D02* -X003687Y017012D01* -X003688Y017011D02* -X003688Y017011D01* -X003764Y016854D02* -X004890Y016854D01* -X004890Y016695D02* -X003840Y016695D01* -X003905Y016560D02* -X003905Y016560D01* -X003909Y016537D02* -X004905Y016537D01* -X004985Y016378D02* -X003933Y016378D01* -X003957Y016220D02* -X005110Y016220D01* -X005110Y016061D02* -X003980Y016061D01* -X003980Y016064D02* -X003980Y016064D01* -X003956Y015903D02* -X005110Y015903D01* -X005110Y015744D02* -X003932Y015744D01* -X003908Y015586D02* -X005110Y015586D01* -X005110Y015427D02* -X003837Y015427D01* -X003761Y015269D02* -X005110Y015269D01* -X005110Y015110D02* -X003681Y015110D01* -X003688Y015118D02* -X003688Y015118D01* -X003534Y014952D02* -X004986Y014952D01* -X004960Y014793D02* -X003387Y014793D01* -X003347Y014751D02* -X003347Y014751D01* -X003147Y014635D02* -X004960Y014635D01* -X004960Y014476D02* -X002808Y014476D01* -X002914Y014500D02* -X002914Y014500D01* -X002426Y014389D02* -X002426Y014389D01* -X001926Y014426D02* -X001926Y014426D01* -X001799Y014476D02* -X000780Y014476D01* -X000780Y014318D02* -X004960Y014318D01* -X005004Y014159D02* -X000780Y014159D01* -X000780Y014001D02* -X005260Y014001D01* -X005260Y013842D02* -X000780Y013842D01* -X000780Y013684D02* -X005260Y013684D01* -X005000Y013604D02* -X005000Y013024D01* -X005000Y013604D01* -X005000Y013525D02* -X005000Y013525D01* -X005000Y013367D02* -X005000Y013367D01* -X005000Y013208D02* -X005000Y013208D01* -X005000Y013050D02* -X005000Y013050D01* -X005000Y013024D02* -X005000Y013024D01* -X005000Y012891D02* -X005000Y012891D01* -X005000Y012733D02* -X005000Y012733D01* -X005000Y012574D02* -X005000Y012574D01* -X003675Y013050D02* -X000780Y013050D01* -X000780Y013208D02* -X003675Y013208D01* -X001460Y014609D02* -X001460Y014609D01* -X001428Y014635D02* -X000780Y014635D01* -X000780Y014793D02* -X001229Y014793D01* -X001048Y014952D02* -X000780Y014952D01* -X000780Y015110D02* -X000940Y015110D01* -X000832Y015269D02* -X000780Y015269D01* -X000786Y015335D02* -X000786Y015335D01* -X003347Y017378D02* -X003347Y017378D01* -X003392Y017329D02* -X004890Y017329D01* -X004890Y017171D02* -X003539Y017171D01* -X003157Y017488D02* -X004945Y017488D01* -X007755Y017488D02* -X008978Y017488D01* -X008819Y017329D02* -X007810Y017329D01* -X007810Y017171D02* -X008770Y017171D01* -X008770Y017012D02* -X007810Y017012D01* -X007810Y016854D02* -X008770Y016854D01* -X008770Y016695D02* -X007810Y016695D01* -X007795Y016537D02* -X008770Y016537D01* -X008770Y016378D02* -X007715Y016378D01* -X009330Y016378D02* -X009525Y016378D01* -X009525Y016220D02* -X009330Y016220D01* -X009330Y016061D02* -X009525Y016061D01* -X009525Y015903D02* -X009330Y015903D01* -X009330Y015744D02* -X009525Y015744D01* -X009525Y015586D02* -X009330Y015586D01* -X009330Y015427D02* -X009525Y015427D01* -X009676Y015269D02* -X009330Y015269D01* -X009330Y015110D02* -X009642Y015110D01* -X009680Y014952D02* -X009330Y014952D01* -X009330Y014793D02* -X009839Y014793D01* -X010161Y014793D02* -X013933Y014793D01* -X013946Y014761D02* -X014047Y014661D01* -X014179Y014606D01* -X014321Y014606D01* -X014453Y014661D01* -X014554Y014761D01* -X014608Y014893D01* -X014608Y015036D01* -X014557Y015160D01* -X014631Y015160D01* -X014725Y015254D01* -X014725Y016922D01* -X014631Y017015D01* -X013869Y017015D01* -X013775Y016922D01* -X013775Y015254D01* -X013869Y015160D01* -X013943Y015160D01* -X013892Y015036D01* -X013892Y014893D01* -X013946Y014761D01* -X013892Y014952D02* -X010320Y014952D01* -X010358Y015110D02* -X013923Y015110D01* -X013775Y015269D02* -X010324Y015269D01* -X010475Y015427D02* -X013775Y015427D01* -X013775Y015586D02* -X010475Y015586D01* -X010475Y015744D02* -X013775Y015744D01* -X013775Y015903D02* -X010475Y015903D01* -X010475Y016061D02* -X013775Y016061D01* -X013775Y016220D02* -X010494Y016220D01* -X010475Y016378D02* -X013775Y016378D01* -X013775Y016537D02* -X010475Y016537D01* -X010475Y016695D02* -X013775Y016695D01* -X013775Y016854D02* -X010475Y016854D01* -X010475Y017012D02* -X013866Y017012D01* -X014634Y017012D02* -X016406Y017012D01* -X016564Y016854D02* -X014725Y016854D01* -X014725Y016695D02* -X016723Y016695D01* -X016890Y016537D02* -X014725Y016537D01* -X014725Y016378D02* -X016908Y016378D01* -X016994Y016220D02* -X014725Y016220D01* -X014725Y016061D02* -X017242Y016061D01* -X017458Y016061D02* -X018242Y016061D01* -X018258Y016054D02* -X018441Y016054D01* -X018611Y016124D01* -X018740Y016254D01* -X018810Y016423D01* -X018810Y017206D01* -X018740Y017375D01* -X018611Y017504D01* -X018441Y017574D01* -X018258Y017574D01* -X018089Y017504D01* -X017960Y017375D01* -X017890Y017206D01* -X017890Y016423D01* -X017960Y016254D01* -X018089Y016124D01* -X018258Y016054D01* -X018458Y016061D02* -X019139Y016061D01* -X019139Y015903D02* -X014725Y015903D01* -X014725Y015744D02* -X019160Y015744D01* -X019209Y015586D02* -X014725Y015586D01* -X014725Y015427D02* -X019258Y015427D01* -X019332Y015269D02* -X014725Y015269D01* -X014577Y015110D02* -X019440Y015110D01* -X019548Y014952D02* -X014608Y014952D01* -X014567Y014793D02* -X019729Y014793D01* -X019928Y014635D02* -X014390Y014635D01* -X014110Y014635D02* -X009330Y014635D01* -X010000Y015114D02* -X010000Y016262D01* -X010250Y016214D01* -X009525Y016537D02* -X009330Y016537D01* -X009330Y016695D02* -X009525Y016695D01* -X009525Y016854D02* -X009330Y016854D01* -X009330Y017012D02* -X009525Y017012D01* -X006280Y014001D02* -X006240Y014001D01* -X006500Y013714D02* -X006500Y013024D01* -X006790Y013050D02* -X009525Y013050D01* -X009525Y013208D02* -X006790Y013208D01* -X006790Y013367D02* -X009252Y013367D01* -X009093Y013525D02* -X006809Y013525D01* -X006790Y012891D02* -X009525Y012891D01* -X009525Y012733D02* -X006790Y012733D01* -X006790Y012574D02* -X009564Y012574D01* -X010475Y012891D02* -X011417Y012891D01* -X011310Y013050D02* -X010475Y013050D01* -X012630Y011454D02* -X013290Y011454D01* -X013300Y011464D01* -X013622Y011623D02* -X016793Y011623D01* -X016763Y011465D02* -X015064Y011465D01* -X014330Y011465D02* -X014170Y011465D01* -X014170Y011306D02* -X014330Y011306D01* -X014330Y011148D02* -X014170Y011148D01* -X014170Y010672D02* -X014330Y010672D01* -X014330Y010514D02* -X014170Y010514D01* -X013350Y010114D02* -X012630Y010114D01* -X013469Y011782D02* -X016899Y011782D01* -X017501Y011782D02* -X017802Y011782D01* -X018476Y011306D02* -X022320Y011306D01* -X022320Y011148D02* -X018597Y011148D01* -X018637Y010989D02* -X022320Y010989D01* -X022320Y010831D02* -X018673Y010831D01* -X018831Y010672D02* -X022320Y010672D01* -X022320Y010514D02* -X018939Y010514D01* -X018940Y010355D02* -X022320Y010355D01* -X022320Y010197D02* -X018940Y010197D01* -X018940Y010038D02* -X022320Y010038D01* -X022320Y009880D02* -X018940Y009880D01* -X018940Y009721D02* -X020204Y009721D01* -X020268Y009758D02* -X020012Y009611D01* -X019804Y009402D01* -X019656Y009147D01* -X019580Y008862D01* -X019580Y008567D01* -X019656Y008282D01* -X019804Y008027D01* -X020012Y007818D01* -X020268Y007671D01* -X020553Y007594D01* -X020847Y007594D01* -X021132Y007671D01* -X021388Y007818D01* -X021596Y008027D01* -X021744Y008282D01* -X021820Y008567D01* -X021820Y008862D01* -X021744Y009147D01* -X021596Y009402D01* -X021388Y009611D01* -X021132Y009758D01* -X020847Y009834D01* -X020553Y009834D01* -X020268Y009758D01* -X019965Y009563D02* -X018940Y009563D01* -X018940Y009404D02* -X019806Y009404D01* -X019714Y009246D02* -X018940Y009246D01* -X018940Y009087D02* -X019640Y009087D01* -X019598Y008929D02* -X018940Y008929D01* -X018940Y008770D02* -X019580Y008770D01* -X019580Y008612D02* -X018940Y008612D01* -X018940Y008453D02* -X019610Y008453D01* -X019653Y008295D02* -X018940Y008295D01* -X018940Y008136D02* -X019740Y008136D01* -X019853Y007978D02* -X018940Y007978D01* -X018940Y007819D02* -X020011Y007819D01* -X020304Y007661D02* -X018940Y007661D01* -X018940Y007502D02* -X022320Y007502D01* -X022320Y007344D02* -X018931Y007344D01* -X018810Y007185D02* -X022320Y007185D01* -X022320Y007027D02* -X018652Y007027D01* -X018493Y006868D02* -X022320Y006868D01* -X022320Y006710D02* -X021056Y006710D01* -X021547Y006551D02* -X022320Y006551D01* -X022320Y006393D02* -X021821Y006393D01* -X021981Y006234D02* -X022320Y006234D01* -X022320Y006076D02* -X022128Y006076D01* -X022233Y005917D02* -X022320Y005917D01* -X022309Y005759D02* -X022320Y005759D01* -X020528Y006710D02* -X018335Y006710D01* -X018176Y006551D02* -X020042Y006551D01* -X019801Y006393D02* -X018018Y006393D01* -X017859Y006234D02* -X019603Y006234D01* -X019479Y006076D02* -X017701Y006076D01* -X017542Y005917D02* -X019371Y005917D01* -X019276Y005759D02* -X017384Y005759D01* -X017225Y005600D02* -X019227Y005600D01* -X019178Y005442D02* -X017067Y005442D01* -X016908Y005283D02* -X019139Y005283D01* -X019139Y005125D02* -X016738Y005125D01* -X016670Y005096D02* -X014732Y005096D01* -X014732Y003656D01* -X014639Y003562D01* -X013916Y003562D01* -X013822Y003656D01* -X013822Y006632D01* -X013774Y006632D01* -X013703Y006561D01* -X013571Y006506D01* -X013429Y006506D01* -X013297Y006561D01* -X013196Y006661D01* -X013142Y006793D01* -X013142Y006936D01* -X013196Y007067D01* -X013297Y007168D01* -X013429Y007222D01* -X013571Y007222D01* -X013703Y007168D01* -X013759Y007112D01* -X013802Y007112D01* -X013802Y007128D01* -X014277Y007128D01* -X014277Y007386D01* -X013958Y007386D01* -X013912Y007374D01* -X013871Y007350D01* -X013838Y007317D01* -X013814Y007276D01* -X013802Y007230D01* -X013802Y007128D01* -X014277Y007128D01* -X014277Y007128D01* -X014277Y007128D01* -X014277Y007386D01* -X014592Y007386D01* -X014594Y007388D01* -X014635Y007412D01* -X014681Y007424D01* -X014952Y007424D01* -X014952Y007036D01* -X015048Y007036D01* -X015475Y007036D01* -X015475Y007268D01* -X015463Y007314D01* -X015439Y007355D01* -X015406Y007388D01* -X015365Y007412D01* -X015319Y007424D01* -X015048Y007424D01* -X015048Y007036D01* -X015048Y006940D01* -X015475Y006940D01* -X015475Y006709D01* -X015463Y006663D01* -X015439Y006622D01* -X015418Y006600D01* -X015449Y006569D01* -X015579Y006622D01* -X015721Y006622D01* -X015853Y006568D01* -X015954Y006467D01* -X016008Y006336D01* -X016008Y006193D01* -X015954Y006061D01* -X015853Y005961D01* -X015721Y005906D01* -X015579Y005906D01* -X015455Y005957D01* -X015455Y005918D01* -X015369Y005832D01* -X016379Y005832D01* -X017460Y006914D01* -X017460Y009106D01* -X017448Y009094D01* -X017440Y009091D01* -X017440Y008767D01* -X017403Y008678D01* -X017336Y008611D01* -X016886Y008161D01* -X016798Y008124D01* -X015840Y008124D01* -X015840Y008003D01* -X015746Y007909D01* -X015664Y007909D01* -X015664Y007791D01* -X015627Y007702D01* -X015453Y007528D01* -X015453Y007528D01* -X015386Y007461D01* -X015298Y007424D01* -X013299Y007424D01* -X012799Y006924D01* -X012711Y006888D01* -X011878Y006888D01* -X011878Y005599D01* -X011897Y005618D01* -X012029Y005672D01* -X012171Y005672D01* -X012303Y005618D01* -X012404Y005517D01* -X012458Y005386D01* -X012458Y005243D01* -X012404Y005111D01* -X012303Y005011D01* -X012171Y004956D01* -X012029Y004956D01* -X011897Y005011D01* -X011878Y005030D01* -X011878Y004218D01* -X011886Y004205D01* -X011898Y004159D01* -X011898Y004057D01* -X011423Y004057D01* -X011423Y004057D01* -X011898Y004057D01* -X011898Y003954D01* -X011886Y003909D01* -X011878Y003895D01* -X011878Y003656D01* -X011784Y003562D01* -X011061Y003562D01* -X011014Y003610D01* -X010999Y003601D01* -X010954Y003589D01* -X010722Y003589D01* -X010722Y004016D01* -X010626Y004016D01* -X010626Y003589D01* -X010394Y003589D01* -X010349Y003601D01* -X010308Y003625D01* -X010286Y003647D01* -X010248Y003609D01* -X009604Y003609D01* -X009510Y003703D01* -X009510Y003818D01* -X009453Y003761D01* -X009321Y003706D01* -X009179Y003706D01* -X009053Y003758D01* -X009053Y003698D02* -X009515Y003698D01* -X009250Y004064D02* -X009926Y004064D01* -X010286Y004482D02* -X010254Y004514D01* -X010265Y004517D01* -X010306Y004540D01* -X010339Y004574D01* -X010363Y004615D01* -X010375Y004661D01* -X010375Y004892D01* -X009948Y004892D01* -X009948Y004988D01* -X010375Y004988D01* -X010375Y005220D01* -X010363Y005266D01* -X010339Y005307D01* -X010318Y005328D01* -X010355Y005366D01* -X010355Y005608D01* -X010968Y005608D01* -X010968Y005481D01* -X010968Y004536D01* -X010954Y004540D01* -X010722Y004540D01* -X010722Y004112D01* -X010948Y004112D01* -X010948Y004057D01* -X011423Y004057D01* -X011406Y004040D01* -X010674Y004064D01* -X010722Y004016D02* -X010722Y004112D01* -X010626Y004112D01* -X010626Y004540D01* -X010394Y004540D01* -X010349Y004527D01* -X010308Y004504D01* -X010286Y004482D01* -X010277Y004491D02* -X010295Y004491D01* -X010372Y004649D02* -X010968Y004649D01* -X010968Y004808D02* -X010375Y004808D01* -X010375Y005125D02* -X010968Y005125D01* -X010968Y005283D02* -X010353Y005283D01* -X010355Y005442D02* -X010968Y005442D01* -X010968Y005600D02* -X010355Y005600D01* -X010060Y005848D02* -X009900Y005688D01* -X009324Y005688D01* -X009200Y005564D01* -X009200Y005064D01* -X009000Y004864D01* -X008696Y004864D01* -X009108Y004649D02* -X009428Y004649D01* -X009425Y004808D02* -X009283Y004808D01* -X009419Y004966D02* -X009852Y004966D01* -X009948Y004966D02* -X010968Y004966D01* -X011423Y005336D02* -X011445Y005314D01* -X012100Y005314D01* -X011880Y005600D02* -X011878Y005600D01* -X011878Y005759D02* -X013822Y005759D01* -X013822Y005917D02* -X011878Y005917D01* -X011878Y006076D02* -X013822Y006076D01* -X013822Y006234D02* -X011878Y006234D01* -X011878Y006393D02* -X013822Y006393D01* -X013822Y006551D02* -X013680Y006551D01* -X013320Y006551D02* -X011878Y006551D01* -X011878Y006710D02* -X013176Y006710D01* -X013142Y006868D02* -X011878Y006868D01* -X012902Y007027D02* -X013180Y007027D01* -X013060Y007185D02* -X013339Y007185D01* -X013219Y007344D02* -X013865Y007344D01* -X013802Y007185D02* -X013661Y007185D01* -X013507Y006872D02* -X013500Y006864D01* -X013507Y006872D02* -X014277Y006872D01* -X014277Y007128D02* -X014861Y007128D01* -X015000Y006988D01* -X015048Y007027D02* -X017460Y007027D01* -X017460Y007185D02* -X015475Y007185D01* -X015446Y007344D02* -X017460Y007344D01* -X017460Y007502D02* -X015427Y007502D01* -X015586Y007661D02* -X017460Y007661D01* -X017460Y007819D02* -X015664Y007819D01* -X015815Y007978D02* -X017460Y007978D01* -X017460Y008136D02* -X016827Y008136D01* -X017020Y008295D02* -X017460Y008295D01* -X017460Y008453D02* -X017178Y008453D01* -X017337Y008612D02* -X017460Y008612D01* -X017460Y008770D02* -X017440Y008770D01* -X017440Y008929D02* -X017460Y008929D01* -X017460Y009087D02* -X017440Y009087D01* -X016960Y009087D02* -X015079Y009087D01* -X015002Y008929D02* -X016960Y008929D01* -X016817Y008770D02* -X015795Y008770D01* -X015840Y008612D02* -X016658Y008612D01* -X018191Y009563D02* -X018209Y009563D01* -X018209Y009721D02* -X018191Y009721D01* -X018191Y009880D02* -X018209Y009880D01* -X018209Y009973D02* -X018191Y009973D01* -X018191Y010421D01* -X018164Y010421D01* -X018093Y010410D01* -X018025Y010388D01* -X017960Y010355D01* -X017940Y010341D01* -X017940Y010606D01* -X017952Y010594D01* -X018113Y010527D01* -X018287Y010527D01* -X018295Y010530D01* -X018460Y010365D01* -X018460Y010341D01* -X018440Y010355D01* -X018375Y010388D01* -X018307Y010410D01* -X018236Y010421D01* -X018209Y010421D01* -X018209Y009973D01* -X018209Y010038D02* -X018191Y010038D01* -X018191Y010197D02* -X018209Y010197D01* -X018209Y010355D02* -X018191Y010355D01* -X018311Y010514D02* -X017940Y010514D01* -X017940Y010355D02* -X017960Y010355D01* -X018440Y010355D02* -X018460Y010355D01* -X018700Y010464D02* -X018200Y010964D01* -X018700Y010464D02* -X018700Y007414D01* -X016622Y005336D01* -X014277Y005336D01* -X014277Y005592D02* -X016478Y005592D01* -X017700Y006814D01* -X017415Y006868D02* -X015475Y006868D01* -X015475Y006710D02* -X017256Y006710D01* -X017098Y006551D02* -X015869Y006551D01* -X015984Y006393D02* -X016939Y006393D01* -X016781Y006234D02* -X016008Y006234D01* -X015960Y006076D02* -X016622Y006076D01* -X016464Y005917D02* -X015748Y005917D01* -X015552Y005917D02* -X015454Y005917D01* -X015650Y006264D02* -X015024Y006264D01* -X015000Y006240D01* -X014952Y007185D02* -X015048Y007185D01* -X015048Y007344D02* -X014952Y007344D01* -X014277Y007344D02* -X014277Y007344D01* -X014277Y007185D02* -X014277Y007185D01* -X014265Y007978D02* -X011559Y007978D01* -X011559Y008136D02* -X014240Y008136D01* -X014628Y008453D02* -X014724Y008453D01* -X014724Y008612D02* -X014628Y008612D01* -X014628Y008770D02* -X014724Y008770D01* -X018419Y009563D02* -X018460Y009563D01* -X021196Y009721D02* -X022320Y009721D01* -X022320Y009563D02* -X021435Y009563D01* -X021594Y009404D02* -X022320Y009404D01* -X022320Y009246D02* -X021686Y009246D01* -X021760Y009087D02* -X022320Y009087D01* -X022320Y008929D02* -X021802Y008929D01* -X021820Y008770D02* -X022320Y008770D01* -X022320Y008612D02* -X021820Y008612D01* -X021790Y008453D02* -X022320Y008453D01* -X022320Y008295D02* -X021747Y008295D01* -X021660Y008136D02* -X022320Y008136D01* -X022320Y007978D02* -X021547Y007978D01* -X021389Y007819D02* -X022320Y007819D01* -X022320Y007661D02* -X021096Y007661D01* -X019139Y004966D02* -X018618Y004966D01* -X018710Y004808D02* -X019141Y004808D01* -X019190Y004649D02* -X018710Y004649D01* -X017201Y004966D02* -X014732Y004966D01* -X014732Y004808D02* -X014987Y004808D01* -X013822Y004808D02* -X011878Y004808D01* -X011878Y004966D02* -X012004Y004966D01* -X012196Y004966D02* -X013822Y004966D01* -X013822Y005125D02* -X012409Y005125D01* -X012458Y005283D02* -X013822Y005283D01* -X013822Y005442D02* -X012435Y005442D01* -X012320Y005600D02* -X013822Y005600D01* -X013822Y004649D02* -X011878Y004649D01* -X011878Y004491D02* -X013822Y004491D01* -X013822Y004332D02* -X011878Y004332D01* -X011894Y004174D02* -X013822Y004174D01* -X013822Y004015D02* -X011898Y004015D01* -X011878Y003857D02* -X013822Y003857D01* -X013822Y003698D02* -X011878Y003698D01* -X011423Y004057D02* -X010948Y004057D01* -X010948Y004016D01* -X010722Y004016D01* -X010722Y004015D02* -X010626Y004015D01* -X010626Y003857D02* -X010722Y003857D01* -X010722Y003698D02* -X010626Y003698D01* -X010626Y004174D02* -X010722Y004174D01* -X010722Y004332D02* -X010626Y004332D01* -X010626Y004491D02* -X010722Y004491D01* -X011423Y004057D02* -X011423Y004057D01* -X011423Y005848D02* -X010060Y005848D01* -X009890Y005848D02* -X009900Y005688D01* -X009510Y006076D02* -X009053Y006076D01* -X009053Y005917D02* -X009250Y005917D01* -X009055Y005759D02* -X009053Y005759D01* -X009000Y006234D02* -X010191Y006234D01* -X010032Y006393D02* -X004790Y006393D01* -X004566Y005759D02* -X004540Y005759D01* -X004300Y005314D02* -X004300Y008064D01* -X003800Y008564D01* -X004300Y005314D02* -X004700Y004914D01* -X004954Y004914D01* -X005004Y004864D01* -X002964Y003550D02* -X002964Y003550D01* -X008678Y006551D02* -X008715Y006551D01* -X008715Y006484D02* -X008917Y006484D01* -X008963Y006497D01* -X009004Y006520D01* -X009037Y006554D01* -X009061Y006595D01* -X009073Y006641D01* -X009073Y006896D01* -X008715Y006896D01* -X008715Y006484D01* -X008715Y006710D02* -X008678Y006710D01* -X008678Y006868D02* -X008715Y006868D01* -X009073Y006868D02* -X009557Y006868D01* -X009715Y006710D02* -X009073Y006710D01* -X009035Y006551D02* -X009874Y006551D01* -X009398Y007027D02* -X009073Y007027D01* -X014745Y012416D02* -X019620Y012416D01* -X019580Y012574D02* -X014745Y012574D01* -X014250Y014964D02* -X014250Y016088D01* -X016722Y017488D02* -X017073Y017488D01* -X016941Y017329D02* -X016881Y017329D01* -X017627Y017488D02* -X018073Y017488D01* -X017941Y017329D02* -X017759Y017329D01* -X017810Y017171D02* -X017890Y017171D01* -X017890Y017012D02* -X017810Y017012D01* -X017810Y016854D02* -X017890Y016854D01* -X017890Y016695D02* -X017810Y016695D01* -X017810Y016537D02* -X017890Y016537D01* -X017908Y016378D02* -X017792Y016378D01* -X017706Y016220D02* -X017994Y016220D01* -X018706Y016220D02* -X019139Y016220D01* -X019158Y016378D02* -X018792Y016378D01* -X018810Y016537D02* -X019207Y016537D01* -X019256Y016695D02* -X018810Y016695D01* -X018810Y016854D02* -X019328Y016854D01* -X019436Y017012D02* -X018810Y017012D01* -X018810Y017171D02* -X019544Y017171D01* -X019722Y017329D02* -X018759Y017329D01* -X018627Y017488D02* -X019921Y017488D01* -X021473Y013525D02* -X022320Y013525D01* -X022320Y013367D02* -X021617Y013367D01* -X021708Y013208D02* -X022320Y013208D01* -X022320Y013050D02* -X021770Y013050D01* -X021812Y012891D02* -X022320Y012891D01* -X022320Y012733D02* -X021820Y012733D01* -X021820Y012574D02* -X022320Y012574D01* -X022320Y012416D02* -X021780Y012416D01* -X021729Y012257D02* -X022320Y012257D01* -X022320Y012099D02* -X021638Y012099D01* -X021510Y011940D02* -X022320Y011940D01* -X022320Y011782D02* -X021325Y011782D01* -X017110Y004808D02* -X017010Y004808D01* -X016972Y004174D02* -X017110Y004174D01* -X016255Y004174D02* -X016145Y004174D01* -X016183Y004332D02* -X016217Y004332D01* -X000856Y012257D02* -X000780Y012257D01* -X000780Y012891D02* -X000876Y012891D01* -D26* -X004150Y011564D03* -X006500Y013714D03* -X010000Y015114D03* -X011650Y013164D03* -X013300Y011464D03* -X013350Y010114D03* -X013550Y008764D03* -X013500Y006864D03* -X012100Y005314D03* -X009250Y004064D03* -X015200Y004514D03* -X015650Y006264D03* -X015850Y009914D03* -X014250Y014964D03* -D27* -X011650Y013164D02* -X011348Y013467D01* -X010000Y013467D01* -X009952Y013514D01* -X009500Y013514D01* -X009050Y013964D01* -X009050Y017164D01* -X009300Y017414D01* -X016400Y017414D01* -X017000Y016814D01* -X017350Y016814D01* -X014250Y010982D02* -X014052Y010784D01* -X012630Y010784D01* -X012632Y009447D02* -X012630Y009444D01* -X012632Y009447D02* -X014250Y009447D01* -X013550Y008764D02* -X012640Y008764D01* -X012630Y008774D01* -M02* +G75*%MOIN*%%OFA0B0*%%FSLAX24Y24*%%IPPOS*%%LPD*%G04This is a comment,:*%AMOC8*5,1,8,0,0,1.08239,22.5*%%ADD10C,0.0000*%%ADD11R,0.0260X0.0800*%%ADD12R,0.0591X0.0157*%%ADD13R,0.4098X0.4252*%%ADD14R,0.0850X0.0420*%%ADD15R,0.0630X0.1575*%%ADD16R,0.0591X0.0512*%%ADD17R,0.0512X0.0591*%%ADD18R,0.0630X0.1535*%%ADD19R,0.1339X0.0748*%%ADD20C,0.0004*%%ADD21C,0.0554*%%ADD22R,0.0394X0.0500*%%ADD23C,0.0600*%%ADD24R,0.0472X0.0472*%%ADD25C,0.0160*%%ADD26C,0.0396*%%ADD27C,0.0240*%D10*X000300Y003064D02*X000300Y018064D01*X022800Y018064D01*X022800Y003064D01*X000300Y003064D01*X001720Y005114D02*X001722Y005164D01*X001728Y005214D01*X001738Y005263D01*X001752Y005311D01*X001769Y005358D01*X001790Y005403D01*X001815Y005447D01*X001843Y005488D01*X001875Y005527D01*X001909Y005564D01*X001946Y005598D01*X001986Y005628D01*X002028Y005655D01*X002072Y005679D01*X002118Y005700D01*X002165Y005716D01*X002213Y005729D01*X002263Y005738D01*X002312Y005743D01*X002363Y005744D01*X002413Y005741D01*X002462Y005734D01*X002511Y005723D01*X002559Y005708D01*X002605Y005690D01*X002650Y005668D01*X002693Y005642D01*X002734Y005613D01*X002773Y005581D01*X002809Y005546D01*X002841Y005508D01*X002871Y005468D01*X002898Y005425D01*X002921Y005381D01*X002940Y005335D01*X002956Y005287D01*X002968Y005238D01*X002976Y005189D01*X002980Y005139D01*X002980Y005089D01*X002976Y005039D01*X002968Y004990D01*X002956Y004941D01*X002940Y004893D01*X002921Y004847D01*X002898Y004803D01*X002871Y004760D01*X002841Y004720D01*X002809Y004682D01*X002773Y004647D01*X002734Y004615D01*X002693Y004586D01*X002650Y004560D01*X002605Y004538D01*X002559Y004520D01*X002511Y004505D01*X002462Y004494D01*X002413Y004487D01*X002363Y004484D01*X002312Y004485D01*X002263Y004490D01*X002213Y004499D01*X002165Y004512D01*X002118Y004528D01*X002072Y004549D01*X002028Y004573D01*X001986Y004600D01*X001946Y004630D01*X001909Y004664D01*X001875Y004701D01*X001843Y004740D01*X001815Y004781D01*X001790Y004825D01*X001769Y004870D01*X001752Y004917D01*X001738Y004965D01*X001728Y005014D01*X001722Y005064D01*X001720Y005114D01*X001670Y016064D02*X001672Y016114D01*X001678Y016164D01*X001688Y016213D01*X001702Y016261D01*X001719Y016308D01*X001740Y016353D01*X001765Y016397D01*X001793Y016438D01*X001825Y016477D01*X001859Y016514D01*X001896Y016548D01*X001936Y016578D01*X001978Y016605D01*X002022Y016629D01*X002068Y016650D01*X002115Y016666D01*X002163Y016679D01*X002213Y016688D01*X002262Y016693D01*X002313Y016694D01*X002363Y016691D01*X002412Y016684D01*X002461Y016673D01*X002509Y016658D01*X002555Y016640D01*X002600Y016618D01*X002643Y016592D01*X002684Y016563D01*X002723Y016531D01*X002759Y016496D01*X002791Y016458D01*X002821Y016418D01*X002848Y016375D01*X002871Y016331D01*X002890Y016285D01*X002906Y016237D01*X002918Y016188D01*X002926Y016139D01*X002930Y016089D01*X002930Y016039D01*X002926Y015989D01*X002918Y015940D01*X002906Y015891D01*X002890Y015843D01*X002871Y015797D01*X002848Y015753D01*X002821Y015710D01*X002791Y015670D01*X002759Y015632D01*X002723Y015597D01*X002684Y015565D01*X002643Y015536D01*X002600Y015510D01*X002555Y015488D01*X002509Y015470D01*X002461Y015455D01*X002412Y015444D01*X002363Y015437D01*X002313Y015434D01*X002262Y015435D01*X002213Y015440D01*X002163Y015449D01*X002115Y015462D01*X002068Y015478D01*X002022Y015499D01*X001978Y015523D01*X001936Y015550D01*X001896Y015580D01*X001859Y015614D01*X001825Y015651D01*X001793Y015690D01*X001765Y015731D01*X001740Y015775D01*X001719Y015820D01*X001702Y015867D01*X001688Y015915D01*X001678Y015964D01*X001672Y016014D01*X001670Y016064D01*X020060Y012714D02*X020062Y012764D01*X020068Y012814D01*X020078Y012863D01*X020091Y012912D01*X020109Y012959D01*X020130Y013005D01*X020154Y013048D01*X020182Y013090D01*X020213Y013130D01*X020247Y013167D01*X020284Y013201D01*X020324Y013232D01*X020366Y013260D01*X020409Y013284D01*X020455Y013305D01*X020502Y013323D01*X020551Y013336D01*X020600Y013346D01*X020650Y013352D01*X020700Y013354D01*X020750Y013352D01*X020800Y013346D01*X020849Y013336D01*X020898Y013323D01*X020945Y013305D01*X020991Y013284D01*X021034Y013260D01*X021076Y013232D01*X021116Y013201D01*X021153Y013167D01*X021187Y013130D01*X021218Y013090D01*X021246Y013048D01*X021270Y013005D01*X021291Y012959D01*X021309Y012912D01*X021322Y012863D01*X021332Y012814D01*X021338Y012764D01*X021340Y012714D01*X021338Y012664D01*X021332Y012614D01*X021322Y012565D01*X021309Y012516D01*X021291Y012469D01*X021270Y012423D01*X021246Y012380D01*X021218Y012338D01*X021187Y012298D01*X021153Y012261D01*X021116Y012227D01*X021076Y012196D01*X021034Y012168D01*X020991Y012144D01*X020945Y012123D01*X020898Y012105D01*X020849Y012092D01*X020800Y012082D01*X020750Y012076D01*X020700Y012074D01*X020650Y012076D01*X020600Y012082D01*X020551Y012092D01*X020502Y012105D01*X020455Y012123D01*X020409Y012144D01*X020366Y012168D01*X020324Y012196D01*X020284Y012227D01*X020247Y012261D01*X020213Y012298D01*X020182Y012338D01*X020154Y012380D01*X020130Y012423D01*X020109Y012469D01*X020091Y012516D01*X020078Y012565D01*X020068Y012614D01*X020062Y012664D01*X020060Y012714D01*X020170Y016064D02*X020172Y016114D01*X020178Y016164D01*X020188Y016213D01*X020202Y016261D01*X020219Y016308D01*X020240Y016353D01*X020265Y016397D01*X020293Y016438D01*X020325Y016477D01*X020359Y016514D01*X020396Y016548D01*X020436Y016578D01*X020478Y016605D01*X020522Y016629D01*X020568Y016650D01*X020615Y016666D01*X020663Y016679D01*X020713Y016688D01*X020762Y016693D01*X020813Y016694D01*X020863Y016691D01*X020912Y016684D01*X020961Y016673D01*X021009Y016658D01*X021055Y016640D01*X021100Y016618D01*X021143Y016592D01*X021184Y016563D01*X021223Y016531D01*X021259Y016496D01*X021291Y016458D01*X021321Y016418D01*X021348Y016375D01*X021371Y016331D01*X021390Y016285D01*X021406Y016237D01*X021418Y016188D01*X021426Y016139D01*X021430Y016089D01*X021430Y016039D01*X021426Y015989D01*X021418Y015940D01*X021406Y015891D01*X021390Y015843D01*X021371Y015797D01*X021348Y015753D01*X021321Y015710D01*X021291Y015670D01*X021259Y015632D01*X021223Y015597D01*X021184Y015565D01*X021143Y015536D01*X021100Y015510D01*X021055Y015488D01*X021009Y015470D01*X020961Y015455D01*X020912Y015444D01*X020863Y015437D01*X020813Y015434D01*X020762Y015435D01*X020713Y015440D01*X020663Y015449D01*X020615Y015462D01*X020568Y015478D01*X020522Y015499D01*X020478Y015523D01*X020436Y015550D01*X020396Y015580D01*X020359Y015614D01*X020325Y015651D01*X020293Y015690D01*X020265Y015731D01*X020240Y015775D01*X020219Y015820D01*X020202Y015867D01*X020188Y015915D01*X020178Y015964D01*X020172Y016014D01*X020170Y016064D01*X020060Y008714D02*X020062Y008764D01*X020068Y008814D01*X020078Y008863D01*X020091Y008912D01*X020109Y008959D01*X020130Y009005D01*X020154Y009048D01*X020182Y009090D01*X020213Y009130D01*X020247Y009167D01*X020284Y009201D01*X020324Y009232D01*X020366Y009260D01*X020409Y009284D01*X020455Y009305D01*X020502Y009323D01*X020551Y009336D01*X020600Y009346D01*X020650Y009352D01*X020700Y009354D01*X020750Y009352D01*X020800Y009346D01*X020849Y009336D01*X020898Y009323D01*X020945Y009305D01*X020991Y009284D01*X021034Y009260D01*X021076Y009232D01*X021116Y009201D01*X021153Y009167D01*X021187Y009130D01*X021218Y009090D01*X021246Y009048D01*X021270Y009005D01*X021291Y008959D01*X021309Y008912D01*X021322Y008863D01*X021332Y008814D01*X021338Y008764D01*X021340Y008714D01*X021338Y008664D01*X021332Y008614D01*X021322Y008565D01*X021309Y008516D01*X021291Y008469D01*X021270Y008423D01*X021246Y008380D01*X021218Y008338D01*X021187Y008298D01*X021153Y008261D01*X021116Y008227D01*X021076Y008196D01*X021034Y008168D01*X020991Y008144D01*X020945Y008123D01*X020898Y008105D01*X020849Y008092D01*X020800Y008082D01*X020750Y008076D01*X020700Y008074D01*X020650Y008076D01*X020600Y008082D01*X020551Y008092D01*X020502Y008105D01*X020455Y008123D01*X020409Y008144D01*X020366Y008168D01*X020324Y008196D01*X020284Y008227D01*X020247Y008261D01*X020213Y008298D01*X020182Y008338D01*X020154Y008380D01*X020130Y008423D01*X020109Y008469D01*X020091Y008516D01*X020078Y008565D01*X020068Y008614D01*X020062Y008664D01*X020060Y008714D01*X020170Y005064D02*X020172Y005114D01*X020178Y005164D01*X020188Y005213D01*X020202Y005261D01*X020219Y005308D01*X020240Y005353D01*X020265Y005397D01*X020293Y005438D01*X020325Y005477D01*X020359Y005514D01*X020396Y005548D01*X020436Y005578D01*X020478Y005605D01*X020522Y005629D01*X020568Y005650D01*X020615Y005666D01*X020663Y005679D01*X020713Y005688D01*X020762Y005693D01*X020813Y005694D01*X020863Y005691D01*X020912Y005684D01*X020961Y005673D01*X021009Y005658D01*X021055Y005640D01*X021100Y005618D01*X021143Y005592D01*X021184Y005563D01*X021223Y005531D01*X021259Y005496D01*X021291Y005458D01*X021321Y005418D01*X021348Y005375D01*X021371Y005331D01*X021390Y005285D01*X021406Y005237D01*X021418Y005188D01*X021426Y005139D01*X021430Y005089D01*X021430Y005039D01*X021426Y004989D01*X021418Y004940D01*X021406Y004891D01*X021390Y004843D01*X021371Y004797D01*X021348Y004753D01*X021321Y004710D01*X021291Y004670D01*X021259Y004632D01*X021223Y004597D01*X021184Y004565D01*X021143Y004536D01*X021100Y004510D01*X021055Y004488D01*X021009Y004470D01*X020961Y004455D01*X020912Y004444D01*X020863Y004437D01*X020813Y004434D01*X020762Y004435D01*X020713Y004440D01*X020663Y004449D01*X020615Y004462D01*X020568Y004478D01*X020522Y004499D01*X020478Y004523D01*X020436Y004550D01*X020396Y004580D01*X020359Y004614D01*X020325Y004651D01*X020293Y004690D01*X020265Y004731D01*X020240Y004775D01*X020219Y004820D01*X020202Y004867D01*X020188Y004915D01*X020178Y004964D01*X020172Y005014D01*X020170Y005064D01*D11*X006500Y010604D03*X006000Y010604D03*X005500Y010604D03*X005000Y010604D03*X005000Y013024D03*X005500Y013024D03*X006000Y013024D03*X006500Y013024D03*D12*X011423Y007128D03*X011423Y006872D03*X011423Y006616D03*X011423Y006360D03*X011423Y006104D03*X011423Y005848D03*X011423Y005592D03*X011423Y005336D03*X011423Y005080D03*X011423Y004825D03*X011423Y004569D03*X011423Y004313D03*X011423Y004057D03*X011423Y003801D03*X014277Y003801D03*X014277Y004057D03*X014277Y004313D03*X014277Y004569D03*X014277Y004825D03*X014277Y005080D03*X014277Y005336D03*X014277Y005592D03*X014277Y005848D03*X014277Y006104D03*X014277Y006360D03*X014277Y006616D03*X014277Y006872D03*X014277Y007128D03*D13*X009350Y010114D03*D14*X012630Y010114D03*X012630Y010784D03*X012630Y011454D03*X012630Y009444D03*X012630Y008774D03*D15*X010000Y013467D03*X010000Y016262D03*D16*X004150Y012988D03*X004150Y012240D03*X009900Y005688D03*X009900Y004940D03*X015000Y006240D03*X015000Y006988D03*D17*X014676Y008364D03*X015424Y008364D03*X017526Y004514D03*X018274Y004514D03*X010674Y004064D03*X009926Y004064D03*X004174Y009564D03*X003426Y009564D03*X005376Y014564D03*X006124Y014564D03*D18*X014250Y016088D03*X014250Y012741D03*D19*X014250Y010982D03*X014250Y009447D03*D20*X022869Y007639D02*X022869Y013789D01*D21*X018200Y011964D03*X017200Y011464D03*X017200Y010464D03*X018200Y009964D03*X018200Y010964D03*X017200Y009464D03*D22*X008696Y006914D03*X008696Y005864D03*X008696Y004864D03*X008696Y003814D03*X005004Y003814D03*X005004Y004864D03*X005004Y005864D03*X005004Y006914D03*D23*X001800Y008564D02*X001200Y008564D01*X001200Y009564D02*X001800Y009564D01*X001800Y010564D02*X001200Y010564D01*X001200Y011564D02*X001800Y011564D01*X001800Y012564D02*X001200Y012564D01*X005350Y016664D02*X005350Y017264D01*X006350Y017264D02*X006350Y016664D01*X007350Y016664D02*X007350Y017264D01*X017350Y017114D02*X017350Y016514D01*X018350Y016514D02*X018350Y017114D01*D24*X016613Y004514D03*X015787Y004514D03*D25*X015200Y004514D01*X014868Y004649D02*X014732Y004649D01*X014842Y004586D02*X014842Y004443D01*X014896Y004311D01*X014997Y004211D01*X015129Y004156D01*X015271Y004156D01*X015395Y004207D01*X015484Y004118D01*X016089Y004118D01*X016183Y004212D01*X016183Y004817D01*X016089Y004911D01*X015484Y004911D01*X015395Y004821D01*X015271Y004872D01*X015129Y004872D01*X014997Y004818D01*X014896Y004717D01*X014842Y004586D01*X014842Y004491D02*X014732Y004491D01*X014732Y004332D02*X014888Y004332D01*X014732Y004174D02*X015086Y004174D01*X015314Y004174D02*X015428Y004174D01*X014732Y004015D02*X019505Y004015D01*X019568Y003922D02*X019568Y003922D01*X019568Y003922D01*X019286Y004335D01*X019286Y004335D01*X019139Y004814D01*X019139Y005315D01*X019286Y005793D01*X019286Y005793D01*X019568Y006207D01*X019568Y006207D01*X019960Y006519D01*X019960Y006519D01*X020426Y006702D01*X020926Y006740D01*X020926Y006740D01*X021414Y006628D01*X021414Y006628D01*X021847Y006378D01*X021847Y006378D01*X022188Y006011D01*X022188Y006011D01*X022320Y005737D01*X022320Y015392D01*X022188Y015118D01*X022188Y015118D01*X021847Y014751D01*X021847Y014751D01*X021414Y014500D01*X021414Y014500D01*X020926Y014389D01*X020926Y014389D01*X020426Y014426D01*X020426Y014426D01*X019960Y014609D01*X019960Y014609D01*X019568Y014922D01*X019568Y014922D01*X019568Y014922D01*X019286Y015335D01*X019286Y015335D01*X019139Y015814D01*X019139Y016315D01*X019286Y016793D01*X019286Y016793D01*X019568Y017207D01*X019568Y017207D01*X019568Y017207D01*X019960Y017519D01*X019960Y017519D01*X020126Y017584D01*X016626Y017584D01*X016637Y017573D01*X016924Y017287D01*X016960Y017375D01*X017089Y017504D01*X017258Y017574D01*X017441Y017574D01*X017611Y017504D01*X017740Y017375D01*X017810Y017206D01*X017810Y016423D01*X017740Y016254D01*X017611Y016124D01*X017441Y016054D01*X017258Y016054D01*X017089Y016124D01*X016960Y016254D01*X016890Y016423D01*X016890Y016557D01*X016841Y016577D01*X016284Y017134D01*X010456Y017134D01*X010475Y017116D01*X010475Y016310D01*X010475Y016310D01*X010495Y016216D01*X010477Y016123D01*X010475Y016120D01*X010475Y015408D01*X010381Y015315D01*X010305Y015315D01*X010358Y015186D01*X010358Y015043D01*X010304Y014911D01*X010203Y014811D01*X010071Y014756D01*X009929Y014756D01*X009797Y014811D01*X009696Y014911D01*X009642Y015043D01*X009642Y015186D01*X009695Y015315D01*X009619Y015315D01*X009525Y015408D01*X009525Y017116D01*X009544Y017134D01*X009416Y017134D01*X009330Y017048D01*X009330Y014080D01*X009525Y013885D01*X009525Y014320D01*X009619Y014414D01*X010381Y014414D01*X010475Y014320D01*X010475Y013747D01*X011403Y013747D01*X011506Y013704D01*X011688Y013522D01*X011721Y013522D01*X011853Y013468D01*X011954Y013367D01*X013755Y013367D01*X013755Y013525D02*X011685Y013525D01*X011526Y013684D02*X013893Y013684D01*X013911Y013689D02*X013866Y013677D01*X013825Y013653D01*X013791Y013619D01*X013767Y013578D01*X013755Y013533D01*X013755Y012819D01*X014173Y012819D01*X014173Y013689D01*X013911Y013689D01*X014173Y013684D02*X014327Y013684D01*X014327Y013689D02*X014327Y012819D01*X014173Y012819D01*X014173Y012664D01*X014327Y012664D01*X014327Y011793D01*X014589Y011793D01*X014634Y011806D01*X014675Y011829D01*X014709Y011863D01*X014733Y011904D01*X014745Y011950D01*X014745Y012664D01*X014327Y012664D01*X014327Y012819D01*X014745Y012819D01*X014745Y013533D01*X014733Y013578D01*X014709Y013619D01*X014675Y013653D01*X014634Y013677D01*X014589Y013689D01*X014327Y013689D01*X014327Y013525D02*X014173Y013525D01*X014173Y013367D02*X014327Y013367D01*X014327Y013208D02*X014173Y013208D01*X014173Y013050D02*X014327Y013050D01*X014327Y012891D02*X014173Y012891D01*X014173Y012733D02*X010475Y012733D01*X010475Y012613D02*X010475Y013187D01*X011232Y013187D01*X011292Y013126D01*X011292Y013093D01*X011346Y012961D01*X011447Y012861D01*X011579Y012806D01*X011721Y012806D01*X011853Y012861D01*X011954Y012961D01*X012008Y013093D01*X012008Y013236D01*X011954Y013367D01*X012008Y013208D02*X013755Y013208D01*X013755Y013050D02*X011990Y013050D01*X011883Y012891D02*X013755Y012891D01*X013755Y012664D02*X013755Y011950D01*X013767Y011904D01*X013791Y011863D01*X013825Y011829D01*X013866Y011806D01*X013911Y011793D01*X014173Y011793D01*X014173Y012664D01*X013755Y012664D01*X013755Y012574D02*X010436Y012574D01*X010475Y012613D02*X010381Y012519D01*X009619Y012519D01*X009525Y012613D01*X009525Y013234D01*X009444Y013234D01*X009341Y013277D01*X009263Y013356D01*X009263Y013356D01*X008813Y013806D01*X008770Y013909D01*X008770Y017220D01*X008813Y017323D01*X009074Y017584D01*X007681Y017584D01*X007740Y017525D01*X007810Y017356D01*X007810Y016573D01*X007740Y016404D01*X007611Y016274D01*X007441Y016204D01*X007258Y016204D01*X007089Y016274D01*X006960Y016404D01*X006890Y016573D01*X006890Y017356D01*X006960Y017525D01*X007019Y017584D01*X006681Y017584D01*X006740Y017525D01*X006810Y017356D01*X006810Y016573D01*X006740Y016404D01*X006611Y016274D01*X006590Y016266D01*X006590Y015367D01*X006553Y015278D01*X006340Y015065D01*X006340Y015020D01*X006446Y015020D01*X006540Y014926D01*X006540Y014203D01*X006446Y014109D01*X006240Y014109D01*X006240Y013961D01*X006297Y014018D01*X006429Y014072D01*X006571Y014072D01*X006703Y014018D01*X006804Y013917D01*X006858Y013786D01*X006858Y013643D01*X006804Y013511D01*X006786Y013494D01*X006790Y013491D01*X006790Y012558D01*X006696Y012464D01*X006304Y012464D01*X006250Y012518D01*X006196Y012464D01*X005804Y012464D01*X005750Y012518D01*X005696Y012464D01*X005304Y012464D01*X005264Y012504D01*X005241Y012480D01*X005199Y012457D01*X005154Y012444D01*X005000Y012444D01*X005000Y013024D01*X005000Y013024D01*X005000Y012444D01*X004846Y012444D01*X004801Y012457D01*X004759Y012480D01*X004726Y012514D01*X004702Y012555D01*X004690Y012601D01*X004690Y013024D01*X005000Y013024D01*X005000Y013024D01*X004964Y012988D01*X004150Y012988D01*X004198Y012940D02*X004198Y013036D01*X004625Y013036D01*X004625Y013268D01*X004613Y013314D01*X004589Y013355D01*X004556Y013388D01*X004515Y013412D01*X004469Y013424D01*X004198Y013424D01*X004198Y013036D01*X004102Y013036D01*X004102Y012940D01*X003675Y012940D01*X003675Y012709D01*X003687Y012663D01*X003711Y012622D01*X003732Y012600D01*X003695Y012562D01*X003695Y011918D01*X003788Y011824D01*X003904Y011824D01*X003846Y011767D01*X003792Y011636D01*X003792Y011493D01*X003846Y011361D01*X003947Y011261D01*X004079Y011206D01*X004221Y011206D01*X004353Y011261D01*X004454Y011361D01*X004508Y011493D01*X004508Y011636D01*X004454Y011767D01*X004396Y011824D01*X004512Y011824D01*X004605Y011918D01*X004605Y012562D01*X004568Y012600D01*X004589Y012622D01*X004613Y012663D01*X004625Y012709D01*X004625Y012940D01*X004198Y012940D01*X004198Y013050D02*X004102Y013050D01*X004102Y013036D02*X004102Y013424D01*X003831Y013424D01*X003785Y013412D01*X003744Y013388D01*X003711Y013355D01*X003687Y013314D01*X003675Y013268D01*X003675Y013036D01*X004102Y013036D01*X004102Y013208D02*X004198Y013208D01*X004198Y013367D02*X004102Y013367D01*X003723Y013367D02*X000780Y013367D01*X000780Y013525D02*X004720Y013525D01*X004726Y013535D02*X004702Y013494D01*X004690Y013448D01*X004690Y013024D01*X005000Y013024D01*X005000Y012264D01*X005750Y011514D01*X005750Y010604D01*X005500Y010604D01*X005500Y010024D01*X005654Y010024D01*X005699Y010037D01*X005741Y010060D01*X005750Y010070D01*X005759Y010060D01*X005801Y010037D01*X005846Y010024D01*X006000Y010024D01*X006154Y010024D01*X006199Y010037D01*X006241Y010060D01*X006260Y010080D01*X006260Y008267D01*X006297Y008178D01*X006364Y008111D01*X006364Y008111D01*X006821Y007654D01*X006149Y007654D01*X005240Y008564D01*X005240Y010080D01*X005259Y010060D01*X005301Y010037D01*X005346Y010024D01*X005500Y010024D01*X005500Y010604D01*X005500Y010604D01*X005500Y010604D01*X005690Y010604D01*X006000Y010604D01*X006000Y010024D01*X006000Y010604D01*X006000Y010604D01*X006000Y010604D01*X005750Y010604D01*X005500Y010604D02*X006000Y010604D01*X006000Y011184D01*X005846Y011184D01*X005801Y011172D01*X005759Y011148D01*X005741Y011148D01*X005699Y011172D01*X005654Y011184D01*X005500Y011184D01*X005346Y011184D01*X005301Y011172D01*X005259Y011148D01*X005213Y011148D01*X005196Y011164D02*X005236Y011125D01*X005259Y011148D01*X005196Y011164D02*X004804Y011164D01*X004710Y011071D01*X004710Y010138D01*X004760Y010088D01*X004760Y009309D01*X004753Y009324D01*X004590Y009488D01*X004590Y009926D01*X004496Y010020D01*X003852Y010020D01*X003800Y009968D01*X003748Y010020D01*X003104Y010020D01*X003010Y009926D01*X003010Y009804D01*X002198Y009804D01*X002190Y009825D01*X002061Y009954D01*X001891Y010024D01*X001108Y010024D01*X000939Y009954D01*X000810Y009825D01*X000780Y009752D01*X000780Y010376D01*X000810Y010304D01*X000939Y010174D01*X001108Y010104D01*X001891Y010104D01*X002061Y010174D01*X002190Y010304D01*X002260Y010473D01*X002260Y010656D01*X002190Y010825D01*X002061Y010954D01*X001891Y011024D01*X001108Y011024D01*X000939Y010954D01*X000810Y010825D01*X000780Y010752D01*X000780Y011376D01*X000810Y011304D01*X000939Y011174D01*X001108Y011104D01*X001891Y011104D01*X002061Y011174D01*X002190Y011304D01*X002260Y011473D01*X002260Y011656D01*X002190Y011825D01*X002061Y011954D01*X001891Y012024D01*X001108Y012024D01*X000939Y011954D01*X000810Y011825D01*X000780Y011752D01*X000780Y012376D01*X000810Y012304D01*X000939Y012174D01*X001108Y012104D01*X001891Y012104D01*X002061Y012174D01*X002190Y012304D01*X002260Y012473D01*X002260Y012656D01*X002190Y012825D01*X002061Y012954D01*X001891Y013024D01*X001108Y013024D01*X000939Y012954D01*X000810Y012825D01*X000780Y012752D01*X000780Y015356D01*X000786Y015335D01*X001068Y014922D01*X001068Y014922D01*X001068Y014922D01*X001460Y014609D01*X001926Y014426D01*X002426Y014389D01*X002914Y014500D01*X003347Y014751D01*X003347Y014751D01*X003688Y015118D01*X003905Y015569D01*X003980Y016064D01*X003905Y016560D01*X003688Y017011D01*X003347Y017378D01*X002990Y017584D01*X005019Y017584D01*X004960Y017525D01*X004890Y017356D01*X004890Y016573D01*X004960Y016404D01*X005089Y016274D01*X005110Y016266D01*X005110Y015020D01*X005054Y015020D01*X004960Y014926D01*X004960Y014203D01*X005054Y014109D01*X005260Y014109D01*X005260Y013549D01*X005241Y013568D01*X005199Y013592D01*X005154Y013604D01*X005000Y013604D01*X004846Y013604D01*X004801Y013592D01*X004759Y013568D01*X004726Y013535D01*X004690Y013367D02*X004577Y013367D01*X004625Y013208D02*X004690Y013208D01*X004690Y013050D02*X004625Y013050D01*X004625Y012891D02*X004690Y012891D01*X004690Y012733D02*X004625Y012733D01*X004593Y012574D02*X004697Y012574D01*X004605Y012416D02*X013755Y012416D01*X013755Y012257D02*X011559Y012257D01*X011559Y012307D02*X011465Y012400D01*X007235Y012400D01*X007141Y012307D01*X007141Y008013D01*X006740Y008414D01*X006740Y010088D01*X006790Y010138D01*X006790Y011071D01*X006696Y011164D01*X006304Y011164D01*X006264Y011125D01*X006241Y011148D01*X006287Y011148D01*X006241Y011148D02*X006199Y011172D01*X006154Y011184D01*X006000Y011184D01*X006000Y010604D01*X006000Y010604D01*X006000Y010672D02*X006000Y010672D01*X006000Y010514D02*X006000Y010514D01*X006000Y010355D02*X006000Y010355D01*X006000Y010197D02*X006000Y010197D01*X006000Y010038D02*X006000Y010038D01*X006202Y010038D02*X006260Y010038D01*X006260Y009880D02*X005240Y009880D01*X005240Y010038D02*X005297Y010038D01*X005500Y010038D02*X005500Y010038D01*X005500Y010197D02*X005500Y010197D01*X005500Y010355D02*X005500Y010355D01*X005500Y010514D02*X005500Y010514D01*X005500Y010604D02*X005500Y011184D01*X005500Y010604D01*X005500Y010604D01*X005500Y010672D02*X005500Y010672D01*X005500Y010831D02*X005500Y010831D01*X005500Y010989D02*X005500Y010989D01*X005500Y011148D02*X005500Y011148D01*X005741Y011148D02*X005750Y011139D01*X005759Y011148D01*X006000Y011148D02*X006000Y011148D01*X006000Y010989D02*X006000Y010989D01*X006000Y010831D02*X006000Y010831D01*X006500Y010604D02*X006500Y008314D01*X007150Y007664D01*X009450Y007664D01*X010750Y006364D01*X011419Y006364D01*X011423Y006360D01*X011377Y006364D01*X011423Y006104D02*X010660Y006104D01*X009350Y007414D01*X006050Y007414D01*X005000Y008464D01*X005000Y010604D01*X004710Y010672D02*X002253Y010672D01*X002260Y010514D02*X004710Y010514D01*X004710Y010355D02*X002211Y010355D01*X002083Y010197D02*X004710Y010197D01*X004760Y010038D02*X000780Y010038D01*X000780Y009880D02*X000865Y009880D01*X000917Y010197D02*X000780Y010197D01*X000780Y010355D02*X000789Y010355D01*X000780Y010831D02*X000816Y010831D01*X000780Y010989D02*X001024Y010989D01*X001003Y011148D02*X000780Y011148D01*X000780Y011306D02*X000809Y011306D01*X000780Y011782D02*X000792Y011782D01*X000780Y011940D02*X000925Y011940D01*X000780Y012099D02*X003695Y012099D01*X003695Y012257D02*X002144Y012257D01*X002236Y012416D02*X003695Y012416D01*X003707Y012574D02*X002260Y012574D01*X002228Y012733D02*X003675Y012733D01*X003675Y012891D02*X002124Y012891D01*X002075Y011940D02*X003695Y011940D01*X003861Y011782D02*X002208Y011782D01*X002260Y011623D02*X003792Y011623D01*X003804Y011465D02*X002257Y011465D01*X002191Y011306D02*X003902Y011306D01*X004150Y011564D02*X004150Y012240D01*X004605Y012257D02*X007141Y012257D01*X007141Y012099D02*X004605Y012099D01*X004605Y011940D02*X007141Y011940D01*X007141Y011782D02*X004439Y011782D01*X004508Y011623D02*X007141Y011623D01*X007141Y011465D02*X004496Y011465D01*X004398Y011306D02*X007141Y011306D01*X007141Y011148D02*X006713Y011148D01*X006790Y010989D02*X007141Y010989D01*X007141Y010831D02*X006790Y010831D01*X006790Y010672D02*X007141Y010672D01*X007141Y010514D02*X006790Y010514D01*X006790Y010355D02*X007141Y010355D01*X007141Y010197D02*X006790Y010197D01*X006740Y010038D02*X007141Y010038D01*X007141Y009880D02*X006740Y009880D01*X006740Y009721D02*X007141Y009721D01*X007141Y009563D02*X006740Y009563D01*X006740Y009404D02*X007141Y009404D01*X007141Y009246D02*X006740Y009246D01*X006740Y009087D02*X007141Y009087D01*X007141Y008929D02*X006740Y008929D01*X006740Y008770D02*X007141Y008770D01*X007141Y008612D02*X006740Y008612D01*X006740Y008453D02*X007141Y008453D01*X007141Y008295D02*X006859Y008295D01*X007017Y008136D02*X007141Y008136D01*X006656Y007819D02*X005984Y007819D01*X005826Y007978D02*X006497Y007978D01*X006339Y008136D02*X005667Y008136D01*X005509Y008295D02*X006260Y008295D01*X006260Y008453D02*X005350Y008453D01*X005240Y008612D02*X006260Y008612D01*X006260Y008770D02*X005240Y008770D01*X005240Y008929D02*X006260Y008929D01*X006260Y009087D02*X005240Y009087D01*X005240Y009246D02*X006260Y009246D01*X006260Y009404D02*X005240Y009404D01*X005240Y009563D02*X006260Y009563D01*X006260Y009721D02*X005240Y009721D01*X004760Y009721D02*X004590Y009721D01*X004590Y009563D02*X004760Y009563D01*X004760Y009404D02*X004673Y009404D01*X004550Y009188D02*X004174Y009564D01*X004590Y009880D02*X004760Y009880D01*X004550Y009188D02*X004550Y006114D01*X004800Y005864D01*X005004Y005864D01*X004647Y005678D02*X004647Y005548D01*X004740Y005454D01*X005267Y005454D01*X005360Y005548D01*X005360Y006181D01*X005267Y006274D01*X004790Y006274D01*X004790Y006504D01*X005267Y006504D01*X005360Y006598D01*X005360Y007231D01*X005267Y007324D01*X004790Y007324D01*X004790Y008344D01*X004797Y008328D01*X005847Y007278D01*X005914Y007211D01*X006002Y007174D01*X008320Y007174D01*X008320Y006933D01*X008678Y006933D01*X008678Y006896D01*X008320Y006896D01*X008320Y006641D01*X008332Y006595D01*X008356Y006554D01*X008389Y006520D01*X008430Y006497D01*X008476Y006484D01*X008678Y006484D01*X008678Y006896D01*X008715Y006896D01*X008715Y006933D01*X009073Y006933D01*X009073Y007174D01*X009251Y007174D01*X010337Y006088D01*X010278Y006088D01*X010262Y006104D01*X009538Y006104D01*X009445Y006011D01*X009445Y005928D01*X009276Y005928D01*X009188Y005892D01*X009064Y005768D01*X009053Y005757D01*X009053Y006181D01*X008960Y006274D01*X008433Y006274D01*X008340Y006181D01*X008340Y005548D01*X008433Y005454D01*X008960Y005454D01*X008960Y005455D01*X008960Y005274D01*X008960Y005274D01*X008433Y005274D01*X008340Y005181D01*X008340Y004548D01*X008433Y004454D01*X008960Y004454D01*X009053Y004548D01*X009053Y004627D01*X009136Y004661D01*X009203Y004728D01*X009403Y004928D01*X009428Y004988D01*X009852Y004988D01*X009852Y004892D01*X009425Y004892D01*X009425Y004661D01*X009437Y004615D01*X009461Y004574D01*X009494Y004540D01*X009535Y004517D01*X009581Y004504D01*X009589Y004504D01*X009510Y004426D01*X009510Y004311D01*X009453Y004368D01*X009321Y004422D01*X009179Y004422D01*X009047Y004368D01*X008984Y004304D01*X008899Y004304D01*X008811Y004268D01*X008767Y004224D01*X008433Y004224D01*X008340Y004131D01*X008340Y003544D01*X005360Y003544D01*X005360Y004131D01*X005267Y004224D01*X004740Y004224D01*X004647Y004131D01*X004647Y003544D01*X002937Y003544D01*X002964Y003550D01*X003397Y003801D01*X003397Y003801D01*X003738Y004168D01*X003955Y004619D01*X004030Y005114D01*X003955Y005610D01*X003738Y006061D01*X003397Y006428D01*X002964Y006678D01*X002964Y006678D01*X002476Y006790D01*X002476Y006790D01*X001976Y006752D01*X001510Y006569D01*X001118Y006257D01*X000836Y005843D01*X000780Y005660D01*X000780Y008376D01*X000810Y008304D01*X000939Y008174D01*X001108Y008104D01*X001891Y008104D01*X002061Y008174D01*X002190Y008304D01*X002198Y008324D01*X003701Y008324D01*X004060Y007965D01*X004060Y005267D01*X004097Y005178D01*X004164Y005111D01*X004497Y004778D01*X004564Y004711D01*X004647Y004677D01*X004647Y004548D01*X004740Y004454D01*X005267Y004454D01*X005360Y004548D01*X005360Y005181D01*X005267Y005274D01*X004740Y005274D01*X004710Y005244D01*X004540Y005414D01*X004540Y005785D01*X004647Y005678D01*X004647Y005600D02*X004540Y005600D01*X004540Y005442D02*X008960Y005442D01*X008960Y005283D02*X004670Y005283D01*X004309Y004966D02*X004008Y004966D01*X004030Y005114D02*X004030Y005114D01*X004028Y005125D02*X004150Y005125D01*X004060Y005283D02*X004005Y005283D01*X003981Y005442D02*X004060Y005442D01*X004060Y005600D02*X003957Y005600D01*X003883Y005759D02*X004060Y005759D01*X004060Y005917D02*X003807Y005917D01*X003738Y006061D02*X003738Y006061D01*X003724Y006076D02*X004060Y006076D01*X004060Y006234D02*X003577Y006234D01*X003430Y006393D02*X004060Y006393D01*X004060Y006551D02*X003184Y006551D01*X003397Y006428D02*X003397Y006428D01*X002825Y006710D02*X004060Y006710D01*X004060Y006868D02*X000780Y006868D01*X000780Y006710D02*X001868Y006710D01*X001976Y006752D02*X001976Y006752D01*X001510Y006569D02*X001510Y006569D01*X001488Y006551D02*X000780Y006551D01*X000780Y006393D02*X001289Y006393D01*X001118Y006257D02*X001118Y006257D01*X001118Y006257D01*X001103Y006234D02*X000780Y006234D01*X000780Y006076D02*X000995Y006076D01*X000887Y005917D02*X000780Y005917D01*X000836Y005843D02*X000836Y005843D01*X000810Y005759D02*X000780Y005759D01*X000780Y007027D02*X004060Y007027D01*X004060Y007185D02*X000780Y007185D01*X000780Y007344D02*X004060Y007344D01*X004060Y007502D02*X000780Y007502D01*X000780Y007661D02*X004060Y007661D01*X004060Y007819D02*X000780Y007819D01*X000780Y007978D02*X004047Y007978D01*X003889Y008136D02*X001969Y008136D01*X002181Y008295D02*X003730Y008295D01*X003800Y008564D02*X001500Y008564D01*X001031Y008136D02*X000780Y008136D01*X000780Y008295D02*X000819Y008295D01*X001500Y009564D02*X003426Y009564D01*X003010Y009880D02*X002135Y009880D01*X002184Y010831D02*X004710Y010831D01*X004710Y010989D02*X001976Y010989D01*X001997Y011148D02*X004787Y011148D01*X005702Y010038D02*X005797Y010038D01*X004830Y008295D02*X004790Y008295D01*X004790Y008136D02*X004989Y008136D01*X005147Y007978D02*X004790Y007978D01*X004790Y007819D02*X005306Y007819D01*X005464Y007661D02*X004790Y007661D01*X004790Y007502D02*X005623Y007502D01*X005781Y007344D02*X004790Y007344D01*X005360Y007185D02*X005976Y007185D01*X006143Y007661D02*X006814Y007661D01*X005360Y007027D02*X008320Y007027D01*X008320Y006868D02*X005360Y006868D01*X005360Y006710D02*X008320Y006710D01*X008358Y006551D02*X005314Y006551D01*X005307Y006234D02*X008393Y006234D01*X008340Y006076D02*X005360Y006076D01*X005360Y005917D02*X008340Y005917D01*X008340Y005759D02*X005360Y005759D01*X005360Y005600D02*X008340Y005600D01*X008340Y005125D02*X005360Y005125D01*X005360Y004966D02*X008340Y004966D01*X008340Y004808D02*X005360Y004808D01*X005360Y004649D02*X008340Y004649D01*X008397Y004491D02*X005303Y004491D01*X005317Y004174D02*X008383Y004174D01*X008340Y004015D02*X005360Y004015D01*X005360Y003857D02*X008340Y003857D01*X008340Y003698D02*X005360Y003698D01*X004647Y003698D02*X003220Y003698D01*X003449Y003857D02*X004647Y003857D01*X004647Y004015D02*X003596Y004015D01*X003738Y004168D02*X003738Y004168D01*X003741Y004174D02*X004690Y004174D01*X004704Y004491D02*X003894Y004491D01*X003955Y004619D02*X003955Y004619D01*X003960Y004649D02*X004647Y004649D01*X004467Y004808D02*X003984Y004808D01*X003817Y004332D02*X009012Y004332D01*X008996Y004491D02*X009575Y004491D01*X009510Y004332D02*X009488Y004332D01*X009250Y004064D02*X008946Y004064D01*X008696Y003814D01*X009053Y003758D02*X009053Y003544D01*X020126Y003544D01*X019960Y003609D01*X019960Y003609D01*X019568Y003922D01*X019650Y003857D02*X014732Y003857D01*X014732Y003698D02*X019848Y003698D01*X019397Y004174D02*X018704Y004174D01*X018710Y004195D02*X018710Y004466D01*X018322Y004466D01*X018322Y004039D01*X018554Y004039D01*X018599Y004051D01*X018640Y004075D01*X018674Y004109D01*X018698Y004150D01*X018710Y004195D01*X018710Y004332D02*X019288Y004332D01*X019238Y004491D02*X018322Y004491D01*X018322Y004466D02*X018322Y004562D01*X018710Y004562D01*X018710Y004833D01*X018698Y004879D01*X018674Y004920D01*X018640Y004954D01*X018599Y004977D01*X018554Y004990D01*X018322Y004990D01*X018322Y004562D01*X018226Y004562D01*X018226Y004990D01*X017994Y004990D01*X017949Y004977D01*X017908Y004954D01*X017886Y004932D01*X017848Y004970D01*X017204Y004970D01*X017110Y004876D01*X017110Y004754D01*X017010Y004754D01*X017010Y004817D01*X016916Y004911D01*X016311Y004911D01*X016217Y004817D01*X016217Y004212D01*X016311Y004118D01*X016916Y004118D01*X017010Y004212D01*X017010Y004274D01*X017110Y004274D01*X017110Y004153D01*X017204Y004059D01*X017848Y004059D01*X017886Y004097D01*X017908Y004075D01*X017949Y004051D01*X017994Y004039D01*X018226Y004039D01*X018226Y004466D01*X018322Y004466D01*X018322Y004332D02*X018226Y004332D01*X018226Y004174D02*X018322Y004174D01*X018322Y004649D02*X018226Y004649D01*X018226Y004808D02*X018322Y004808D01*X018322Y004966D02*X018226Y004966D01*X017930Y004966D02*X017851Y004966D01*X017526Y004514D02*X016613Y004514D01*X016217Y004491D02*X016183Y004491D01*X016183Y004649D02*X016217Y004649D01*X016217Y004808D02*X016183Y004808D01*X016670Y005096D02*X016758Y005133D01*X018836Y007211D01*X018903Y007278D01*X018940Y007367D01*X018940Y010512D01*X018903Y010600D01*X018634Y010870D01*X018637Y010877D01*X018637Y011051D01*X018571Y011212D01*X018448Y011335D01*X018287Y011401D01*X018113Y011401D01*X017952Y011335D01*X017829Y011212D01*X017818Y011185D01*X017634Y011370D01*X017637Y011377D01*X017637Y011551D01*X017571Y011712D01*X017448Y011835D01*X017287Y011901D01*X017113Y011901D01*X016952Y011835D01*X016829Y011712D01*X016763Y011551D01*X016763Y011377D01*X016829Y011217D01*X016952Y011094D01*X017113Y011027D01*X017287Y011027D01*X017295Y011030D01*X017460Y010865D01*X017460Y010823D01*X017448Y010835D01*X017287Y010901D01*X017113Y010901D01*X016952Y010835D01*X016829Y010712D01*X016763Y010551D01*X016763Y010377D01*X016829Y010217D01*X016952Y010094D01*X017113Y010027D01*X017287Y010027D01*X017448Y010094D01*X017460Y010106D01*X017460Y009823D01*X017448Y009835D01*X017287Y009901D01*X017113Y009901D01*X016952Y009835D01*X016829Y009712D01*X016763Y009551D01*X016763Y009377D01*X016829Y009217D01*X016952Y009094D01*X016960Y009091D01*X016960Y008914D01*X016651Y008604D01*X015840Y008604D01*X015840Y008726D01*X015746Y008820D01*X015102Y008820D01*X015064Y008782D01*X015042Y008804D01*X015001Y008827D01*X014956Y008840D01*X014724Y008840D01*X014724Y008412D01*X014628Y008412D01*X014628Y008316D01*X014240Y008316D01*X014240Y008045D01*X014252Y008000D01*X014276Y007959D01*X014310Y007925D01*X014345Y007904D01*X013152Y007904D01*X013064Y007868D01*X012997Y007800D01*X012564Y007368D01*X011375Y007368D01*X011372Y007366D01*X011061Y007366D01*X010968Y007273D01*X010968Y006604D01*X010849Y006604D01*X009625Y007828D01*X011465Y007828D01*X011559Y007922D01*X011559Y012307D01*X011559Y012099D02*X013755Y012099D01*X013758Y011940D02*X011559Y011940D01*X011559Y011782D02*X012096Y011782D01*X012139Y011824D02*X012045Y011731D01*X012045Y011178D01*X012090Y011133D01*X012061Y011105D01*X012037Y011064D01*X012025Y011018D01*X012025Y010809D01*X012605Y010809D01*X012605Y010759D01*X012025Y010759D01*X012025Y010551D01*X012037Y010505D01*X012061Y010464D01*X012090Y010435D01*X012045Y010391D01*X012045Y009838D01*X012104Y009779D01*X012045Y009721D01*X012045Y009168D01*X012104Y009109D01*X012045Y009051D01*X012045Y008498D01*X012139Y008404D01*X013121Y008404D01*X013201Y008484D01*X013324Y008484D01*X013347Y008461D01*X013479Y008406D01*X013621Y008406D01*X013753Y008461D01*X013854Y008561D01*X013908Y008693D01*X013908Y008836D01*X013876Y008913D01*X014986Y008913D01*X015079Y009006D01*X015079Y009887D01*X014986Y009981D01*X013682Y009981D01*X013708Y010043D01*X013708Y010186D01*X013654Y010317D01*X013553Y010418D01*X013421Y010472D01*X013279Y010472D01*X013176Y010430D01*X013170Y010435D01*X013199Y010464D01*X013223Y010505D01*X013235Y010551D01*X013235Y010759D01*X012655Y010759D01*X012655Y010809D01*X013235Y010809D01*X013235Y011018D01*X013223Y011064D01*X013199Y011105D01*X013176Y011128D01*X013229Y011106D01*X013371Y011106D01*X013401Y011118D01*X013401Y011062D01*X014170Y011062D01*X014170Y010902D01*X014330Y010902D01*X014330Y010428D01*X014943Y010428D01*X014989Y010440D01*X015030Y010464D01*X015063Y010498D01*X015087Y010539D01*X015099Y010584D01*X015099Y010902D01*X014330Y010902D01*X014330Y011062D01*X015099Y011062D01*X015099Y011380D01*X015087Y011426D01*X015063Y011467D01*X015030Y011500D01*X014989Y011524D01*X014943Y011536D01*X014330Y011536D01*X014330Y011062D01*X014170Y011062D01*X014170Y011536D01*X013658Y011536D01*X013604Y011667D01*X013503Y011768D01*X013371Y011822D01*X013229Y011822D01*X013154Y011792D01*X013121Y011824D01*X012139Y011824D01*X012045Y011623D02*X011559Y011623D01*X011559Y011465D02*X012045Y011465D01*X012045Y011306D02*X011559Y011306D01*X011559Y011148D02*X012075Y011148D01*X012025Y010989D02*X011559Y010989D01*X011559Y010831D02*X012025Y010831D01*X012025Y010672D02*X011559Y010672D01*X011559Y010514D02*X012035Y010514D01*X012045Y010355D02*X011559Y010355D01*X011559Y010197D02*X012045Y010197D01*X012045Y010038D02*X011559Y010038D01*X011559Y009880D02*X012045Y009880D01*X012046Y009721D02*X011559Y009721D01*X011559Y009563D02*X012045Y009563D01*X012045Y009404D02*X011559Y009404D01*X011559Y009246D02*X012045Y009246D01*X012082Y009087D02*X011559Y009087D01*X011559Y008929D02*X012045Y008929D01*X012045Y008770D02*X011559Y008770D01*X011559Y008612D02*X012045Y008612D01*X012090Y008453D02*X011559Y008453D01*X011559Y008295D02*X014240Y008295D01*X014240Y008412D02*X014628Y008412D01*X014628Y008840D01*X014396Y008840D01*X014351Y008827D01*X014310Y008804D01*X014276Y008770D01*X014252Y008729D01*X014240Y008683D01*X014240Y008412D01*X014240Y008453D02*X013735Y008453D01*X013874Y008612D02*X014240Y008612D01*X014276Y008770D02*X013908Y008770D01*X013365Y008453D02*X013170Y008453D01*X013016Y007819D02*X009634Y007819D01*X009793Y007661D02*X012857Y007661D01*X012699Y007502D02*X009951Y007502D01*X010110Y007344D02*X011039Y007344D01*X010968Y007185D02*X010268Y007185D01*X010427Y007027D02*X010968Y007027D01*X010968Y006868D02*X010585Y006868D01*X010744Y006710D02*X010968Y006710D01*X011423Y007128D02*X012663Y007128D01*X013200Y007664D01*X015250Y007664D01*X015424Y007838D01*X015424Y008364D01*X016750Y008364D01*X017200Y008814D01*X017200Y009464D01*X016817Y009246D02*X015079Y009246D01*X015079Y009404D02*X016763Y009404D01*X016768Y009563D02*X015079Y009563D01*X015079Y009721D02*X016839Y009721D01*X017061Y009880D02*X015079Y009880D01*X015073Y010514D02*X016763Y010514D01*X016772Y010355D02*X013615Y010355D01*X013557Y010428D02*X014170Y010428D01*X014170Y010902D01*X013401Y010902D01*X013401Y010584D01*X013413Y010539D01*X013437Y010498D01*X013470Y010464D01*X013511Y010440D01*X013557Y010428D01*X013427Y010514D02*X013225Y010514D01*X013235Y010672D02*X013401Y010672D01*X013401Y010831D02*X013235Y010831D01*X013235Y010989D02*X014170Y010989D01*X014170Y010831D02*X014330Y010831D01*X014330Y010989D02*X017336Y010989D01*X017452Y010831D02*X017460Y010831D01*X017700Y010964D02*X017200Y011464D01*X016792Y011306D02*X015099Y011306D01*X015099Y011148D02*X016898Y011148D01*X016948Y010831D02*X015099Y010831D01*X015099Y010672D02*X016813Y010672D01*X016849Y010197D02*X013703Y010197D01*X013706Y010038D02*X017086Y010038D01*X017314Y010038D02*X017460Y010038D01*X017460Y009880D02*X017339Y009880D01*X017940Y009588D02*X017960Y009573D01*X018025Y009541D01*X018093Y009518D01*X018164Y009507D01*X018191Y009507D01*X018191Y009956D01*X018209Y009956D01*X018209Y009507D01*X018236Y009507D01*X018307Y009518D01*X018375Y009541D01*X018440Y009573D01*X018460Y009588D01*X018460Y007514D01*X017940Y006994D01*X017940Y009588D01*X017940Y009563D02*X017981Y009563D01*X017940Y009404D02*X018460Y009404D01*X018460Y009246D02*X017940Y009246D01*X017940Y009087D02*X018460Y009087D01*X018460Y008929D02*X017940Y008929D01*X017940Y008770D02*X018460Y008770D01*X018460Y008612D02*X017940Y008612D01*X017940Y008453D02*X018460Y008453D01*X018460Y008295D02*X017940Y008295D01*X017940Y008136D02*X018460Y008136D01*X018460Y007978D02*X017940Y007978D01*X017940Y007819D02*X018460Y007819D01*X018460Y007661D02*X017940Y007661D01*X017940Y007502D02*X018449Y007502D01*X018290Y007344D02*X017940Y007344D01*X017940Y007185D02*X018132Y007185D01*X017973Y007027D02*X017940Y007027D01*X017700Y006814D02*X017700Y010964D01*X017697Y011306D02*X017924Y011306D01*X017952Y011594D02*X018113Y011527D01*X018287Y011527D01*X018448Y011594D01*X018571Y011717D01*X018637Y011877D01*X018637Y012051D01*X018571Y012212D01*X018448Y012335D01*X018287Y012401D01*X018113Y012401D01*X017952Y012335D01*X017829Y012212D01*X017763Y012051D01*X017763Y011877D01*X017829Y011717D01*X017952Y011594D01*X017923Y011623D02*X017607Y011623D01*X017637Y011465D02*X022320Y011465D01*X022320Y011623D02*X020956Y011623D01*X020847Y011594D02*X021132Y011671D01*X021388Y011818D01*X021596Y012027D01*X021744Y012282D01*X021820Y012567D01*X021820Y012862D01*X021744Y013147D01*X021596Y013402D01*X021388Y013611D01*X021132Y013758D01*X020847Y013834D01*X020553Y013834D01*X020268Y013758D01*X020012Y013611D01*X019804Y013402D01*X019656Y013147D01*X019580Y012862D01*X019580Y012567D01*X019656Y012282D01*X019804Y012027D01*X020012Y011818D01*X020268Y011671D01*X020553Y011594D01*X020847Y011594D01*X020444Y011623D02*X018477Y011623D01*X018598Y011782D02*X020075Y011782D01*X019890Y011940D02*X018637Y011940D01*X018617Y012099D02*X019762Y012099D01*X019671Y012257D02*X018525Y012257D01*X017875Y012257D02*X014745Y012257D01*X014745Y012099D02*X017783Y012099D01*X017763Y011940D02*X014742Y011940D01*X014327Y011940D02*X014173Y011940D01*X014173Y012099D02*X014327Y012099D01*X014327Y012257D02*X014173Y012257D01*X014173Y012416D02*X014327Y012416D01*X014327Y012574D02*X014173Y012574D01*X014327Y012733D02*X019580Y012733D01*X019588Y012891D02*X014745Y012891D01*X014745Y013050D02*X019630Y013050D01*X019692Y013208D02*X014745Y013208D01*X014745Y013367D02*X019783Y013367D01*X019927Y013525D02*X014745Y013525D01*X014607Y013684D02*X020139Y013684D01*X021261Y013684D02*X022320Y013684D01*X022320Y013842D02*X010475Y013842D01*X010475Y014001D02*X022320Y014001D01*X022320Y014159D02*X010475Y014159D01*X010475Y014318D02*X022320Y014318D01*X022320Y014476D02*X021308Y014476D01*X021647Y014635D02*X022320Y014635D01*X022320Y014793D02*X021887Y014793D01*X021847Y014751D02*X021847Y014751D01*X022034Y014952D02*X022320Y014952D01*X022320Y015110D02*X022181Y015110D01*X022261Y015269D02*X022320Y015269D01*X020299Y014476D02*X009330Y014476D01*X009330Y014318D02*X009525Y014318D01*X009525Y014159D02*X009330Y014159D01*X009409Y014001D02*X009525Y014001D01*X008935Y013684D02*X006858Y013684D01*X006835Y013842D02*X008797Y013842D01*X008770Y014001D02*X006720Y014001D01*X006496Y014159D02*X008770Y014159D01*X008770Y014318D02*X006540Y014318D01*X006540Y014476D02*X008770Y014476D01*X008770Y014635D02*X006540Y014635D01*X006540Y014793D02*X008770Y014793D01*X008770Y014952D02*X006514Y014952D01*X006385Y015110D02*X008770Y015110D01*X008770Y015269D02*X006544Y015269D01*X006590Y015427D02*X008770Y015427D01*X008770Y015586D02*X006590Y015586D01*X006590Y015744D02*X008770Y015744D01*X008770Y015903D02*X006590Y015903D01*X006590Y016061D02*X008770Y016061D01*X008770Y016220D02*X007479Y016220D01*X007221Y016220D02*X006590Y016220D01*X006715Y016378D02*X006985Y016378D01*X006905Y016537D02*X006795Y016537D01*X006810Y016695D02*X006890Y016695D01*X006890Y016854D02*X006810Y016854D01*X006810Y017012D02*X006890Y017012D01*X006890Y017171D02*X006810Y017171D01*X006810Y017329D02*X006890Y017329D01*X006945Y017488D02*X006755Y017488D01*X006350Y016964D02*X006350Y015414D01*X006100Y015164D01*X006100Y014588D01*X006124Y014564D01*X006000Y014490D01*X006000Y013024D01*X005500Y013024D02*X005500Y014440D01*X005376Y014564D01*X005350Y014590D01*X005350Y016964D01*X004890Y017012D02*X003687Y017012D01*X003688Y017011D02*X003688Y017011D01*X003764Y016854D02*X004890Y016854D01*X004890Y016695D02*X003840Y016695D01*X003905Y016560D02*X003905Y016560D01*X003909Y016537D02*X004905Y016537D01*X004985Y016378D02*X003933Y016378D01*X003957Y016220D02*X005110Y016220D01*X005110Y016061D02*X003980Y016061D01*X003980Y016064D02*X003980Y016064D01*X003956Y015903D02*X005110Y015903D01*X005110Y015744D02*X003932Y015744D01*X003908Y015586D02*X005110Y015586D01*X005110Y015427D02*X003837Y015427D01*X003761Y015269D02*X005110Y015269D01*X005110Y015110D02*X003681Y015110D01*X003688Y015118D02*X003688Y015118D01*X003534Y014952D02*X004986Y014952D01*X004960Y014793D02*X003387Y014793D01*X003347Y014751D02*X003347Y014751D01*X003147Y014635D02*X004960Y014635D01*X004960Y014476D02*X002808Y014476D01*X002914Y014500D02*X002914Y014500D01*X002426Y014389D02*X002426Y014389D01*X001926Y014426D02*X001926Y014426D01*X001799Y014476D02*X000780Y014476D01*X000780Y014318D02*X004960Y014318D01*X005004Y014159D02*X000780Y014159D01*X000780Y014001D02*X005260Y014001D01*X005260Y013842D02*X000780Y013842D01*X000780Y013684D02*X005260Y013684D01*X005000Y013604D02*X005000Y013024D01*X005000Y013604D01*X005000Y013525D02*X005000Y013525D01*X005000Y013367D02*X005000Y013367D01*X005000Y013208D02*X005000Y013208D01*X005000Y013050D02*X005000Y013050D01*X005000Y013024D02*X005000Y013024D01*X005000Y012891D02*X005000Y012891D01*X005000Y012733D02*X005000Y012733D01*X005000Y012574D02*X005000Y012574D01*X003675Y013050D02*X000780Y013050D01*X000780Y013208D02*X003675Y013208D01*X001460Y014609D02*X001460Y014609D01*X001428Y014635D02*X000780Y014635D01*X000780Y014793D02*X001229Y014793D01*X001048Y014952D02*X000780Y014952D01*X000780Y015110D02*X000940Y015110D01*X000832Y015269D02*X000780Y015269D01*X000786Y015335D02*X000786Y015335D01*X003347Y017378D02*X003347Y017378D01*X003392Y017329D02*X004890Y017329D01*X004890Y017171D02*X003539Y017171D01*X003157Y017488D02*X004945Y017488D01*X007755Y017488D02*X008978Y017488D01*X008819Y017329D02*X007810Y017329D01*X007810Y017171D02*X008770Y017171D01*X008770Y017012D02*X007810Y017012D01*X007810Y016854D02*X008770Y016854D01*X008770Y016695D02*X007810Y016695D01*X007795Y016537D02*X008770Y016537D01*X008770Y016378D02*X007715Y016378D01*X009330Y016378D02*X009525Y016378D01*X009525Y016220D02*X009330Y016220D01*X009330Y016061D02*X009525Y016061D01*X009525Y015903D02*X009330Y015903D01*X009330Y015744D02*X009525Y015744D01*X009525Y015586D02*X009330Y015586D01*X009330Y015427D02*X009525Y015427D01*X009676Y015269D02*X009330Y015269D01*X009330Y015110D02*X009642Y015110D01*X009680Y014952D02*X009330Y014952D01*X009330Y014793D02*X009839Y014793D01*X010161Y014793D02*X013933Y014793D01*X013946Y014761D02*X014047Y014661D01*X014179Y014606D01*X014321Y014606D01*X014453Y014661D01*X014554Y014761D01*X014608Y014893D01*X014608Y015036D01*X014557Y015160D01*X014631Y015160D01*X014725Y015254D01*X014725Y016922D01*X014631Y017015D01*X013869Y017015D01*X013775Y016922D01*X013775Y015254D01*X013869Y015160D01*X013943Y015160D01*X013892Y015036D01*X013892Y014893D01*X013946Y014761D01*X013892Y014952D02*X010320Y014952D01*X010358Y015110D02*X013923Y015110D01*X013775Y015269D02*X010324Y015269D01*X010475Y015427D02*X013775Y015427D01*X013775Y015586D02*X010475Y015586D01*X010475Y015744D02*X013775Y015744D01*X013775Y015903D02*X010475Y015903D01*X010475Y016061D02*X013775Y016061D01*X013775Y016220D02*X010494Y016220D01*X010475Y016378D02*X013775Y016378D01*X013775Y016537D02*X010475Y016537D01*X010475Y016695D02*X013775Y016695D01*X013775Y016854D02*X010475Y016854D01*X010475Y017012D02*X013866Y017012D01*X014634Y017012D02*X016406Y017012D01*X016564Y016854D02*X014725Y016854D01*X014725Y016695D02*X016723Y016695D01*X016890Y016537D02*X014725Y016537D01*X014725Y016378D02*X016908Y016378D01*X016994Y016220D02*X014725Y016220D01*X014725Y016061D02*X017242Y016061D01*X017458Y016061D02*X018242Y016061D01*X018258Y016054D02*X018441Y016054D01*X018611Y016124D01*X018740Y016254D01*X018810Y016423D01*X018810Y017206D01*X018740Y017375D01*X018611Y017504D01*X018441Y017574D01*X018258Y017574D01*X018089Y017504D01*X017960Y017375D01*X017890Y017206D01*X017890Y016423D01*X017960Y016254D01*X018089Y016124D01*X018258Y016054D01*X018458Y016061D02*X019139Y016061D01*X019139Y015903D02*X014725Y015903D01*X014725Y015744D02*X019160Y015744D01*X019209Y015586D02*X014725Y015586D01*X014725Y015427D02*X019258Y015427D01*X019332Y015269D02*X014725Y015269D01*X014577Y015110D02*X019440Y015110D01*X019548Y014952D02*X014608Y014952D01*X014567Y014793D02*X019729Y014793D01*X019928Y014635D02*X014390Y014635D01*X014110Y014635D02*X009330Y014635D01*X010000Y015114D02*X010000Y016262D01*X010250Y016214D01*X009525Y016537D02*X009330Y016537D01*X009330Y016695D02*X009525Y016695D01*X009525Y016854D02*X009330Y016854D01*X009330Y017012D02*X009525Y017012D01*X006280Y014001D02*X006240Y014001D01*X006500Y013714D02*X006500Y013024D01*X006790Y013050D02*X009525Y013050D01*X009525Y013208D02*X006790Y013208D01*X006790Y013367D02*X009252Y013367D01*X009093Y013525D02*X006809Y013525D01*X006790Y012891D02*X009525Y012891D01*X009525Y012733D02*X006790Y012733D01*X006790Y012574D02*X009564Y012574D01*X010475Y012891D02*X011417Y012891D01*X011310Y013050D02*X010475Y013050D01*X012630Y011454D02*X013290Y011454D01*X013300Y011464D01*X013622Y011623D02*X016793Y011623D01*X016763Y011465D02*X015064Y011465D01*X014330Y011465D02*X014170Y011465D01*X014170Y011306D02*X014330Y011306D01*X014330Y011148D02*X014170Y011148D01*X014170Y010672D02*X014330Y010672D01*X014330Y010514D02*X014170Y010514D01*X013350Y010114D02*X012630Y010114D01*X013469Y011782D02*X016899Y011782D01*X017501Y011782D02*X017802Y011782D01*X018476Y011306D02*X022320Y011306D01*X022320Y011148D02*X018597Y011148D01*X018637Y010989D02*X022320Y010989D01*X022320Y010831D02*X018673Y010831D01*X018831Y010672D02*X022320Y010672D01*X022320Y010514D02*X018939Y010514D01*X018940Y010355D02*X022320Y010355D01*X022320Y010197D02*X018940Y010197D01*X018940Y010038D02*X022320Y010038D01*X022320Y009880D02*X018940Y009880D01*X018940Y009721D02*X020204Y009721D01*X020268Y009758D02*X020012Y009611D01*X019804Y009402D01*X019656Y009147D01*X019580Y008862D01*X019580Y008567D01*X019656Y008282D01*X019804Y008027D01*X020012Y007818D01*X020268Y007671D01*X020553Y007594D01*X020847Y007594D01*X021132Y007671D01*X021388Y007818D01*X021596Y008027D01*X021744Y008282D01*X021820Y008567D01*X021820Y008862D01*X021744Y009147D01*X021596Y009402D01*X021388Y009611D01*X021132Y009758D01*X020847Y009834D01*X020553Y009834D01*X020268Y009758D01*X019965Y009563D02*X018940Y009563D01*X018940Y009404D02*X019806Y009404D01*X019714Y009246D02*X018940Y009246D01*X018940Y009087D02*X019640Y009087D01*X019598Y008929D02*X018940Y008929D01*X018940Y008770D02*X019580Y008770D01*X019580Y008612D02*X018940Y008612D01*X018940Y008453D02*X019610Y008453D01*X019653Y008295D02*X018940Y008295D01*X018940Y008136D02*X019740Y008136D01*X019853Y007978D02*X018940Y007978D01*X018940Y007819D02*X020011Y007819D01*X020304Y007661D02*X018940Y007661D01*X018940Y007502D02*X022320Y007502D01*X022320Y007344D02*X018931Y007344D01*X018810Y007185D02*X022320Y007185D01*X022320Y007027D02*X018652Y007027D01*X018493Y006868D02*X022320Y006868D01*X022320Y006710D02*X021056Y006710D01*X021547Y006551D02*X022320Y006551D01*X022320Y006393D02*X021821Y006393D01*X021981Y006234D02*X022320Y006234D01*X022320Y006076D02*X022128Y006076D01*X022233Y005917D02*X022320Y005917D01*X022309Y005759D02*X022320Y005759D01*X020528Y006710D02*X018335Y006710D01*X018176Y006551D02*X020042Y006551D01*X019801Y006393D02*X018018Y006393D01*X017859Y006234D02*X019603Y006234D01*X019479Y006076D02*X017701Y006076D01*X017542Y005917D02*X019371Y005917D01*X019276Y005759D02*X017384Y005759D01*X017225Y005600D02*X019227Y005600D01*X019178Y005442D02*X017067Y005442D01*X016908Y005283D02*X019139Y005283D01*X019139Y005125D02*X016738Y005125D01*X016670Y005096D02*X014732Y005096D01*X014732Y003656D01*X014639Y003562D01*X013916Y003562D01*X013822Y003656D01*X013822Y006632D01*X013774Y006632D01*X013703Y006561D01*X013571Y006506D01*X013429Y006506D01*X013297Y006561D01*X013196Y006661D01*X013142Y006793D01*X013142Y006936D01*X013196Y007067D01*X013297Y007168D01*X013429Y007222D01*X013571Y007222D01*X013703Y007168D01*X013759Y007112D01*X013802Y007112D01*X013802Y007128D01*X014277Y007128D01*X014277Y007386D01*X013958Y007386D01*X013912Y007374D01*X013871Y007350D01*X013838Y007317D01*X013814Y007276D01*X013802Y007230D01*X013802Y007128D01*X014277Y007128D01*X014277Y007128D01*X014277Y007128D01*X014277Y007386D01*X014592Y007386D01*X014594Y007388D01*X014635Y007412D01*X014681Y007424D01*X014952Y007424D01*X014952Y007036D01*X015048Y007036D01*X015475Y007036D01*X015475Y007268D01*X015463Y007314D01*X015439Y007355D01*X015406Y007388D01*X015365Y007412D01*X015319Y007424D01*X015048Y007424D01*X015048Y007036D01*X015048Y006940D01*X015475Y006940D01*X015475Y006709D01*X015463Y006663D01*X015439Y006622D01*X015418Y006600D01*X015449Y006569D01*X015579Y006622D01*X015721Y006622D01*X015853Y006568D01*X015954Y006467D01*X016008Y006336D01*X016008Y006193D01*X015954Y006061D01*X015853Y005961D01*X015721Y005906D01*X015579Y005906D01*X015455Y005957D01*X015455Y005918D01*X015369Y005832D01*X016379Y005832D01*X017460Y006914D01*X017460Y009106D01*X017448Y009094D01*X017440Y009091D01*X017440Y008767D01*X017403Y008678D01*X017336Y008611D01*X016886Y008161D01*X016798Y008124D01*X015840Y008124D01*X015840Y008003D01*X015746Y007909D01*X015664Y007909D01*X015664Y007791D01*X015627Y007702D01*X015453Y007528D01*X015453Y007528D01*X015386Y007461D01*X015298Y007424D01*X013299Y007424D01*X012799Y006924D01*X012711Y006888D01*X011878Y006888D01*X011878Y005599D01*X011897Y005618D01*X012029Y005672D01*X012171Y005672D01*X012303Y005618D01*X012404Y005517D01*X012458Y005386D01*X012458Y005243D01*X012404Y005111D01*X012303Y005011D01*X012171Y004956D01*X012029Y004956D01*X011897Y005011D01*X011878Y005030D01*X011878Y004218D01*X011886Y004205D01*X011898Y004159D01*X011898Y004057D01*X011423Y004057D01*X011423Y004057D01*X011898Y004057D01*X011898Y003954D01*X011886Y003909D01*X011878Y003895D01*X011878Y003656D01*X011784Y003562D01*X011061Y003562D01*X011014Y003610D01*X010999Y003601D01*X010954Y003589D01*X010722Y003589D01*X010722Y004016D01*X010626Y004016D01*X010626Y003589D01*X010394Y003589D01*X010349Y003601D01*X010308Y003625D01*X010286Y003647D01*X010248Y003609D01*X009604Y003609D01*X009510Y003703D01*X009510Y003818D01*X009453Y003761D01*X009321Y003706D01*X009179Y003706D01*X009053Y003758D01*X009053Y003698D02*X009515Y003698D01*X009250Y004064D02*X009926Y004064D01*X010286Y004482D02*X010254Y004514D01*X010265Y004517D01*X010306Y004540D01*X010339Y004574D01*X010363Y004615D01*X010375Y004661D01*X010375Y004892D01*X009948Y004892D01*X009948Y004988D01*X010375Y004988D01*X010375Y005220D01*X010363Y005266D01*X010339Y005307D01*X010318Y005328D01*X010355Y005366D01*X010355Y005608D01*X010968Y005608D01*X010968Y005481D01*X010968Y004536D01*X010954Y004540D01*X010722Y004540D01*X010722Y004112D01*X010948Y004112D01*X010948Y004057D01*X011423Y004057D01*X011406Y004040D01*X010674Y004064D01*X010722Y004016D02*X010722Y004112D01*X010626Y004112D01*X010626Y004540D01*X010394Y004540D01*X010349Y004527D01*X010308Y004504D01*X010286Y004482D01*X010277Y004491D02*X010295Y004491D01*X010372Y004649D02*X010968Y004649D01*X010968Y004808D02*X010375Y004808D01*X010375Y005125D02*X010968Y005125D01*X010968Y005283D02*X010353Y005283D01*X010355Y005442D02*X010968Y005442D01*X010968Y005600D02*X010355Y005600D01*X010060Y005848D02*X009900Y005688D01*X009324Y005688D01*X009200Y005564D01*X009200Y005064D01*X009000Y004864D01*X008696Y004864D01*X009108Y004649D02*X009428Y004649D01*X009425Y004808D02*X009283Y004808D01*X009419Y004966D02*X009852Y004966D01*X009948Y004966D02*X010968Y004966D01*X011423Y005336D02*X011445Y005314D01*X012100Y005314D01*X011880Y005600D02*X011878Y005600D01*X011878Y005759D02*X013822Y005759D01*X013822Y005917D02*X011878Y005917D01*X011878Y006076D02*X013822Y006076D01*X013822Y006234D02*X011878Y006234D01*X011878Y006393D02*X013822Y006393D01*X013822Y006551D02*X013680Y006551D01*X013320Y006551D02*X011878Y006551D01*X011878Y006710D02*X013176Y006710D01*X013142Y006868D02*X011878Y006868D01*X012902Y007027D02*X013180Y007027D01*X013060Y007185D02*X013339Y007185D01*X013219Y007344D02*X013865Y007344D01*X013802Y007185D02*X013661Y007185D01*X013507Y006872D02*X013500Y006864D01*X013507Y006872D02*X014277Y006872D01*X014277Y007128D02*X014861Y007128D01*X015000Y006988D01*X015048Y007027D02*X017460Y007027D01*X017460Y007185D02*X015475Y007185D01*X015446Y007344D02*X017460Y007344D01*X017460Y007502D02*X015427Y007502D01*X015586Y007661D02*X017460Y007661D01*X017460Y007819D02*X015664Y007819D01*X015815Y007978D02*X017460Y007978D01*X017460Y008136D02*X016827Y008136D01*X017020Y008295D02*X017460Y008295D01*X017460Y008453D02*X017178Y008453D01*X017337Y008612D02*X017460Y008612D01*X017460Y008770D02*X017440Y008770D01*X017440Y008929D02*X017460Y008929D01*X017460Y009087D02*X017440Y009087D01*X016960Y009087D02*X015079Y009087D01*X015002Y008929D02*X016960Y008929D01*X016817Y008770D02*X015795Y008770D01*X015840Y008612D02*X016658Y008612D01*X018191Y009563D02*X018209Y009563D01*X018209Y009721D02*X018191Y009721D01*X018191Y009880D02*X018209Y009880D01*X018209Y009973D02*X018191Y009973D01*X018191Y010421D01*X018164Y010421D01*X018093Y010410D01*X018025Y010388D01*X017960Y010355D01*X017940Y010341D01*X017940Y010606D01*X017952Y010594D01*X018113Y010527D01*X018287Y010527D01*X018295Y010530D01*X018460Y010365D01*X018460Y010341D01*X018440Y010355D01*X018375Y010388D01*X018307Y010410D01*X018236Y010421D01*X018209Y010421D01*X018209Y009973D01*X018209Y010038D02*X018191Y010038D01*X018191Y010197D02*X018209Y010197D01*X018209Y010355D02*X018191Y010355D01*X018311Y010514D02*X017940Y010514D01*X017940Y010355D02*X017960Y010355D01*X018440Y010355D02*X018460Y010355D01*X018700Y010464D02*X018200Y010964D01*X018700Y010464D02*X018700Y007414D01*X016622Y005336D01*X014277Y005336D01*X014277Y005592D02*X016478Y005592D01*X017700Y006814D01*X017415Y006868D02*X015475Y006868D01*X015475Y006710D02*X017256Y006710D01*X017098Y006551D02*X015869Y006551D01*X015984Y006393D02*X016939Y006393D01*X016781Y006234D02*X016008Y006234D01*X015960Y006076D02*X016622Y006076D01*X016464Y005917D02*X015748Y005917D01*X015552Y005917D02*X015454Y005917D01*X015650Y006264D02*X015024Y006264D01*X015000Y006240D01*X014952Y007185D02*X015048Y007185D01*X015048Y007344D02*X014952Y007344D01*X014277Y007344D02*X014277Y007344D01*X014277Y007185D02*X014277Y007185D01*X014265Y007978D02*X011559Y007978D01*X011559Y008136D02*X014240Y008136D01*X014628Y008453D02*X014724Y008453D01*X014724Y008612D02*X014628Y008612D01*X014628Y008770D02*X014724Y008770D01*X018419Y009563D02*X018460Y009563D01*X021196Y009721D02*X022320Y009721D01*X022320Y009563D02*X021435Y009563D01*X021594Y009404D02*X022320Y009404D01*X022320Y009246D02*X021686Y009246D01*X021760Y009087D02*X022320Y009087D01*X022320Y008929D02*X021802Y008929D01*X021820Y008770D02*X022320Y008770D01*X022320Y008612D02*X021820Y008612D01*X021790Y008453D02*X022320Y008453D01*X022320Y008295D02*X021747Y008295D01*X021660Y008136D02*X022320Y008136D01*X022320Y007978D02*X021547Y007978D01*X021389Y007819D02*X022320Y007819D01*X022320Y007661D02*X021096Y007661D01*X019139Y004966D02*X018618Y004966D01*X018710Y004808D02*X019141Y004808D01*X019190Y004649D02*X018710Y004649D01*X017201Y004966D02*X014732Y004966D01*X014732Y004808D02*X014987Y004808D01*X013822Y004808D02*X011878Y004808D01*X011878Y004966D02*X012004Y004966D01*X012196Y004966D02*X013822Y004966D01*X013822Y005125D02*X012409Y005125D01*X012458Y005283D02*X013822Y005283D01*X013822Y005442D02*X012435Y005442D01*X012320Y005600D02*X013822Y005600D01*X013822Y004649D02*X011878Y004649D01*X011878Y004491D02*X013822Y004491D01*X013822Y004332D02*X011878Y004332D01*X011894Y004174D02*X013822Y004174D01*X013822Y004015D02*X011898Y004015D01*X011878Y003857D02*X013822Y003857D01*X013822Y003698D02*X011878Y003698D01*X011423Y004057D02*X010948Y004057D01*X010948Y004016D01*X010722Y004016D01*X010722Y004015D02*X010626Y004015D01*X010626Y003857D02*X010722Y003857D01*X010722Y003698D02*X010626Y003698D01*X010626Y004174D02*X010722Y004174D01*X010722Y004332D02*X010626Y004332D01*X010626Y004491D02*X010722Y004491D01*X011423Y004057D02*X011423Y004057D01*X011423Y005848D02*X010060Y005848D01*X009890Y005848D02*X009900Y005688D01*X009510Y006076D02*X009053Y006076D01*X009053Y005917D02*X009250Y005917D01*X009055Y005759D02*X009053Y005759D01*X009000Y006234D02*X010191Y006234D01*X010032Y006393D02*X004790Y006393D01*X004566Y005759D02*X004540Y005759D01*X004300Y005314D02*X004300Y008064D01*X003800Y008564D01*X004300Y005314D02*X004700Y004914D01*X004954Y004914D01*X005004Y004864D01*X002964Y003550D02*X002964Y003550D01*X008678Y006551D02*X008715Y006551D01*X008715Y006484D02*X008917Y006484D01*X008963Y006497D01*X009004Y006520D01*X009037Y006554D01*X009061Y006595D01*X009073Y006641D01*X009073Y006896D01*X008715Y006896D01*X008715Y006484D01*X008715Y006710D02*X008678Y006710D01*X008678Y006868D02*X008715Y006868D01*X009073Y006868D02*X009557Y006868D01*X009715Y006710D02*X009073Y006710D01*X009035Y006551D02*X009874Y006551D01*X009398Y007027D02*X009073Y007027D01*X014745Y012416D02*X019620Y012416D01*X019580Y012574D02*X014745Y012574D01*X014250Y014964D02*X014250Y016088D01*X016722Y017488D02*X017073Y017488D01*X016941Y017329D02*X016881Y017329D01*X017627Y017488D02*X018073Y017488D01*X017941Y017329D02*X017759Y017329D01*X017810Y017171D02*X017890Y017171D01*X017890Y017012D02*X017810Y017012D01*X017810Y016854D02*X017890Y016854D01*X017890Y016695D02*X017810Y016695D01*X017810Y016537D02*X017890Y016537D01*X017908Y016378D02*X017792Y016378D01*X017706Y016220D02*X017994Y016220D01*X018706Y016220D02*X019139Y016220D01*X019158Y016378D02*X018792Y016378D01*X018810Y016537D02*X019207Y016537D01*X019256Y016695D02*X018810Y016695D01*X018810Y016854D02*X019328Y016854D01*X019436Y017012D02*X018810Y017012D01*X018810Y017171D02*X019544Y017171D01*X019722Y017329D02*X018759Y017329D01*X018627Y017488D02*X019921Y017488D01*X021473Y013525D02*X022320Y013525D01*X022320Y013367D02*X021617Y013367D01*X021708Y013208D02*X022320Y013208D01*X022320Y013050D02*X021770Y013050D01*X021812Y012891D02*X022320Y012891D01*X022320Y012733D02*X021820Y012733D01*X021820Y012574D02*X022320Y012574D01*X022320Y012416D02*X021780Y012416D01*X021729Y012257D02*X022320Y012257D01*X022320Y012099D02*X021638Y012099D01*X021510Y011940D02*X022320Y011940D01*X022320Y011782D02*X021325Y011782D01*X017110Y004808D02*X017010Y004808D01*X016972Y004174D02*X017110Y004174D01*X016255Y004174D02*X016145Y004174D01*X016183Y004332D02*X016217Y004332D01*X000856Y012257D02*X000780Y012257D01*X000780Y012891D02*X000876Y012891D01*D26*X004150Y011564D03*X006500Y013714D03*X010000Y015114D03*X011650Y013164D03*X013300Y011464D03*X013350Y010114D03*X013550Y008764D03*X013500Y006864D03*X012100Y005314D03*X009250Y004064D03*X015200Y004514D03*X015650Y006264D03*X015850Y009914D03*X014250Y014964D03*D27*X011650Y013164D02*X011348Y013467D01*X010000Y013467D01*X009952Y013514D01*X009500Y013514D01*X009050Y013964D01*X009050Y017164D01*X009300Y017414D01*X016400Y017414D01*X017000Y016814D01*X017350Y016814D01*X014250Y010982D02*X014052Y010784D01*X012630Y010784D01*X012632Y009447D02*X012630Y009444D01*X012632Y009447D02*X014250Y009447D01*X013550Y008764D02*X012640Y008764D01*X012630Y008774D01*M02*
\ No newline at end of file diff --git a/gerber/tests/test_am_statements.py b/gerber/tests/test_am_statements.py new file mode 100644 index 0000000..98a7332 --- /dev/null +++ b/gerber/tests/test_am_statements.py @@ -0,0 +1,383 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# Author: Hamilton Kibbe <ham@hamiltonkib.be> + +from .tests import * +from ..am_statements import * +from ..am_statements import inch, metric + + +def test_AMPrimitive_ctor(): + for exposure in ('on', 'off', 'ON', 'OFF'): + for code in (0, 1, 2, 4, 5, 6, 7, 20, 21, 22): + p = AMPrimitive(code, exposure) + assert_equal(p.code, code) + assert_equal(p.exposure, exposure.lower()) + + +def test_AMPrimitive_validation(): + assert_raises(TypeError, AMPrimitive, '1', 'off') + assert_raises(ValueError, AMPrimitive, 0, 'exposed') + assert_raises(ValueError, AMPrimitive, 3, 'off') + + +def test_AMPrimitive_conversion(): + p = AMPrimitive(4, 'on') + assert_raises(NotImplementedError, p.to_inch) + assert_raises(NotImplementedError, p.to_metric) + + +def test_AMCommentPrimitive_ctor(): + c = AMCommentPrimitive(0, ' This is a comment *') + assert_equal(c.code, 0) + assert_equal(c.comment, 'This is a comment') + + +def test_AMCommentPrimitive_validation(): + assert_raises(ValueError, AMCommentPrimitive, 1, 'This is a comment') + + +def test_AMCommentPrimitive_factory(): + c = AMCommentPrimitive.from_gerber('0 Rectangle with rounded corners. *') + assert_equal(c.code, 0) + assert_equal(c.comment, 'Rectangle with rounded corners.') + + +def test_AMCommentPrimitive_dump(): + c = AMCommentPrimitive(0, 'Rectangle with rounded corners.') + assert_equal(c.to_gerber(), '0 Rectangle with rounded corners. *') + + +def test_AMCommentPrimitive_conversion(): + c = AMCommentPrimitive(0, 'Rectangle with rounded corners.') + ci = c + cm = c + ci.to_inch() + cm.to_metric() + assert_equal(c, ci) + assert_equal(c, cm) + + +def test_AMCommentPrimitive_string(): + c = AMCommentPrimitive(0, 'Test Comment') + assert_equal(str(c), '<Aperture Macro Comment: Test Comment>') + + +def test_AMCirclePrimitive_ctor(): + test_cases = ((1, 'on', 0, (0, 0)), + (1, 'off', 1, (0, 1)), + (1, 'on', 2.5, (0, 2)), + (1, 'off', 5.0, (3, 3))) + for code, exposure, diameter, position in test_cases: + c = AMCirclePrimitive(code, exposure, diameter, position) + assert_equal(c.code, code) + assert_equal(c.exposure, exposure) + assert_equal(c.diameter, diameter) + assert_equal(c.position, position) + + +def test_AMCirclePrimitive_validation(): + assert_raises(ValueError, AMCirclePrimitive, 2, 'on', 0, (0, 0)) + + +def test_AMCirclePrimitive_factory(): + c = AMCirclePrimitive.from_gerber('1,0,5,0,0*') + assert_equal(c.code, 1) + assert_equal(c.exposure, 'off') + assert_equal(c.diameter, 5) + assert_equal(c.position, (0, 0)) + + +def test_AMCirclePrimitive_dump(): + c = AMCirclePrimitive(1, 'off', 5, (0, 0)) + assert_equal(c.to_gerber(), '1,0,5,0,0*') + c = AMCirclePrimitive(1, 'on', 5, (0, 0)) + assert_equal(c.to_gerber(), '1,1,5,0,0*') + + +def test_AMCirclePrimitive_conversion(): + c = AMCirclePrimitive(1, 'off', 25.4, (25.4, 0)) + c.to_inch() + assert_equal(c.diameter, 1) + assert_equal(c.position, (1, 0)) + + c = AMCirclePrimitive(1, 'off', 1, (1, 0)) + c.to_metric() + assert_equal(c.diameter, 25.4) + assert_equal(c.position, (25.4, 0)) + + +def test_AMVectorLinePrimitive_validation(): + assert_raises(ValueError, AMVectorLinePrimitive, + 3, 'on', 0.1, (0, 0), (3.3, 5.4), 0) + + +def test_AMVectorLinePrimitive_factory(): + l = AMVectorLinePrimitive.from_gerber('20,1,0.9,0,0.45,12,0.45,0*') + assert_equal(l.code, 20) + assert_equal(l.exposure, 'on') + assert_equal(l.width, 0.9) + assert_equal(l.start, (0, 0.45)) + assert_equal(l.end, (12, 0.45)) + assert_equal(l.rotation, 0) + + +def test_AMVectorLinePrimitive_dump(): + l = AMVectorLinePrimitive.from_gerber('20,1,0.9,0,0.45,12,0.45,0*') + assert_equal(l.to_gerber(), '20,1,0.9,0.0,0.45,12.0,0.45,0.0*') + + +def test_AMVectorLinePrimtive_conversion(): + l = AMVectorLinePrimitive(20, 'on', 25.4, (0, 0), (25.4, 25.4), 0) + l.to_inch() + assert_equal(l.width, 1) + assert_equal(l.start, (0, 0)) + assert_equal(l.end, (1, 1)) + + l = AMVectorLinePrimitive(20, 'on', 1, (0, 0), (1, 1), 0) + l.to_metric() + assert_equal(l.width, 25.4) + assert_equal(l.start, (0, 0)) + assert_equal(l.end, (25.4, 25.4)) + + +def test_AMOutlinePrimitive_validation(): + assert_raises(ValueError, AMOutlinePrimitive, 7, 'on', + (0, 0), [(3.3, 5.4), (4.0, 5.4), (0, 0)], 0) + assert_raises(ValueError, AMOutlinePrimitive, 4, 'on', + (0, 0), [(3.3, 5.4), (4.0, 5.4), (0, 1)], 0) + + +def test_AMOutlinePrimitive_factory(): + o = AMOutlinePrimitive.from_gerber('4,1,3,0,0,3,3,3,0,0,0,0*') + assert_equal(o.code, 4) + assert_equal(o.exposure, 'on') + assert_equal(o.start_point, (0, 0)) + assert_equal(o.points, [(3, 3), (3, 0), (0, 0)]) + assert_equal(o.rotation, 0) + + +def test_AMOUtlinePrimitive_dump(): + o = AMOutlinePrimitive(4, 'on', (0, 0), [(3, 3), (3, 0), (0, 0)], 0) + # New lines don't matter for Gerber, but we insert them to make it easier to remove + # For test purposes we can ignore them + assert_equal(o.to_gerber().replace('\n', ''), '4,1,3,0,0,3,3,3,0,0,0,0*') + + + +def test_AMOutlinePrimitive_conversion(): + o = AMOutlinePrimitive( + 4, 'on', (0, 0), [(25.4, 25.4), (25.4, 0), (0, 0)], 0) + o.to_inch() + assert_equal(o.start_point, (0, 0)) + assert_equal(o.points, ((1., 1.), (1., 0.), (0., 0.))) + + o = AMOutlinePrimitive(4, 'on', (0, 0), [(1, 1), (1, 0), (0, 0)], 0) + o.to_metric() + assert_equal(o.start_point, (0, 0)) + assert_equal(o.points, ((25.4, 25.4), (25.4, 0), (0, 0))) + + +def test_AMPolygonPrimitive_validation(): + assert_raises(ValueError, AMPolygonPrimitive, 6, 'on', 3, (3.3, 5.4), 3, 0) + assert_raises(ValueError, AMPolygonPrimitive, 5, 'on', 2, (3.3, 5.4), 3, 0) + assert_raises(ValueError, AMPolygonPrimitive, 5, 'on', 13, (3.3, 5.4), 3, 0) + + +def test_AMPolygonPrimitive_factory(): + p = AMPolygonPrimitive.from_gerber('5,1,3,3.3,5.4,3,0') + assert_equal(p.code, 5) + assert_equal(p.exposure, 'on') + assert_equal(p.vertices, 3) + assert_equal(p.position, (3.3, 5.4)) + assert_equal(p.diameter, 3) + assert_equal(p.rotation, 0) + + +def test_AMPolygonPrimitive_dump(): + p = AMPolygonPrimitive(5, 'on', 3, (3.3, 5.4), 3, 0) + assert_equal(p.to_gerber(), '5,1,3,3.3,5.4,3,0*') + + +def test_AMPolygonPrimitive_conversion(): + p = AMPolygonPrimitive(5, 'off', 3, (25.4, 0), 25.4, 0) + p.to_inch() + assert_equal(p.diameter, 1) + assert_equal(p.position, (1, 0)) + + p = AMPolygonPrimitive(5, 'off', 3, (1, 0), 1, 0) + p.to_metric() + assert_equal(p.diameter, 25.4) + assert_equal(p.position, (25.4, 0)) + + +def test_AMMoirePrimitive_validation(): + assert_raises(ValueError, AMMoirePrimitive, 7, + (0, 0), 5.1, 0.2, 0.4, 6, 0.1, 6.1, 0) + + +def test_AMMoirePrimitive_factory(): + m = AMMoirePrimitive.from_gerber('6,0,0,5,0.5,0.5,2,0.1,6,0*') + assert_equal(m.code, 6) + assert_equal(m.position, (0, 0)) + assert_equal(m.diameter, 5) + assert_equal(m.ring_thickness, 0.5) + assert_equal(m.gap, 0.5) + assert_equal(m.max_rings, 2) + assert_equal(m.crosshair_thickness, 0.1) + assert_equal(m.crosshair_length, 6) + assert_equal(m.rotation, 0) + + +def test_AMMoirePrimitive_dump(): + m = AMMoirePrimitive.from_gerber('6,0,0,5,0.5,0.5,2,0.1,6,0*') + assert_equal(m.to_gerber(), '6,0,0,5.0,0.5,0.5,2,0.1,6.0,0.0*') + + +def test_AMMoirePrimitive_conversion(): + m = AMMoirePrimitive(6, (25.4, 25.4), 25.4, 25.4, 25.4, 6, 25.4, 25.4, 0) + m.to_inch() + assert_equal(m.position, (1., 1.)) + assert_equal(m.diameter, 1.) + assert_equal(m.ring_thickness, 1.) + assert_equal(m.gap, 1.) + assert_equal(m.crosshair_thickness, 1.) + assert_equal(m.crosshair_length, 1.) + + m = AMMoirePrimitive(6, (1, 1), 1, 1, 1, 6, 1, 1, 0) + m.to_metric() + assert_equal(m.position, (25.4, 25.4)) + assert_equal(m.diameter, 25.4) + assert_equal(m.ring_thickness, 25.4) + assert_equal(m.gap, 25.4) + assert_equal(m.crosshair_thickness, 25.4) + assert_equal(m.crosshair_length, 25.4) + + +def test_AMThermalPrimitive_validation(): + assert_raises(ValueError, AMThermalPrimitive, 8, (0.0, 0.0), 7, 5, 0.2, 0.0) + assert_raises(TypeError, AMThermalPrimitive, 7, (0.0, '0'), 7, 5, 0.2, 0.0) + + + +def test_AMThermalPrimitive_factory(): + t = AMThermalPrimitive.from_gerber('7,0,0,7,6,0.2,45*') + assert_equal(t.code, 7) + assert_equal(t.position, (0, 0)) + assert_equal(t.outer_diameter, 7) + assert_equal(t.inner_diameter, 6) + assert_equal(t.gap, 0.2) + assert_equal(t.rotation, 45) + + + +def test_AMThermalPrimitive_dump(): + t = AMThermalPrimitive.from_gerber('7,0,0,7,6,0.2,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() + assert_equal(t.position, (1., 1.)) + assert_equal(t.outer_diameter, 1.) + assert_equal(t.inner_diameter, 1.) + assert_equal(t.gap, 1.) + + t = AMThermalPrimitive(7, (1, 1), 1, 1, 1, 0) + t.to_metric() + assert_equal(t.position, (25.4, 25.4)) + assert_equal(t.outer_diameter, 25.4) + assert_equal(t.inner_diameter, 25.4) + assert_equal(t.gap, 25.4) + + +def test_AMCenterLinePrimitive_validation(): + assert_raises(ValueError, AMCenterLinePrimitive, + 22, 1, 0.2, 0.5, (0, 0), 0) + + +def test_AMCenterLinePrimtive_factory(): + l = AMCenterLinePrimitive.from_gerber('21,1,6.8,1.2,3.4,0.6,0*') + assert_equal(l.code, 21) + assert_equal(l.exposure, 'on') + assert_equal(l.width, 6.8) + assert_equal(l.height, 1.2) + assert_equal(l.center, (3.4, 0.6)) + assert_equal(l.rotation, 0) + + +def test_AMCenterLinePrimitive_dump(): + l = AMCenterLinePrimitive.from_gerber('21,1,6.8,1.2,3.4,0.6,0*') + assert_equal(l.to_gerber(), '21,1,6.8,1.2,3.4,0.6,0.0*') + + +def test_AMCenterLinePrimitive_conversion(): + l = AMCenterLinePrimitive(21, 'on', 25.4, 25.4, (25.4, 25.4), 0) + l.to_inch() + assert_equal(l.width, 1.) + assert_equal(l.height, 1.) + assert_equal(l.center, (1., 1.)) + + l = AMCenterLinePrimitive(21, 'on', 1, 1, (1, 1), 0) + l.to_metric() + assert_equal(l.width, 25.4) + assert_equal(l.height, 25.4) + assert_equal(l.center, (25.4, 25.4)) + + +def test_AMLowerLeftLinePrimitive_validation(): + assert_raises(ValueError, AMLowerLeftLinePrimitive, + 23, 1, 0.2, 0.5, (0, 0), 0) + + +def test_AMLowerLeftLinePrimtive_factory(): + l = AMLowerLeftLinePrimitive.from_gerber('22,1,6.8,1.2,3.4,0.6,0*') + assert_equal(l.code, 22) + assert_equal(l.exposure, 'on') + assert_equal(l.width, 6.8) + assert_equal(l.height, 1.2) + assert_equal(l.lower_left, (3.4, 0.6)) + assert_equal(l.rotation, 0) + + +def test_AMLowerLeftLinePrimitive_dump(): + l = AMLowerLeftLinePrimitive.from_gerber('22,1,6.8,1.2,3.4,0.6,0*') + assert_equal(l.to_gerber(), '22,1,6.8,1.2,3.4,0.6,0.0*') + + +def test_AMLowerLeftLinePrimitive_conversion(): + l = AMLowerLeftLinePrimitive(22, 'on', 25.4, 25.4, (25.4, 25.4), 0) + l.to_inch() + assert_equal(l.width, 1.) + assert_equal(l.height, 1.) + assert_equal(l.lower_left, (1., 1.)) + + l = AMLowerLeftLinePrimitive(22, 'on', 1, 1, (1, 1), 0) + l.to_metric() + assert_equal(l.width, 25.4) + assert_equal(l.height, 25.4) + assert_equal(l.lower_left, (25.4, 25.4)) + + +def test_AMUnsupportPrimitive(): + u = AMUnsupportPrimitive.from_gerber('Test') + assert_equal(u.primitive, 'Test') + u = AMUnsupportPrimitive('Test') + assert_equal(u.to_gerber(), 'Test') + + +def test_AMUnsupportPrimitive_smoketest(): + u = AMUnsupportPrimitive.from_gerber('Test') + u.to_inch() + u.to_metric() + + +def test_inch(): + assert_equal(inch(25.4), 1) + + +def test_metric(): + assert_equal(metric(1), 25.4) diff --git a/gerber/tests/test_cairo_backend.py b/gerber/tests/test_cairo_backend.py new file mode 100644 index 0000000..625a23e --- /dev/null +++ b/gerber/tests/test_cairo_backend.py @@ -0,0 +1,189 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# Author: Garret Fick <garret@ficksworkshop.com> +import io +import os + +from ..render.cairo_backend import GerberCairoContext +from ..rs274x import read +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') + + +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') + + +def test_holes_dont_clear(): + """Umaco example that an aperture with a hole does not clear the area""" + + _test_render('resources/example_holes_dont_clear.gbr', 'golden/example_holes_dont_clear.png') + + +def test_render_am_exposure_modifier(): + """Umaco example that an aperture macro with a hole does not clear the area""" + + _test_render('resources/example_am_exposure_modifier.gbr', 'golden/example_am_exposure_modifier.png') + + +def _resolve_path(path): + return os.path.join(os.path.dirname(__file__), + path) + + +def _test_render(gerber_path, png_expected_path, create_output_path = None): + """Render the gerber file and compare to the expected PNG output. + + Parameters + ---------- + gerber_path : string + Path to Gerber file to open + png_expected_path : string + Path to the PNG file to compare to + create_output : string|None + If not None, write the generated PNG to the specified path. + This is primarily to help with + """ + + gerber_path = _resolve_path(gerber_path) + png_expected_path = _resolve_path(png_expected_path) + if create_output_path: + create_output_path = _resolve_path(create_output_path) + + gerber = read(gerber_path) + + # Create PNG image to the memory stream + ctx = GerberCairoContext() + gerber.render(ctx) + + actual_bytes = ctx.dump(None) + + # If we want to write the file bytes, do it now. This happens + if create_output_path: + with open(create_output_path, 'wb') as out_file: + out_file.write(actual_bytes) + # Creating the output is dangerous - it could overwrite the expected result. + # So if we are creating the output, we make the test fail on purpose so you + # won't forget to disable this + assert_false(True, 'Test created the output %s. This needs to be disabled to make sure the test behaves correctly' % (create_output_path,)) + + # Read the expected PNG file + + with open(png_expected_path, 'rb') as expected_file: + expected_bytes = expected_file.read() + + # Don't directly use assert_equal otherwise any failure pollutes the test results + equal = (expected_bytes == actual_bytes) + assert_true(equal) + + return gerber diff --git a/gerber/tests/test_cam.py b/gerber/tests/test_cam.py index ce4ec44..a557e8c 100644 --- a/gerber/tests/test_cam.py +++ b/gerber/tests/test_cam.py @@ -4,7 +4,7 @@ # Author: Hamilton Kibbe <ham@hamiltonkib.be> from ..cam import CamFile, FileSettings -from tests import * +from .tests import * def test_filesettings_defaults(): @@ -54,15 +54,107 @@ def test_filesettings_dict_assign(): assert_equal(fs.zero_suppression, 'leading') assert_equal(fs.format, (1, 2)) + def test_camfile_init(): """ Smoke test CamFile test """ cf = CamFile() + def test_camfile_settings(): """ Test CamFile Default Settings """ cf = CamFile() assert_equal(cf.settings, FileSettings()) + + +def test_bounds_override_smoketest(): + cf = CamFile() + cf.bounds + + +def test_zeros(): + """ Test zero/zero_suppression interaction + """ + fs = FileSettings() + assert_equal(fs.zero_suppression, 'trailing') + assert_equal(fs.zeros, 'leading') + + fs['zero_suppression'] = 'leading' + assert_equal(fs.zero_suppression, 'leading') + assert_equal(fs.zeros, 'trailing') + + fs.zero_suppression = 'trailing' + assert_equal(fs.zero_suppression, 'trailing') + assert_equal(fs.zeros, 'leading') + + fs['zeros'] = 'trailing' + assert_equal(fs.zeros, 'trailing') + assert_equal(fs.zero_suppression, 'leading') + + fs.zeros = 'leading' + assert_equal(fs.zeros, 'leading') + assert_equal(fs.zero_suppression, 'trailing') + + fs = FileSettings(zeros='leading') + assert_equal(fs.zeros, 'leading') + assert_equal(fs.zero_suppression, 'trailing') + + fs = FileSettings(zero_suppression='leading') + assert_equal(fs.zeros, 'trailing') + assert_equal(fs.zero_suppression, 'leading') + + fs = FileSettings(zeros='leading', zero_suppression='trailing') + assert_equal(fs.zeros, 'leading') + assert_equal(fs.zero_suppression, 'trailing') + + fs = FileSettings(zeros='trailing', zero_suppression='leading') + assert_equal(fs.zeros, 'trailing') + assert_equal(fs.zero_suppression, 'leading') + + +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) + + # degrees kelvin isn't a valid unit for a CAM file + assert_raises(ValueError, FileSettings, 'absolute', + 'degrees kelvin', None, (2, 5), None) + + assert_raises(ValueError, FileSettings, 'absolute', + 'inch', 'leading', (2, 5), 'leading') -
\ No newline at end of file + # Technnically this should be an error, but Eangle files often do this incorrectly so we + # allow it + #assert_raises(ValueError, FileSettings, 'absolute', + # 'inch', 'following', (2, 5), None) + +======= + assert_raises(ValueError, FileSettings, 'absolute-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', + 'inch', None, (2, 5, 6), None) + + +def test_key_validation(): + fs = FileSettings() + assert_raises(KeyError, fs.__getitem__, 'octopus') + assert_raises(KeyError, fs.__setitem__, 'octopus', 'do not care') + assert_raises(ValueError, fs.__setitem__, 'notation', 'absolute-ish') + assert_raises(ValueError, fs.__setitem__, 'units', 'degrees kelvin') + assert_raises(ValueError, fs.__setitem__, 'zero_suppression', 'following') + assert_raises(ValueError, fs.__setitem__, 'zeros', 'following') + assert_raises(ValueError, fs.__setitem__, 'format', (2, 5, 6)) diff --git a/gerber/tests/test_common.py b/gerber/tests/test_common.py index 1e1efe5..357ed18 100644 --- a/gerber/tests/test_common.py +++ b/gerber/tests/test_common.py @@ -2,23 +2,40 @@ # -*- coding: utf-8 -*- # Author: Hamilton Kibbe <ham@hamiltonkib.be> -from ..common import read +from ..exceptions import ParseError +from ..common import read, loads from ..excellon import ExcellonFile from ..rs274x import GerberFile -from tests import * +from .tests import * import os NCDRILL_FILE = os.path.join(os.path.dirname(__file__), - 'resources/ncdrill.DRD') + 'resources/ncdrill.DRD') TOP_COPPER_FILE = os.path.join(os.path.dirname(__file__), - 'resources/top_copper.GTL') + 'resources/top_copper.GTL') + def test_file_type_detection(): """ Test file type detection """ ncdrill = read(NCDRILL_FILE) top_copper = read(TOP_COPPER_FILE) - assert(isinstance(ncdrill, ExcellonFile)) - assert(isinstance(top_copper, GerberFile)) + assert_true(isinstance(ncdrill, ExcellonFile)) + assert_true(isinstance(top_copper, GerberFile)) + + +def test_load_from_string(): + with open(NCDRILL_FILE, 'rU') as f: + ncdrill = loads(f.read()) + with open(TOP_COPPER_FILE, 'rU') as f: + top_copper = loads(f.read()) + assert_true(isinstance(ncdrill, ExcellonFile)) + assert_true(isinstance(top_copper, GerberFile)) + + +def test_file_type_validation(): + """ Test file format validation + """ + assert_raises(ParseError, read, 'LICENSE') diff --git a/gerber/tests/test_excellon.py b/gerber/tests/test_excellon.py index 72e3d7d..1402938 100644 --- a/gerber/tests/test_excellon.py +++ b/gerber/tests/test_excellon.py @@ -2,31 +2,198 @@ # -*- coding: utf-8 -*- # Author: Hamilton Kibbe <ham@hamiltonkib.be> -from ..excellon import read, detect_excellon_format, ExcellonFile -from tests import * - import os +from ..cam import FileSettings +from ..excellon import read, detect_excellon_format, ExcellonFile, ExcellonParser +from ..excellon_statements import ExcellonTool +from .tests import * + + NCDRILL_FILE = os.path.join(os.path.dirname(__file__), - 'resources/ncdrill.DRD') + 'resources/ncdrill.DRD') + def test_format_detection(): """ Test file type detection """ - settings = detect_excellon_format(NCDRILL_FILE) + with open(NCDRILL_FILE, "rU") as f: + data = f.read() + settings = detect_excellon_format(data) assert_equal(settings['format'], (2, 4)) - assert_equal(settings['zero_suppression'], 'leading') + assert_equal(settings['zeros'], 'trailing') + + settings = detect_excellon_format(filename=NCDRILL_FILE) + assert_equal(settings['format'], (2, 4)) + assert_equal(settings['zeros'], 'trailing') + def test_read(): ncdrill = read(NCDRILL_FILE) assert(isinstance(ncdrill, ExcellonFile)) - + + +def test_write(): + ncdrill = read(NCDRILL_FILE) + ncdrill.write('test.ncd') + with open(NCDRILL_FILE, "rU") as src: + srclines = src.readlines() + with open('test.ncd', "rU") as res: + for idx, line in enumerate(res): + assert_equal(line.strip(), srclines[idx].strip()) + os.remove('test.ncd') + + def test_read_settings(): ncdrill = read(NCDRILL_FILE) - assert_equal(ncdrill.settings.format, (2, 4)) - assert_equal(ncdrill.settings.zero_suppression, 'leading') - - - - - + assert_equal(ncdrill.settings['format'], (2, 4)) + assert_equal(ncdrill.settings['zeros'], 'trailing') + + +def test_bounds(): + ncdrill = read(NCDRILL_FILE) + xbound, ybound = ncdrill.bounds + assert_array_almost_equal(xbound, (0.1300, 2.1430)) + assert_array_almost_equal(ybound, (0.3946, 1.7164)) + + +def test_report(): + ncdrill = read(NCDRILL_FILE) + + +def test_conversion(): + import copy + ncdrill = read(NCDRILL_FILE) + assert_equal(ncdrill.settings.units, 'inch') + ncdrill_inch = copy.deepcopy(ncdrill) + ncdrill.to_metric() + assert_equal(ncdrill.settings.units, 'metric') + inch_primitives = ncdrill_inch.primitives + for tool in iter(ncdrill_inch.tools.values()): + tool.to_metric() + for primitive in inch_primitives: + primitive.to_metric() + for statement in ncdrill_inch.statements: + statement.to_metric() + + for m_tool, i_tool in zip(iter(ncdrill.tools.values()), + iter(ncdrill_inch.tools.values())): + assert_equal(i_tool, m_tool) + + for m, i in zip(ncdrill.primitives, inch_primitives): + assert_equal(m.position, i.position, '%s not equal to %s' % (m, i)) + assert_equal(m.diameter, i.diameter, '%s not equal to %s' % (m, i)) + + +def test_parser_hole_count(): + settings = FileSettings(**detect_excellon_format(NCDRILL_FILE)) + p = ExcellonParser(settings) + p.parse(NCDRILL_FILE) + assert_equal(p.hole_count, 36) + + +def test_parser_hole_sizes(): + settings = FileSettings(**detect_excellon_format(NCDRILL_FILE)) + p = ExcellonParser(settings) + p.parse(NCDRILL_FILE) + assert_equal(p.hole_sizes, [0.0236, 0.0354, 0.04, 0.126, 0.128]) + + +def test_parse_whitespace(): + p = ExcellonParser(FileSettings()) + assert_equal(p._parse_line(' '), None) + + +def test_parse_comment(): + p = ExcellonParser(FileSettings()) + p._parse_line(';A comment') + assert_equal(p.statements[0].comment, 'A comment') + + +def test_parse_format_comment(): + p = ExcellonParser(FileSettings()) + p._parse_line('; FILE_FORMAT=9:9 ') + assert_equal(p.format, (9, 9)) + + +def test_parse_header(): + p = ExcellonParser(FileSettings()) + p._parse_line('M48 ') + assert_equal(p.state, 'HEADER') + p._parse_line('M95 ') + assert_equal(p.state, 'DRILL') + + +def test_parse_rout(): + p = ExcellonParser(FileSettings()) + p._parse_line('G00 ') + assert_equal(p.state, 'ROUT') + p._parse_line('G05 ') + assert_equal(p.state, 'DRILL') + + +def test_parse_version(): + p = ExcellonParser(FileSettings()) + p._parse_line('VER,1 ') + assert_equal(p.statements[0].version, 1) + p._parse_line('VER,2 ') + assert_equal(p.statements[1].version, 2) + + +def test_parse_format(): + p = ExcellonParser(FileSettings()) + p._parse_line('FMAT,1 ') + assert_equal(p.statements[0].format, 1) + p._parse_line('FMAT,2 ') + assert_equal(p.statements[1].format, 2) + + +def test_parse_units(): + settings = FileSettings(units='inch', zeros='trailing') + p = ExcellonParser(settings) + p._parse_line(';METRIC,LZ') + assert_equal(p.units, 'inch') + assert_equal(p.zeros, 'trailing') + p._parse_line('METRIC,LZ') + assert_equal(p.units, 'metric') + assert_equal(p.zeros, 'leading') + + +def test_parse_incremental_mode(): + settings = FileSettings(units='inch', zeros='trailing') + p = ExcellonParser(settings) + assert_equal(p.notation, 'absolute') + p._parse_line('ICI,ON ') + assert_equal(p.notation, 'incremental') + p._parse_line('ICI,OFF ') + assert_equal(p.notation, 'absolute') + + +def test_parse_absolute_mode(): + settings = FileSettings(units='inch', zeros='trailing') + p = ExcellonParser(settings) + assert_equal(p.notation, 'absolute') + p._parse_line('ICI,ON ') + assert_equal(p.notation, 'incremental') + p._parse_line('G90 ') + assert_equal(p.notation, 'absolute') + + +def test_parse_repeat_hole(): + p = ExcellonParser(FileSettings()) + p.active_tool = ExcellonTool(FileSettings(), number=8) + p._parse_line('R03X1.5Y1.5') + assert_equal(p.statements[0].count, 3) + + +def test_parse_incremental_position(): + p = ExcellonParser(FileSettings(notation='incremental')) + p._parse_line('X01Y01') + p._parse_line('X01Y01') + assert_equal(p.pos, [2., 2.]) + + +def test_parse_unknown(): + p = ExcellonParser(FileSettings()) + p._parse_line('Not A Valid Statement') + assert_equal(p.statements[0].stmt, 'Not A Valid Statement') diff --git a/gerber/tests/test_excellon_statements.py b/gerber/tests/test_excellon_statements.py index f2e17ee..8e6e06e 100644 --- a/gerber/tests/test_excellon_statements.py +++ b/gerber/tests/test_excellon_statements.py @@ -3,31 +3,62 @@ # Author: Hamilton Kibbe <ham@hamiltonkib.be> -from .tests import assert_equal, assert_raises +from .tests import assert_equal, assert_not_equal, assert_raises from ..excellon_statements import * from ..cam import FileSettings +def test_excellon_statement_implementation(): + stmt = ExcellonStatement() + assert_raises(NotImplementedError, stmt.from_excellon, None) + assert_raises(NotImplementedError, stmt.to_excellon) + + +def test_excellontstmt(): + """ Smoke test ExcellonStatement + """ + stmt = ExcellonStatement() + stmt.to_inch() + stmt.to_metric() + stmt.offset() + + def test_excellontool_factory(): - """ Test ExcellonTool factory method + """ Test ExcellonTool factory methods """ - exc_line = 'T8F00S00C0.12500' + exc_line = 'T8F01B02S00003H04Z05C0.12500' settings = FileSettings(format=(2, 5), zero_suppression='trailing', - units='inch', notation='absolute') + units='inch', notation='absolute') tool = ExcellonTool.from_excellon(exc_line, settings) + assert_equal(tool.number, 8) + assert_equal(tool.diameter, 0.125) + assert_equal(tool.feed_rate, 1) + assert_equal(tool.retract_rate, 2) + assert_equal(tool.rpm, 3) + assert_equal(tool.max_hit_count, 4) + assert_equal(tool.depth_offset, 5) + + stmt = {'number': 8, 'feed_rate': 1, 'retract_rate': 2, 'rpm': 3, + 'diameter': 0.125, 'max_hit_count': 4, 'depth_offset': 5} + tool = ExcellonTool.from_dict(settings, stmt) + assert_equal(tool.number, 8) assert_equal(tool.diameter, 0.125) - assert_equal(tool.feed_rate, 0) - assert_equal(tool.rpm, 0) + assert_equal(tool.feed_rate, 1) + assert_equal(tool.retract_rate, 2) + assert_equal(tool.rpm, 3) + assert_equal(tool.max_hit_count, 4) + assert_equal(tool.depth_offset, 5) def test_excellontool_dump(): """ Test ExcellonTool to_excellon() """ - exc_lines = ['T1F00S00C0.01200', 'T2F00S00C0.01500', 'T3F00S00C0.01968', - 'T4F00S00C0.02800', 'T5F00S00C0.03300', 'T6F00S00C0.03800', - 'T7F00S00C0.04300', 'T8F00S00C0.12500', 'T9F00S00C0.13000', ] + exc_lines = ['T01F0S0C0.01200', 'T02F0S0C0.01500', 'T03F0S0C0.01968', + 'T04F0S0C0.02800', 'T05F0S0C0.03300', 'T06F0S0C0.03800', + 'T07F0S0C0.04300', 'T08F0S0C0.12500', 'T09F0S0C0.13000', + 'T08B01F02H03S00003C0.12500Z04', 'T01F0S300.999C0.01200'] settings = FileSettings(format=(2, 5), zero_suppression='trailing', - units='inch', notation='absolute') + units='inch', notation='absolute') for line in exc_lines: tool = ExcellonTool.from_excellon(line, settings) assert_equal(tool.to_excellon(), line) @@ -35,7 +66,7 @@ def test_excellontool_dump(): def test_excellontool_order(): settings = FileSettings(format=(2, 5), zero_suppression='trailing', - units='inch', notation='absolute') + units='inch', notation='absolute') line = 'T8F00S00C0.12500' tool1 = ExcellonTool.from_excellon(line, settings) line = 'T8C0.12500F00S00' @@ -45,6 +76,47 @@ def test_excellontool_order(): assert_equal(tool1.rpm, tool2.rpm) +def test_excellontool_conversion(): + tool = ExcellonTool.from_dict(FileSettings(units='metric'), + {'number': 8, 'diameter': 25.4}) + tool.to_inch() + assert_equal(tool.diameter, 1.) + tool = ExcellonTool.from_dict(FileSettings(units='inch'), + {'number': 8, 'diameter': 1.}) + tool.to_metric() + assert_equal(tool.diameter, 25.4) + + # Shouldn't change units if we're already using target units + tool = ExcellonTool.from_dict(FileSettings(units='inch'), + {'number': 8, 'diameter': 25.4}) + tool.to_inch() + assert_equal(tool.diameter, 25.4) + tool = ExcellonTool.from_dict(FileSettings(units='metric'), + {'number': 8, 'diameter': 1.}) + tool.to_metric() + assert_equal(tool.diameter, 1.) + + +def test_excellontool_repr(): + tool = ExcellonTool.from_dict(FileSettings(), + {'number': 8, 'diameter': 0.125}) + assert_equal(str(tool), '<ExcellonTool 08: 0.125in. dia.>') + tool = ExcellonTool.from_dict(FileSettings(units='metric'), + {'number': 8, 'diameter': 0.125}) + assert_equal(str(tool), '<ExcellonTool 08: 0.125mm dia.>') + + +def test_excellontool_equality(): + t = ExcellonTool.from_dict( + FileSettings(), {'number': 8, 'diameter': 0.125}) + t1 = ExcellonTool.from_dict( + FileSettings(), {'number': 8, 'diameter': 0.125}) + assert_equal(t, t1) + t1 = ExcellonTool.from_dict(FileSettings(units='metric'), + {'number': 8, 'diameter': 0.125}) + assert_not_equal(t, t1) + + def test_toolselection_factory(): """ Test ToolSelectionStmt factory method """ @@ -54,6 +126,9 @@ def test_toolselection_factory(): stmt = ToolSelectionStmt.from_excellon('T0223') assert_equal(stmt.tool, 2) assert_equal(stmt.compensation_index, 23) + stmt = ToolSelectionStmt.from_excellon('T042') + assert_equal(stmt.tool, 42) + assert_equal(stmt.compensation_index, None) def test_toolselection_dump(): @@ -65,32 +140,202 @@ def test_toolselection_dump(): assert_equal(stmt.to_excellon(), line) +def test_z_axis_infeed_rate_factory(): + """ Test ZAxisInfeedRateStmt factory method + """ + stmt = ZAxisInfeedRateStmt.from_excellon('F01') + assert_equal(stmt.rate, 1) + stmt = ZAxisInfeedRateStmt.from_excellon('F2') + assert_equal(stmt.rate, 2) + stmt = ZAxisInfeedRateStmt.from_excellon('F03') + assert_equal(stmt.rate, 3) + + +def test_z_axis_infeed_rate_dump(): + """ Test ZAxisInfeedRateStmt to_excellon() + """ + inputs = [ + ('F01', 'F01'), + ('F2', 'F02'), + ('F00003', 'F03') + ] + for input_rate, expected_output in inputs: + stmt = ZAxisInfeedRateStmt.from_excellon(input_rate) + assert_equal(stmt.to_excellon(), expected_output) + + def test_coordinatestmt_factory(): """ Test CoordinateStmt factory method """ + settings = FileSettings(format=(2, 5), zero_suppression='trailing', + units='inch', notation='absolute') + line = 'X0278207Y0065293' - stmt = CoordinateStmt.from_excellon(line) + stmt = CoordinateStmt.from_excellon(line, settings) assert_equal(stmt.x, 2.78207) assert_equal(stmt.y, 0.65293) - line = 'X02945' - stmt = CoordinateStmt.from_excellon(line) - assert_equal(stmt.x, 2.945) + # line = 'X02945' + # stmt = CoordinateStmt.from_excellon(line) + # assert_equal(stmt.x, 2.945) + + # line = 'Y00575' + # stmt = CoordinateStmt.from_excellon(line) + # assert_equal(stmt.y, 0.575) + + settings = FileSettings(format=(2, 4), zero_suppression='leading', + units='inch', notation='absolute') + + line = 'X9660Y4639' + stmt = CoordinateStmt.from_excellon(line, settings) + assert_equal(stmt.x, 0.9660) + assert_equal(stmt.y, 0.4639) + assert_equal(stmt.to_excellon(settings), "X9660Y4639") + assert_equal(stmt.units, 'inch') - line = 'Y00575' - stmt = CoordinateStmt.from_excellon(line) - assert_equal(stmt.y, 0.575) + settings.units = 'metric' + stmt = CoordinateStmt.from_excellon(line, settings) + assert_equal(stmt.units, 'metric') def test_coordinatestmt_dump(): """ Test CoordinateStmt to_excellon() """ - lines = ['X0278207Y0065293', 'X0243795', 'Y0082528', 'Y0086028', - 'X0251295Y0081528', 'X02525Y0078', 'X0255Y00575', 'Y0052', - 'X02675', 'Y00575', 'X02425', 'Y0052', 'X023', ] + lines = ['X278207Y65293', 'X243795', 'Y82528', 'Y86028', + 'X251295Y81528', 'X2525Y78', 'X255Y575', 'Y52', + 'X2675', 'Y575', 'X2425', 'Y52', 'X23', ] + settings = FileSettings(format=(2, 4), zero_suppression='leading', + units='inch', notation='absolute') for line in lines: - stmt = CoordinateStmt.from_excellon(line) - assert_equal(stmt.to_excellon(), line) + stmt = CoordinateStmt.from_excellon(line, settings) + assert_equal(stmt.to_excellon(settings), line) + + +def test_coordinatestmt_conversion(): + + settings = FileSettings() + settings.units = 'metric' + stmt = CoordinateStmt.from_excellon('X254Y254', settings) + + # No effect + stmt.to_metric() + assert_equal(stmt.x, 25.4) + assert_equal(stmt.y, 25.4) + + stmt.to_inch() + assert_equal(stmt.units, 'inch') + assert_equal(stmt.x, 1.) + assert_equal(stmt.y, 1.) + + # No effect + stmt.to_inch() + assert_equal(stmt.x, 1.) + assert_equal(stmt.y, 1.) + + settings.units = 'inch' + stmt = CoordinateStmt.from_excellon('X01Y01', settings) + + # No effect + stmt.to_inch() + assert_equal(stmt.x, 1.) + assert_equal(stmt.y, 1.) + + stmt.to_metric() + assert_equal(stmt.units, 'metric') + assert_equal(stmt.x, 25.4) + assert_equal(stmt.y, 25.4) + + # No effect + stmt.to_metric() + assert_equal(stmt.x, 25.4) + assert_equal(stmt.y, 25.4) + + +def test_coordinatestmt_offset(): + stmt = CoordinateStmt.from_excellon('X01Y01', FileSettings()) + stmt.offset() + assert_equal(stmt.x, 1) + assert_equal(stmt.y, 1) + stmt.offset(1, 0) + assert_equal(stmt.x, 2.) + assert_equal(stmt.y, 1.) + stmt.offset(0, 1) + assert_equal(stmt.x, 2.) + assert_equal(stmt.y, 2.) + + +def test_coordinatestmt_string(): + settings = FileSettings(format=(2, 4), zero_suppression='leading', + units='inch', notation='absolute') + stmt = CoordinateStmt.from_excellon('X9660Y4639', settings) + assert_equal(str(stmt), '<Coordinate Statement: X: 0.966 Y: 0.4639 >') + + +def test_repeathole_stmt_factory(): + stmt = RepeatHoleStmt.from_excellon('R0004X015Y32', + FileSettings(zeros='leading', + units='inch')) + assert_equal(stmt.count, 4) + assert_equal(stmt.xdelta, 1.5) + assert_equal(stmt.ydelta, 32) + assert_equal(stmt.units, 'inch') + + stmt = RepeatHoleStmt.from_excellon('R0004X015Y32', + FileSettings(zeros='leading', + units='metric')) + assert_equal(stmt.units, 'metric') + + +def test_repeatholestmt_dump(): + line = 'R4X015Y32' + stmt = RepeatHoleStmt.from_excellon(line, FileSettings()) + assert_equal(stmt.to_excellon(FileSettings()), line) + + +def test_repeatholestmt_conversion(): + line = 'R4X0254Y254' + settings = FileSettings() + settings.units = 'metric' + stmt = RepeatHoleStmt.from_excellon(line, settings) + + # No effect + stmt.to_metric() + assert_equal(stmt.xdelta, 2.54) + assert_equal(stmt.ydelta, 25.4) + + stmt.to_inch() + assert_equal(stmt.units, 'inch') + assert_equal(stmt.xdelta, 0.1) + assert_equal(stmt.ydelta, 1.) + + # no effect + stmt.to_inch() + assert_equal(stmt.xdelta, 0.1) + assert_equal(stmt.ydelta, 1.) + + line = 'R4X01Y1' + settings.units = 'inch' + stmt = RepeatHoleStmt.from_excellon(line, settings) + + # no effect + stmt.to_inch() + assert_equal(stmt.xdelta, 1.) + assert_equal(stmt.ydelta, 10.) + + stmt.to_metric() + assert_equal(stmt.units, 'metric') + assert_equal(stmt.xdelta, 25.4) + assert_equal(stmt.ydelta, 254.) + + # No effect + stmt.to_metric() + assert_equal(stmt.xdelta, 25.4) + assert_equal(stmt.ydelta, 254.) + + +def test_repeathole_str(): + stmt = RepeatHoleStmt.from_excellon('R4X015Y32', FileSettings()) + assert_equal(str(stmt), '<Repeat Hole: 4 times, offset X: 1.5 Y: 32>') def test_commentstmt_factory(): @@ -118,18 +363,147 @@ def test_commentstmt_dump(): assert_equal(stmt.to_excellon(), line) +def test_header_begin_stmt(): + stmt = HeaderBeginStmt() + assert_equal(stmt.to_excellon(None), 'M48') + + +def test_header_end_stmt(): + stmt = HeaderEndStmt() + assert_equal(stmt.to_excellon(None), 'M95') + + +def test_rewindstop_stmt(): + stmt = RewindStopStmt() + assert_equal(stmt.to_excellon(None), '%') + + +def test_z_axis_rout_position_stmt(): + stmt = ZAxisRoutPositionStmt() + assert_equal(stmt.to_excellon(None), 'M15') + + +def test_retract_with_clamping_stmt(): + stmt = RetractWithClampingStmt() + assert_equal(stmt.to_excellon(None), 'M16') + + +def test_retract_without_clamping_stmt(): + stmt = RetractWithoutClampingStmt() + assert_equal(stmt.to_excellon(None), 'M17') + + +def test_cutter_compensation_off_stmt(): + stmt = CutterCompensationOffStmt() + assert_equal(stmt.to_excellon(None), 'G40') + + +def test_cutter_compensation_left_stmt(): + stmt = CutterCompensationLeftStmt() + assert_equal(stmt.to_excellon(None), 'G41') + + +def test_cutter_compensation_right_stmt(): + stmt = CutterCompensationRightStmt() + assert_equal(stmt.to_excellon(None), 'G42') + + +def test_endofprogramstmt_factory(): + settings = FileSettings(units='inch') + stmt = EndOfProgramStmt.from_excellon('M30X01Y02', settings) + assert_equal(stmt.x, 1.) + assert_equal(stmt.y, 2.) + assert_equal(stmt.units, 'inch') + settings.units = 'metric' + stmt = EndOfProgramStmt.from_excellon('M30X01', settings) + assert_equal(stmt.x, 1.) + assert_equal(stmt.y, None) + assert_equal(stmt.units, 'metric') + stmt = EndOfProgramStmt.from_excellon('M30Y02', FileSettings()) + assert_equal(stmt.x, None) + assert_equal(stmt.y, 2.) + + +def test_endofprogramStmt_dump(): + lines = ['M30X01Y02', ] + for line in lines: + stmt = EndOfProgramStmt.from_excellon(line, FileSettings()) + assert_equal(stmt.to_excellon(FileSettings()), line) + + +def test_endofprogramstmt_conversion(): + settings = FileSettings() + settings.units = 'metric' + stmt = EndOfProgramStmt.from_excellon('M30X0254Y254', settings) + # No effect + stmt.to_metric() + assert_equal(stmt.x, 2.54) + assert_equal(stmt.y, 25.4) + + stmt.to_inch() + assert_equal(stmt.units, 'inch') + assert_equal(stmt.x, 0.1) + assert_equal(stmt.y, 1.0) + + # No effect + stmt.to_inch() + assert_equal(stmt.x, 0.1) + assert_equal(stmt.y, 1.0) + + settings.units = 'inch' + stmt = EndOfProgramStmt.from_excellon('M30X01Y1', settings) + + # No effect + stmt.to_inch() + assert_equal(stmt.x, 1.) + assert_equal(stmt.y, 10.0) + + stmt.to_metric() + assert_equal(stmt.units, 'metric') + assert_equal(stmt.x, 25.4) + assert_equal(stmt.y, 254.) + + # No effect + stmt.to_metric() + assert_equal(stmt.x, 25.4) + assert_equal(stmt.y, 254.) + + +def test_endofprogramstmt_offset(): + stmt = EndOfProgramStmt(1, 1) + stmt.offset() + assert_equal(stmt.x, 1) + assert_equal(stmt.y, 1) + stmt.offset(1, 0) + assert_equal(stmt.x, 2.) + assert_equal(stmt.y, 1.) + stmt.offset(0, 1) + assert_equal(stmt.x, 2.) + assert_equal(stmt.y, 2.) + + def test_unitstmt_factory(): """ Test UnitStmt factory method """ line = 'INCH,LZ' stmt = UnitStmt.from_excellon(line) assert_equal(stmt.units, 'inch') - assert_equal(stmt.zero_suppression, 'trailing') + assert_equal(stmt.zeros, 'leading') + + line = 'INCH,TZ' + stmt = UnitStmt.from_excellon(line) + assert_equal(stmt.units, 'inch') + assert_equal(stmt.zeros, 'trailing') + + line = 'METRIC,LZ' + stmt = UnitStmt.from_excellon(line) + assert_equal(stmt.units, 'metric') + assert_equal(stmt.zeros, 'leading') line = 'METRIC,TZ' stmt = UnitStmt.from_excellon(line) assert_equal(stmt.units, 'metric') - assert_equal(stmt.zero_suppression, 'leading') + assert_equal(stmt.zeros, 'trailing') def test_unitstmt_dump(): @@ -141,6 +515,16 @@ def test_unitstmt_dump(): assert_equal(stmt.to_excellon(), line) +def test_unitstmt_conversion(): + stmt = UnitStmt.from_excellon('METRIC,TZ') + stmt.to_inch() + assert_equal(stmt.units, 'inch') + + stmt = UnitStmt.from_excellon('INCH,TZ') + stmt.to_metric() + assert_equal(stmt.units, 'metric') + + def test_incrementalmode_factory(): """ Test IncrementalModeStmt factory method """ @@ -188,6 +572,7 @@ def test_versionstmt_dump(): stmt = VersionStmt.from_excellon(line) assert_equal(stmt.to_excellon(), line) + def test_versionstmt_validation(): """ Test VersionStmt input validation """ @@ -268,3 +653,48 @@ def test_measmodestmt_validation(): """ assert_raises(ValueError, MeasuringModeStmt.from_excellon, 'M70') assert_raises(ValueError, MeasuringModeStmt, 'millimeters') + + +def test_measmodestmt_conversion(): + line = 'M72' + stmt = MeasuringModeStmt.from_excellon(line) + assert_equal(stmt.units, 'inch') + stmt.to_metric() + assert_equal(stmt.units, 'metric') + + line = 'M71' + stmt = MeasuringModeStmt.from_excellon(line) + assert_equal(stmt.units, 'metric') + stmt.to_inch() + assert_equal(stmt.units, 'inch') + + +def test_routemode_stmt(): + stmt = RouteModeStmt() + assert_equal(stmt.to_excellon(FileSettings()), 'G00') + + +def test_linearmode_stmt(): + stmt = LinearModeStmt() + assert_equal(stmt.to_excellon(FileSettings()), 'G01') + + +def test_drillmode_stmt(): + stmt = DrillModeStmt() + assert_equal(stmt.to_excellon(FileSettings()), 'G05') + + +def test_absolutemode_stmt(): + stmt = AbsoluteModeStmt() + assert_equal(stmt.to_excellon(FileSettings()), 'G90') + + +def test_unknownstmt(): + stmt = UnknownStmt('TEST') + assert_equal(stmt.stmt, 'TEST') + assert_equal(str(stmt), '<Unknown Statement: TEST>') + + +def test_unknownstmt_dump(): + stmt = UnknownStmt('TEST') + assert_equal(stmt.to_excellon(FileSettings()), 'TEST') diff --git a/gerber/tests/test_gerber_statements.py b/gerber/tests/test_gerber_statements.py index a463c9d..2157390 100644 --- a/gerber/tests/test_gerber_statements.py +++ b/gerber/tests/test_gerber_statements.py @@ -5,6 +5,19 @@ from .tests import * from ..gerber_statements import * +from ..cam import FileSettings + + +def test_Statement_smoketest(): + stmt = Statement('Test') + assert_equal(stmt.type, 'Test') + stmt.to_metric() + assert_in('units=metric', str(stmt)) + stmt.to_inch() + assert_in('units=inch', str(stmt)) + stmt.to_metric() + stmt.offset(1, 1) + assert_in('type=Test', str(stmt)) def test_FSParamStmt_factory(): @@ -24,6 +37,7 @@ def test_FSParamStmt_factory(): assert_equal(fs.notation, 'incremental') assert_equal(fs.format, (2, 7)) + def test_FSParamStmt(): """ Test FSParamStmt initialization """ @@ -37,6 +51,7 @@ def test_FSParamStmt(): assert_equal(stmt.notation, notation) assert_equal(stmt.format, fmt) + def test_FSParamStmt_dump(): """ Test FSParamStmt to_gerber() """ @@ -48,6 +63,23 @@ def test_FSParamStmt_dump(): fs = FSParamStmt.from_dict(stmt) assert_equal(fs.to_gerber(), '%FSTIX25Y25*%') + settings = FileSettings(zero_suppression='leading', notation='absolute') + assert_equal(fs.to_gerber(settings), '%FSLAX25Y25*%') + + +def test_FSParamStmt_string(): + """ Test FSParamStmt.__str__() + """ + stmt = {'param': 'FS', 'zero': 'L', 'notation': 'A', 'x': '27'} + fs = FSParamStmt.from_dict(stmt) + assert_equal(str(fs), + '<Format Spec: 2:7 leading zero suppression absolute notation>') + + stmt = {'param': 'FS', 'zero': 'T', 'notation': 'I', 'x': '25'} + fs = FSParamStmt.from_dict(stmt) + assert_equal(str(fs), + '<Format Spec: 2:5 trailing zero suppression incremental notation>') + def test_MOParamStmt_factory(): """ Test MOParamStruct factory @@ -64,6 +96,13 @@ def test_MOParamStmt_factory(): assert_equal(mo.param, 'MO') assert_equal(mo.mode, 'metric') + stmt = {'param': 'MO'} + mo = MOParamStmt.from_dict(stmt) + assert_equal(mo.mode, None) + stmt = {'param': 'MO', 'mo': 'degrees kelvin'} + assert_raises(ValueError, MOParamStmt.from_dict, stmt) + + def test_MOParamStmt(): """ Test MOParamStmt initialization """ @@ -89,6 +128,30 @@ def test_MOParamStmt_dump(): assert_equal(mo.to_gerber(), '%MOMM*%') +def test_MOParamStmt_conversion(): + stmt = {'param': 'MO', 'mo': 'MM'} + mo = MOParamStmt.from_dict(stmt) + mo.to_inch() + assert_equal(mo.mode, 'inch') + + stmt = {'param': 'MO', 'mo': 'IN'} + mo = MOParamStmt.from_dict(stmt) + mo.to_metric() + assert_equal(mo.mode, 'metric') + + +def test_MOParamStmt_string(): + """ Test MOParamStmt.__str__() + """ + stmt = {'param': 'MO', 'mo': 'IN'} + mo = MOParamStmt.from_dict(stmt) + assert_equal(str(mo), '<Mode: inches>') + + stmt = {'param': 'MO', 'mo': 'MM'} + mo = MOParamStmt.from_dict(stmt) + assert_equal(str(mo), '<Mode: millimeters>') + + def test_IPParamStmt_factory(): """ Test IPParamStruct factory """ @@ -100,6 +163,7 @@ def test_IPParamStmt_factory(): ip = IPParamStmt.from_dict(stmt) assert_equal(ip.ip, 'negative') + def test_IPParamStmt(): """ Test IPParamStmt initialization """ @@ -122,14 +186,44 @@ def test_IPParamStmt_dump(): assert_equal(ip.to_gerber(), '%IPNEG*%') +def test_IPParamStmt_string(): + stmt = {'param': 'IP', 'ip': 'POS'} + ip = IPParamStmt.from_dict(stmt) + assert_equal(str(ip), '<Image Polarity: positive>') + + stmt = {'param': 'IP', 'ip': 'NEG'} + ip = IPParamStmt.from_dict(stmt) + assert_equal(str(ip), '<Image Polarity: negative>') + + +def test_IRParamStmt_factory(): + stmt = {'param': 'IR', 'angle': '45'} + ir = IRParamStmt.from_dict(stmt) + assert_equal(ir.param, 'IR') + assert_equal(ir.angle, 45) + + +def test_IRParamStmt_dump(): + stmt = {'param': 'IR', 'angle': '45'} + ir = IRParamStmt.from_dict(stmt) + assert_equal(ir.to_gerber(), '%IR45*%') + + +def test_IRParamStmt_string(): + stmt = {'param': 'IR', 'angle': '45'} + ir = IRParamStmt.from_dict(stmt) + assert_equal(str(ir), '<Image Angle: 45>') + + def test_OFParamStmt_factory(): - """ Test OFParamStmt factory + """ Test OFParamStmt factory """ stmt = {'param': 'OF', 'a': '0.1234567', 'b': '0.1234567'} of = OFParamStmt.from_dict(stmt) assert_equal(of.a, 0.1234567) assert_equal(of.b, 0.1234567) + def test_OFParamStmt(): """ Test IPParamStmt initialization """ @@ -139,13 +233,142 @@ def test_OFParamStmt(): assert_equal(stmt.param, param) assert_equal(stmt.a, val) assert_equal(stmt.b, val) - + + def test_OFParamStmt_dump(): """ Test OFParamStmt to_gerber() """ - stmt = {'param': 'OF', 'a': '0.1234567', 'b': '0.1234567'} + stmt = {'param': 'OF', 'a': '0.123456', 'b': '0.123456'} + of = OFParamStmt.from_dict(stmt) + assert_equal(of.to_gerber(), '%OFA0.12345B0.12345*%') + + +def test_OFParamStmt_conversion(): + stmt = {'param': 'OF', 'a': '2.54', 'b': '25.4'} + of = OFParamStmt.from_dict(stmt) + of.units = 'metric' + + # No effect + of.to_metric() + assert_equal(of.a, 2.54) + assert_equal(of.b, 25.4) + + of.to_inch() + assert_equal(of.units, 'inch') + assert_equal(of.a, 0.1) + assert_equal(of.b, 1.0) + + # No effect + of.to_inch() + assert_equal(of.a, 0.1) + assert_equal(of.b, 1.0) + + stmt = {'param': 'OF', 'a': '0.1', 'b': '1.0'} + of = OFParamStmt.from_dict(stmt) + of.units = 'inch' + + # No effect + of.to_inch() + assert_equal(of.a, 0.1) + assert_equal(of.b, 1.0) + + of.to_metric() + assert_equal(of.units, 'metric') + assert_equal(of.a, 2.54) + assert_equal(of.b, 25.4) + + # No effect + of.to_metric() + assert_equal(of.a, 2.54) + assert_equal(of.b, 25.4) + + +def test_OFParamStmt_offset(): + s = OFParamStmt('OF', 0, 0) + s.offset(1, 0) + assert_equal(s.a, 1.) + assert_equal(s.b, 0.) + s.offset(0, 1) + assert_equal(s.a, 1.) + assert_equal(s.b, 1.) + + +def test_OFParamStmt_string(): + """ Test OFParamStmt __str__ + """ + stmt = {'param': 'OF', 'a': '0.123456', 'b': '0.123456'} of = OFParamStmt.from_dict(stmt) - assert_equal(of.to_gerber(), '%OFA0.123456B0.123456*%') + assert_equal(str(of), '<Offset: X: 0.123456 Y: 0.123456 >') + + +def test_SFParamStmt_factory(): + stmt = {'param': 'SF', 'a': '1.4', 'b': '0.9'} + sf = SFParamStmt.from_dict(stmt) + assert_equal(sf.param, 'SF') + assert_equal(sf.a, 1.4) + assert_equal(sf.b, 0.9) + + +def test_SFParamStmt_dump(): + stmt = {'param': 'SF', 'a': '1.4', 'b': '0.9'} + sf = SFParamStmt.from_dict(stmt) + assert_equal(sf.to_gerber(), '%SFA1.4B0.9*%') + + +def test_SFParamStmt_conversion(): + stmt = {'param': 'OF', 'a': '2.54', 'b': '25.4'} + of = SFParamStmt.from_dict(stmt) + of.units = 'metric' + of.to_metric() + + # No effect + assert_equal(of.a, 2.54) + assert_equal(of.b, 25.4) + + of.to_inch() + assert_equal(of.units, 'inch') + assert_equal(of.a, 0.1) + assert_equal(of.b, 1.0) + + # No effect + of.to_inch() + assert_equal(of.a, 0.1) + assert_equal(of.b, 1.0) + + stmt = {'param': 'OF', 'a': '0.1', 'b': '1.0'} + of = SFParamStmt.from_dict(stmt) + of.units = 'inch' + + # No effect + of.to_inch() + assert_equal(of.a, 0.1) + assert_equal(of.b, 1.0) + + of.to_metric() + assert_equal(of.units, 'metric') + assert_equal(of.a, 2.54) + assert_equal(of.b, 25.4) + + # No effect + of.to_metric() + assert_equal(of.a, 2.54) + assert_equal(of.b, 25.4) + + +def test_SFParamStmt_offset(): + s = SFParamStmt('OF', 0, 0) + s.offset(1, 0) + assert_equal(s.a, 1.) + assert_equal(s.b, 0.) + s.offset(0, 1) + assert_equal(s.a, 1.) + assert_equal(s.b, 1.) + + +def test_SFParamStmt_string(): + stmt = {'param': 'SF', 'a': '1.4', 'b': '0.9'} + sf = SFParamStmt.from_dict(stmt) + assert_equal(str(sf), '<Scale Factor: X: 1.4 Y: 0.9>') def test_LPParamStmt_factory(): @@ -159,6 +382,7 @@ def test_LPParamStmt_factory(): lp = LPParamStmt.from_dict(stmt) assert_equal(lp.lp, 'dark') + def test_LPParamStmt_dump(): """ Test LPParamStmt to_gerber() """ @@ -171,6 +395,130 @@ def test_LPParamStmt_dump(): assert_equal(lp.to_gerber(), '%LPD*%') +def test_LPParamStmt_string(): + """ Test LPParamStmt.__str__() + """ + stmt = {'param': 'LP', 'lp': 'D'} + lp = LPParamStmt.from_dict(stmt) + assert_equal(str(lp), '<Level Polarity: dark>') + + stmt = {'param': 'LP', 'lp': 'C'} + lp = LPParamStmt.from_dict(stmt) + assert_equal(str(lp), '<Level Polarity: clear>') + + +def test_AMParamStmt_factory(): + name = 'DONUTVAR' + macro = ( +'''0 Test Macro. * +1,1,1.5,0,0* +20,1,0.9,0,0.45,12,0.45,0* +21,1,6.8,1.2,3.4,0.6,0* +22,1,6.8,1.2,0,0,0* +4,1,4,0.1,0.1,0.5,0.1,0.5,0.5,0.1,0.5,0.1,0.1,0* +5,1,8,0,0,8,0* +6,0,0,5,0.5,0.5,2,0.1,6,0* +7,0,0,7,6,0.2,0* +8,THIS IS AN UNSUPPORTED PRIMITIVE* +''') + s = AMParamStmt.from_dict({'param': 'AM', 'name': name, 'macro': macro}) + s.build() + assert_equal(len(s.primitives), 10) + assert_true(isinstance(s.primitives[0], AMCommentPrimitive)) + assert_true(isinstance(s.primitives[1], AMCirclePrimitive)) + assert_true(isinstance(s.primitives[2], AMVectorLinePrimitive)) + assert_true(isinstance(s.primitives[3], AMCenterLinePrimitive)) + assert_true(isinstance(s.primitives[4], AMLowerLeftLinePrimitive)) + assert_true(isinstance(s.primitives[5], AMOutlinePrimitive)) + assert_true(isinstance(s.primitives[6], AMPolygonPrimitive)) + assert_true(isinstance(s.primitives[7], AMMoirePrimitive)) + assert_true(isinstance(s.primitives[8], AMThermalPrimitive)) + assert_true(isinstance(s.primitives[9], AMUnsupportPrimitive)) + + +def testAMParamStmt_conversion(): + name = 'POLYGON' + macro = '5,1,8,25.4,25.4,25.4,0*' + s = AMParamStmt.from_dict({'param': 'AM', 'name': name, 'macro': macro}) + + s.build() + s.units = 'metric' + + # No effect + s.to_metric() + assert_equal(s.primitives[0].position, (25.4, 25.4)) + assert_equal(s.primitives[0].diameter, 25.4) + + s.to_inch() + assert_equal(s.units, 'inch') + assert_equal(s.primitives[0].position, (1., 1.)) + assert_equal(s.primitives[0].diameter, 1.) + + # No effect + s.to_inch() + assert_equal(s.primitives[0].position, (1., 1.)) + assert_equal(s.primitives[0].diameter, 1.) + + macro = '5,1,8,1,1,1,0*' + s = AMParamStmt.from_dict({'param': 'AM', 'name': name, 'macro': macro}) + s.build() + s.units = 'inch' + + # No effect + s.to_inch() + assert_equal(s.primitives[0].position, (1., 1.)) + assert_equal(s.primitives[0].diameter, 1.) + + s.to_metric() + assert_equal(s.units, 'metric') + assert_equal(s.primitives[0].position, (25.4, 25.4)) + assert_equal(s.primitives[0].diameter, 25.4) + + # No effect + s.to_metric() + assert_equal(s.primitives[0].position, (25.4, 25.4)) + assert_equal(s.primitives[0].diameter, 25.4) + + +def test_AMParamStmt_dump(): + name = 'POLYGON' + macro = '5,1,8,25.4,25.4,25.4,0.0' + s = AMParamStmt.from_dict({'param': 'AM', 'name': name, 'macro': macro}) + s.build() + assert_equal(s.to_gerber(), '%AMPOLYGON*5,1,8,25.4,25.4,25.4,0.0*%') + + s = AMParamStmt.from_dict({'param': 'AM', 'name': 'OC8', 'macro': '5,1,8,0,0,1.08239X$1,22.5'}) + s.build() + assert_equal(s.to_gerber(), '%AMOC8*5,1,8,0,0,1.08239X$1,22.5*%') + + +def test_AMParamStmt_string(): + name = 'POLYGON' + macro = '5,1,8,25.4,25.4,25.4,0*' + s = AMParamStmt.from_dict({'param': 'AM', 'name': name, 'macro': macro}) + s.build() + assert_equal(str(s), '<Aperture Macro POLYGON: 5,1,8,25.4,25.4,25.4,0*>') + + +def test_ASParamStmt_factory(): + stmt = {'param': 'AS', 'mode': 'AXBY'} + s = ASParamStmt.from_dict(stmt) + assert_equal(s.param, 'AS') + assert_equal(s.mode, 'AXBY') + + +def test_ASParamStmt_dump(): + stmt = {'param': 'AS', 'mode': 'AXBY'} + s = ASParamStmt.from_dict(stmt) + assert_equal(s.to_gerber(), '%ASAXBY*%') + + +def test_ASParamStmt_string(): + stmt = {'param': 'AS', 'mode': 'AXBY'} + s = ASParamStmt.from_dict(stmt) + assert_equal(str(s), '<Axis Select: AXBY>') + + def test_INParamStmt_factory(): """ Test INParamStmt factory """ @@ -178,6 +526,7 @@ def test_INParamStmt_factory(): inp = INParamStmt.from_dict(stmt) assert_equal(inp.name, 'test') + def test_INParamStmt_dump(): """ Test INParamStmt to_gerber() """ @@ -186,6 +535,12 @@ def test_INParamStmt_dump(): assert_equal(inp.to_gerber(), '%INtest*%') +def test_INParamStmt_string(): + stmt = {'param': 'IN', 'name': 'test'} + inp = INParamStmt.from_dict(stmt) + assert_equal(str(inp), '<Image Name: test>') + + def test_LNParamStmt_factory(): """ Test LNParamStmt factory """ @@ -193,6 +548,7 @@ def test_LNParamStmt_factory(): lnp = LNParamStmt.from_dict(stmt) assert_equal(lnp.name, 'test') + def test_LNParamStmt_dump(): """ Test LNParamStmt to_gerber() """ @@ -200,6 +556,13 @@ def test_LNParamStmt_dump(): lnp = LNParamStmt.from_dict(stmt) assert_equal(lnp.to_gerber(), '%LNtest*%') + +def test_LNParamStmt_string(): + stmt = {'param': 'LN', 'name': 'test'} + lnp = LNParamStmt.from_dict(stmt) + assert_equal(str(lnp), '<Level Name: test>') + + def test_comment_stmt(): """ Test comment statement """ @@ -207,6 +570,7 @@ def test_comment_stmt(): assert_equal(stmt.type, 'COMMENT') assert_equal(stmt.comment, 'A comment') + def test_comment_stmt_dump(): """ Test CommentStmt to_gerber() """ @@ -214,12 +578,18 @@ def test_comment_stmt_dump(): assert_equal(stmt.to_gerber(), 'G04A comment*') +def test_comment_stmt_string(): + stmt = CommentStmt('A comment') + assert_equal(str(stmt), '<Comment: A comment>') + + def test_eofstmt(): """ Test EofStmt """ stmt = EofStmt() assert_equal(stmt.type, 'EOF') + def test_eofstmt_dump(): """ Test EofStmt to_gerber() """ @@ -227,6 +597,10 @@ def test_eofstmt_dump(): assert_equal(stmt.to_gerber(), 'M02*') +def test_eofstmt_string(): + assert_equal(str(EofStmt()), '<EOF Statement>') + + def test_quadmodestmt_factory(): """ Test QuadrantModeStmt.from_gerber() """ @@ -239,6 +613,7 @@ def test_quadmodestmt_factory(): stmt = QuadrantModeStmt.from_gerber(line) assert_equal(stmt.mode, 'multi-quadrant') + def test_quadmodestmt_validation(): """ Test QuadrantModeStmt input validation """ @@ -301,3 +676,275 @@ def test_unknownstmt_dump(): stmt = UnknownStmt(line) assert_equal(stmt.to_gerber(), line) + +def test_statement_string(): + """ Test Statement.__str__() + """ + stmt = Statement('PARAM') + assert_in('type=PARAM', str(stmt)) + stmt.test = 'PASS' + assert_in('test=PASS', str(stmt)) + assert_in('type=PARAM', str(stmt)) + + +def test_ADParamStmt_factory(): + """ Test ADParamStmt factory + """ + stmt = {'param': 'AD', 'd': 0, 'shape': 'C'} + ad = ADParamStmt.from_dict(stmt) + assert_equal(ad.d, 0) + assert_equal(ad.shape, 'C') + + stmt = {'param': 'AD', 'd': 1, 'shape': 'R'} + ad = ADParamStmt.from_dict(stmt) + assert_equal(ad.d, 1) + assert_equal(ad.shape, 'R') + + stmt = {'param': 'AD', 'd': 1, 'shape': 'C', "modifiers": "1.42"} + ad = ADParamStmt.from_dict(stmt) + assert_equal(ad.d, 1) + assert_equal(ad.shape, 'C') + assert_equal(ad.modifiers, [(1.42,)]) + + stmt = {'param': 'AD', 'd': 1, 'shape': 'C', "modifiers": "1.42X"} + ad = ADParamStmt.from_dict(stmt) + assert_equal(ad.d, 1) + assert_equal(ad.shape, 'C') + assert_equal(ad.modifiers, [(1.42,)]) + + stmt = {'param': 'AD', 'd': 1, 'shape': 'R', "modifiers": "1.42X1.24"} + ad = ADParamStmt.from_dict(stmt) + assert_equal(ad.d, 1) + assert_equal(ad.shape, 'R') + assert_equal(ad.modifiers, [(1.42, 1.24)]) + + +def test_ADParamStmt_conversion(): + stmt = {'param': 'AD', 'd': 0, 'shape': 'C', + 'modifiers': '25.4X25.4,25.4X25.4'} + ad = ADParamStmt.from_dict(stmt) + ad.units = 'metric' + + # No effect + ad.to_metric() + assert_equal(ad.modifiers[0], (25.4, 25.4)) + assert_equal(ad.modifiers[1], (25.4, 25.4)) + + ad.to_inch() + assert_equal(ad.units, 'inch') + assert_equal(ad.modifiers[0], (1., 1.)) + assert_equal(ad.modifiers[1], (1., 1.)) + + # No effect + ad.to_inch() + assert_equal(ad.modifiers[0], (1., 1.)) + assert_equal(ad.modifiers[1], (1., 1.)) + + stmt = {'param': 'AD', 'd': 0, 'shape': 'C', 'modifiers': '1X1,1X1'} + ad = ADParamStmt.from_dict(stmt) + ad.units = 'inch' + + # No effect + ad.to_inch() + assert_equal(ad.modifiers[0], (1., 1.)) + assert_equal(ad.modifiers[1], (1., 1.)) + + ad.to_metric() + assert_equal(ad.modifiers[0], (25.4, 25.4)) + assert_equal(ad.modifiers[1], (25.4, 25.4)) + + # No effect + ad.to_metric() + assert_equal(ad.modifiers[0], (25.4, 25.4)) + assert_equal(ad.modifiers[1], (25.4, 25.4)) + + +def test_ADParamStmt_dump(): + stmt = {'param': 'AD', 'd': 0, 'shape': 'C'} + ad = ADParamStmt.from_dict(stmt) + assert_equal(ad.to_gerber(), '%ADD0C*%') + stmt = {'param': 'AD', 'd': 0, 'shape': 'C', 'modifiers': '1X1,1X1'} + ad = ADParamStmt.from_dict(stmt) + assert_equal(ad.to_gerber(), '%ADD0C,1X1,1X1*%') + + +def test_ADPamramStmt_string(): + stmt = {'param': 'AD', 'd': 0, 'shape': 'C'} + ad = ADParamStmt.from_dict(stmt) + assert_equal(str(ad), '<Aperture Definition: 0: circle>') + + stmt = {'param': 'AD', 'd': 0, 'shape': 'R'} + ad = ADParamStmt.from_dict(stmt) + assert_equal(str(ad), '<Aperture Definition: 0: rectangle>') + + stmt = {'param': 'AD', 'd': 0, 'shape': 'O'} + ad = ADParamStmt.from_dict(stmt) + assert_equal(str(ad), '<Aperture Definition: 0: obround>') + + stmt = {'param': 'AD', 'd': 0, 'shape': 'test'} + ad = ADParamStmt.from_dict(stmt) + assert_equal(str(ad), '<Aperture Definition: 0: test>') + + +def test_MIParamStmt_factory(): + stmt = {'param': 'MI', 'a': 1, 'b': 1} + mi = MIParamStmt.from_dict(stmt) + assert_equal(mi.a, 1) + assert_equal(mi.b, 1) + + +def test_MIParamStmt_dump(): + stmt = {'param': 'MI', 'a': 1, 'b': 1} + mi = MIParamStmt.from_dict(stmt) + assert_equal(mi.to_gerber(), '%MIA1B1*%') + stmt = {'param': 'MI', 'a': 1} + mi = MIParamStmt.from_dict(stmt) + assert_equal(mi.to_gerber(), '%MIA1B0*%') + stmt = {'param': 'MI', 'b': 1} + mi = MIParamStmt.from_dict(stmt) + assert_equal(mi.to_gerber(), '%MIA0B1*%') + + +def test_MIParamStmt_string(): + stmt = {'param': 'MI', 'a': 1, 'b': 1} + mi = MIParamStmt.from_dict(stmt) + assert_equal(str(mi), '<Image Mirror: A=1 B=1>') + + stmt = {'param': 'MI', 'b': 1} + mi = MIParamStmt.from_dict(stmt) + assert_equal(str(mi), '<Image Mirror: A=0 B=1>') + + stmt = {'param': 'MI', 'a': 1} + mi = MIParamStmt.from_dict(stmt) + assert_equal(str(mi), '<Image Mirror: A=1 B=0>') + + +def test_coordstmt_ctor(): + cs = CoordStmt('G04', 0.0, 0.1, 0.2, 0.3, 'D01', FileSettings()) + assert_equal(cs.function, 'G04') + assert_equal(cs.x, 0.0) + assert_equal(cs.y, 0.1) + assert_equal(cs.i, 0.2) + assert_equal(cs.j, 0.3) + assert_equal(cs.op, 'D01') + + +def test_coordstmt_factory(): + stmt = {'function': 'G04', 'x': '0', 'y': '001', + 'i': '002', 'j': '003', 'op': 'D01'} + cs = CoordStmt.from_dict(stmt, FileSettings()) + assert_equal(cs.function, 'G04') + assert_equal(cs.x, 0.0) + assert_equal(cs.y, 0.1) + assert_equal(cs.i, 0.2) + assert_equal(cs.j, 0.3) + assert_equal(cs.op, 'D01') + + +def test_coordstmt_dump(): + cs = CoordStmt('G04', 0.0, 0.1, 0.2, 0.3, 'D01', FileSettings()) + assert_equal(cs.to_gerber(FileSettings()), 'G04X0Y001I002J003D01*') + + +def test_coordstmt_conversion(): + cs = CoordStmt('G71', 25.4, 25.4, 25.4, 25.4, 'D01', FileSettings()) + cs.units = 'metric' + + # No effect + cs.to_metric() + assert_equal(cs.x, 25.4) + assert_equal(cs.y, 25.4) + assert_equal(cs.i, 25.4) + assert_equal(cs.j, 25.4) + assert_equal(cs.function, 'G71') + + cs.to_inch() + assert_equal(cs.units, 'inch') + assert_equal(cs.x, 1.) + assert_equal(cs.y, 1.) + assert_equal(cs.i, 1.) + assert_equal(cs.j, 1.) + assert_equal(cs.function, 'G70') + + # No effect + cs.to_inch() + assert_equal(cs.x, 1.) + assert_equal(cs.y, 1.) + assert_equal(cs.i, 1.) + assert_equal(cs.j, 1.) + assert_equal(cs.function, 'G70') + + cs = CoordStmt('G70', 1., 1., 1., 1., 'D01', FileSettings()) + cs.units = 'inch' + + # No effect + cs.to_inch() + assert_equal(cs.x, 1.) + assert_equal(cs.y, 1.) + assert_equal(cs.i, 1.) + assert_equal(cs.j, 1.) + assert_equal(cs.function, 'G70') + + cs.to_metric() + assert_equal(cs.x, 25.4) + assert_equal(cs.y, 25.4) + assert_equal(cs.i, 25.4) + assert_equal(cs.j, 25.4) + assert_equal(cs.function, 'G71') + + # No effect + cs.to_metric() + assert_equal(cs.x, 25.4) + assert_equal(cs.y, 25.4) + assert_equal(cs.i, 25.4) + assert_equal(cs.j, 25.4) + assert_equal(cs.function, 'G71') + + +def test_coordstmt_offset(): + c = CoordStmt('G71', 0, 0, 0, 0, 'D01', FileSettings()) + c.offset(1, 0) + assert_equal(c.x, 1.) + assert_equal(c.y, 0.) + assert_equal(c.i, 1.) + assert_equal(c.j, 0.) + c.offset(0, 1) + assert_equal(c.x, 1.) + assert_equal(c.y, 1.) + assert_equal(c.i, 1.) + assert_equal(c.j, 1.) + + +def test_coordstmt_string(): + cs = CoordStmt('G04', 0, 1, 2, 3, 'D01', FileSettings()) + assert_equal(str(cs), + '<Coordinate Statement: Fn: G04 X: 0 Y: 1 I: 2 J: 3 Op: Lights On>') + cs = CoordStmt('G04', None, None, None, None, 'D02', FileSettings()) + assert_equal(str(cs), '<Coordinate Statement: Fn: G04 Op: Lights Off>') + cs = CoordStmt('G04', None, None, None, None, 'D03', FileSettings()) + assert_equal(str(cs), '<Coordinate Statement: Fn: G04 Op: Flash>') + cs = CoordStmt('G04', None, None, None, None, 'TEST', FileSettings()) + assert_equal(str(cs), '<Coordinate Statement: Fn: G04 Op: TEST>') + + +def test_aperturestmt_ctor(): + ast = ApertureStmt(3, False) + assert_equal(ast.d, 3) + assert_equal(ast.deprecated, False) + ast = ApertureStmt(4, True) + assert_equal(ast.d, 4) + assert_equal(ast.deprecated, True) + ast = ApertureStmt(4, 1) + assert_equal(ast.d, 4) + assert_equal(ast.deprecated, True) + ast = ApertureStmt(3) + assert_equal(ast.d, 3) + assert_equal(ast.deprecated, False) + + +def test_aperturestmt_dump(): + ast = ApertureStmt(3, False) + assert_equal(ast.to_gerber(), 'D3*') + ast = ApertureStmt(3, True) + assert_equal(ast.to_gerber(), 'G54D3*') + assert_equal(str(ast), '<Aperture: 3>') diff --git a/gerber/tests/test_ipc356.py b/gerber/tests/test_ipc356.py new file mode 100644 index 0000000..45bb01b --- /dev/null +++ b/gerber/tests/test_ipc356.py @@ -0,0 +1,139 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# Author: Hamilton Kibbe <ham@hamiltonkib.be> +from ..ipc356 import * +from ..cam import FileSettings +from .tests import * + +import os + +IPC_D_356_FILE = os.path.join(os.path.dirname(__file__), + 'resources/ipc-d-356.ipc') + + +def test_read(): + ipcfile = read(IPC_D_356_FILE) + assert(isinstance(ipcfile, IPC_D_356)) + + +def test_parser(): + ipcfile = read(IPC_D_356_FILE) + assert_equal(ipcfile.settings.units, 'inch') + assert_equal(ipcfile.settings.angle_units, 'degrees') + assert_equal(len(ipcfile.comments), 3) + assert_equal(len(ipcfile.parameters), 4) + assert_equal(len(ipcfile.test_records), 105) + assert_equal(len(ipcfile.components), 21) + assert_equal(len(ipcfile.vias), 14) + assert_equal(ipcfile.test_records[-1].net_name, 'A_REALLY_LONG_NET_NAME') + assert_equal(ipcfile.outlines[0].type, 'BOARD_EDGE') + assert_equal(set(ipcfile.outlines[0].points), + {(0., 0.), (2.25, 0.), (2.25, 1.5), (0., 1.5), (0.13, 0.024)}) + + +def test_comment(): + c = IPC356_Comment('Layer Stackup:') + assert_equal(c.comment, 'Layer Stackup:') + c = IPC356_Comment.from_line('C Layer Stackup: ') + assert_equal(c.comment, 'Layer Stackup:') + assert_raises(ValueError, IPC356_Comment.from_line, 'P JOB') + assert_equal(str(c), '<IPC-D-356 Comment: Layer Stackup:>') + + +def test_parameter(): + p = IPC356_Parameter('VER', 'IPC-D-356A') + assert_equal(p.parameter, 'VER') + assert_equal(p.value, 'IPC-D-356A') + p = IPC356_Parameter.from_line('P VER IPC-D-356A ') + assert_equal(p.parameter, 'VER') + assert_equal(p.value, 'IPC-D-356A') + assert_raises(ValueError, IPC356_Parameter.from_line, + 'C Layer Stackup: ') + assert_equal(str(p), '<IPC-D-356 Parameter: VER=IPC-D-356A>') + + +def test_eof(): + e = IPC356_EndOfFile() + assert_equal(e.to_netlist(), '999') + assert_equal(str(e), '<IPC-D-356 EOF>') + + +def test_outline(): + type = 'BOARD_EDGE' + points = [(0.01, 0.01), (2., 2.), (4., 2.), (4., 6.)] + b = IPC356_Outline(type, points) + assert_equal(b.type, type) + assert_equal(b.points, points) + b = IPC356_Outline.from_line('389BOARD_EDGE X100Y100 X20000Y20000 X40000 Y60000', + FileSettings(units='inch')) + assert_equal(b.type, 'BOARD_EDGE') + assert_equal(b.points, points) + + +def test_test_record(): + assert_raises(ValueError, IPC356_TestRecord.from_line, + 'P JOB', FileSettings()) + record_string = '317+5VDC VIA - D0150PA00X 006647Y 012900X0000 S3' + r = IPC356_TestRecord.from_line(record_string, FileSettings(units='inch')) + assert_equal(r.feature_type, 'through-hole') + assert_equal(r.net_name, '+5VDC') + assert_equal(r.id, 'VIA') + assert_almost_equal(r.hole_diameter, 0.015) + assert_true(r.plated) + assert_equal(r.access, 'both') + assert_almost_equal(r.x_coord, 0.6647) + assert_almost_equal(r.y_coord, 1.29) + assert_equal(r.rect_x, 0.) + assert_equal(r.soldermask_info, 'both') + r = IPC356_TestRecord.from_line(record_string, FileSettings(units='metric')) + assert_almost_equal(r.hole_diameter, 0.15) + assert_almost_equal(r.x_coord, 6.647) + assert_almost_equal(r.y_coord, 12.9) + assert_equal(r.rect_x, 0.) + assert_equal(str(r), '<IPC-D-356 +5VDC Test Record: through-hole>') + + record_string = '327+3.3VDC R40 -1 PA01X 032100Y 007124X0236Y0315R180 S0' + r = IPC356_TestRecord.from_line(record_string, FileSettings(units='inch')) + assert_equal(r.feature_type, 'smt') + assert_equal(r.net_name, '+3.3VDC') + assert_equal(r.id, 'R40') + assert_equal(r.pin, '1') + assert_true(r.plated) + assert_equal(r.access, 'top') + assert_almost_equal(r.x_coord, 3.21) + assert_almost_equal(r.y_coord, 0.7124) + assert_almost_equal(r.rect_x, 0.0236) + assert_almost_equal(r.rect_y, 0.0315) + assert_equal(r.rect_rotation, 180) + assert_equal(r.soldermask_info, 'none') + r = IPC356_TestRecord.from_line( + record_string, FileSettings(units='metric')) + assert_almost_equal(r.x_coord, 32.1) + assert_almost_equal(r.y_coord, 7.124) + assert_almost_equal(r.rect_x, 0.236) + assert_almost_equal(r.rect_y, 0.315) + + record_string = '317 J4 -M2 D0330PA00X 012447Y 008030X0000 S1' + r = IPC356_TestRecord.from_line(record_string, FileSettings(units='inch')) + assert_equal(r.feature_type, 'through-hole') + assert_equal(r.id, 'J4') + assert_equal(r.pin, 'M2') + assert_almost_equal(r.hole_diameter, 0.033) + assert_true(r.plated) + assert_equal(r.access, 'both') + assert_almost_equal(r.x_coord, 1.2447) + assert_almost_equal(r.y_coord, 0.8030) + assert_almost_equal(r.rect_x, 0.) + assert_equal(r.soldermask_info, 'primary side') + + record_string = '317SCL COMMUNICATION-1 D 40PA00X 34000Y 20000X 600Y1200R270 ' + r = IPC356_TestRecord.from_line(record_string, FileSettings(units='inch')) + assert_equal(r.feature_type, 'through-hole') + assert_equal(r.net_name, 'SCL') + assert_equal(r.id, 'COMMUNICATION') + assert_equal(r.pin, '1') + assert_almost_equal(r.hole_diameter, 0.004) + assert_true(r.plated) + assert_almost_equal(r.x_coord, 3.4) + assert_almost_equal(r.y_coord, 2.0) diff --git a/gerber/tests/test_layers.py b/gerber/tests/test_layers.py new file mode 100644 index 0000000..3f2bcfc --- /dev/null +++ b/gerber/tests/test_layers.py @@ -0,0 +1,33 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# Author: Hamilton Kibbe <ham@hamiltonkib.be> + +from .tests import * +from ..layers import guess_layer_class, hints + + +def test_guess_layer_class(): + """ Test layer type inferred correctly from filename + """ + + # Add any specific test cases here (filename, layer_class) + test_vectors = [(None, 'unknown'), ('NCDRILL.TXT', 'unknown'), + ('example_board.gtl', 'top'), + ('exampmle_board.sst', 'topsilk'), + ('ipc-d-356.ipc', 'ipc_netlist'), ] + + for hint in hints: + for ext in hint.ext: + assert_equal(hint.layer, guess_layer_class('board.{}'.format(ext))) + for name in hint.name: + assert_equal(hint.layer, guess_layer_class('{}.pho'.format(name))) + + for filename, layer_class in test_vectors: + assert_equal(layer_class, guess_layer_class(filename)) + + +def test_sort_layers(): + """ Test layer ordering + """ + pass diff --git a/gerber/tests/test_primitives.py b/gerber/tests/test_primitives.py new file mode 100644 index 0000000..c49b558 --- /dev/null +++ b/gerber/tests/test_primitives.py @@ -0,0 +1,1270 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# Author: Hamilton Kibbe <ham@hamiltonkib.be> +from operator import add + +from ..primitives import * +from .tests import * + + +def test_primitive_smoketest(): + p = Primitive() + try: + p.bounding_box + assert_false(True, 'should have thrown the exception') + except NotImplementedError: + pass + #assert_raises(NotImplementedError, p.bounding_box) + + p.to_metric() + p.to_inch() + try: + p.offset(1, 1) + assert_false(True, 'should have thrown the exception') + except NotImplementedError: + pass + + +def test_line_angle(): + """ Test Line primitive angle calculation + """ + cases = [((0, 0), (1, 0), math.radians(0)), + ((0, 0), (1, 1), math.radians(45)), + ((0, 0), (0, 1), math.radians(90)), + ((0, 0), (-1, 1), math.radians(135)), + ((0, 0), (-1, 0), math.radians(180)), + ((0, 0), (-1, -1), math.radians(225)), + ((0, 0), (0, -1), math.radians(270)), + ((0, 0), (1, -1), math.radians(315)), ] + for start, end, expected in cases: + l = Line(start, end, 0) + line_angle = (l.angle + 2 * math.pi) % (2 * math.pi) + assert_almost_equal(line_angle, expected) + + +def test_line_bounds(): + """ Test Line primitive bounding box calculation + """ + cases = [((0, 0), (1, 1), ((-1, 2), (-1, 2))), + ((-1, -1), (1, 1), ((-2, 2), (-2, 2))), + ((1, 1), (-1, -1), ((-2, 2), (-2, 2))), + ((-1, 1), (1, -1), ((-2, 2), (-2, 2))), ] + + c = Circle((0, 0), 2) + r = Rectangle((0, 0), 2, 2) + for shape in (c, r): + for start, end, expected in cases: + l = Line(start, end, shape) + assert_equal(l.bounding_box, expected) + # Test a non-square rectangle + r = Rectangle((0, 0), 3, 2) + cases = [((0, 0), (1, 1), ((-1.5, 2.5), (-1, 2))), + ((-1, -1), (1, 1), ((-2.5, 2.5), (-2, 2))), + ((1, 1), (-1, -1), ((-2.5, 2.5), (-2, 2))), + ((-1, 1), (1, -1), ((-2.5, 2.5), (-2, 2))), ] + for start, end, expected in cases: + l = Line(start, end, r) + assert_equal(l.bounding_box, expected) + + +def test_line_vertices(): + c = Circle((0, 0), 2) + l = Line((0, 0), (1, 1), c) + assert_equal(l.vertices, None) + + # All 4 compass points, all 4 quadrants and the case where start == end + test_cases = [((0, 0), (1, 0), ((-1, -1), (-1, 1), (2, 1), (2, -1))), + ((0, 0), (1, 1), ((-1, -1), (-1, 1), + (0, 2), (2, 2), (2, 0), (1, -1))), + ((0, 0), (0, 1), ((-1, -1), (-1, 2), (1, 2), (1, -1))), + ((0, 0), (-1, 1), ((-1, -1), (-2, 0), + (-2, 2), (0, 2), (1, 1), (1, -1))), + ((0, 0), (-1, 0), ((-2, -1), (-2, 1), (1, 1), (1, -1))), + ((0, 0), (-1, -1), ((-2, -2), (1, -1), + (1, 1), (-1, 1), (-2, 0), (0, -2))), + ((0, 0), (0, -1), ((-1, -2), (-1, 1), (1, 1), (1, -2))), + ((0, 0), (1, -1), ((-1, -1), (0, -2), + (2, -2), (2, 0), (1, 1), (-1, 1))), + ((0, 0), (0, 0), ((-1, -1), (-1, 1), (1, 1), (1, -1))), ] + r = Rectangle((0, 0), 2, 2) + + for start, end, vertices in test_cases: + l = Line(start, end, r) + assert_equal(set(vertices), set(l.vertices)) + + +def test_line_conversion(): + c = Circle((0, 0), 25.4, units='metric') + l = Line((2.54, 25.4), (254.0, 2540.0), c, units='metric') + + # No effect + l.to_metric() + assert_equal(l.start, (2.54, 25.4)) + assert_equal(l.end, (254.0, 2540.0)) + assert_equal(l.aperture.diameter, 25.4) + + l.to_inch() + assert_equal(l.start, (0.1, 1.0)) + assert_equal(l.end, (10.0, 100.0)) + assert_equal(l.aperture.diameter, 1.0) + + # No effect + l.to_inch() + assert_equal(l.start, (0.1, 1.0)) + assert_equal(l.end, (10.0, 100.0)) + assert_equal(l.aperture.diameter, 1.0) + + c = Circle((0, 0), 1.0, units='inch') + l = Line((0.1, 1.0), (10.0, 100.0), c, units='inch') + + # No effect + l.to_inch() + assert_equal(l.start, (0.1, 1.0)) + assert_equal(l.end, (10.0, 100.0)) + assert_equal(l.aperture.diameter, 1.0) + + l.to_metric() + assert_equal(l.start, (2.54, 25.4)) + assert_equal(l.end, (254.0, 2540.0)) + assert_equal(l.aperture.diameter, 25.4) + + # No effect + l.to_metric() + assert_equal(l.start, (2.54, 25.4)) + assert_equal(l.end, (254.0, 2540.0)) + assert_equal(l.aperture.diameter, 25.4) + + r = Rectangle((0, 0), 25.4, 254.0, units='metric') + l = Line((2.54, 25.4), (254.0, 2540.0), r, units='metric') + l.to_inch() + assert_equal(l.start, (0.1, 1.0)) + assert_equal(l.end, (10.0, 100.0)) + assert_equal(l.aperture.width, 1.0) + assert_equal(l.aperture.height, 10.0) + + r = Rectangle((0, 0), 1.0, 10.0, units='inch') + l = Line((0.1, 1.0), (10.0, 100.0), r, units='inch') + l.to_metric() + assert_equal(l.start, (2.54, 25.4)) + assert_equal(l.end, (254.0, 2540.0)) + assert_equal(l.aperture.width, 25.4) + assert_equal(l.aperture.height, 254.0) + + +def test_line_offset(): + c = Circle((0, 0), 1) + l = Line((0, 0), (1, 1), c) + l.offset(1, 0) + assert_equal(l.start, (1., 0.)) + assert_equal(l.end, (2., 1.)) + l.offset(0, 1) + assert_equal(l.start, (1., 1.)) + assert_equal(l.end, (2., 2.)) + + +def test_arc_radius(): + """ Test Arc primitive radius calculation + """ + cases = [((-3, 4), (5, 0), (0, 0), 5), + ((0, 1), (1, 0), (0, 0), 1), ] + + for start, end, center, radius in cases: + a = Arc(start, end, center, 'clockwise', 0, 'single-quadrant') + assert_equal(a.radius, radius) + + +def test_arc_sweep_angle(): + """ Test Arc primitive sweep angle calculation + """ + cases = [((1, 0), (0, 1), (0, 0), 'counterclockwise', math.radians(90)), + ((1, 0), (0, 1), (0, 0), 'clockwise', math.radians(270)), + ((1, 0), (-1, 0), (0, 0), 'clockwise', math.radians(180)), + ((1, 0), (-1, 0), (0, 0), 'counterclockwise', math.radians(180)), ] + + for start, end, center, direction, sweep in cases: + c = Circle((0,0), 1) + a = Arc(start, end, center, direction, c, 'single-quadrant') + assert_equal(a.sweep_angle, sweep) + + +def test_arc_bounds(): + """ Test Arc primitive bounding box calculation + """ + cases = [((1, 0), (0, 1), (0, 0), 'clockwise', ((-1.5, 1.5), (-1.5, 1.5))), + ((1, 0), (0, 1), (0, 0), 'counterclockwise', + ((-0.5, 1.5), (-0.5, 1.5))), + # TODO: ADD MORE TEST CASES HERE + ] + for start, end, center, direction, bounds in cases: + c = Circle((0,0), 1) + a = Arc(start, end, center, direction, c, 'single-quadrant') + assert_equal(a.bounding_box, bounds) + + +def test_arc_conversion(): + c = Circle((0, 0), 25.4, units='metric') + a = Arc((2.54, 25.4), (254.0, 2540.0), (25400.0, 254000.0), + 'clockwise', c, 'single-quadrant', units='metric') + + # No effect + a.to_metric() + assert_equal(a.start, (2.54, 25.4)) + assert_equal(a.end, (254.0, 2540.0)) + assert_equal(a.center, (25400.0, 254000.0)) + assert_equal(a.aperture.diameter, 25.4) + + a.to_inch() + assert_equal(a.start, (0.1, 1.0)) + assert_equal(a.end, (10.0, 100.0)) + assert_equal(a.center, (1000.0, 10000.0)) + assert_equal(a.aperture.diameter, 1.0) + + # no effect + a.to_inch() + assert_equal(a.start, (0.1, 1.0)) + assert_equal(a.end, (10.0, 100.0)) + assert_equal(a.center, (1000.0, 10000.0)) + 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.to_metric() + assert_equal(a.start, (2.54, 25.4)) + assert_equal(a.end, (254.0, 2540.0)) + assert_equal(a.center, (25400.0, 254000.0)) + assert_equal(a.aperture.diameter, 25.4) + + +def test_arc_offset(): + c = Circle((0, 0), 1) + a = Arc((0, 0), (1, 1), (2, 2), 'clockwise', c, 'single-quadrant') + a.offset(1, 0) + assert_equal(a.start, (1., 0.)) + assert_equal(a.end, (2., 1.)) + assert_equal(a.center, (3., 2.)) + a.offset(0, 1) + assert_equal(a.start, (1., 1.)) + assert_equal(a.end, (2., 2.)) + assert_equal(a.center, (3., 3.)) + + +def test_circle_radius(): + """ Test Circle primitive radius calculation + """ + 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 + """ + c = Circle((1, 1), 2) + 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, None) + + c.to_inch() + assert_equal(c.position, (0.1, 1.)) + assert_equal(c.diameter, 10.) + assert_equal(c.hole_diameter, None) + + # no effect + c.to_inch() + assert_equal(c.position, (0.1, 1.)) + assert_equal(c.diameter, 10.) + assert_equal(c.hole_diameter, None) + + # Circle initially metric, with hole + c = Circle((2.54, 25.4), 254.0, 127.0, units='metric') + + c.to_metric() #shouldn't do antyhing + assert_equal(c.position, (2.54, 25.4)) + assert_equal(c.diameter, 254.) + assert_equal(c.hole_diameter, 127.) + + c.to_inch() + assert_equal(c.position, (0.1, 1.)) + assert_equal(c.diameter, 10.) + assert_equal(c.hole_diameter, 5.) + + # no effect + c.to_inch() + assert_equal(c.position, (0.1, 1.)) + assert_equal(c.diameter, 10.) + assert_equal(c.hole_diameter, 5.) + + # Circle initially inch, no hole + c = Circle((0.1, 1.0), 10.0, units='inch') + # No effect + c.to_inch() + assert_equal(c.position, (0.1, 1.)) + assert_equal(c.diameter, 10.) + assert_equal(c.hole_diameter, None) + + c.to_metric() + assert_equal(c.position, (2.54, 25.4)) + assert_equal(c.diameter, 254.) + assert_equal(c.hole_diameter, None) + + # no effect + c.to_metric() + assert_equal(c.position, (2.54, 25.4)) + assert_equal(c.diameter, 254.) + assert_equal(c.hole_diameter, None) + + c = Circle((0.1, 1.0), 10.0, 5.0, units='inch') + #No effect + c.to_inch() + assert_equal(c.position, (0.1, 1.)) + assert_equal(c.diameter, 10.) + assert_equal(c.hole_diameter, 5.) + + c.to_metric() + assert_equal(c.position, (2.54, 25.4)) + assert_equal(c.diameter, 254.) + assert_equal(c.hole_diameter, 127.) + + # no effect + c.to_metric() + assert_equal(c.position, (2.54, 25.4)) + assert_equal(c.diameter, 254.) + assert_equal(c.hole_diameter, 127.) + + + +def test_circle_offset(): + c = Circle((0, 0), 1) + c.offset(1, 0) + assert_equal(c.position, (1., 0.)) + c.offset(0, 1) + assert_equal(c.position, (1., 1.)) + + +def test_ellipse_ctor(): + """ Test ellipse creation + """ + e = Ellipse((2, 2), 3, 2) + assert_equal(e.position, (2, 2)) + assert_equal(e.width, 3) + assert_equal(e.height, 2) + + +def test_ellipse_bounds(): + """ Test ellipse bounding box calculation + """ + e = Ellipse((2, 2), 4, 2) + assert_equal(e.bounding_box, ((0, 4), (1, 3))) + e = Ellipse((2, 2), 4, 2, rotation=90) + assert_equal(e.bounding_box, ((1, 3), (0, 4))) + e = Ellipse((2, 2), 4, 2, rotation=180) + assert_equal(e.bounding_box, ((0, 4), (1, 3))) + e = Ellipse((2, 2), 4, 2, rotation=270) + assert_equal(e.bounding_box, ((1, 3), (0, 4))) + + +def test_ellipse_conversion(): + e = Ellipse((2.54, 25.4), 254.0, 2540., units='metric') + + # No effect + e.to_metric() + assert_equal(e.position, (2.54, 25.4)) + assert_equal(e.width, 254.) + assert_equal(e.height, 2540.) + + e.to_inch() + assert_equal(e.position, (0.1, 1.)) + assert_equal(e.width, 10.) + assert_equal(e.height, 100.) + + # No effect + e.to_inch() + assert_equal(e.position, (0.1, 1.)) + assert_equal(e.width, 10.) + assert_equal(e.height, 100.) + + e = Ellipse((0.1, 1.), 10.0, 100., units='inch') + + # no effect + e.to_inch() + assert_equal(e.position, (0.1, 1.)) + assert_equal(e.width, 10.) + assert_equal(e.height, 100.) + + e.to_metric() + assert_equal(e.position, (2.54, 25.4)) + assert_equal(e.width, 254.) + assert_equal(e.height, 2540.) + + # No effect + e.to_metric() + assert_equal(e.position, (2.54, 25.4)) + assert_equal(e.width, 254.) + assert_equal(e.height, 2540.) + + +def test_ellipse_offset(): + e = Ellipse((0, 0), 1, 2) + e.offset(1, 0) + assert_equal(e.position, (1., 0.)) + e.offset(0, 1) + assert_equal(e.position, (1., 1.)) + + +def test_rectangle_ctor(): + """ Test rectangle creation + """ + test_cases = (((0, 0), 1, 1), ((0, 0), 1, 2), ((1, 1), 1, 2)) + for pos, width, height in test_cases: + r = Rectangle(pos, width, height) + 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 + """ + r = Rectangle((0, 0), 2, 2) + xbounds, ybounds = r.bounding_box + assert_array_almost_equal(xbounds, (-1, 1)) + assert_array_almost_equal(ybounds, (-1, 1)) + r = Rectangle((0, 0), 2, 2, rotation=45) + xbounds, ybounds = r.bounding_box + assert_array_almost_equal(xbounds, (-math.sqrt(2), math.sqrt(2))) + 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() + assert_equal(r.position, (2.54, 25.4)) + assert_equal(r.width, 254.0) + assert_equal(r.height, 2540.0) + + r.to_inch() + assert_equal(r.position, (0.1, 1.0)) + assert_equal(r.width, 10.0) + assert_equal(r.height, 100.0) + + r.to_inch() + 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)) + assert_equal(r.width, 10.0) + assert_equal(r.height, 100.0) + + r.to_metric() + assert_equal(r.position, (2.54, 25.4)) + assert_equal(r.width, 254.0) + assert_equal(r.height, 2540.0) + + r.to_metric() + 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) + r.offset(1, 0) + assert_equal(r.position, (1., 0.)) + r.offset(0, 1) + assert_equal(r.position, (1., 1.)) + + +def test_diamond_ctor(): + """ Test diamond creation + """ + test_cases = (((0, 0), 1, 1), ((0, 0), 1, 2), ((1, 1), 1, 2)) + for pos, width, height in test_cases: + d = Diamond(pos, width, height) + assert_equal(d.position, pos) + assert_equal(d.width, width) + assert_equal(d.height, height) + + +def test_diamond_bounds(): + """ Test diamond bounding box calculation + """ + d = Diamond((0, 0), 2, 2) + xbounds, ybounds = d.bounding_box + assert_array_almost_equal(xbounds, (-1, 1)) + assert_array_almost_equal(ybounds, (-1, 1)) + d = Diamond((0, 0), math.sqrt(2), math.sqrt(2), rotation=45) + xbounds, ybounds = d.bounding_box + assert_array_almost_equal(xbounds, (-1, 1)) + assert_array_almost_equal(ybounds, (-1, 1)) + + +def test_diamond_conversion(): + d = Diamond((2.54, 25.4), 254.0, 2540.0, units='metric') + + d.to_metric() + assert_equal(d.position, (2.54, 25.4)) + assert_equal(d.width, 254.0) + assert_equal(d.height, 2540.0) + + d.to_inch() + assert_equal(d.position, (0.1, 1.0)) + assert_equal(d.width, 10.0) + assert_equal(d.height, 100.0) + + d.to_inch() + assert_equal(d.position, (0.1, 1.0)) + assert_equal(d.width, 10.0) + assert_equal(d.height, 100.0) + + d = Diamond((0.1, 1.0), 10.0, 100.0, units='inch') + + d.to_inch() + assert_equal(d.position, (0.1, 1.0)) + assert_equal(d.width, 10.0) + assert_equal(d.height, 100.0) + + d.to_metric() + assert_equal(d.position, (2.54, 25.4)) + assert_equal(d.width, 254.0) + assert_equal(d.height, 2540.0) + + d.to_metric() + assert_equal(d.position, (2.54, 25.4)) + assert_equal(d.width, 254.0) + assert_equal(d.height, 2540.0) + + +def test_diamond_offset(): + d = Diamond((0, 0), 1, 2) + d.offset(1, 0) + assert_equal(d.position, (1., 0.)) + d.offset(0, 1) + assert_equal(d.position, (1., 1.)) + + +def test_chamfer_rectangle_ctor(): + """ Test chamfer rectangle creation + """ + test_cases = (((0, 0), 1, 1, 0.2, (True, True, False, False)), + ((0, 0), 1, 2, 0.3, (True, True, True, True)), + ((1, 1), 1, 2, 0.4, (False, False, False, False))) + for pos, width, height, chamfer, corners in test_cases: + r = ChamferRectangle(pos, width, height, chamfer, corners) + assert_equal(r.position, pos) + assert_equal(r.width, width) + assert_equal(r.height, height) + assert_equal(r.chamfer, chamfer) + assert_array_almost_equal(r.corners, corners) + + +def test_chamfer_rectangle_bounds(): + """ Test chamfer rectangle bounding box calculation + """ + r = ChamferRectangle((0, 0), 2, 2, 0.2, (True, True, False, False)) + xbounds, ybounds = r.bounding_box + assert_array_almost_equal(xbounds, (-1, 1)) + assert_array_almost_equal(ybounds, (-1, 1)) + r = ChamferRectangle( + (0, 0), 2, 2, 0.2, (True, True, False, False), rotation=45) + xbounds, ybounds = r.bounding_box + assert_array_almost_equal(xbounds, (-math.sqrt(2), math.sqrt(2))) + assert_array_almost_equal(ybounds, (-math.sqrt(2), math.sqrt(2))) + + +def test_chamfer_rectangle_conversion(): + r = ChamferRectangle((2.54, 25.4), 254.0, 2540.0, 0.254, + (True, True, False, False), 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.chamfer, 0.254) + + 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.chamfer, 0.01) + + 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.chamfer, 0.01) + + r = ChamferRectangle((0.1, 1.0), 10.0, 100.0, 0.01, + (True, True, False, False), 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.chamfer, 0.01) + + 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.chamfer, 0.254) + + 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.chamfer, 0.254) + + +def test_chamfer_rectangle_offset(): + r = ChamferRectangle((0, 0), 1, 2, 0.01, (True, True, False, False)) + r.offset(1, 0) + assert_equal(r.position, (1., 0.)) + r.offset(0, 1) + assert_equal(r.position, (1., 1.)) + + +def test_round_rectangle_ctor(): + """ Test round rectangle creation + """ + test_cases = (((0, 0), 1, 1, 0.2, (True, True, False, False)), + ((0, 0), 1, 2, 0.3, (True, True, True, True)), + ((1, 1), 1, 2, 0.4, (False, False, False, False))) + for pos, width, height, radius, corners in test_cases: + r = RoundRectangle(pos, width, height, radius, corners) + assert_equal(r.position, pos) + assert_equal(r.width, width) + assert_equal(r.height, height) + assert_equal(r.radius, radius) + assert_array_almost_equal(r.corners, corners) + + +def test_round_rectangle_bounds(): + """ Test round rectangle bounding box calculation + """ + r = RoundRectangle((0, 0), 2, 2, 0.2, (True, True, False, False)) + xbounds, ybounds = r.bounding_box + assert_array_almost_equal(xbounds, (-1, 1)) + assert_array_almost_equal(ybounds, (-1, 1)) + r = RoundRectangle((0, 0), 2, 2, 0.2, + (True, True, False, False), rotation=45) + xbounds, ybounds = r.bounding_box + assert_array_almost_equal(xbounds, (-math.sqrt(2), math.sqrt(2))) + assert_array_almost_equal(ybounds, (-math.sqrt(2), math.sqrt(2))) + + +def test_round_rectangle_conversion(): + r = RoundRectangle((2.54, 25.4), 254.0, 2540.0, 0.254, + (True, True, False, False), 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.radius, 0.254) + + 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.radius, 0.01) + + 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.radius, 0.01) + + r = RoundRectangle((0.1, 1.0), 10.0, 100.0, 0.01, + (True, True, False, False), 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.radius, 0.01) + + 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.radius, 0.254) + + 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.radius, 0.254) + + +def test_round_rectangle_offset(): + r = RoundRectangle((0, 0), 1, 2, 0.01, (True, True, False, False)) + r.offset(1, 0) + assert_equal(r.position, (1., 0.)) + r.offset(0, 1) + assert_equal(r.position, (1., 1.)) + + +def test_obround_ctor(): + """ Test obround creation + """ + test_cases = (((0, 0), 1, 1), + ((0, 0), 1, 2), + ((1, 1), 1, 2)) + for pos, width, height in test_cases: + o = Obround(pos, width, height) + assert_equal(o.position, pos) + assert_equal(o.width, width) + assert_equal(o.height, height) + + +def test_obround_bounds(): + """ Test obround bounding box calculation + """ + o = Obround((2, 2), 2, 4) + xbounds, ybounds = o.bounding_box + assert_array_almost_equal(xbounds, (1, 3)) + assert_array_almost_equal(ybounds, (0, 4)) + o = Obround((2, 2), 4, 2) + xbounds, ybounds = o.bounding_box + assert_array_almost_equal(xbounds, (0, 4)) + assert_array_almost_equal(ybounds, (1, 3)) + + +def test_obround_orientation(): + o = Obround((0, 0), 2, 1) + assert_equal(o.orientation, 'horizontal') + o = Obround((0, 0), 1, 2) + assert_equal(o.orientation, 'vertical') + + +def test_obround_subshapes(): + o = Obround((0, 0), 1, 4) + ss = o.subshapes + assert_array_almost_equal(ss['rectangle'].position, (0, 0)) + assert_array_almost_equal(ss['circle1'].position, (0, 1.5)) + assert_array_almost_equal(ss['circle2'].position, (0, -1.5)) + o = Obround((0, 0), 4, 1) + ss = o.subshapes + assert_array_almost_equal(ss['rectangle'].position, (0, 0)) + assert_array_almost_equal(ss['circle1'].position, (1.5, 0)) + assert_array_almost_equal(ss['circle2'].position, (-1.5, 0)) + + +def test_obround_conversion(): + o = Obround((2.54, 25.4), 254.0, 2540.0, units='metric') + + # No effect + o.to_metric() + assert_equal(o.position, (2.54, 25.4)) + assert_equal(o.width, 254.0) + assert_equal(o.height, 2540.0) + + o.to_inch() + assert_equal(o.position, (0.1, 1.0)) + assert_equal(o.width, 10.0) + assert_equal(o.height, 100.0) + + # No effect + o.to_inch() + assert_equal(o.position, (0.1, 1.0)) + assert_equal(o.width, 10.0) + assert_equal(o.height, 100.0) + + o = Obround((0.1, 1.0), 10.0, 100.0, units='inch') + + # No effect + o.to_inch() + assert_equal(o.position, (0.1, 1.0)) + assert_equal(o.width, 10.0) + assert_equal(o.height, 100.0) + + o.to_metric() + assert_equal(o.position, (2.54, 25.4)) + assert_equal(o.width, 254.0) + assert_equal(o.height, 2540.0) + + # No effect + o.to_metric() + assert_equal(o.position, (2.54, 25.4)) + assert_equal(o.width, 254.0) + assert_equal(o.height, 2540.0) + + +def test_obround_offset(): + o = Obround((0, 0), 1, 2) + o.offset(1, 0) + assert_equal(o.position, (1., 0.)) + o.offset(0, 1) + assert_equal(o.position, (1., 1.)) + + +def test_polygon_ctor(): + """ Test polygon creation + """ + test_cases = (((0, 0), 3, 5, 0), + ((0, 0), 5, 6, 0), + ((1, 1), 7, 7, 45)) + for pos, sides, radius, hole_diameter in test_cases: + p = Polygon(pos, sides, radius, hole_diameter) + assert_equal(p.position, pos) + assert_equal(p.sides, sides) + assert_equal(p.radius, radius) + assert_equal(p.hole_diameter, hole_diameter) + + + +def test_polygon_bounds(): + """ Test polygon bounding box calculation + """ + p = Polygon((2, 2), 3, 2, 0) + xbounds, ybounds = p.bounding_box + assert_array_almost_equal(xbounds, (0, 4)) + assert_array_almost_equal(ybounds, (0, 4)) + p = Polygon((2, 2), 3, 4, 0) + xbounds, ybounds = p.bounding_box + assert_array_almost_equal(xbounds, (-2, 6)) + assert_array_almost_equal(ybounds, (-2, 6)) + + +def test_polygon_conversion(): + p = Polygon((2.54, 25.4), 3, 254.0, 0, units='metric') + + # No effect + p.to_metric() + assert_equal(p.position, (2.54, 25.4)) + assert_equal(p.radius, 254.0) + + p.to_inch() + assert_equal(p.position, (0.1, 1.0)) + assert_equal(p.radius, 10.0) + + # No effect + p.to_inch() + assert_equal(p.position, (0.1, 1.0)) + assert_equal(p.radius, 10.0) + + p = Polygon((0.1, 1.0), 3, 10.0, 0, units='inch') + + # No effect + p.to_inch() + assert_equal(p.position, (0.1, 1.0)) + assert_equal(p.radius, 10.0) + + p.to_metric() + assert_equal(p.position, (2.54, 25.4)) + assert_equal(p.radius, 254.0) + + # No effect + p.to_metric() + assert_equal(p.position, (2.54, 25.4)) + assert_equal(p.radius, 254.0) + + +def test_polygon_offset(): + p = Polygon((0, 0), 5, 10, 0) + p.offset(1, 0) + assert_equal(p.position, (1., 0.)) + p.offset(0, 1) + assert_equal(p.position, (1., 1.)) + + +def test_region_ctor(): + """ Test Region creation + """ + apt = Circle((0, 0), 0) + lines = (Line((0, 0), (1, 0), apt), Line((1, 0), (1, 1), apt), + Line((1, 1), (0, 1), apt), Line((0, 1), (0, 0), apt)) + points = ((0, 0), (1, 0), (1, 1), (0, 1)) + r = Region(lines) + for i, p in enumerate(lines): + assert_equal(r.primitives[i], p) + + +def test_region_bounds(): + """ Test region bounding box calculation + """ + apt = Circle((0, 0), 0) + lines = (Line((0, 0), (1, 0), apt), Line((1, 0), (1, 1), apt), + Line((1, 1), (0, 1), apt), Line((0, 1), (0, 0), apt)) + r = Region(lines) + xbounds, ybounds = r.bounding_box + assert_array_almost_equal(xbounds, (0, 1)) + assert_array_almost_equal(ybounds, (0, 1)) + + +def test_region_offset(): + apt = Circle((0, 0), 0) + lines = (Line((0, 0), (1, 0), apt), Line((1, 0), (1, 1), apt), + Line((1, 1), (0, 1), apt), Line((0, 1), (0, 0), apt)) + r = Region(lines) + xlim, ylim = r.bounding_box + r.offset(0, 1) + new_xlim, new_ylim = r.bounding_box + assert_array_almost_equal(new_xlim, xlim) + assert_array_almost_equal(new_ylim, tuple([y + 1 for y in ylim])) + + +def test_round_butterfly_ctor(): + """ Test round butterfly creation + """ + test_cases = (((0, 0), 3), ((0, 0), 5), ((1, 1), 7)) + for pos, diameter in test_cases: + b = RoundButterfly(pos, diameter) + assert_equal(b.position, pos) + assert_equal(b.diameter, diameter) + assert_equal(b.radius, diameter / 2.) + + +def test_round_butterfly_ctor_validation(): + """ Test RoundButterfly argument validation + """ + assert_raises(TypeError, RoundButterfly, 3, 5) + assert_raises(TypeError, RoundButterfly, (3, 4, 5), 5) + + +def test_round_butterfly_conversion(): + b = RoundButterfly((2.54, 25.4), 254.0, units='metric') + + # No Effect + b.to_metric() + assert_equal(b.position, (2.54, 25.4)) + assert_equal(b.diameter, (254.0)) + + b.to_inch() + assert_equal(b.position, (0.1, 1.0)) + assert_equal(b.diameter, 10.0) + + # No effect + b.to_inch() + assert_equal(b.position, (0.1, 1.0)) + assert_equal(b.diameter, 10.0) + + b = RoundButterfly((0.1, 1.0), 10.0, units='inch') + + # No effect + b.to_inch() + assert_equal(b.position, (0.1, 1.0)) + assert_equal(b.diameter, 10.0) + + b.to_metric() + assert_equal(b.position, (2.54, 25.4)) + assert_equal(b.diameter, (254.0)) + + # No Effect + b.to_metric() + assert_equal(b.position, (2.54, 25.4)) + assert_equal(b.diameter, (254.0)) + + +def test_round_butterfly_offset(): + b = RoundButterfly((0, 0), 1) + b.offset(1, 0) + assert_equal(b.position, (1., 0.)) + b.offset(0, 1) + assert_equal(b.position, (1., 1.)) + + +def test_round_butterfly_bounds(): + """ Test RoundButterfly bounding box calculation + """ + b = RoundButterfly((0, 0), 2) + xbounds, ybounds = b.bounding_box + assert_array_almost_equal(xbounds, (-1, 1)) + assert_array_almost_equal(ybounds, (-1, 1)) + + +def test_square_butterfly_ctor(): + """ Test SquareButterfly creation + """ + test_cases = (((0, 0), 3), ((0, 0), 5), ((1, 1), 7)) + for pos, side in test_cases: + b = SquareButterfly(pos, side) + assert_equal(b.position, pos) + assert_equal(b.side, side) + + +def test_square_butterfly_ctor_validation(): + """ Test SquareButterfly argument validation + """ + assert_raises(TypeError, SquareButterfly, 3, 5) + assert_raises(TypeError, SquareButterfly, (3, 4, 5), 5) + + +def test_square_butterfly_bounds(): + """ Test SquareButterfly bounding box calculation + """ + b = SquareButterfly((0, 0), 2) + xbounds, ybounds = b.bounding_box + assert_array_almost_equal(xbounds, (-1, 1)) + assert_array_almost_equal(ybounds, (-1, 1)) + + +def test_squarebutterfly_conversion(): + b = SquareButterfly((2.54, 25.4), 254.0, units='metric') + + # No effect + b.to_metric() + assert_equal(b.position, (2.54, 25.4)) + assert_equal(b.side, (254.0)) + + b.to_inch() + assert_equal(b.position, (0.1, 1.0)) + assert_equal(b.side, 10.0) + + # No effect + b.to_inch() + assert_equal(b.position, (0.1, 1.0)) + assert_equal(b.side, 10.0) + + b = SquareButterfly((0.1, 1.0), 10.0, units='inch') + + # No effect + b.to_inch() + assert_equal(b.position, (0.1, 1.0)) + assert_equal(b.side, 10.0) + + b.to_metric() + assert_equal(b.position, (2.54, 25.4)) + assert_equal(b.side, (254.0)) + + # No effect + b.to_metric() + assert_equal(b.position, (2.54, 25.4)) + assert_equal(b.side, (254.0)) + + +def test_square_butterfly_offset(): + b = SquareButterfly((0, 0), 1) + b.offset(1, 0) + assert_equal(b.position, (1., 0.)) + b.offset(0, 1) + assert_equal(b.position, (1., 1.)) + + +def test_donut_ctor(): + """ Test Donut primitive creation + """ + test_cases = (((0, 0), 'round', 3, 5), ((0, 0), 'square', 5, 7), + ((1, 1), 'hexagon', 7, 9), ((2, 2), 'octagon', 9, 11)) + for pos, shape, in_d, out_d in test_cases: + d = Donut(pos, shape, in_d, out_d) + assert_equal(d.position, pos) + assert_equal(d.shape, shape) + assert_equal(d.inner_diameter, in_d) + assert_equal(d.outer_diameter, out_d) + + +def test_donut_ctor_validation(): + assert_raises(TypeError, Donut, 3, 'round', 5, 7) + assert_raises(TypeError, Donut, (3, 4, 5), 'round', 5, 7) + assert_raises(ValueError, Donut, (0, 0), 'triangle', 3, 5) + assert_raises(ValueError, Donut, (0, 0), 'round', 5, 3) + + +def test_donut_bounds(): + d = Donut((0, 0), 'round', 0.0, 2.0) + xbounds, ybounds = d.bounding_box + assert_equal(xbounds, (-1., 1.)) + assert_equal(ybounds, (-1., 1.)) + + +def test_donut_conversion(): + d = Donut((2.54, 25.4), 'round', 254.0, 2540.0, units='metric') + + # No effect + d.to_metric() + assert_equal(d.position, (2.54, 25.4)) + assert_equal(d.inner_diameter, 254.0) + assert_equal(d.outer_diameter, 2540.0) + + d.to_inch() + assert_equal(d.position, (0.1, 1.0)) + assert_equal(d.inner_diameter, 10.0) + assert_equal(d.outer_diameter, 100.0) + + # No effect + d.to_inch() + assert_equal(d.position, (0.1, 1.0)) + assert_equal(d.inner_diameter, 10.0) + assert_equal(d.outer_diameter, 100.0) + + d = Donut((0.1, 1.0), 'round', 10.0, 100.0, units='inch') + + # No effect + d.to_inch() + assert_equal(d.position, (0.1, 1.0)) + assert_equal(d.inner_diameter, 10.0) + assert_equal(d.outer_diameter, 100.0) + + d.to_metric() + assert_equal(d.position, (2.54, 25.4)) + assert_equal(d.inner_diameter, 254.0) + assert_equal(d.outer_diameter, 2540.0) + + # No effect + d.to_metric() + assert_equal(d.position, (2.54, 25.4)) + assert_equal(d.inner_diameter, 254.0) + assert_equal(d.outer_diameter, 2540.0) + + +def test_donut_offset(): + d = Donut((0, 0), 'round', 1, 10) + d.offset(1, 0) + assert_equal(d.position, (1., 0.)) + d.offset(0, 1) + assert_equal(d.position, (1., 1.)) + + +def test_drill_ctor(): + """ Test drill primitive creation + """ + test_cases = (((0, 0), 2), ((1, 1), 3), ((2, 2), 5)) + for position, diameter in test_cases: + d = Drill(position, diameter, None) + assert_equal(d.position, position) + assert_equal(d.diameter, diameter) + assert_equal(d.radius, diameter / 2.) + + +def test_drill_ctor_validation(): + """ Test drill argument 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 + assert_array_almost_equal(xbounds, (-1, 1)) + assert_array_almost_equal(ybounds, (-1, 1)) + d = Drill((1, 2), 2, None) + xbounds, ybounds = d.bounding_box + assert_array_almost_equal(xbounds, (0, 2)) + assert_array_almost_equal(ybounds, (1, 3)) + + +def test_drill_conversion(): + d = Drill((2.54, 25.4), 254., None, units='metric') + + # No effect + d.to_metric() + assert_equal(d.position, (2.54, 25.4)) + assert_equal(d.diameter, 254.0) + + d.to_inch() + assert_equal(d.position, (0.1, 1.0)) + assert_equal(d.diameter, 10.0) + + # No effect + d.to_inch() + assert_equal(d.position, (0.1, 1.0)) + assert_equal(d.diameter, 10.0) + + d = Drill((0.1, 1.0), 10., None, units='inch') + + # No effect + d.to_inch() + assert_equal(d.position, (0.1, 1.0)) + assert_equal(d.diameter, 10.0) + + d.to_metric() + assert_equal(d.position, (2.54, 25.4)) + assert_equal(d.diameter, 254.0) + + # No effect + d.to_metric() + assert_equal(d.position, (2.54, 25.4)) + assert_equal(d.diameter, 254.0) + + +def test_drill_offset(): + d = Drill((0, 0), 1., None) + d.offset(1, 0) + assert_equal(d.position, (1., 0.)) + d.offset(0, 1) + assert_equal(d.position, (1., 1.)) + + +def test_drill_equality(): + d = Drill((2.54, 25.4), 254., None) + d1 = Drill((2.54, 25.4), 254., None) + assert_equal(d, d1) + d1 = Drill((2.54, 25.4), 254.2, None) + assert_not_equal(d, d1) diff --git a/gerber/tests/test_rs274x.py b/gerber/tests/test_rs274x.py index f66a09e..d5acfe8 100644 --- a/gerber/tests/test_rs274x.py +++ b/gerber/tests/test_rs274x.py @@ -2,15 +2,55 @@ # -*- coding: utf-8 -*- # Author: Hamilton Kibbe <ham@hamiltonkib.be> +import os + from ..rs274x import read, GerberFile -from tests import * +from .tests import * -import os TOP_COPPER_FILE = os.path.join(os.path.dirname(__file__), - 'resources/top_copper.GTL') + 'resources/top_copper.GTL') + +MULTILINE_READ_FILE = os.path.join(os.path.dirname(__file__), + 'resources/multiline_read.ger') def test_read(): top_copper = read(TOP_COPPER_FILE) assert(isinstance(top_copper, GerberFile)) + + +def test_multiline_read(): + multiline = read(MULTILINE_READ_FILE) + assert(isinstance(multiline, GerberFile)) + assert_equal(10, len(multiline.statements)) + + +def test_comments_parameter(): + top_copper = read(TOP_COPPER_FILE) + assert_equal(top_copper.comments[0], 'This is a comment,:') + + +def test_size_parameter(): + top_copper = read(TOP_COPPER_FILE) + size = top_copper.size + assert_almost_equal(size[0], 2.256900, 6) + assert_almost_equal(size[1], 1.500000, 6) + + +def test_conversion(): + import copy + top_copper = read(TOP_COPPER_FILE) + assert_equal(top_copper.units, 'inch') + top_copper_inch = copy.deepcopy(top_copper) + top_copper.to_metric() + for statement in top_copper_inch.statements: + statement.to_metric() + for primitive in top_copper_inch.primitives: + primitive.to_metric() + assert_equal(top_copper.units, 'metric') + for i, m in zip(top_copper.statements, top_copper_inch.statements): + assert_equal(i, m) + + for i, m in zip(top_copper.primitives, top_copper_inch.primitives): + assert_equal(i, m) diff --git a/gerber/tests/test_rs274x_backend.py b/gerber/tests/test_rs274x_backend.py new file mode 100644 index 0000000..89512f0 --- /dev/null +++ b/gerber/tests/test_rs274x_backend.py @@ -0,0 +1,185 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# Author: Garret Fick <garret@ficksworkshop.com> +import io +import os + +from ..render.rs274x_backend import Rs274xContext +from ..rs274x import read +from .tests import * + +def test_render_two_boxes(): + """Umaco exapmle of two boxes""" + _test_render('resources/example_two_square_boxes.gbr', 'golden/example_two_square_boxes.gbr') + + +def _test_render_single_quadrant(): + """Umaco exapmle of a single quadrant arc""" + + # TODO there is probably a bug here + _test_render('resources/example_single_quadrant.gbr', 'golden/example_single_quadrant.gbr') + + +def _test_render_simple_contour(): + """Umaco exapmle of a simple arrow-shaped contour""" + _test_render('resources/example_simple_contour.gbr', 'golden/example_simple_contour.gbr') + + +def _test_render_single_contour_1(): + """Umaco example of a single contour + + The resulting image for this test is used by other tests because they must generate the same output.""" + _test_render('resources/example_single_contour_1.gbr', 'golden/example_single_contour.gbr') + + +def _test_render_single_contour_2(): + """Umaco exapmle of a single contour, alternate contour end order + + The resulting image for this test is used by other tests because they must generate the same output.""" + _test_render('resources/example_single_contour_2.gbr', 'golden/example_single_contour.gbr') + + +def _test_render_single_contour_3(): + """Umaco exapmle of a single contour with extra line""" + _test_render('resources/example_single_contour_3.gbr', 'golden/example_single_contour_3.gbr') + + +def _test_render_not_overlapping_contour(): + """Umaco example of D02 staring a second contour""" + _test_render('resources/example_not_overlapping_contour.gbr', 'golden/example_not_overlapping_contour.gbr') + + +def _test_render_not_overlapping_touching(): + """Umaco example of D02 staring a second contour""" + _test_render('resources/example_not_overlapping_touching.gbr', 'golden/example_not_overlapping_touching.gbr') + + +def _test_render_overlapping_touching(): + """Umaco example of D02 staring a second contour""" + _test_render('resources/example_overlapping_touching.gbr', 'golden/example_overlapping_touching.gbr') + + +def _test_render_overlapping_contour(): + """Umaco example of D02 staring a second contour""" + _test_render('resources/example_overlapping_contour.gbr', 'golden/example_overlapping_contour.gbr') + + +def _DISABLED_test_render_level_holes(): + """Umaco example of using multiple levels to create multiple holes""" + + # TODO This is clearly rendering wrong. I'm temporarily checking this in because there are more + # rendering fixes in the related repository that may resolve these. + _test_render('resources/example_level_holes.gbr', 'golden/example_overlapping_contour.gbr') + + +def _DISABLED_test_render_cutin(): + """Umaco example of using a cutin""" + + # TODO This is clearly rendering wrong. + _test_render('resources/example_cutin.gbr', 'golden/example_cutin.gbr') + + +def _test_render_fully_coincident(): + """Umaco example of coincident lines rendering two contours""" + + _test_render('resources/example_fully_coincident.gbr', 'golden/example_fully_coincident.gbr') + + +def _test_render_coincident_hole(): + """Umaco example of coincident lines rendering a hole in the contour""" + + _test_render('resources/example_coincident_hole.gbr', 'golden/example_coincident_hole.gbr') + + +def _test_render_cutin_multiple(): + """Umaco example of a region with multiple cutins""" + + _test_render('resources/example_cutin_multiple.gbr', 'golden/example_cutin_multiple.gbr') + + +def _test_flash_circle(): + """Umaco example a simple circular flash with and without a hole""" + + _test_render('resources/example_flash_circle.gbr', 'golden/example_flash_circle.gbr') + + +def _test_flash_rectangle(): + """Umaco example a simple rectangular flash with and without a hole""" + + _test_render('resources/example_flash_rectangle.gbr', 'golden/example_flash_rectangle.gbr') + + +def _test_flash_obround(): + """Umaco example a simple obround flash with and without a hole""" + + _test_render('resources/example_flash_obround.gbr', 'golden/example_flash_obround.gbr') + + +def _test_flash_polygon(): + """Umaco example a simple polygon flash with and without a hole""" + + _test_render('resources/example_flash_polygon.gbr', 'golden/example_flash_polygon.gbr') + + +def _test_holes_dont_clear(): + """Umaco example that an aperture with a hole does not clear the area""" + + _test_render('resources/example_holes_dont_clear.gbr', 'golden/example_holes_dont_clear.gbr') + + +def _test_render_am_exposure_modifier(): + """Umaco example that an aperture macro with a hole does not clear the area""" + + _test_render('resources/example_am_exposure_modifier.gbr', 'golden/example_am_exposure_modifier.gbr') + + +def _resolve_path(path): + return os.path.join(os.path.dirname(__file__), + path) + + +def _test_render(gerber_path, png_expected_path, create_output_path = None): + """Render the gerber file and compare to the expected PNG output. + + Parameters + ---------- + gerber_path : string + Path to Gerber file to open + png_expected_path : string + Path to the PNG file to compare to + create_output : string|None + If not None, write the generated PNG to the specified path. + This is primarily to help with + """ + + gerber_path = _resolve_path(gerber_path) + png_expected_path = _resolve_path(png_expected_path) + if create_output_path: + create_output_path = _resolve_path(create_output_path) + + gerber = read(gerber_path) + + # Create GBR output from the input file + ctx = Rs274xContext(gerber.settings) + gerber.render(ctx) + + actual_contents = ctx.dump() + + # If we want to write the file bytes, do it now. This happens + if create_output_path: + with open(create_output_path, 'wb') as out_file: + out_file.write(actual_contents.getvalue()) + # Creating the output is dangerous - it could overwrite the expected result. + # So if we are creating the output, we make the test fail on purpose so you + # won't forget to disable this + assert_false(True, 'Test created the output %s. This needs to be disabled to make sure the test behaves correctly' % (create_output_path,)) + + # Read the expected PNG file + + with open(png_expected_path, 'r') as expected_file: + expected_contents = expected_file.read() + + assert_equal(expected_contents, actual_contents.getvalue()) + + return gerber diff --git a/gerber/tests/test_utils.py b/gerber/tests/test_utils.py index 001a32f..35f6f47 100644 --- a/gerber/tests/test_utils.py +++ b/gerber/tests/test_utils.py @@ -3,8 +3,8 @@ # Author: Hamilton Kibbe <ham@hamiltonkib.be> -from .tests import assert_equal -from ..utils import decimal_string, parse_gerber_value, write_gerber_value +from .tests import assert_equal, assert_raises +from ..utils import * def test_zero_suppression(): @@ -19,10 +19,11 @@ def test_zero_suppression(): ('1000', 0.01), ('10000', 0.1), ('100000', 1.0), ('1000000', 10.0), ('-1', -0.00001), ('-10', -0.0001), ('-100', -0.001), ('-1000', -0.01), ('-10000', -0.1), - ('-100000', -1.0), ('-1000000', -10.0), ] + ('-100000', -1.0), ('-1000000', -10.0), + ('0', 0.0)] for string, value in test_cases: - assert(value == parse_gerber_value(string, fmt, zero_suppression)) - assert(string == write_gerber_value(value, fmt, zero_suppression)) + assert_equal(value, parse_gerber_value(string, fmt, zero_suppression)) + assert_equal(string, write_gerber_value(value, fmt, zero_suppression)) # Test trailing zero suppression zero_suppression = 'trailing' @@ -30,10 +31,14 @@ def test_zero_suppression(): ('00001', 0.001), ('000001', 0.0001), ('0000001', 0.00001), ('-1', -10.0), ('-01', -1.0), ('-001', -0.1), ('-0001', -0.01), ('-00001', -0.001), - ('-000001', -0.0001), ('-0000001', -0.00001)] + ('-000001', -0.0001), ('-0000001', -0.00001), + ('0', 0.0)] for string, value in test_cases: - assert(value == parse_gerber_value(string, fmt, zero_suppression)) - assert(string == write_gerber_value(value, fmt, zero_suppression)) + assert_equal(value, parse_gerber_value(string, fmt, zero_suppression)) + assert_equal(string, write_gerber_value(value, fmt, zero_suppression)) + + assert_equal(write_gerber_value(0.000000001, fmt, 'leading'), '0') + assert_equal(write_gerber_value(0.000000001, fmt, 'trailing'), '0') def test_format(): @@ -46,10 +51,11 @@ def test_format(): ((2, 1), '1', 0.1), ((2, 7), '-1', -0.0000001), ((2, 6), '-1', -0.000001), ((2, 5), '-1', -0.00001), ((2, 4), '-1', -0.0001), ((2, 3), '-1', -0.001), - ((2, 2), '-1', -0.01), ((2, 1), '-1', -0.1), ] + ((2, 2), '-1', -0.01), ((2, 1), '-1', -0.1), + ((2, 6), '0', 0)] for fmt, string, value in test_cases: - assert(value == parse_gerber_value(string, fmt, zero_suppression)) - assert(string == write_gerber_value(value, fmt, zero_suppression)) + assert_equal(value, parse_gerber_value(string, fmt, zero_suppression)) + assert_equal(string, write_gerber_value(value, fmt, zero_suppression)) zero_suppression = 'trailing' test_cases = [((6, 5), '1', 100000.0), ((5, 5), '1', 10000.0), @@ -57,10 +63,11 @@ def test_format(): ((2, 5), '1', 10.0), ((1, 5), '1', 1.0), ((6, 5), '-1', -100000.0), ((5, 5), '-1', -10000.0), ((4, 5), '-1', -1000.0), ((3, 5), '-1', -100.0), - ((2, 5), '-1', -10.0), ((1, 5), '-1', -1.0), ] + ((2, 5), '-1', -10.0), ((1, 5), '-1', -1.0), + ((2, 5), '0', 0)] for fmt, string, value in test_cases: - assert(value == parse_gerber_value(string, fmt, zero_suppression)) - assert(string == write_gerber_value(value, fmt, zero_suppression)) + assert_equal(value, parse_gerber_value(string, fmt, zero_suppression)) + assert_equal(string, write_gerber_value(value, fmt, zero_suppression)) def test_decimal_truncation(): @@ -69,8 +76,8 @@ def test_decimal_truncation(): value = 1.123456789 for x in range(10): result = decimal_string(value, precision=x) - calculated = '1.' + ''.join(str(y) for y in range(1,x+1)) - assert(result == calculated) + calculated = '1.' + ''.join(str(y) for y in range(1, x + 1)) + assert_equal(result, calculated) def test_decimal_padding(): @@ -81,3 +88,42 @@ def test_decimal_padding(): assert_equal(decimal_string(value, precision=4, padding=True), '1.1230') assert_equal(decimal_string(value, precision=5, padding=True), '1.12300') assert_equal(decimal_string(value, precision=6, padding=True), '1.123000') + assert_equal(decimal_string(0, precision=6, padding=True), '0.000000') + + +def test_parse_format_validation(): + """ Test parse_gerber_value() format validation + """ + assert_raises(ValueError, parse_gerber_value, '00001111', (7, 5)) + assert_raises(ValueError, parse_gerber_value, '00001111', (5, 8)) + assert_raises(ValueError, parse_gerber_value, '00001111', (13, 1)) + + +def test_write_format_validation(): + """ Test write_gerber_value() format validation + """ + assert_raises(ValueError, write_gerber_value, 69.0, (7, 5)) + assert_raises(ValueError, write_gerber_value, 69.0, (5, 8)) + assert_raises(ValueError, write_gerber_value, 69.0, (13, 1)) + + +def test_detect_format_with_short_file(): + """ Verify file format detection works with short files + """ + assert_equal('unknown', detect_file_format('gerber/tests/__init__.py')) + + +def test_validate_coordinates(): + assert_raises(TypeError, validate_coordinates, 3) + assert_raises(TypeError, validate_coordinates, 3.1) + assert_raises(TypeError, validate_coordinates, '14') + assert_raises(TypeError, validate_coordinates, (0,)) + assert_raises(TypeError, validate_coordinates, (0, 1, 2)) + assert_raises(TypeError, validate_coordinates, (0, 'string')) + + +def test_convex_hull(): + points = [(0, 0), (1, 0), (1, 1), (0.5, 0.5), (0, 1), (0, 0)] + expected = [(0, 0), (1, 0), (1, 1), (0, 1), (0, 0)] + assert_equal(set(convex_hull(points)), set(expected)) +
\ No newline at end of file diff --git a/gerber/tests/tests.py b/gerber/tests/tests.py index 29b7899..ac08208 100644 --- a/gerber/tests/tests.py +++ b/gerber/tests/tests.py @@ -7,6 +7,7 @@ from nose.tools import assert_in from nose.tools import assert_not_in from nose.tools import assert_equal from nose.tools import assert_not_equal +from nose.tools import assert_almost_equal from nose.tools import assert_true from nose.tools import assert_false from nose.tools import assert_raises @@ -14,5 +15,11 @@ from nose.tools import raises from nose import with_setup __all__ = ['assert_in', 'assert_not_in', 'assert_equal', 'assert_not_equal', - 'assert_true', 'assert_false', 'assert_raises', 'raises', - 'with_setup' ] + 'assert_almost_equal', 'assert_array_almost_equal', 'assert_true', + 'assert_false', 'assert_raises', 'raises', 'with_setup'] + + +def assert_array_almost_equal(arr1, arr2, decimal=6): + assert_equal(len(arr1), len(arr2)) + for i in range(len(arr1)): + assert_almost_equal(arr1[i], arr2[i], decimal) diff --git a/gerber/utils.py b/gerber/utils.py index 7749e22..ef9c39e 100644 --- a/gerber/utils.py +++ b/gerber/utils.py @@ -23,8 +23,13 @@ This module provides utility functions for working with Gerber and Excellon files. """ -# Author: Hamilton Kibbe <ham@hamiltonkib.be> -# License: +import os +from math import radians, sin, cos +from operator import sub +from copy import deepcopy +from pyhull.convex_hull import ConvexHull + +MILLIMETERS_PER_INCH = 25.4 def parse_gerber_value(value, format=(2, 5), zero_suppression='trailing'): @@ -50,7 +55,7 @@ def parse_gerber_value(value, format=(2, 5), zero_suppression='trailing'): (number of integer-part digits, number of decimal-part digits) zero_suppression : string - Zero-suppression mode. May be 'leading' or 'trailing' + Zero-suppression mode. May be 'leading', 'trailing' or 'none' Returns ------- @@ -73,19 +78,22 @@ def parse_gerber_value(value, format=(2, 5), zero_suppression='trailing'): raise ValueError('Parser only supports precision up to 6:7 format') # Remove extraneous information - #value = value.strip() value = value.lstrip('+') negative = '-' in value if negative: value = value.lstrip('-') + missing_digits = MAX_DIGITS - len(value) - digits = list('0' * MAX_DIGITS) - offset = 0 if zero_suppression == 'trailing' else (MAX_DIGITS - len(value)) - for i, digit in enumerate(value): - digits[i + offset] = digit + if zero_suppression == 'trailing': + digits = list(value + ('0' * missing_digits)) + elif zero_suppression == 'leading': + digits = list(('0' * missing_digits) + value) + else: + digits = list(value) - result = float(''.join(digits[:integer_digits] + ['.'] + digits[integer_digits:])) + result = float( + ''.join(digits[:integer_digits] + ['.'] + digits[integer_digits:])) return -result if negative else result @@ -111,7 +119,7 @@ def write_gerber_value(value, format=(2, 5), zero_suppression='trailing'): (number of integer-part digits, number of decimal-part digits) zero_suppression : string - Zero-suppression mode. May be 'leading' or 'trailing' + Zero-suppression mode. May be 'leading', 'trailing' or 'none' Returns ------- @@ -125,9 +133,10 @@ def write_gerber_value(value, format=(2, 5), zero_suppression='trailing'): if MAX_DIGITS > 13 or integer_digits > 6 or decimal_digits > 7: raise ValueError('Parser only supports precision up to 6:7 format') - # Edge case... + # Edge case... (per Gerber spec we should return 0 in all cases, see page + # 77) if value == 0: - return '00' + return '0' # negative sign affects padding, so deal with it at the end... negative = value < 0.0 @@ -138,14 +147,22 @@ def write_gerber_value(value, format=(2, 5), zero_suppression='trailing'): fmtstring = '%%0%d.0%df' % (MAX_DIGITS + 1, decimal_digits) digits = [val for val in fmtstring % value if val != '.'] + # If all the digits are 0, return '0'. + digit_sum = sum([int(digit) for digit in digits]) + if digit_sum == 0: + return '0' + # Suppression... if zero_suppression == 'trailing': - while digits[-1] == '0': + while digits and digits[-1] == '0': digits.pop() - else: - while digits[0] == '0': + elif zero_suppression == 'leading': + while digits and digits[0] == '0': digits.pop(0) + if not digits: + return '0' + return ''.join(digits) if not negative else ''.join(['-'] + digits) @@ -173,38 +190,159 @@ def decimal_string(value, precision=6, padding=False): integer, decimal = floatstr.split('.') elif ',' in floatstr: integer, decimal = floatstr.split(',') + else: + integer, decimal = floatstr, "0" + if len(decimal) > precision: decimal = decimal[:precision] elif padding: decimal = decimal + (precision - len(decimal)) * '0' + if integer or decimal: return ''.join([integer, '.', decimal]) else: return int(floatstr) -def detect_file_format(filename): +def detect_file_format(data): """ Determine format of a file Parameters ---------- - filename : string - Filename of the file to read. + data : string + string containing file data. Returns ------- format : string - File format. either 'excellon' or 'rs274x' + File format. 'excellon' or 'rs274x' or 'unknown' """ - - # Read the first 20 lines - with open(filename, 'r') as f: - lines = [next(f) for x in xrange(20)] - - # Look for + lines = data.split('\n') for line in lines: if 'M48' in line: return 'excellon' elif '%FS' in line: - return'rs274x' + return 'rs274x' + elif ((len(line.split()) >= 2) and + (line.split()[0] == 'P') and (line.split()[1] == 'JOB')): + return 'ipc_d_356' return 'unknown' + + +def validate_coordinates(position): + if position is not None: + if len(position) != 2: + raise TypeError('Position must be a tuple (n=2) of coordinates') + else: + for coord in position: + if not (isinstance(coord, int) or isinstance(coord, float)): + raise TypeError('Coordinates must be integers or floats') + + +def metric(value): + """ Convert inch value to millimeters + + Parameters + ---------- + value : float + A value in inches. + + Returns + ------- + value : float + The equivalent value expressed in millimeters. + """ + return value * MILLIMETERS_PER_INCH + + +def inch(value): + """ Convert millimeter value to inches + + Parameters + ---------- + value : float + A value in millimeters. + + Returns + ------- + value : float + The equivalent value expressed in inches. + """ + return value / MILLIMETERS_PER_INCH + + +def rotate_point(point, angle, center=(0.0, 0.0)): + """ Rotate a point about another point. + + Parameters + ----------- + point : tuple(<float>, <float>) + Point to rotate about origin or center point + + angle : float + Angle to rotate the point [degrees] + + center : tuple(<float>, <float>) + Coordinates about which the point is rotated. Defaults to the origin. + + Returns + ------- + rotated_point : tuple(<float>, <float>) + `point` rotated about `center` by `angle` degrees. + """ + angle = radians(angle) + + cos_angle = cos(angle) + sin_angle = sin(angle) + + return ( + cos_angle * (point[0] - center[0]) - sin_angle * (point[1] - center[1]) + center[0], + sin_angle * (point[0] - center[0]) + cos_angle * (point[1] - center[1]) + center[1]) + +def nearly_equal(point1, point2, ndigits = 6): + '''Are the points nearly equal''' + + return round(point1[0] - point2[0], ndigits) == 0 and round(point1[1] - point2[1], ndigits) == 0 + + +def sq_distance(point1, point2): + + diff1 = point1[0] - point2[0] + diff2 = point1[1] - point2[1] + return diff1 * diff1 + diff2 * diff2 + + +def listdir(directory, ignore_hidden=True, ignore_os=True): + """ List files in given directory. + Differs from os.listdir() in that hidden and OS-generated files are ignored + by default. + + Parameters + ---------- + directory : str + path to the directory for which to list files. + + ignore_hidden : bool + If True, ignore files beginning with a leading '.' + + ignore_os : bool + If True, ignore OS-generated files, e.g. Thumbs.db + + Returns + ------- + files : list + list of files in specified directory + """ + os_files = ('.DS_Store', 'Thumbs.db', 'ethumbs.db') + files = os.listdir(directory) + if ignore_hidden: + files = [f for f in files if not f.startswith('.')] + if ignore_os: + files = [f for f in files if not f in os_files] + return files + + +def convex_hull(points): + vertices = ConvexHull(points).vertices + return [points[idx] for idx in + set([point for pair in vertices for point in pair])] |