From 695e3d9220be8773f6630bb5c512d122b8576742 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Sun, 28 Sep 2014 18:07:15 -0400 Subject: Added excellon support and refactored project --- gerber/__main__.py | 33 ++- gerber/excellon.py | 165 +++++++++++++ gerber/gerber.py | 305 +++++++++++++++++++++++ gerber/parser.py | 363 --------------------------- gerber/render.py | 185 -------------- gerber/render/__init__.py | 28 +++ gerber/render/apertures.py | 58 +++++ gerber/render/render.py | 133 ++++++++++ gerber/render/svg.py | 171 +++++++++++++ gerber/render_svg.py | 106 -------- gerber/statements.py | 605 +++++++++++++++++++++++++++++++++++++++++++++ gerber/tests/__init__.py | 0 gerber/tests/test_utils.py | 68 +++++ gerber/utils.py | 40 ++- 14 files changed, 1591 insertions(+), 669 deletions(-) create mode 100755 gerber/excellon.py create mode 100644 gerber/gerber.py delete mode 100644 gerber/parser.py delete mode 100644 gerber/render.py create mode 100644 gerber/render/__init__.py create mode 100644 gerber/render/apertures.py create mode 100644 gerber/render/render.py create mode 100644 gerber/render/svg.py delete mode 100644 gerber/render_svg.py create mode 100644 gerber/statements.py create mode 100644 gerber/tests/__init__.py create mode 100644 gerber/tests/test_utils.py (limited to 'gerber') diff --git a/gerber/__main__.py b/gerber/__main__.py index 6f861cf..d32fa01 100644 --- a/gerber/__main__.py +++ b/gerber/__main__.py @@ -16,16 +16,23 @@ # limitations under the License. if __name__ == '__main__': - from .parser import GerberParser - from .render import GerberContext - - import sys - - if len(sys.argv) < 2: - print >> sys.stderr, "Usage: python -m gerber ..." - sys.exit(1) - - for filename in sys.argv[1:]: - print "parsing %s" % filename - g = GerberParser(GerberContext()) - g.parse(filename) + from .gerber import GerberFile + from .excellon import ExcellonParser + from .render import GerberSvgContext + + #import sys + # + #if len(sys.argv) < 2: + # print >> sys.stderr, "Usage: python -m gerber ..." + # sys.exit(1) + # + ##for filename in sys.argv[1]: + ## print "parsing %s" % filename + ctx = GerberSvgContext() + g = GerberFile.read('SCB.GTL') + g.render('test.svg', ctx) + p = ExcellonParser(ctx) + p.parse('ncdrill.txt') + p.dump('testwithdrill.svg') + + diff --git a/gerber/excellon.py b/gerber/excellon.py new file mode 100755 index 0000000..fef5844 --- /dev/null +++ b/gerber/excellon.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python +import re +from itertools import tee, izip +from .utils import parse_gerber_value + + +INCH = 0 +METRIC = 1 + +ABSOLUTE = 0 +INCREMENTAL = 1 + +LZ = 0 +TZ = 1 + +class Tool(object): + + @classmethod + def from_line(cls, line, settings): + commands = re.split('([BCFHSTZ])', line)[1:] + commands = [(command, value) for command, value in pairwise(commands)] + args = {} + format = settings['format'] + zero_suppression = settings['zero_suppression'] + for cmd, val in commands: + if cmd == 'B': + args['retract_rate'] = parse_gerber_value(val, format, zero_suppression) + elif cmd == 'C': + args['diameter'] = parse_gerber_value(val, format, zero_suppression) + elif cmd == 'F': + args['feed_rate'] = parse_gerber_value(val, format, zero_suppression) + elif cmd == 'H': + args['max_hit_count'] = parse_gerber_value(val, format, zero_suppression) + elif cmd == 'S': + args['rpm'] = 1000 * parse_gerber_value(val, format, zero_suppression) + elif cmd == 'T': + args['number'] = int(val) + elif cmd == 'Z': + args['depth_offset'] = parse_gerber_value(val, format, zero_suppression) + return cls(settings, **args) + + def __init__(self, settings, **kwargs): + self.number = kwargs.get('number') + self.feed_rate = kwargs.get('feed_rate') + self.retract_rate = kwargs.get('retract_rate') + self.rpm = kwargs.get('rpm') + self.diameter = kwargs.get('diameter') + self.max_hit_count = kwargs.get('max_hit_count') + self.depth_offset = kwargs.get('depth_offset') + self.units = settings.get('units', INCH) + + def __repr__(self): + unit = 'in.' if self.units == INCH else 'mm' + return '' % (self.number, self.diameter, unit) + + + +class ExcellonParser(object): + def __init__(self, ctx=None): + self.ctx=ctx + self.notation = 'absolute' + self.units = 'inch' + self.zero_suppression = 'trailing' + self.format = (2,5) + self.state = 'INIT' + self.tools = {} + self.hits = [] + self.active_tool = None + self.pos = [0., 0.] + if ctx is not None: + self.ctx.set_coord_format(zero_suppression='trailing', format=[2,5], notation='absolute') + def parse(self, filename): + with open(filename, 'r') as f: + for line in f: + self._parse(line) + + def dump(self, filename): + self.ctx.dump(filename) + + def _parse(self, line): + if 'M48' in line: + self.state = 'HEADER' + + if 'G00' in line: + self.state = 'ROUT' + + if 'G05' in line: + self.state = 'DRILL' + + elif line[0] == '%' and self.state == 'HEADER': + self.state = 'DRILL' + + if 'INCH' in line or line.strip() == 'M72': + self.units = 'INCH' + + elif 'METRIC' in line or line.strip() == 'M71': + self.units = 'METRIC' + + if 'LZ' in line: + self.zeros = 'L' + + elif 'TZ' in line: + self.zeros = 'T' + + if 'ICI' in line and 'ON' in line or line.strip() == 'G91': + self.notation = 'incremental' + + if 'ICI' in line and 'OFF' in line or line.strip() == 'G90': + self.notation = 'incremental' + + zs = self._settings()['zero_suppression'] + fmt = self._settings()['format'] + + # tool definition + if line[0] == 'T' and self.state == 'HEADER': + tool = Tool.from_line(line,self._settings()) + self.tools[tool.number] = tool + + elif line[0] == 'T' and self.state != 'HEADER': + self.active_tool = self.tools[int(line.strip().split('T')[1])] + + + if line[0] in ['X', 'Y']: + x = None + y = None + if line[0] == 'X': + splitline = line.strip('X').split('Y') + x = parse_gerber_value(splitline[0].strip(), fmt, zs) + if len(splitline) == 2: + y = parse_gerber_value(splitline[1].strip(), fmt,zs) + else: + y = parse_gerber_value(line.strip(' Y'), fmt,zs) + + 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': + self.hits.append((self.active_tool, self.pos)) + if self.ctx is not None: + self.ctx.drill(self.pos[0], self.pos[1], + self.active_tool.diameter) + + def _settings(self): + return {'units':self.units, 'zero_suppression':self.zero_suppression, + 'format': self.format} + +def pairwise(iterator): + itr = iter(iterator) + while True: + yield tuple([itr.next() for i in range(2)]) + +if __name__ == '__main__': + tools = [] + settings = {'units':INCH, 'zeros':LZ} + p = parser() + p.parse('examples/ncdrill.txt') + + \ No newline at end of file diff --git a/gerber/gerber.py b/gerber/gerber.py new file mode 100644 index 0000000..31d9b82 --- /dev/null +++ b/gerber/gerber.py @@ -0,0 +1,305 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# copyright 2014 Hamilton Kibbe +# Modified from parser.py by Paulo Henrique Silva +# +# 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 re +import json +from .statements import * + + + + +class GerberFile(object): + """ A class representing a single gerber file + + The GerberFile class represents a single gerber file. + + Parameters + ---------- + filename : string + Parameter. + + zero_suppression : string + Zero-suppression mode. May be either 'leading' or 'trailing' + + notation : string + Notation mode. May be either 'absolute' or 'incremental' + + format : tuple (int, int) + Gerber precision format expressed as a tuple containing: + (number of integer-part digits, number of decimal-part digits) + + Attributes + ---------- + comments: list of strings + List of comments contained in the gerber file. + + units : string + either 'inch' or 'metric'. + + size : tuple, (, ) + Size in [self.units] of the layer described by the gerber file. + + bounds: tuple, ((, ), (, )) + boundaries of the layer described by the gerber file. + `bounds` is stored as ((min x, max x), (min y, max y)) + + """ + + @classmethod + def read(cls, filename): + """ Read data from filename and return a GerberFile + """ + return GerberParser().parse(filename) + + def __init__(self, statements, settings, filename=None): + self.filename = filename + self.statements = statements + self.settings = settings + + @property + def comments(self): + return [comment.comment for comment in self.statements + if isinstance(comment, CommentStmt)] + + @property + def units(self): + return self.settings['units'] + + @property + def size(self): + xbounds, ybounds = self.bounds + return (xbounds[1] - xbounds[0], ybounds[1] - ybounds[0]) + + @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)]: + if stmt.x is not None and stmt.x < xbounds[0]: + xbounds[0] = stmt.x + if stmt.x is not None and stmt.x > xbounds[1]: + xbounds[1] = stmt.x + if stmt.i is not None and stmt.i < xbounds[0]: + xbounds[0] = stmt.i + if stmt.i is not None and stmt.i > xbounds[1]: + xbounds[1] = stmt.i + if stmt.y is not None and stmt.y < ybounds[0]: + ybounds[0] = stmt.y + if stmt.y is not None and stmt.y > ybounds[1]: + ybounds[1] = stmt.y + if stmt.j is not None and stmt.j < ybounds[0]: + ybounds[0] = stmt.j + if stmt.j is not None and stmt.j > ybounds[1]: + ybounds[1] = stmt.j + + return (xbounds, ybounds) + + def write(self, filename): + """ Write data out to a gerber file + """ + with open(filename, 'w') as f: + for statement in self.statements: + f.write(statement.to_gerber()) + + def render(self, filename, ctx): + """ Generate image of layer. + """ + ctx.set_bounds(self.bounds) + for statement in self.statements: + ctx.evaluate(statement) + ctx.dump(filename) + + + +class GerberParser(object): + """ GerberParser + """ + NUMBER = r"[\+-]?\d+" + DECIMAL = r"[\+-]?\d+([.]?\d+)?" + STRING = r"[a-zA-Z0-9_+\-/!?<>”’(){}.\|&@# :]+" + NAME = "[a-zA-Z_$][a-zA-Z_$0-9]+" + FUNCTION = r"G\d{2}" + + COORD_OP = r"D[0]?[123]" + + FS = r"(?PFS)(?P(L|T))?(?P(A|I))X(?P[0-7][0-7])Y(?P[0-7][0-7])" + MO = r"(?PMO)(?P(MM|IN))" + IP = r"(?PIP)(?P(POS|NEG))" + LP = r"(?PLP)(?P(D|C))" + AD_CIRCLE = r"(?PAD)D(?P\d+)(?PC)[,](?P[^,]*)" + AD_RECT = r"(?PAD)D(?P\d+)(?PR)[,](?P[^,]*)" + AD_OBROUND = r"(?PAD)D(?P\d+)(?PO)[,](?P[^,]*)" + AD_POLY = r"(?PAD)D(?P\d+)(?PP)[,](?P[^,]*)" + AD_MACRO = r"(?PAD)D(?P\d+)+(?P{name})[,](?P[^,]*)".format(name=NAME) + AM = r"(?PAM)(?P{name})\*(?P.*)".format(name=NAME) + + # begin deprecated + OF = r"(?POF)(A(?P{decimal}))?(B(?P{decimal}))?".format(decimal=DECIMAL) + IN = r"(?PIN)(?P.*)" + LN = r"(?PLN)(?P.*)" + # end deprecated + + PARAMS = (FS, MO, IP, LP, AD_CIRCLE, AD_RECT, AD_OBROUND, AD_MACRO, AD_POLY, AM, OF, IN, LN) + PARAM_STMT = [re.compile(r"%{0}\*%".format(p)) for p in PARAMS] + + COORD_STMT = re.compile(( + r"(?P{function})?" + r"(X(?P{number}))?(Y(?P{number}))?" + r"(I(?P{number}))?(J(?P{number}))?" + r"(?P{op})?\*".format(number=NUMBER, function=FUNCTION, op=COORD_OP))) + + APERTURE_STMT = re.compile(r"(G54)?D(?P\d+)\*") + + COMMENT_STMT = re.compile(r"G04(?P[^*]*)(\*)?") + + EOF_STMT = re.compile(r"(?PM02)\*") + + def __init__(self): + self.settings = {} + self.statements = [] + + def parse(self, filename): + fp = open(filename, "r") + data = fp.readlines() + + for stmt in self._parse(data): + self.statements.append(stmt) + + return GerberFile(self.statements, self.settings, filename) + + def dump_json(self): + stmts = {"statements": [stmt.__dict__ for stmt in self.statements]} + return json.dumps(stmts) + + def dump_str(self): + s = "" + for stmt in self.statements: + s += str(stmt) + "\n" + return s + + def _parse(self, data): + oldline = '' + + for i, line in enumerate(data): + line = oldline + line.strip() + + # skip empty lines + if not len(line): + continue + + # deal with multi-line parameters + if line.startswith("%") and not line.endswith("%"): + oldline = line + continue + + did_something = True # make sure we do at least one loop + while did_something and len(line) > 0: + did_something = False + + # coord + (coord, r) = self._match_one(self.COORD_STMT, line) + if coord: + yield CoordStmt.from_dict(coord, self.settings) + line = r + did_something = True + continue + + # aperture selection + (aperture, r) = self._match_one(self.APERTURE_STMT, line) + if aperture: + yield ApertureStmt(**aperture) + + did_something = True + line = r + continue + + # comment + (comment, r) = self._match_one(self.COMMENT_STMT, line) + if comment: + yield CommentStmt(comment["comment"]) + did_something = True + line = r + continue + + # parameter + (param, r) = self._match_one_from_many(self.PARAM_STMT, line) + if param: + if param["param"] == "FS": + stmt = FSParamStmt.from_dict(param) + self.settings = {'zero_suppression': stmt.zero_suppression, + 'format': stmt.format, + 'notation': stmt.notation} + yield stmt + elif param["param"] == "MO": + 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) + elif param["param"] == "OF": + yield OFParamStmt.from_dict(param) + elif param["param"] == "IN": + yield INParamStmt.from_dict(param) + elif param["param"] == "LN": + yield LNParamStmtfrom_dict(param) + else: + yield UnknownStmt(line) + did_something = True + line = r + continue + + # eof + (eof, r) = self._match_one(self.EOF_STMT, line) + if eof: + yield EofStmt() + did_something = True + 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) + oldline = line + + def _match_one(self, expr, data): + match = expr.match(data) + if match is None: + return ({}, None) + else: + return (match.groupdict(), data[match.end(0):]) + + def _match_one_from_many(self, exprs, data): + for expr in exprs: + match = expr.match(data) + if match: + return (match.groupdict(), data[match.end(0):]) + + return ({}, None) diff --git a/gerber/parser.py b/gerber/parser.py deleted file mode 100644 index 8f89211..0000000 --- a/gerber/parser.py +++ /dev/null @@ -1,363 +0,0 @@ -#! /usr/bin/env python -# -*- coding: utf-8 -*- - -# Copyright 2013-2014 Paulo Henrique Silva - -# 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 re -import json - - -class Statement(object): - def __init__(self, type): - self.type = type - - def __str__(self): - s = "<{0} ".format(self.__class__.__name__) - - for key, value in self.__dict__.items(): - s += "{0}={1} ".format(key, value) - - s = s.rstrip() + ">" - return s - - -class ParamStmt(Statement): - def __init__(self, param): - Statement.__init__(self, "PARAM") - self.param = param - - - -class FSParamStmt(ParamStmt): - def __init__(self, param, zero="L", notation="A", x="24", y="24"): - ParamStmt.__init__(self, param) - self.zero = zero - self.notation = notation - self.x = x - self.y = y - - def to_gerber(self): - return '%FS{0}{1}X{2}Y{3}*%'.format(self.zero, self.notation, - self.x, self.y) - -class MOParamStmt(ParamStmt): - def __init__(self, param, mo): - ParamStmt.__init__(self, param) - self.mo = mo - - def to_gerber(self): - return '%MO{0}*%'.format(self.mo) - -class IPParamStmt(ParamStmt): - def __init__(self, param, ip): - ParamStmt.__init__(self, param) - self.ip = ip - - def to_gerber(self): - return '%IP{0}*%'.format(self.ip) - - -class OFParamStmt(ParamStmt): - def __init__(self, param, a, b): - ParamStmt.__init__(self, param) - self.a = a - self.b = b - - def to_gerber(self): - ret = '%OF' - if self.a: - ret += 'A' + self.a - if self.b: - ret += 'B' + self.b - return ret + '*%' - - -class LPParamStmt(ParamStmt): - def __init__(self, param, lp): - ParamStmt.__init__(self, param) - self.lp = lp - - def to_gerber(self): - return '%LP{0}*%'.format(self.lp) - - -class ADParamStmt(ParamStmt): - def __init__(self, param, d, shape, modifiers): - ParamStmt.__init__(self, param) - self.d = d - self.shape = shape - self.modifiers = [[x for x in m.split("X")] for m in modifiers.split(",")] - - def to_gerber(self): - return '%ADD{0}{1},{2}*%'.format(self.d, self.shape, - ','.join(['X'.join(e) for e in self.modifiers])) - -class AMParamStmt(ParamStmt): - def __init__(self, param, name, macro): - ParamStmt.__init__(self, param) - self.name = name - self.macro = macro - - def to_gerber(self): - #think this is right... - return '%AM{0}*{1}*%'.format(self.name, self.macro) - -class INParamStmt(ParamStmt): - def __init__(self, param, name): - ParamStmt.__init__(self, param) - self.name = name - - def to_gerber(self): - return '%IN{0}*%'.format(self.name) - - -class LNParamStmt(ParamStmt): - def __init__(self, param, name): - ParamStmt.__init__(self, param) - self.name = name - - def to_gerber(self): - return '%LN{0}*%'.format(self.name) - -class CoordStmt(Statement): - def __init__(self, function, x, y, i, j, op): - Statement.__init__(self, "COORD") - self.function = function - self.x = x - self.y = y - self.i = i - self.j = j - self.op = op - - def to_gerber(self): - ret = '' - if self.function: - ret += self.function - if self.x: - ret += 'X{0}'.format(self.x) - if self.y: - ret += 'Y{0}'.format(self.y) - if self.i: - ret += 'I{0}'.format(self.i) - if self.j: - ret += 'J{0}'.format(self.j) - if self.op: - ret += self.op - return ret + '*' - - -class ApertureStmt(Statement): - def __init__(self, d): - Statement.__init__(self, "APERTURE") - self.d = int(d) - - def to_gerber(self): - return 'G54D{0}*'.format(self.d) - -class CommentStmt(Statement): - def __init__(self, comment): - Statement.__init__(self, "COMMENT") - self.comment = comment - - def to_gerber(self): - return 'G04{0}*'.format(self.comment) - -class EofStmt(Statement): - def __init__(self): - Statement.__init__(self, "EOF") - - def to_gerber(self): - return 'M02*' - -class UnknownStmt(Statement): - def __init__(self, line): - Statement.__init__(self, "UNKNOWN") - self.line = line - - -class GerberParser(object): - NUMBER = r"[\+-]?\d+" - DECIMAL = r"[\+-]?\d+([.]?\d+)?" - STRING = r"[a-zA-Z0-9_+\-/!?<>”’(){}.\|&@# :]+" - NAME = "[a-zA-Z_$][a-zA-Z_$0-9]+" - FUNCTION = r"G\d{2}" - - COORD_OP = r"D[0]?[123]" - - FS = r"(?PFS)(?P(L|T))?(?P(A|I))X(?P[0-7][0-7])Y(?P[0-7][0-7])" - MO = r"(?PMO)(?P(MM|IN))" - IP = r"(?PIP)(?P(POS|NEG))" - LP = r"(?PLP)(?P(D|C))" - AD_CIRCLE = r"(?PAD)D(?P\d+)(?PC)[,](?P[^,]*)" - AD_RECT = r"(?PAD)D(?P\d+)(?PR)[,](?P[^,]*)" - AD_OBROUND = r"(?PAD)D(?P\d+)(?PO)[,](?P[^,]*)" - AD_POLY = r"(?PAD)D(?P\d+)(?PP)[,](?P[^,]*)" - AD_MACRO = r"(?PAD)D(?P\d+)+(?P{name})[,](?P[^,]*)".format(name=NAME) - AM = r"(?PAM)(?P{name})\*(?P.*)".format(name=NAME) - - # begin deprecated - OF = r"(?POF)(A(?P{decimal}))?(B(?P{decimal}))?".format(decimal=DECIMAL) - IN = r"(?PIN)(?P.*)" - LN = r"(?PLN)(?P.*)" - # end deprecated - - PARAMS = (FS, MO, IP, LP, AD_CIRCLE, AD_RECT, AD_OBROUND, AD_MACRO, AD_POLY, AM, OF, IN, LN) - PARAM_STMT = [re.compile(r"%{0}\*%".format(p)) for p in PARAMS] - - COORD_STMT = re.compile(( - r"(?P{function})?" - r"(X(?P{number}))?(Y(?P{number}))?" - r"(I(?P{number}))?(J(?P{number}))?" - r"(?P{op})?\*".format(number=NUMBER, function=FUNCTION, op=COORD_OP))) - - APERTURE_STMT = re.compile(r"(G54)?D(?P\d+)\*") - - #COMMENT_STMT = re.compile(r"G04(?P{string})(\*)?".format(string=STRING)) - #spec is unclear on whether all chars allowed in comment string - - #seems reasonable to be more permissive. - COMMENT_STMT = re.compile(r"G04(?P[^*]*)(\*)?") - - EOF_STMT = re.compile(r"(?PM02)\*") - - def __init__(self, ctx=None): - self.statements = [] - self.ctx = ctx - - def parse(self, filename): - fp = open(filename, "r") - data = fp.readlines() - - for stmt in self._parse(data): - self.statements.append(stmt) - if self.ctx: - self.ctx.evaluate(stmt) - - def dump_json(self): - stmts = {"statements": [stmt.__dict__ for stmt in self.statements]} - return json.dumps(stmts) - - def dump_str(self): - s = "" - for stmt in self.statements: - s += str(stmt) + "\n" - return s - - def dump(self): - self.ctx.dump() - - def _parse(self, data): - oldline = '' - - for i, line in enumerate(data): - line = oldline + line.strip() - - # skip empty lines - if not len(line): - continue - - # deal with multi-line parameters - if line.startswith("%") and not line.endswith("%"): - oldline = line - continue - - did_something = True # make sure we do at least one loop - while did_something and len(line) > 0: - did_something = False - # coord - (coord, r) = self._match_one(self.COORD_STMT, line) - if coord: - yield CoordStmt(**coord) - line = r - did_something = True - continue - - # aperture selection - (aperture, r) = self._match_one(self.APERTURE_STMT, line) - if aperture: - yield ApertureStmt(**aperture) - - did_something = True - line = r - continue - - # comment - (comment, r) = self._match_one(self.COMMENT_STMT, line) - if comment: - yield CommentStmt(comment["comment"]) - did_something = True - line = r - continue - - # parameter - (param, r) = self._match_one_from_many(self.PARAM_STMT, line) - if param: - if param["param"] == "FS": - yield FSParamStmt(**param) - elif param["param"] == "MO": - yield MOParamStmt(**param) - elif param["param"] == "IP": - yield IPParamStmt(**param) - elif param["param"] == "LP": - yield LPParamStmt(**param) - elif param["param"] == "AD": - yield ADParamStmt(**param) - elif param["param"] == "AM": - yield AMParamStmt(**param) - elif param["param"] == "OF": - yield OFParamStmt(**param) - elif param["param"] == "IN": - yield INParamStmt(**param) - elif param["param"] == "LN": - yield LNParamStmt(**param) - else: - yield UnknownStmt(line) - did_something = True - line = r - continue - - # eof - (eof, r) = self._match_one(self.EOF_STMT, line) - if eof: - yield EofStmt() - did_something = True - 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) - oldline = line - - def _match_one(self, expr, data): - match = expr.match(data) - if match is None: - return ({}, None) - else: - return (match.groupdict(), data[match.end(0):]) - - def _match_one_from_many(self, exprs, data): - for expr in exprs: - match = expr.match(data) - if match: - return (match.groupdict(), data[match.end(0):]) - - return ({}, None) diff --git a/gerber/render.py b/gerber/render.py deleted file mode 100644 index 201f793..0000000 --- a/gerber/render.py +++ /dev/null @@ -1,185 +0,0 @@ -#! /usr/bin/env python -# -*- coding: utf-8 -*- - -# Copyright 2013-2014 Paulo Henrique Silva - -# 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 .parser import CommentStmt, UnknownStmt, EofStmt, ParamStmt, CoordStmt, ApertureStmt - -IMAGE_POLARITY_POSITIVE = 1 -IMAGE_POLARITY_NEGATIVE = 2 - -LEVEL_POLARITY_DARK = 1 -LEVEL_POLARITY_CLEAR = 2 - -NOTATION_ABSOLUTE = 1 -NOTATION_INCREMENTAL = 2 - -UNIT_INCH = 1 -UNIT_MM = 2 - -INTERPOLATION_LINEAR = 1 -INTERPOLATION_ARC = 2 - - -class GerberCoordFormat(object): - def __init__(self, zeroes, x, y): - self.omit_leading_zeroes = True if zeroes == "L" else False - self.omit_trailing_zeroes = True if zeroes == "T" else False - self.x_int_digits, self.x_dec_digits = [int(d) for d in x] - self.y_int_digits, self.y_dec_digits = [int(d) for d in y] - - def resolve(self, x, y): - new_x = x - new_y = y - - if new_x is not None: - negative = "-" in new_x - new_x = new_x.replace("-", "") - - missing_zeroes = (self.x_int_digits + self.x_dec_digits) - len(new_x) - - if missing_zeroes and self.omit_leading_zeroes: - new_x = (missing_zeroes * "0") + new_x - elif missing_zeroes and self.omit_trailing_zeroes: - new_x += missing_zeroes * "0" - - new_x = float("{0}{1}.{2}".format("-" if negative else "", - new_x[:self.x_int_digits], - new_x[self.x_int_digits:])) - - if new_y is not None: - negative = "-" in new_y - new_y = new_y.replace("-", "") - - missing_zeroes = (self.y_int_digits + self.y_dec_digits) - len(new_y) - - if missing_zeroes and self.omit_leading_zeroes: - new_y = (missing_zeroes * "0") + new_y - elif missing_zeroes and self.omit_trailing_zeroes: - new_y += missing_zeroes * "0" - - new_y = float("{0}{1}.{2}".format("-" if negative else "", - new_y[:self.y_int_digits], - new_y[self.y_int_digits:])) - - return new_x, new_y - - -class GerberContext(object): - coord_format = None - coord_notation = NOTATION_ABSOLUTE - coord_unit = None - - x = 0 - y = 0 - - aperture = 0 - interpolation = INTERPOLATION_LINEAR - - image_polarity = IMAGE_POLARITY_POSITIVE - level_polarity = LEVEL_POLARITY_DARK - - def __init__(self): - pass - - def set_coord_format(self, zeroes, x, y): - self.coord_format = GerberCoordFormat(zeroes, x, y) - - def set_coord_notation(self, notation): - self.coord_notation = NOTATION_ABSOLUTE if notation == "A" else NOTATION_INCREMENTAL - - def set_coord_unit(self, unit): - self.coord_unit = UNIT_INCH if unit == "IN" else UNIT_MM - - def set_image_polarity(self, polarity): - self.image_polarity = IMAGE_POLARITY_POSITIVE if polarity == "POS" else IMAGE_POLARITY_NEGATIVE - - def set_level_polarity(self, polarity): - self.level_polarity = LEVEL_POLARITY_DARK if polarity == "D" else LEVEL_POLARITY_CLEAR - - def set_interpolation(self, interpolation): - self.interpolation = INTERPOLATION_LINEAR if interpolation in ("G01", "G1") else INTERPOLATION_ARC - - def set_aperture(self, d): - self.aperture = d - - def resolve(self, x, y): - x, y = self.coord_format.resolve(x, y) - return x or self.x, y or self.y - - def define_aperture(self, d, shape, modifiers): - pass - - def move(self, x, y, resolve=True): - if resolve: - self.x, self.y = self.resolve(x, y) - else: - self.x, self.y = x, y - - def stroke(self, x, y): - pass - - def line(self, x, y): - pass - - def arc(self, x, y): - pass - - def flash(self, x, y): - pass - - def evaluate(self, stmt): - if isinstance(stmt, (CommentStmt, UnknownStmt, EofStmt)): - return - - elif isinstance(stmt, ParamStmt): - self._evaluate_param(stmt) - - elif isinstance(stmt, CoordStmt): - self._evaluate_coord(stmt) - - elif isinstance(stmt, ApertureStmt): - self._evaluate_aperture(stmt) - - else: - raise Exception("Invalid statement to evaluate") - - def _evaluate_param(self, stmt): - if stmt.param == "FS": - self.set_coord_format(stmt.zero, stmt.x, stmt.y) - self.set_coord_notation(stmt.notation) - elif stmt.param == "MO:": - self.set_coord_unit(stmt.mo) - elif stmt.param == "IP:": - self.set_image_polarity(stmt.ip) - elif stmt.param == "LP:": - self.set_level_polarity(stmt.lp) - elif stmt.param == "AD": - self.define_aperture(stmt.d, stmt.shape, stmt.modifiers) - - def _evaluate_coord(self, stmt): - - if stmt.function in ("G01", "G1", "G02", "G2", "G03", "G3"): - self.set_interpolation(stmt.function) - - if stmt.op == "D01": - self.stroke(stmt.x, stmt.y) - elif stmt.op == "D02": - self.move(stmt.x, stmt.y) - elif stmt.op == "D03": - self.flash(stmt.x, stmt.y) - - def _evaluate_aperture(self, stmt): - self.set_aperture(stmt.d) diff --git a/gerber/render/__init__.py b/gerber/render/__init__.py new file mode 100644 index 0000000..cc87ee0 --- /dev/null +++ b/gerber/render/__init__.py @@ -0,0 +1,28 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# copyright 2014 Hamilton Kibbe + +# 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 +============ +**Gerber Renderers** + +This module provides contexts for rendering images of gerber layers. Currently +SVG is the only supported format. +""" + + +from svg import GerberSvgContext + diff --git a/gerber/render/apertures.py b/gerber/render/apertures.py new file mode 100644 index 0000000..55e6a30 --- /dev/null +++ b/gerber/render/apertures.py @@ -0,0 +1,58 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright 2014 Hamilton Kibbe + +# 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. +""" + + +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.') + + +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 \ No newline at end of file diff --git a/gerber/render/render.py b/gerber/render/render.py new file mode 100644 index 0000000..e15a36f --- /dev/null +++ b/gerber/render/render.py @@ -0,0 +1,133 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# copyright 2014 Hamilton Kibbe +# Modified from code by Paulo Henrique Silva + +# 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 ..statements import ( + CommentStmt, UnknownStmt, EofStmt, ParamStmt, CoordStmt, ApertureStmt +) + + +class GerberContext(object): + settings = {} + + x = 0 + y = 0 + + aperture = 0 + interpolation = 'linear' + + image_polarity = 'positive' + level_polarity = 'dark' + + def __init__(self): + pass + + def set_format(self, settings): + self.settings = settings + + def set_coord_format(self, zero_suppression, format, notation): + self.settings['zero_suppression'] = zero_suppression + self.settings['format'] = format + self.settings['notation'] = notation + + def set_coord_notation(self, notation): + self.settings['notation'] = notation + + def set_coord_unit(self, unit): + self.settings['units'] = unit + + def set_image_polarity(self, polarity): + self.image_polarity = polarity + + def set_level_polarity(self, polarity): + self.level_polarity = polarity + + def set_interpolation(self, interpolation): + self.interpolation = 'linear' if interpolation in ("G01", "G1") else 'arc' + + def set_aperture(self, d): + self.aperture = d + + def resolve(self, x, y): + return x or self.x, y or self.y + + def define_aperture(self, d, shape, modifiers): + pass + + def move(self, x, y, resolve=True): + if resolve: + self.x, self.y = self.resolve(x, y) + else: + self.x, self.y = x, y + + def stroke(self, x, y): + pass + + def line(self, x, y): + pass + + def arc(self, x, y): + pass + + def flash(self, x, y): + pass + + def drill(self, x, y, diameter): + pass + + def evaluate(self, stmt): + if isinstance(stmt, (CommentStmt, UnknownStmt, EofStmt)): + return + + elif isinstance(stmt, ParamStmt): + self._evaluate_param(stmt) + + elif isinstance(stmt, CoordStmt): + self._evaluate_coord(stmt) + + elif isinstance(stmt, ApertureStmt): + self._evaluate_aperture(stmt) + + else: + raise Exception("Invalid statement to evaluate") + + def _evaluate_param(self, stmt): + if stmt.param == "FS": + self.set_coord_format(stmt.zero_suppression, stmt.format, stmt.notation) + self.set_coord_notation(stmt.notation) + elif stmt.param == "MO:": + self.set_coord_unit(stmt.mode) + elif stmt.param == "IP:": + self.set_image_polarity(stmt.ip) + elif stmt.param == "LP:": + self.set_level_polarity(stmt.lp) + elif stmt.param == "AD": + self.define_aperture(stmt.d, stmt.shape, stmt.modifiers) + + def _evaluate_coord(self, stmt): + if stmt.function in ("G01", "G1", "G02", "G2", "G03", "G3"): + self.set_interpolation(stmt.function) + + if stmt.op == "D01": + self.stroke(stmt.x, stmt.y) + elif stmt.op == "D02": + self.move(stmt.x, stmt.y) + elif stmt.op == "D03": + self.flash(stmt.x, stmt.y) + + def _evaluate_aperture(self, stmt): + self.set_aperture(stmt.d) diff --git a/gerber/render/svg.py b/gerber/render/svg.py new file mode 100644 index 0000000..b16e534 --- /dev/null +++ b/gerber/render/svg.py @@ -0,0 +1,171 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright 2014 Hamilton Kibbe +# Based on render_svg.py by Paulo Henrique Silva + +# 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 .apertures import Circle, Rect, Obround, Polygon +import svgwrite + +SCALE = 300 + + +class SvgCircle(Circle): + def draw(self, ctx, x, y): + return ctx.dwg.line(start=(ctx.x * SCALE, -ctx.y * SCALE), + end=(x * SCALE, -y * SCALE), + stroke="rgb(184, 115, 51)", + stroke_width=SCALE * self.diameter, + stroke_linecap="round") + + def flash(self, ctx, x, y): + return [ctx.dwg.circle(center=(x * SCALE, -y * SCALE), + r = SCALE * (self.diameter / 2.0), + fill='rgb(184, 115, 51)'),] + + +class SvgRect(Rect): + def draw(self, ctx, x, y): + return ctx.dwg.line(start=(ctx.x * SCALE, -ctx.y * SCALE), + end=(x * SCALE, -y * SCALE), + stroke="rgb(184, 115, 51)", stroke_width=2, + stroke_linecap="butt") + + def flash(self, ctx, x, y): + xsize, ysize = self.size + return [ctx.dwg.rect(insert=(SCALE * (x - (xsize / 2)), + -SCALE * (y + (ysize / 2))), + size=(SCALE * xsize, SCALE * ysize), + fill="rgb(184, 115, 51)"),] + +class SvgObround(Obround): + def draw(self, ctx, x, y): + pass + + def flash(self, ctx, x, y): + xsize, ysize = self.size + + # horizontal obround + if xsize == ysize: + return [ctx.dwg.circle(center=(x * SCALE, -y * SCALE), + r = SCALE * (x / 2.0), + fill='rgb(184, 115, 51)'),] + if xsize > ysize: + rectx = xsize - ysize + recty = ysize + lcircle = ctx.dwg.circle(center=((x - (rectx / 2.0)) * SCALE, + -y * SCALE), + r = SCALE * (ysize / 2.0), + fill='rgb(184, 115, 51)') + + rcircle = ctx.dwg.circle(center=((x + (rectx / 2.0)) * SCALE, + -y * SCALE), + r = SCALE * (ysize / 2.0), + fill='rgb(184, 115, 51)') + + rect = ctx.dwg.rect(insert=(SCALE * (x - (xsize / 2.)), + -SCALE * (y + (ysize / 2.))), + size=(SCALE * xsize, SCALE * ysize), + fill='rgb(184, 115, 51)') + return [lcircle, rcircle, rect,] + + # Vertical obround + else: + rectx = xsize + recty = ysize - xsize + lcircle = ctx.dwg.circle(center=(x * SCALE, + (y - (recty / 2.)) * -SCALE), + r = SCALE * (xsize / 2.), + fill='rgb(184, 115, 51)') + + ucircle = ctx.dwg.circle(center=(x * SCALE, + (y + (recty / 2.)) * -SCALE), + r = SCALE * (xsize / 2.), + fill='rgb(184, 115, 51)') + + rect = ctx.dwg.rect(insert=(SCALE * (x - (xsize / 2.)), + -SCALE * (y + (ysize / 2.))), + size=(SCALE * xsize, SCALE * ysize), + fill='rgb(184, 115, 51)') + return [lcircle, ucircle, rect,] + + +class GerberSvgContext(GerberContext): + def __init__(self): + GerberContext.__init__(self) + + self.apertures = {} + self.dwg = svgwrite.Drawing() + #self.dwg.add(self.dwg.rect(insert=(0, 0), size=(2000, 2000), fill="black")) + + def set_bounds(self, bounds): + xbounds, ybounds = bounds + size = (SCALE * (xbounds[1] - xbounds[0]), SCALE * (ybounds[1] - ybounds[0])) + self.dwg.add(self.dwg.rect(insert=(SCALE * xbounds[0], -SCALE * ybounds[1]), size=size, fill="black")) + + + def define_aperture(self, d, shape, modifiers): + aperture = None + if shape == 'C': + aperture = SvgCircle(diameter=float(modifiers[0][0])) + elif shape == 'R': + aperture = SvgRect(size=modifiers[0][0:2]) + elif shape == 'O': + aperture = SvgObround(size=modifiers[0][0:2]) + self.apertures[d] = aperture + + def stroke(self, x, y): + super(GerberSvgContext, self).stroke(x, y) + + if self.interpolation == 'linear': + self.line(x, y) + elif self.interpolation == 'arc': + #self.arc(x, y) + self.line(x,y) + + def line(self, x, y): + super(GerberSvgContext, self).line(x, y) + x, y = self.resolve(x, y) + ap = self.apertures.get(self.aperture, None) + if ap is None: + return + self.dwg.add(ap.draw(self, x, y)) + self.move(x, y, resolve=False) + + + def arc(self, x, y): + super(GerberSvgContext, self).arc(x, y) + + + def flash(self, x, y): + super(GerberSvgContext, self).flash(x, y) + x, y = self.resolve(x, y) + ap = self.apertures.get(self.aperture, None) + if ap is None: + return + for shape in ap.flash(self, x, y): + self.dwg.add(shape) + self.move(x, y, resolve=False) + + + def drill(self, x, y, diameter): + hit = self.dwg.circle(center=(x*SCALE, -y*SCALE), r=SCALE*(diameter/2.0), fill='gray') + self.dwg.add(hit) + + def dump(self, filename): + self.dwg.saveas(filename) + + diff --git a/gerber/render_svg.py b/gerber/render_svg.py deleted file mode 100644 index bfe6859..0000000 --- a/gerber/render_svg.py +++ /dev/null @@ -1,106 +0,0 @@ -#! /usr/bin/env python -# -*- coding: utf-8 -*- - -# Copyright 2013-2014 Paulo Henrique Silva - -# 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, INTERPOLATION_LINEAR, INTERPOLATION_ARC -import svgwrite - - -class Shape(object): - pass - - -class Circle(Shape): - def __init__(self, diameter=0.0): - self.diameter = diameter - - def draw(self, ctx, x, y): - return ctx.dwg.line(start=(ctx.x*300, ctx.y*300), end=(x*300, y*300), stroke="rgb(184, 115, 51)", - stroke_width=2, stroke_linecap="round") - - def flash(self, ctx, x, y): - return ctx.dwg.circle(center=(x*300, y*300), r=300*(self.diameter/2.0), fill="rgb(184, 115, 51)") - - -class Rect(Shape): - def __init__(self, size=(0, 0)): - self.size = size - - def draw(self, ctx, x, y): - return ctx.dwg.line(start=(ctx.x*300, ctx.y*300), end=(x*300, y*300), stroke="rgb(184, 115, 51)", - stroke_width=2, stroke_linecap="butt") - - def flash(self, ctx, x, y): - return ctx.dwg.rect(insert=(300*x, 300*y), size=(300*float(self.size[0]), 300*float(self.size[1])), - fill="rgb(184, 115, 51)") - - -class GerberSvgContext(GerberContext): - def __init__(self): - GerberContext.__init__(self) - - self.apertures = {} - self.dwg = svgwrite.Drawing() - self.dwg.add(self.dwg.rect(insert=(0, 0), size=(2000, 2000), fill="black")) - - def define_aperture(self, d, shape, modifiers): - aperture = None - if shape == "C": - aperture = Circle(diameter=float(modifiers[0][0])) - elif shape == "R": - aperture = Rect(size=modifiers[0][0:2]) - - self.apertures[d] = aperture - - def stroke(self, x, y): - super(GerberSvgContext, self).stroke(x, y) - - if self.interpolation == INTERPOLATION_LINEAR: - self.line(x, y) - elif self.interpolation == INTERPOLATION_ARC: - self.arc(x, y) - - def line(self, x, y): - super(GerberSvgContext, self).line(x, y) - - x, y = self.resolve(x, y) - - ap = self.apertures.get(str(self.aperture), None) - if ap is None: - return - - self.dwg.add(ap.draw(self, x, y)) - - self.move(x, y, resolve=False) - - def arc(self, x, y): - super(GerberSvgContext, self).arc(x, y) - - def flash(self, x, y): - super(GerberSvgContext, self).flash(x, y) - - x, y = self.resolve(x, y) - - ap = self.apertures.get(str(self.aperture), None) - if ap is None: - return - - self.dwg.add(ap.flash(self, x, y)) - - self.move(x, y, resolve=False) - - def dump(self): - self.dwg.saveas("teste.svg") diff --git a/gerber/statements.py b/gerber/statements.py new file mode 100644 index 0000000..53f7f78 --- /dev/null +++ b/gerber/statements.py @@ -0,0 +1,605 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- +""" +gerber.statements +================= +**Gerber file statement classes ** + +""" +from .utils import parse_gerber_value, write_gerber_value, decimal_string + + + +__all__ = ['FSParamStmt', 'MOParamStmt','IPParamStmt', 'OFParamStmt', + 'LPParamStmt', 'ADParamStmt', 'AMParamStmt', 'INParamStmt', + 'LNParamStmt', 'CoordStmt', 'ApertureStmt', 'CommentStmt', + 'EofStmt', 'UnknownStmt'] + +class Statement(object): + def __init__(self, type): + self.type = type + + def __str__(self): + s = "<{0} ".format(self.__class__.__name__) + + for key, value in self.__dict__.items(): + s += "{0}={1} ".format(key, value) + + s = s.rstrip() + ">" + return s + + +class ParamStmt(Statement): + def __init__(self, param): + Statement.__init__(self, "PARAM") + self.param = param + + +class FSParamStmt(ParamStmt): + """ FS - Gerber Format Specification Statement + """ + + @classmethod + def from_dict(cls, stmt_dict): + """ + """ + param = stmt_dict.get('param').strip() + zeros = 'leading' if stmt_dict.get('zero') == 'L' else 'trailing' + notation = 'absolute' if stmt_dict.get('notation') == 'A' else 'incremental' + x = map(int,stmt_dict.get('x').strip()) + format = (x[0], x[1]) + if notation == 'incremental': + print('This file uses incremental notation. To quote the gerber \ + file specification:\nIncremental notation is a source of \ + endless confusion. Always use absolute notation.\n\nYou \ + have been warned') + return cls(param, zeros, notation, format) + + def __init__(self, param, zero_suppression='leading', + notation='absolute', format=(2,4)): + """ Initialize FSParamStmt class + + .. note:: + The FS command specifies the format of the coordinate data. It + must only be used once at the beginning of a file. It must be + specified before the first use of coordinate data. + + Parameters + ---------- + param : string + Parameter. + + zero_suppression : string + Zero-suppression mode. May be either 'leading' or 'trailing' + + notation : string + Notation mode. May be either 'absolute' or 'incremental' + + format : tuple (int, int) + Gerber precision format expressed as a tuple containing: + (number of integer-part digits, number of decimal-part digits) + + Returns + ------- + ParamStmt : FSParamStmt + Initialized FSParamStmt class. + + """ + ParamStmt.__init__(self, param) + self.zero_suppression = zero_suppression + 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' + format = ''.join(map(str,self.format)) + return '%FS{0}{1}X{2}Y{3}*%'.format(zero_suppression, notation, + format, format) + + def __str__(self): + return ('' % + (self.format[0], self.format[1], self.zero_suppression, + self.notation)) + + +class MOParamStmt(ParamStmt): + """ MO - Gerber Mode (measurement units) Statement. + """ + + @classmethod + def from_dict(cls, stmt_dict): + param = stmt_dict.get('param') + mo = 'inch' if stmt_dict.get('mo') == 'IN' else 'metric' + return cls(param, mo) + + def __init__(self, param, mo): + """ Initialize MOParamStmt class + + Parameters + ---------- + param : string + Parameter. + + mo : string + Measurement units. May be either 'inch' or 'metric' + + Returns + ------- + ParamStmt : MOParamStmt + Initialized MOParamStmt class. + + """ + ParamStmt.__init__(self, param) + self.mode = mo + + def to_gerber(self): + 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_str) + + +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 'negative' + return '%IP{0}*%'.format(ip) + + def __str__(self): + return ('' % 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, settings): + stmt = '%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_str) + +class LPParamStmt(ParamStmt): + """ LP - Gerber Level Polarity statement + """ + + @classmethod + def from_dict(cls, stmt_dict): + param = stmt_dict.get('lp') + lp = 'clear' if stmt_dict.get('lp') == 'C' else 'dark' + return cls(param, lp) + + def __init__(self, param, lp): + """ Initialize LPParamStmt class + + Parameters + ---------- + param : string + Parameter + + lp : string + Level polarity. May be either 'clear' or 'dark' + + Returns + ------- + ParamStmt : LPParamStmt + Initialized LPParamStmt class. + + """ + ParamStmt.__init__(self, param) + self.lp = lp + + + def to_gerber(self, settings): + lp = 'C' if self.lp == 'clear' else 'dark' + return '%LP{0}*%'.format(self.lp) + + def __str__(self): + return '' % self.lp + + +class ADParamStmt(ParamStmt): + """ AD - Gerber Aperture Definition Statement + """ + + @classmethod + def from_dict(cls, stmt_dict): + param = stmt_dict.get('param') + d = int(stmt_dict.get('d')) + shape = stmt_dict.get('shape') + modifiers = stmt_dict.get('modifiers') + if modifiers is not None: + modifiers = [[float(x) for x in m.split('X')] + for m in modifiers.split(',')] + return cls(param, d, shape, modifiers) + + + def __init__(self, param, d, shape, modifiers): + """ Initialize ADParamStmt class + + Parameters + ---------- + param : string + Parameter code + + d : int + Aperture D-code + + shape : string + aperture name + + modifiers : list of lists of floats + Shape modifiers + + Returns + ------- + ParamStmt : LPParamStmt + Initialized LPParamStmt class. + + """ + ParamStmt.__init__(self, param) + self.d = d + self.shape = shape + self.modifiers = 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])) + + def __str__(self): + if self.shape == 'C': + shape = 'circle' + elif self.shape == 'R': + shape = 'rectangle' + elif self.shape == 'O': + shape = 'oblong' + else: + shape = self.shape + + return '' % (self.d, shape) + + +class AMParamStmt(ParamStmt): + """ AM - Aperture Macro Statement + """ + + @classmethod + def from_dict(cls, stmt_dict): + return cls(**stmt_dict) + + def __init__(self, param, name, macro): + """ Initialize AMParamStmt class + + Parameters + ---------- + param : string + Parameter code + + name : string + Aperture macro name + + macro : string + Aperture macro string + + Returns + ------- + ParamStmt : AMParamStmt + Initialized AMParamStmt class. + + """ + ParamStmt.__init__(self, param) + self.name = name + self.macro = macro + + def to_gerber(self): + return '%AM{0}*{1}*%'.format(self.name, self.macro) + + def __str__(self): + return '' % (self.name, macro) + + +class INParamStmt(ParamStmt): + """ IN - Image Name Statement + """ + @classmethod + def from_dict(cls, stmt_dict): + return cls(**stmt_dict) + + def __init__(self, param, name): + """ Initialize INParamStmt class + + Parameters + ---------- + param : string + Parameter code + + name : string + Image name + + Returns + ------- + ParamStmt : INParamStmt + Initialized INParamStmt class. + + """ + ParamStmt.__init__(self, param) + self.name = name + + def to_gerber(self): + return '%IN{0}*%'.format(self.name) + + def __str__(self): + return '' % self.name + +class LNParamStmt(ParamStmt): + """ LN - Level Name Statement (Deprecated) + """ + @classmethod + def from_dict(cls, stmt_dict): + return cls(**stmt_dict) + + def __init__(self, param, name): + """ Initialize LNParamStmt class + + Parameters + ---------- + param : string + Parameter code + + name : string + Level name + + Returns + ------- + ParamStmt : LNParamStmt + Initialized LNParamStmt class. + + """ + ParamStmt.__init__(self, param) + self.name = name + + def to_gerber(self): + return '%LN{0}*%'.format(self.name) + + def __str__(self): + return '' % self.name + +class CoordStmt(Statement): + """ Coordinate Data Block + """ + + @classmethod + def from_dict(cls, stmt_dict, settings): + zeros = settings['zero_suppression'] + format = settings['format'] + function = stmt_dict.get('function') + x = stmt_dict.get('x') + y = stmt_dict.get('y') + i = stmt_dict.get('i') + j = stmt_dict.get('j') + op = stmt_dict.get('op') + + if x is not None: + x = parse_gerber_value(stmt_dict.get('x'), + format, zeros) + if y is not None: + y = parse_gerber_value(stmt_dict.get('y'), + format, zeros) + if i is not None: + i = parse_gerber_value(stmt_dict.get('i'), + format, zeros) + if j is not None: + j = parse_gerber_value(stmt_dict.get('j'), + format, zeros) + return cls(function, x, y, i, j, op, settings) + + + def __init__(self, function, x, y, i, j, op, settings): + """ Initialize CoordStmt class + + Parameters + ---------- + function : string + function + + x : float + X coordinate + + y : float + Y coordinate + + i : float + Coordinate offset in the X direction + + j : float + Coordinate offset in the Y direction + + op : string + Operation code + + settings : dict {'zero_suppression', 'format'} + Gerber file coordinate format + + Returns + ------- + Statement : CoordStmt + Initialized CoordStmt class. + + """ + Statement.__init__(self, "COORD") + self.zero_suppression = settings['zero_suppression'] + self.format = settings['format'] + self.function = function + self.x = x + self.y = y + self.i = i + self.j = j + self.op = op + + + def to_gerber(self): + 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.op: + ret += self.op + return ret + '*' + + 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.op: + if self.op == 'D01': + op = 'Lights On' + elif self.op == 'D02': + op = 'Lights Off' + elif self.op == 'D03': + op = 'Flash' + else: + op = self.op + coord_str += 'Op: %s' % op + + return '' % coord_str + + +class ApertureStmt(Statement): + """ Aperture Statement + """ + def __init__(self, d): + Statement.__init__(self, "APERTURE") + self.d = int(d) + + def to_gerber(self): + return 'G54D{0}*'.format(self.d) + + def __str__(self): + return '' % self.d + +class CommentStmt(Statement): + """ Comment Statment + """ + def __init__(self, comment): + Statement.__init__(self, "COMMENT") + self.comment = comment + + def to_gerber(self): + return 'G04{0}*'.format(self.comment) + + def __str__(self): + return '' % self.comment + + +class EofStmt(Statement): + """ EOF Statement + """ + def __init__(self): + Statement.__init__(self, "EOF") + + def to_gerber(self): + return 'M02*' + + def __str__(self): + return '' + + +class UnknownStmt(Statement): + """ Unknown Statement + """ + def __init__(self, line): + Statement.__init__(self, "UNKNOWN") + self.line = line + \ No newline at end of file diff --git a/gerber/tests/__init__.py b/gerber/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gerber/tests/test_utils.py b/gerber/tests/test_utils.py new file mode 100644 index 0000000..50e2403 --- /dev/null +++ b/gerber/tests/test_utils.py @@ -0,0 +1,68 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# Author: Hamilton Kibbe + +from ..utils import decimal_string, parse_gerber_value, write_gerber_value + + +def test_zero_suppression(): + """ Test gerber value parser and writer handle zero suppression correctly. + """ + # Default format + format = (2, 5) + + # Test leading zero suppression + zero_suppression = 'leading' + test_cases = [('1', 0.00001), ('10', 0.0001), ('100', 0.001), + ('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),] + for string, value in test_cases: + assert(value == parse_gerber_value(string,format,zero_suppression)) + assert(string == write_gerber_value(value,format,zero_suppression)) + + # Test trailing zero suppression + zero_suppression = 'trailing' + test_cases = [('1', 10.0), ('01', 1.0), ('001', 0.1), ('0001', 0.01), + ('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)] + for string, value in test_cases: + assert(value == parse_gerber_value(string,format,zero_suppression)) + assert(string == write_gerber_value(value,format,zero_suppression)) + + + +def test_format(): + """ Test gerber value parser and writer handle format correctly + """ + zero_suppression = 'leading' + test_cases = [((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,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),] + for format, string, value in test_cases: + assert(value == parse_gerber_value(string,format,zero_suppression)) + assert(string == write_gerber_value(value,format,zero_suppression)) + + zero_suppression = 'trailing' + test_cases = [((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), ((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),] + for format, string, value in test_cases: + assert(value == parse_gerber_value(string,format,zero_suppression)) + assert(string == write_gerber_value(value,format,zero_suppression)) + + +def test_decimal_truncation(): + """ Test decimal string truncates value to the correct precision + """ + 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) \ No newline at end of file diff --git a/gerber/utils.py b/gerber/utils.py index 02a8a14..35b4fd0 100644 --- a/gerber/utils.py +++ b/gerber/utils.py @@ -10,7 +10,7 @@ files. """ # Author: Hamilton Kibbe -# License: MIT +# License: def parse_gerber_value(value, format=(2, 5), zero_suppression='trailing'): """ Convert gerber/excellon formatted string to floating-point number @@ -54,6 +54,7 @@ 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.strip(' +') negative = '-' in value if negative: @@ -67,7 +68,8 @@ def parse_gerber_value(value, format=(2, 5), zero_suppression='trailing'): offset = 0 if zero_suppression == 'trailing' else (MAX_DIGITS - len(value)) for i, digit in enumerate(value): digits[i + offset] = digit - + + result = float(''.join(digits[:integer_digits] + ['.'] + digits[integer_digits:])) return -1.0 * result if negative else result @@ -128,3 +130,37 @@ def write_gerber_value(value, format=(2, 5), zero_suppression='trailing'): return ''.join(digits) if not negative else ''.join(['-'] + digits) + + +def decimal_string(value, precision=6): + """ Convert float to string with limited precision + + Parameters + ---------- + value : float + A floating point value. + + precision : + Maximum number of decimal places to print + + Returns + ------- + value : string + The specified value as a string. + + """ + floatstr = '%0.20g' % value + integer = None + decimal = None + if '.' in floatstr: + integer, decimal = floatstr.split('.') + elif ',' in floatstr: + integer, decimal = floatstr.split(',') + if len(decimal) > precision: + decimal = decimal[:precision] + if integer or decimal: + return ''.join([integer, '.', decimal]) + else: + return int(floatstr) + + -- cgit From 3a5dbcf1e13704b7352d5fb3c4777d7df3fed081 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Sun, 28 Sep 2014 21:17:13 -0400 Subject: added ExcellonFile class --- gerber/__init__.py | 15 ++++ gerber/__main__.py | 10 +-- gerber/excellon.py | 133 +++++++++++++++++++++----------- gerber/gerber.py | 77 +++++++++---------- gerber/render/apertures.py | 9 ++- gerber/render/render.py | 12 +-- gerber/render/svg.py | 50 ++++++------ gerber/statements.py | 187 ++++++++++++++++++++++----------------------- gerber/utils.py | 108 ++++++++++++++++---------- 9 files changed, 336 insertions(+), 265 deletions(-) (limited to 'gerber') diff --git a/gerber/__init__.py b/gerber/__init__.py index 0bf7c24..089d7b6 100644 --- a/gerber/__init__.py +++ b/gerber/__init__.py @@ -14,3 +14,18 @@ # 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. + + +def read(filename): + """ Read a gerber or excellon file and return a representative object. + """ + import gerber + import excellon + from utils import detect_file_format + fmt = detect_file_format(filename) + if fmt == 'rs274x': + return gerber.read(filename) + elif fmt == 'excellon': + return excellon.read(filename) + else: + return None diff --git a/gerber/__main__.py b/gerber/__main__.py index d32fa01..31b70f8 100644 --- a/gerber/__main__.py +++ b/gerber/__main__.py @@ -10,10 +10,10 @@ # 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. +# 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. if __name__ == '__main__': from .gerber import GerberFile @@ -34,5 +34,3 @@ if __name__ == '__main__': p = ExcellonParser(ctx) p.parse('ncdrill.txt') p.dump('testwithdrill.svg') - - diff --git a/gerber/excellon.py b/gerber/excellon.py index fef5844..d92d57c 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -2,19 +2,60 @@ import re from itertools import tee, izip from .utils import parse_gerber_value - - -INCH = 0 -METRIC = 1 -ABSOLUTE = 0 -INCREMENTAL = 1 -LZ = 0 -TZ = 1 +def read(filename): + """ Read data from filename and return an ExcellonFile + """ + return ExcellonParser().parse(filename) -class Tool(object): +class ExcellonFile(object): + """ A class representing a single excellon file + + The ExcellonFile class represents a single excellon file. + + Parameters + ---------- + tools : list + list of gerber file statements + + hits : list of tuples + list of drill hits as (, (x, y)) + settings : dict + Dictionary of gerber file settings + + filename : string + Filename of the source gerber file + + Attributes + ---------- + units : string + either 'inch' or 'metric'. + + """ + def __init__(self, tools, hits, settings, filename): + self.tools = tools + self.hits = hits + self.settings = settings + self.filename = filename + + def report(self): + """ Print drill report + """ + pass + + def render(self, filename, ctx): + """ Generate image of file + """ + for tool, pos in self.hits: + ctx.drill(pos[0], pos[1], tool.diameter) + ctx.dump(filename) + + +class Tool(object): + """ Excellon Tool class + """ @classmethod def from_line(cls, line, settings): commands = re.split('([BCFHSTZ])', line)[1:] @@ -38,7 +79,7 @@ class Tool(object): elif cmd == 'Z': args['depth_offset'] = parse_gerber_value(val, format, zero_suppression) return cls(settings, **args) - + def __init__(self, settings, **kwargs): self.number = kwargs.get('number') self.feed_rate = kwargs.get('feed_rate') @@ -47,79 +88,83 @@ class Tool(object): self.diameter = kwargs.get('diameter') self.max_hit_count = kwargs.get('max_hit_count') self.depth_offset = kwargs.get('depth_offset') - self.units = settings.get('units', INCH) - + self.units = settings.get('units', 'inch') + def __repr__(self): - unit = 'in.' if self.units == INCH else 'mm' + unit = 'in.' if self.units == 'inch' else 'mm' return '' % (self.number, self.diameter, unit) - class ExcellonParser(object): def __init__(self, ctx=None): - self.ctx=ctx + self.ctx = ctx self.notation = 'absolute' self.units = 'inch' self.zero_suppression = 'trailing' - self.format = (2,5) + self.format = (2, 5) self.state = 'INIT' - self.tools = {} + self.tools = [] self.hits = [] self.active_tool = None self.pos = [0., 0.] if ctx is not None: - self.ctx.set_coord_format(zero_suppression='trailing', format=[2,5], notation='absolute') + self.ctx.set_coord_format(zero_suppression='trailing', + format=(2, 5), notation='absolute') + def parse(self, filename): with open(filename, 'r') as f: for line in f: self._parse(line) - + settings = {'notation': self.notation, 'units': self.units, + 'zero_suppression': self.zero_suppression, + 'format': self.format} + return ExcellonFile(self.tools, self.hits, settings, filename) + def dump(self, filename): self.ctx.dump(filename) - + def _parse(self, line): if 'M48' in line: self.state = 'HEADER' - + if 'G00' in line: self.state = 'ROUT' - + if 'G05' in line: self.state = 'DRILL' - + elif line[0] == '%' and self.state == 'HEADER': self.state = 'DRILL' - + if 'INCH' in line or line.strip() == 'M72': - self.units = 'INCH' - + self.units = 'inch' + elif 'METRIC' in line or line.strip() == 'M71': - self.units = 'METRIC' - + self.units = 'metric' + if 'LZ' in line: self.zeros = 'L' - + elif 'TZ' in line: self.zeros = 'T' if 'ICI' in line and 'ON' in line or line.strip() == 'G91': self.notation = 'incremental' - + if 'ICI' in line and 'OFF' in line or line.strip() == 'G90': self.notation = 'incremental' - + zs = self._settings()['zero_suppression'] fmt = self._settings()['format'] - + # tool definition if line[0] == 'T' and self.state == 'HEADER': - tool = Tool.from_line(line,self._settings()) + tool = Tool.from_line(line, self._settings()) self.tools[tool.number] = tool - + elif line[0] == 'T' and self.state != 'HEADER': self.active_tool = self.tools[int(line.strip().split('T')[1])] - if line[0] in ['X', 'Y']: x = None y = None @@ -127,10 +172,9 @@ class ExcellonParser(object): splitline = line.strip('X').split('Y') x = parse_gerber_value(splitline[0].strip(), fmt, zs) if len(splitline) == 2: - y = parse_gerber_value(splitline[1].strip(), fmt,zs) + y = parse_gerber_value(splitline[1].strip(), fmt, zs) else: - y = parse_gerber_value(line.strip(' Y'), fmt,zs) - + y = parse_gerber_value(line.strip(' Y'), fmt, zs) if self.notation == 'absolute': if x is not None: self.pos[0] = x @@ -146,20 +190,17 @@ class ExcellonParser(object): if self.ctx is not None: self.ctx.drill(self.pos[0], self.pos[1], self.active_tool.diameter) - + def _settings(self): - return {'units':self.units, 'zero_suppression':self.zero_suppression, + return {'units': self.units, 'zero_suppression': self.zero_suppression, 'format': self.format} - + + def pairwise(iterator): itr = iter(iterator) while True: yield tuple([itr.next() for i in range(2)]) - + if __name__ == '__main__': - tools = [] - settings = {'units':INCH, 'zeros':LZ} p = parser() p.parse('examples/ncdrill.txt') - - \ No newline at end of file diff --git a/gerber/gerber.py b/gerber/gerber.py index 31d9b82..949037b 100644 --- a/gerber/gerber.py +++ b/gerber/gerber.py @@ -3,7 +3,7 @@ # copyright 2014 Hamilton Kibbe # Modified from parser.py by Paulo Henrique Silva -# +# # 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 @@ -15,33 +15,41 @@ # 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.gerber +============ +**Gerber File module** + +This module provides an RS-274-X class and parser +""" + import re import json from .statements import * +def read(filename): + """ Read data from filename and return a GerberFile + """ + return GerberParser().parse(filename) class GerberFile(object): """ A class representing a single gerber file - - The GerberFile class represents a single gerber file. - + + The GerberFile class represents a single gerber file. + Parameters ---------- + statements : list + list of gerber file statements + + settings : dict + Dictionary of gerber file settings + filename : string - Parameter. - - zero_suppression : string - Zero-suppression mode. May be either 'leading' or 'trailing' - - notation : string - Notation mode. May be either 'absolute' or 'incremental' - - format : tuple (int, int) - Gerber precision format expressed as a tuple containing: - (number of integer-part digits, number of decimal-part digits) + Filename of the source gerber file Attributes ---------- @@ -50,7 +58,7 @@ class GerberFile(object): units : string either 'inch' or 'metric'. - + size : tuple, (, ) Size in [self.units] of the layer described by the gerber file. @@ -59,32 +67,25 @@ class GerberFile(object): `bounds` is stored as ((min x, max x), (min y, max y)) """ - - @classmethod - def read(cls, filename): - """ Read data from filename and return a GerberFile - """ - return GerberParser().parse(filename) - def __init__(self, statements, settings, filename=None): self.filename = filename self.statements = statements self.settings = settings - + @property def comments(self): return [comment.comment for comment in self.statements if isinstance(comment, CommentStmt)] - + @property def units(self): return self.settings['units'] - + @property def size(self): xbounds, ybounds = self.bounds return (xbounds[1] - xbounds[0], ybounds[1] - ybounds[0]) - + @property def bounds(self): xbounds = [0.0, 0.0] @@ -106,9 +107,8 @@ class GerberFile(object): ybounds[0] = stmt.j if stmt.j is not None and stmt.j > ybounds[1]: ybounds[1] = stmt.j - - return (xbounds, ybounds) - + return (xbounds, ybounds) + def write(self, filename): """ Write data out to a gerber file """ @@ -123,8 +123,7 @@ class GerberFile(object): for statement in self.statements: ctx.evaluate(statement) ctx.dump(filename) - - + class GerberParser(object): """ GerberParser @@ -179,7 +178,7 @@ class GerberParser(object): for stmt in self._parse(data): self.statements.append(stmt) - + return GerberFile(self.statements, self.settings, filename) def dump_json(self): @@ -197,7 +196,7 @@ class GerberParser(object): for i, line in enumerate(data): line = oldline + line.strip() - + # skip empty lines if not len(line): continue @@ -207,10 +206,10 @@ class GerberParser(object): oldline = line continue - did_something = True # make sure we do at least one loop + did_something = True # make sure we do at least one loop while did_something and len(line) > 0: did_something = False - + # coord (coord, r) = self._match_one(self.COORD_STMT, line) if coord: @@ -223,7 +222,7 @@ class GerberParser(object): (aperture, r) = self._match_one(self.APERTURE_STMT, line) if aperture: yield ApertureStmt(**aperture) - + did_something = True line = r continue @@ -240,7 +239,7 @@ class GerberParser(object): (param, r) = self._match_one_from_many(self.PARAM_STMT, line) if param: if param["param"] == "FS": - stmt = FSParamStmt.from_dict(param) + stmt = FSParamStmt.from_dict(param) self.settings = {'zero_suppression': stmt.zero_suppression, 'format': stmt.format, 'notation': stmt.notation} @@ -276,7 +275,7 @@ class GerberParser(object): did_something = True line = r continue - + if False: print self.COORD_STMT.pattern print self.APERTURE_STMT.pattern diff --git a/gerber/render/apertures.py b/gerber/render/apertures.py index 55e6a30..f163b1f 100644 --- a/gerber/render/apertures.py +++ b/gerber/render/apertures.py @@ -29,7 +29,7 @@ class Aperture(object): """ 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.') @@ -40,19 +40,22 @@ class Circle(Aperture): 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 \ No newline at end of file + pass diff --git a/gerber/render/render.py b/gerber/render/render.py index e15a36f..c372783 100644 --- a/gerber/render/render.py +++ b/gerber/render/render.py @@ -34,11 +34,11 @@ class GerberContext(object): level_polarity = 'dark' def __init__(self): - pass + pass def set_format(self, settings): self.settings = settings - + def set_coord_format(self, zero_suppression, format, notation): self.settings['zero_suppression'] = zero_suppression self.settings['format'] = format @@ -52,9 +52,9 @@ class GerberContext(object): def set_image_polarity(self, polarity): self.image_polarity = polarity - + def set_level_polarity(self, polarity): - self.level_polarity = polarity + self.level_polarity = polarity def set_interpolation(self, interpolation): self.interpolation = 'linear' if interpolation in ("G01", "G1") else 'arc' @@ -63,8 +63,8 @@ class GerberContext(object): self.aperture = d def resolve(self, x, y): - return x or self.x, y or self.y - + return x or self.x, y or self.y + def define_aperture(self, d, shape, modifiers): pass diff --git a/gerber/render/svg.py b/gerber/render/svg.py index b16e534..7d5c8fd 100644 --- a/gerber/render/svg.py +++ b/gerber/render/svg.py @@ -33,8 +33,8 @@ class SvgCircle(Circle): def flash(self, ctx, x, y): return [ctx.dwg.circle(center=(x * SCALE, -y * SCALE), - r = SCALE * (self.diameter / 2.0), - fill='rgb(184, 115, 51)'),] + r = SCALE * (self.diameter / 2.0), + fill='rgb(184, 115, 51)'), ] class SvgRect(Rect): @@ -47,41 +47,42 @@ class SvgRect(Rect): def flash(self, ctx, x, y): xsize, ysize = self.size return [ctx.dwg.rect(insert=(SCALE * (x - (xsize / 2)), - -SCALE * (y + (ysize / 2))), - size=(SCALE * xsize, SCALE * ysize), - fill="rgb(184, 115, 51)"),] + -SCALE * (y + (ysize / 2))), + size=(SCALE * xsize, SCALE * ysize), + fill="rgb(184, 115, 51)"), ] + class SvgObround(Obround): def draw(self, ctx, x, y): pass - + def flash(self, ctx, x, y): xsize, ysize = self.size - + # horizontal obround if xsize == ysize: return [ctx.dwg.circle(center=(x * SCALE, -y * SCALE), - r = SCALE * (x / 2.0), - fill='rgb(184, 115, 51)'),] + r = SCALE * (x / 2.0), + fill='rgb(184, 115, 51)'), ] if xsize > ysize: rectx = xsize - ysize recty = ysize lcircle = ctx.dwg.circle(center=((x - (rectx / 2.0)) * SCALE, - -y * SCALE), + -y * SCALE), r = SCALE * (ysize / 2.0), fill='rgb(184, 115, 51)') - + rcircle = ctx.dwg.circle(center=((x + (rectx / 2.0)) * SCALE, - -y * SCALE), + -y * SCALE), r = SCALE * (ysize / 2.0), fill='rgb(184, 115, 51)') - + rect = ctx.dwg.rect(insert=(SCALE * (x - (xsize / 2.)), -SCALE * (y + (ysize / 2.))), size=(SCALE * xsize, SCALE * ysize), fill='rgb(184, 115, 51)') - return [lcircle, rcircle, rect,] - + return [lcircle, rcircle, rect, ] + # Vertical obround else: rectx = xsize @@ -90,18 +91,18 @@ class SvgObround(Obround): (y - (recty / 2.)) * -SCALE), r = SCALE * (xsize / 2.), fill='rgb(184, 115, 51)') - + ucircle = ctx.dwg.circle(center=(x * SCALE, (y + (recty / 2.)) * -SCALE), r = SCALE * (xsize / 2.), fill='rgb(184, 115, 51)') - + rect = ctx.dwg.rect(insert=(SCALE * (x - (xsize / 2.)), -SCALE * (y + (ysize / 2.))), size=(SCALE * xsize, SCALE * ysize), fill='rgb(184, 115, 51)') - return [lcircle, ucircle, rect,] - + return [lcircle, ucircle, rect, ] + class GerberSvgContext(GerberContext): def __init__(self): @@ -112,10 +113,9 @@ class GerberSvgContext(GerberContext): #self.dwg.add(self.dwg.rect(insert=(0, 0), size=(2000, 2000), fill="black")) def set_bounds(self, bounds): - xbounds, ybounds = bounds + xbounds, ybounds = bounds size = (SCALE * (xbounds[1] - xbounds[0]), SCALE * (ybounds[1] - ybounds[0])) self.dwg.add(self.dwg.rect(insert=(SCALE * xbounds[0], -SCALE * ybounds[1]), size=size, fill="black")) - def define_aperture(self, d, shape, modifiers): aperture = None @@ -133,8 +133,7 @@ class GerberSvgContext(GerberContext): if self.interpolation == 'linear': self.line(x, y) elif self.interpolation == 'arc': - #self.arc(x, y) - self.line(x,y) + self.arc(x, y) def line(self, x, y): super(GerberSvgContext, self).line(x, y) @@ -145,11 +144,9 @@ class GerberSvgContext(GerberContext): self.dwg.add(ap.draw(self, x, y)) self.move(x, y, resolve=False) - def arc(self, x, y): super(GerberSvgContext, self).arc(x, y) - def flash(self, x, y): super(GerberSvgContext, self).flash(x, y) x, y = self.resolve(x, y) @@ -160,12 +157,9 @@ class GerberSvgContext(GerberContext): self.dwg.add(shape) self.move(x, y, resolve=False) - def drill(self, x, y, diameter): hit = self.dwg.circle(center=(x*SCALE, -y*SCALE), r=SCALE*(diameter/2.0), fill='gray') self.dwg.add(hit) def dump(self, filename): self.dwg.saveas(filename) - - diff --git a/gerber/statements.py b/gerber/statements.py index 53f7f78..418a852 100644 --- a/gerber/statements.py +++ b/gerber/statements.py @@ -3,18 +3,18 @@ """ gerber.statements ================= -**Gerber file statement classes ** +**Gerber file statement classes** """ from .utils import parse_gerber_value, write_gerber_value, decimal_string - -__all__ = ['FSParamStmt', 'MOParamStmt','IPParamStmt', 'OFParamStmt', +__all__ = ['FSParamStmt', 'MOParamStmt', 'IPParamStmt', 'OFParamStmt', 'LPParamStmt', 'ADParamStmt', 'AMParamStmt', 'INParamStmt', 'LNParamStmt', 'CoordStmt', 'ApertureStmt', 'CommentStmt', 'EofStmt', 'UnknownStmt'] + class Statement(object): def __init__(self, type): self.type = type @@ -38,15 +38,15 @@ class ParamStmt(Statement): class FSParamStmt(ParamStmt): """ FS - Gerber Format Specification Statement """ - + @classmethod def from_dict(cls, stmt_dict): - """ + """ """ param = stmt_dict.get('param').strip() zeros = 'leading' if stmt_dict.get('zero') == 'L' else 'trailing' notation = 'absolute' if stmt_dict.get('notation') == 'A' else 'incremental' - x = map(int,stmt_dict.get('x').strip()) + x = map(int, stmt_dict.get('x').strip()) format = (x[0], x[1]) if notation == 'incremental': print('This file uses incremental notation. To quote the gerber \ @@ -54,36 +54,36 @@ class FSParamStmt(ParamStmt): endless confusion. Always use absolute notation.\n\nYou \ have been warned') return cls(param, zeros, notation, format) - + def __init__(self, param, zero_suppression='leading', - notation='absolute', format=(2,4)): + notation='absolute', format=(2, 4)): """ Initialize FSParamStmt class - + .. note:: The FS command specifies the format of the coordinate data. It must only be used once at the beginning of a file. It must be specified before the first use of coordinate data. - + Parameters ---------- param : string Parameter. - + zero_suppression : string Zero-suppression mode. May be either 'leading' or 'trailing' notation : string Notation mode. May be either 'absolute' or 'incremental' - + format : tuple (int, int) - Gerber precision format expressed as a tuple containing: + Gerber precision format expressed as a tuple containing: (number of integer-part digits, number of decimal-part digits) Returns ------- ParamStmt : FSParamStmt Initialized FSParamStmt class. - + """ ParamStmt.__init__(self, param) self.zero_suppression = zero_suppression @@ -93,7 +93,7 @@ class FSParamStmt(ParamStmt): def to_gerber(self): zero_suppression = 'L' if self.zero_suppression == 'leading' else 'T' notation = 'A' if self.notation == 'absolute' else 'I' - format = ''.join(map(str,self.format)) + format = ''.join(map(str, self.format)) return '%FS{0}{1}X{2}Y{3}*%'.format(zero_suppression, notation, format, format) @@ -104,23 +104,23 @@ class FSParamStmt(ParamStmt): class MOParamStmt(ParamStmt): - """ MO - Gerber Mode (measurement units) Statement. + """ MO - Gerber Mode (measurement units) Statement. """ - + @classmethod def from_dict(cls, stmt_dict): param = stmt_dict.get('param') mo = 'inch' if stmt_dict.get('mo') == 'IN' else 'metric' return cls(param, mo) - + def __init__(self, param, mo): """ Initialize MOParamStmt class - + Parameters ---------- param : string Parameter. - + mo : string Measurement units. May be either 'inch' or 'metric' @@ -128,11 +128,11 @@ class MOParamStmt(ParamStmt): ------- ParamStmt : MOParamStmt Initialized MOParamStmt class. - + """ ParamStmt.__init__(self, param) self.mode = mo - + def to_gerber(self): mode = 'MM' if self.mode == 'metric' else 'IN' return '%MO{0}*%'.format(mode) @@ -140,7 +140,7 @@ class MOParamStmt(ParamStmt): def __str__(self): mode_str = 'millimeters' if self.mode == 'metric' else 'inches' return ('' % mode_str) - + class IPParamStmt(ParamStmt): """ IP - Gerber Image Polarity Statement. (Deprecated) @@ -150,15 +150,15 @@ class IPParamStmt(ParamStmt): 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' @@ -166,12 +166,11 @@ class IPParamStmt(ParamStmt): ------- ParamStmt : IPParamStmt Initialized IPParamStmt class. - + """ ParamStmt.__init__(self, param) self.ip = ip - def to_gerber(self): ip = 'POS' if self.ip == 'positive' else 'negative' return '%IP{0}*%'.format(ip) @@ -183,33 +182,33 @@ class IPParamStmt(ParamStmt): 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 @@ -231,24 +230,25 @@ class OFParamStmt(ParamStmt): offset_str += ('Y: %f' % self.b) return ('' % offset_str) + class LPParamStmt(ParamStmt): """ LP - Gerber Level Polarity statement """ - + @classmethod def from_dict(cls, stmt_dict): param = stmt_dict.get('lp') - lp = 'clear' if stmt_dict.get('lp') == 'C' else 'dark' + lp = 'clear' if stmt_dict.get('lp') == 'C' else 'dark' return cls(param, lp) - + def __init__(self, param, lp): """ Initialize LPParamStmt class - + Parameters ---------- param : string Parameter - + lp : string Level polarity. May be either 'clear' or 'dark' @@ -256,12 +256,11 @@ class LPParamStmt(ParamStmt): ------- ParamStmt : LPParamStmt Initialized LPParamStmt class. - + """ ParamStmt.__init__(self, param) self.lp = lp - def to_gerber(self, settings): lp = 'C' if self.lp == 'clear' else 'dark' return '%LP{0}*%'.format(self.lp) @@ -273,7 +272,7 @@ class LPParamStmt(ParamStmt): class ADParamStmt(ParamStmt): """ AD - Gerber Aperture Definition Statement """ - + @classmethod def from_dict(cls, stmt_dict): param = stmt_dict.get('param') @@ -282,38 +281,36 @@ class ADParamStmt(ParamStmt): modifiers = stmt_dict.get('modifiers') if modifiers is not None: modifiers = [[float(x) for x in m.split('X')] - for m in modifiers.split(',')] + for m in modifiers.split(',')] return cls(param, d, shape, modifiers) - - + def __init__(self, param, d, shape, modifiers): """ Initialize ADParamStmt class - + Parameters ---------- param : string Parameter code - + d : int Aperture D-code shape : string aperture name - + modifiers : list of lists of floats Shape modifiers - + Returns ------- ParamStmt : LPParamStmt Initialized LPParamStmt class. - + """ ParamStmt.__init__(self, param) self.d = d self.shape = shape - self.modifiers = modifiers - + self.modifiers = modifiers def to_gerber(self, settings): return '%ADD{0}{1},{2}*%'.format(self.d, self.shape, @@ -328,72 +325,72 @@ class ADParamStmt(ParamStmt): shape = 'oblong' else: shape = self.shape - + return '' % (self.d, shape) class AMParamStmt(ParamStmt): """ AM - Aperture Macro Statement """ - + @classmethod def from_dict(cls, stmt_dict): return cls(**stmt_dict) - + def __init__(self, param, name, macro): """ Initialize AMParamStmt class - + Parameters ---------- param : string Parameter code - + name : string Aperture macro name macro : string Aperture macro string - + Returns ------- ParamStmt : AMParamStmt Initialized AMParamStmt class. - + """ ParamStmt.__init__(self, param) self.name = name self.macro = macro - + def to_gerber(self): return '%AM{0}*{1}*%'.format(self.name, self.macro) def __str__(self): return '' % (self.name, macro) - - + + class INParamStmt(ParamStmt): """ IN - Image Name Statement """ @classmethod def from_dict(cls, stmt_dict): return cls(**stmt_dict) - + def __init__(self, param, name): """ Initialize INParamStmt class - + Parameters ---------- param : string Parameter code - + name : string Image name - + Returns ------- ParamStmt : INParamStmt Initialized INParamStmt class. - + """ ParamStmt.__init__(self, param) self.name = name @@ -404,29 +401,30 @@ class INParamStmt(ParamStmt): def __str__(self): return '' % self.name + class LNParamStmt(ParamStmt): """ LN - Level Name Statement (Deprecated) """ @classmethod def from_dict(cls, stmt_dict): return cls(**stmt_dict) - + def __init__(self, param, name): """ Initialize LNParamStmt class - + Parameters ---------- param : string Parameter code - + name : string Level name - + Returns ------- ParamStmt : LNParamStmt Initialized LNParamStmt class. - + """ ParamStmt.__init__(self, param) self.name = name @@ -437,10 +435,11 @@ class LNParamStmt(ParamStmt): def __str__(self): return '' % self.name + class CoordStmt(Statement): """ Coordinate Data Block """ - + @classmethod def from_dict(cls, stmt_dict, settings): zeros = settings['zero_suppression'] @@ -451,7 +450,7 @@ class CoordStmt(Statement): i = stmt_dict.get('i') j = stmt_dict.get('j') op = stmt_dict.get('op') - + if x is not None: x = parse_gerber_value(stmt_dict.get('x'), format, zeros) @@ -465,39 +464,38 @@ class CoordStmt(Statement): j = parse_gerber_value(stmt_dict.get('j'), format, zeros) return cls(function, x, y, i, j, op, settings) - - + def __init__(self, function, x, y, i, j, op, settings): """ Initialize CoordStmt class - + Parameters ---------- function : string function - + x : float X coordinate - + y : float - Y coordinate - + Y coordinate + i : float Coordinate offset in the X direction - + j : float Coordinate offset in the Y direction - + op : string Operation code - + settings : dict {'zero_suppression', 'format'} - Gerber file coordinate format - + Gerber file coordinate format + Returns ------- Statement : CoordStmt Initialized CoordStmt class. - + """ Statement.__init__(self, "COORD") self.zero_suppression = settings['zero_suppression'] @@ -509,7 +507,6 @@ class CoordStmt(Statement): self.j = j self.op = op - def to_gerber(self): ret = '' if self.function: @@ -518,7 +515,7 @@ class CoordStmt(Statement): 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, + 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, @@ -552,7 +549,7 @@ class CoordStmt(Statement): else: op = self.op coord_str += 'Op: %s' % op - + return '' % coord_str @@ -562,20 +559,21 @@ class ApertureStmt(Statement): def __init__(self, d): Statement.__init__(self, "APERTURE") self.d = int(d) - + def to_gerber(self): return 'G54D{0}*'.format(self.d) def __str__(self): return '' % self.d + class CommentStmt(Statement): """ Comment Statment """ def __init__(self, comment): Statement.__init__(self, "COMMENT") self.comment = comment - + def to_gerber(self): return 'G04{0}*'.format(self.comment) @@ -594,12 +592,11 @@ class EofStmt(Statement): def __str__(self): return '' - - + + class UnknownStmt(Statement): """ Unknown Statement """ def __init__(self, line): Statement.__init__(self, "UNKNOWN") self.line = line - \ No newline at end of file diff --git a/gerber/utils.py b/gerber/utils.py index 35b4fd0..625a9e1 100644 --- a/gerber/utils.py +++ b/gerber/utils.py @@ -10,28 +10,29 @@ files. """ # Author: Hamilton Kibbe -# License: +# License: + def parse_gerber_value(value, format=(2, 5), zero_suppression='trailing'): """ Convert gerber/excellon formatted string to floating-point number - + .. note:: - Format and zero suppression are configurable. Note that the Excellon - and Gerber formats use opposite terminology with respect to leading - and trailing zeros. The Gerber format specifies which zeros are - suppressed, while the Excellon format specifies which zeros are - included. This function uses the Gerber-file convention, so an - Excellon file in LZ (leading zeros) mode would use - `zero_suppression='trailing'` - - + Format and zero suppression are configurable. Note that the Excellon + and Gerber formats use opposite terminology with respect to leading + and trailing zeros. The Gerber format specifies which zeros are + suppressed, while the Excellon format specifies which zeros are + included. This function uses the Gerber-file convention, so an + Excellon file in LZ (leading zeros) mode would use + `zero_suppression='trailing'` + + Parameters ---------- value : string A Gerber/Excellon-formatted string representing a numerical value. format : tuple (int,int) - Gerber/Excellon precision format expressed as a tuple containing: + Gerber/Excellon precision format expressed as a tuple containing: (number of integer-part digits, number of decimal-part digits) zero_suppression : string @@ -41,12 +42,12 @@ def parse_gerber_value(value, format=(2, 5), zero_suppression='trailing'): ------- value : float The specified value as a floating-point number. - + """ # Format precision integer_digits, decimal_digits = format MAX_DIGITS = integer_digits + decimal_digits - + # Absolute maximum number of digits supported. This will handle up to # 6:7 format, which is somewhat supported, even though the gerber spec # only allows up to 6:6 @@ -59,40 +60,39 @@ def parse_gerber_value(value, format=(2, 5), zero_suppression='trailing'): negative = '-' in value if negative: value = value.strip(' -') - + # Handle excellon edge case with explicit decimal. "That was easy!" if '.' in value: return float(value) - + digits = [digit for digit in '0' * MAX_DIGITS] offset = 0 if zero_suppression == 'trailing' else (MAX_DIGITS - len(value)) for i, digit in enumerate(value): digits[i + offset] = digit - - + result = float(''.join(digits[:integer_digits] + ['.'] + digits[integer_digits:])) return -1.0 * result if negative else result - - + + def write_gerber_value(value, format=(2, 5), zero_suppression='trailing'): """ Convert a floating point number to a Gerber/Excellon-formatted string. - + .. note:: - Format and zero suppression are configurable. Note that the Excellon - and Gerber formats use opposite terminology with respect to leading - and trailing zeros. The Gerber format specifies which zeros are - suppressed, while the Excellon format specifies which zeros are - included. This function uses the Gerber-file convention, so an - Excellon file in LZ (leading zeros) mode would use + Format and zero suppression are configurable. Note that the Excellon + and Gerber formats use opposite terminology with respect to leading + and trailing zeros. The Gerber format specifies which zeros are + suppressed, while the Excellon format specifies which zeros are + included. This function uses the Gerber-file convention, so an + Excellon file in LZ (leading zeros) mode would use `zero_suppression='trailing'` - + Parameters ---------- value : float A floating point value. format : tuple (n=2) - Gerber/Excellon precision format expressed as a tuple containing: + Gerber/Excellon precision format expressed as a tuple containing: (number of integer-part digits, number of decimal-part digits) zero_suppression : string @@ -106,12 +106,12 @@ def write_gerber_value(value, format=(2, 5), zero_suppression='trailing'): # Format precision integer_digits, decimal_digits = format MAX_DIGITS = integer_digits + decimal_digits - + if MAX_DIGITS > 13 or integer_digits > 6 or decimal_digits > 7: raise ValueError('Parser only supports precision up to 6:7 format') - + # negative sign affects padding, so deal with it at the end... - negative = value < 0.0 + negative = value < 0.0 if negative: value = -1.0 * value @@ -119,48 +119,72 @@ 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 != '.'] - - # Suppression... + + # Suppression... if zero_suppression == 'trailing': while digits[-1] == '0': digits.pop() else: while digits[0] == '0': digits.pop(0) - + return ''.join(digits) if not negative else ''.join(['-'] + digits) - def decimal_string(value, precision=6): """ Convert float to string with limited precision - + Parameters ---------- value : float A floating point value. - precision : + precision : Maximum number of decimal places to print Returns ------- value : string The specified value as a string. - + """ floatstr = '%0.20g' % value integer = None decimal = None if '.' in floatstr: - integer, decimal = floatstr.split('.') + integer, decimal = floatstr.split('.') elif ',' in floatstr: - integer, decimal = floatstr.split(',') + integer, decimal = floatstr.split(',') if len(decimal) > precision: decimal = decimal[:precision] if integer or decimal: return ''.join([integer, '.', decimal]) else: return int(floatstr) - + +def detect_file_format(filename): + """ Determine format of a file + + Parameters + ---------- + filename : string + Filename of the file to read. + + Returns + ------- + format : string + File format. either 'excellon' or 'rs274x' + """ + + # Read the first 20 lines + with open(filename, 'r') as f: + lines = [next(f) for x in xrange(20)] + + # Look for + for line in lines: + if 'M48' in line: + return 'excellon' + elif '%FS' in line: + return'rs274x' + return 'unknown' -- cgit From 1cb7856e88ce3d8ff7e725e9840b512b7d799e8d Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Mon, 29 Sep 2014 13:35:10 -0400 Subject: Add CNC base classes and settings class --- gerber/cnc.py | 117 +++++++++++++++++++++++++++++++++++++++++++++++++++++ gerber/excellon.py | 91 ++++++++++++++++++++++++++++++++++------- gerber/gerber.py | 21 +++++----- 3 files changed, 206 insertions(+), 23 deletions(-) create mode 100644 gerber/cnc.py (limited to 'gerber') diff --git a/gerber/cnc.py b/gerber/cnc.py new file mode 100644 index 0000000..a7f3b85 --- /dev/null +++ b/gerber/cnc.py @@ -0,0 +1,117 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# copyright 2014 Hamilton Kibbe +# +# 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.cnc +============ +**CNC file classes** + +This module provides common base classes for Excellon/Gerber CNC files +""" + + +class FileSettings(object): + """ CNC File Settings + + Provides a common representation of gerber/excellon file settings + """ + def __init__(self, notation='absolute', units='inch', + zero_suppression='trailing', format=(2,5)): + if notation not in ['absolute', 'incremental']: + raise ValueError('Notation must be either absolute or incremental') + self.notation = notation + + if units not in ['inch', 'metric']: + 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 len(format) != 2: + raise ValueError('Format must be a tuple(n=2) of integers') + self.format = format + + def __getitem__(self, key): + if key == 'notation': + return self.notation + elif key == 'units': + return self.units + elif key =='zero_suppression': + return self.zero_suppression + elif key == 'format': + return self.format + else: + raise KeyError() + +class CncFile(object): + """ Base class for Gerber/Excellon files. + + Provides a common set of settings parameters. + + Parameters + ---------- + settings : FileSettings + The current file configuration. + + filename : string + Name of the file that this CncFile represents. + + Attributes + ---------- + settings : FileSettings + File settings as a FileSettings object + + notation : string + File notation setting. May be either 'absolute' or 'incremental' + + units : string + File units setting. May be 'inch' or 'metric' + + zero_suppression : string + File zero-suppression setting. May be either 'leading' or 'trailling' + + format : tuple (, ) + File decimal representation format as a tuple of (integer digits, + decimal digits) + """ + + def __init__(self, settings=None, filename=None): + if settings is not None: + self.notation = settings['notation'] + self.units = settings['units'] + self.zero_suppression = settings['zero_suppression'] + self.format = settings['format'] + else: + self.notation = 'absolute' + self.units = 'inch' + self.zero_suppression = 'trailing' + self.format = (2,5) + self.filename = filename + + @property + def settings(self): + """ File settings + + Returns + ------- + settings : FileSettings (dict-like) + A FileSettings object with the specified configuration. + """ + return FileSettings(self.notation, self.units, self.zero_suppression, + self.format) diff --git a/gerber/excellon.py b/gerber/excellon.py index d92d57c..5cb33ad 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -2,6 +2,7 @@ import re from itertools import tee, izip from .utils import parse_gerber_value +from .cnc import CncFile, FileSettings def read(filename): @@ -10,7 +11,7 @@ def read(filename): return ExcellonParser().parse(filename) -class ExcellonFile(object): +class ExcellonFile(CncFile): """ A class representing a single excellon file The ExcellonFile class represents a single excellon file. @@ -34,11 +35,10 @@ class ExcellonFile(object): either 'inch' or 'metric'. """ - def __init__(self, tools, hits, settings, filename): + def __init__(self, tools, hits, settings, filename=None): + super(ExcellonFile, self).__init__(settings, filename) self.tools = tools self.hits = hits - self.settings = settings - self.filename = filename def report(self): """ Print drill report @@ -53,11 +53,67 @@ class ExcellonFile(object): ctx.dump(filename) -class Tool(object): +class ExcellonTool(object): """ Excellon Tool class + + Parameters + ---------- + settings : FileSettings (dict-like) + File-wide settings. + + kwargs : dict-like + Tool settings from the excellon statement. Valid keys are: + diameter : Tool diameter [expressed in file units] + rpm : Tool RPM + feed_rate : Z-axis tool feed rate + retract_rate : Z-axis tool retraction rate + max_hit_count : Number of hits allowed before a tool change + depth_offset : Offset of tool depth from tip of tool. + + Attributes + ---------- + number : integer + Tool number from the excellon file + + diameter : float + Tool diameter in file units + + rpm : float + Tool RPM + + feed_rate : float + Tool Z-axis feed rate. + + retract_rate : float + Tool Z-axis retract rate + + depth_offset : float + Offset of depth measurement from tip of tool + + max_hit_count : integer + Maximum number of tool hits allowed before a tool change + + hit_count : integer + Number of tool hits in excellon file. """ + @classmethod def from_line(cls, line, settings): + """ Create a Tool from an excellon gile tool definition line. + + Parameters + ---------- + line : string + Tool definition line from an excellon file. + + settings : FileSettings (dict-like) + Excellon file-wide settings + + Returns + ------- + tool : Tool + An ExcellonTool representing the tool defined in `line` + """ commands = re.split('([BCFHSTZ])', line)[1:] commands = [(command, value) for command, value in pairwise(commands)] args = {} @@ -89,13 +145,19 @@ class Tool(object): self.max_hit_count = kwargs.get('max_hit_count') self.depth_offset = kwargs.get('depth_offset') self.units = settings.get('units', 'inch') + self.hit_count = 0 + + def _hit(self): + self.hit_count += 1 def __repr__(self): unit = 'in.' if self.units == 'inch' else 'mm' - return '' % (self.number, self.diameter, unit) + return '' % (self.number, self.diameter, unit) class ExcellonParser(object): + """ Excellon File Parser + """ def __init__(self, ctx=None): self.ctx = ctx self.notation = 'absolute' @@ -115,13 +177,11 @@ class ExcellonParser(object): with open(filename, 'r') as f: for line in f: self._parse(line) - settings = {'notation': self.notation, 'units': self.units, - 'zero_suppression': self.zero_suppression, - 'format': self.format} - return ExcellonFile(self.tools, self.hits, settings, filename) + return ExcellonFile(self.tools, self.hits, self._settings(), filename) def dump(self, filename): - self.ctx.dump(filename) + if self.ctx is not None: + self.ctx.dump(filename) def _parse(self, line): if 'M48' in line: @@ -159,7 +219,7 @@ class ExcellonParser(object): # tool definition if line[0] == 'T' and self.state == 'HEADER': - tool = Tool.from_line(line, self._settings()) + tool = ExcellonTool.from_line(line, self._settings()) self.tools[tool.number] = tool elif line[0] == 'T' and self.state != 'HEADER': @@ -187,13 +247,16 @@ class ExcellonParser(object): self.pos[1] += y if self.state == 'DRILL': self.hits.append((self.active_tool, self.pos)) + self.active_tool._hit() if self.ctx is not None: self.ctx.drill(self.pos[0], self.pos[1], self.active_tool.diameter) def _settings(self): - return {'units': self.units, 'zero_suppression': self.zero_suppression, - 'format': self.format} + return FileSettings(units=self.units, format=self.format, + zero_suppression=self.zero_suppression, + notation=self.notation) + def pairwise(iterator): diff --git a/gerber/gerber.py b/gerber/gerber.py index 949037b..eb5821c 100644 --- a/gerber/gerber.py +++ b/gerber/gerber.py @@ -27,6 +27,9 @@ This module provides an RS-274-X class and parser import re import json from .statements import * +from .cnc import CncFile, FileSettings + + def read(filename): @@ -35,7 +38,7 @@ def read(filename): return GerberParser().parse(filename) -class GerberFile(object): +class GerberFile(CncFile): """ A class representing a single gerber file The GerberFile class represents a single gerber file. @@ -68,9 +71,8 @@ class GerberFile(object): """ def __init__(self, statements, settings, filename=None): - self.filename = filename + super(GerberFile, self).__init__(settings, filename) self.statements = statements - self.settings = settings @property def comments(self): @@ -90,7 +92,8 @@ class GerberFile(object): 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)]: + for stmt in [stmt for stmt in self.statements + if isinstance(stmt, CoordStmt)]: if stmt.x is not None and stmt.x < xbounds[0]: xbounds[0] = stmt.x if stmt.x is not None and stmt.x > xbounds[1]: @@ -169,7 +172,7 @@ class GerberParser(object): EOF_STMT = re.compile(r"(?PM02)\*") def __init__(self): - self.settings = {} + self.settings = FileSettings() self.statements = [] def parse(self, filename): @@ -240,13 +243,13 @@ class GerberParser(object): if param: if param["param"] == "FS": stmt = FSParamStmt.from_dict(param) - self.settings = {'zero_suppression': stmt.zero_suppression, - 'format': stmt.format, - 'notation': stmt.notation} + self.settings.zero_suppression = stmt.zero_suppression + self.settings.format = stmt.format + self.settings.notation = stmt.notation yield stmt elif param["param"] == "MO": stmt = MOParamStmt.from_dict(param) - self.settings['units'] = stmt.mode + self.settings.units = stmt.mode yield stmt elif param["param"] == "IP": yield IPParamStmt.from_dict(param) -- cgit From 1e170ba1964d852ed1e7787c5bd39018d9b7ed6d Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Tue, 30 Sep 2014 17:17:28 -0400 Subject: Add travis.yml --- gerber/excellon.py | 21 ++++++++++++++++++--- gerber/gerber.py | 4 ---- 2 files changed, 18 insertions(+), 7 deletions(-) (limited to 'gerber') diff --git a/gerber/excellon.py b/gerber/excellon.py index 5cb33ad..dbd9502 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -1,6 +1,21 @@ -#!/usr/bin/env python +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# copyright 2014 Hamilton Kibbe +# +# 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 re -from itertools import tee, izip from .utils import parse_gerber_value from .cnc import CncFile, FileSettings @@ -165,7 +180,7 @@ class ExcellonParser(object): self.zero_suppression = 'trailing' self.format = (2, 5) self.state = 'INIT' - self.tools = [] + self.tools = {} self.hits = [] self.active_tool = None self.pos = [0., 0.] diff --git a/gerber/gerber.py b/gerber/gerber.py index eb5821c..20409f1 100644 --- a/gerber/gerber.py +++ b/gerber/gerber.py @@ -79,10 +79,6 @@ class GerberFile(CncFile): return [comment.comment for comment in self.statements if isinstance(comment, CommentStmt)] - @property - def units(self): - return self.settings['units'] - @property def size(self): xbounds, ybounds = self.bounds -- cgit From f8449ad2b60b8a715d0867325e257a8297193b49 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Tue, 30 Sep 2014 23:42:02 -0400 Subject: tests update --- gerber/__init__.py | 11 +++ gerber/cnc.py | 9 ++- gerber/render/__init__.py | 2 +- gerber/render/svg.py | 165 -------------------------------------- gerber/render/svgwrite_backend.py | 165 ++++++++++++++++++++++++++++++++++++++ gerber/statements.py | 7 +- gerber/tests/test_cnc.py | 50 ++++++++++++ gerber/tests/test_statements.py | 87 ++++++++++++++++++++ gerber/tests/tests.py | 18 +++++ 9 files changed, 338 insertions(+), 176 deletions(-) delete mode 100644 gerber/render/svg.py create mode 100644 gerber/render/svgwrite_backend.py create mode 100644 gerber/tests/test_cnc.py create mode 100644 gerber/tests/test_statements.py create mode 100644 gerber/tests/tests.py (limited to 'gerber') diff --git a/gerber/__init__.py b/gerber/__init__.py index 089d7b6..e31bd6f 100644 --- a/gerber/__init__.py +++ b/gerber/__init__.py @@ -18,6 +18,17 @@ def read(filename): """ Read a gerber or excellon file and return a representative object. + + Parameters + ---------- + filename : string + Filename of the file to read. + + Returns + ------- + file : CncFile subclass + CncFile object representing the file, either GerberFile or + ExcellonFile. Returns None if file is not an Excellon or Gerber file. """ import gerber import excellon diff --git a/gerber/cnc.py b/gerber/cnc.py index a7f3b85..aaa1a42 100644 --- a/gerber/cnc.py +++ b/gerber/cnc.py @@ -29,7 +29,7 @@ class FileSettings(object): Provides a common representation of gerber/excellon file settings """ def __init__(self, notation='absolute', units='inch', - zero_suppression='trailing', format=(2,5)): + zero_suppression='trailing', format=(2, 5)): if notation not in ['absolute', 'incremental']: raise ValueError('Notation must be either absolute or incremental') self.notation = notation @@ -43,7 +43,7 @@ class FileSettings(object): trailling') self.zero_suppression = zero_suppression - if len(format) != 2: + if len(format) != 2: raise ValueError('Format must be a tuple(n=2) of integers') self.format = format @@ -52,13 +52,14 @@ class FileSettings(object): return self.notation elif key == 'units': return self.units - elif key =='zero_suppression': + elif key == 'zero_suppression': return self.zero_suppression elif key == 'format': return self.format else: raise KeyError() + class CncFile(object): """ Base class for Gerber/Excellon files. @@ -101,7 +102,7 @@ class CncFile(object): self.notation = 'absolute' self.units = 'inch' self.zero_suppression = 'trailing' - self.format = (2,5) + self.format = (2, 5) self.filename = filename @property diff --git a/gerber/render/__init__.py b/gerber/render/__init__.py index cc87ee0..0d3527b 100644 --- a/gerber/render/__init__.py +++ b/gerber/render/__init__.py @@ -24,5 +24,5 @@ SVG is the only supported format. """ -from svg import GerberSvgContext +from svgwrite_backend import GerberSvgContext diff --git a/gerber/render/svg.py b/gerber/render/svg.py deleted file mode 100644 index 7d5c8fd..0000000 --- a/gerber/render/svg.py +++ /dev/null @@ -1,165 +0,0 @@ -#! /usr/bin/env python -# -*- coding: utf-8 -*- - -# Copyright 2014 Hamilton Kibbe -# Based on render_svg.py by Paulo Henrique Silva - -# 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 .apertures import Circle, Rect, Obround, Polygon -import svgwrite - -SCALE = 300 - - -class SvgCircle(Circle): - def draw(self, ctx, x, y): - return ctx.dwg.line(start=(ctx.x * SCALE, -ctx.y * SCALE), - end=(x * SCALE, -y * SCALE), - stroke="rgb(184, 115, 51)", - stroke_width=SCALE * self.diameter, - stroke_linecap="round") - - def flash(self, ctx, x, y): - return [ctx.dwg.circle(center=(x * SCALE, -y * SCALE), - r = SCALE * (self.diameter / 2.0), - fill='rgb(184, 115, 51)'), ] - - -class SvgRect(Rect): - def draw(self, ctx, x, y): - return ctx.dwg.line(start=(ctx.x * SCALE, -ctx.y * SCALE), - end=(x * SCALE, -y * SCALE), - stroke="rgb(184, 115, 51)", stroke_width=2, - stroke_linecap="butt") - - def flash(self, ctx, x, y): - xsize, ysize = self.size - return [ctx.dwg.rect(insert=(SCALE * (x - (xsize / 2)), - -SCALE * (y + (ysize / 2))), - size=(SCALE * xsize, SCALE * ysize), - fill="rgb(184, 115, 51)"), ] - - -class SvgObround(Obround): - def draw(self, ctx, x, y): - pass - - def flash(self, ctx, x, y): - xsize, ysize = self.size - - # horizontal obround - if xsize == ysize: - return [ctx.dwg.circle(center=(x * SCALE, -y * SCALE), - r = SCALE * (x / 2.0), - fill='rgb(184, 115, 51)'), ] - if xsize > ysize: - rectx = xsize - ysize - recty = ysize - lcircle = ctx.dwg.circle(center=((x - (rectx / 2.0)) * SCALE, - -y * SCALE), - r = SCALE * (ysize / 2.0), - fill='rgb(184, 115, 51)') - - rcircle = ctx.dwg.circle(center=((x + (rectx / 2.0)) * SCALE, - -y * SCALE), - r = SCALE * (ysize / 2.0), - fill='rgb(184, 115, 51)') - - rect = ctx.dwg.rect(insert=(SCALE * (x - (xsize / 2.)), - -SCALE * (y + (ysize / 2.))), - size=(SCALE * xsize, SCALE * ysize), - fill='rgb(184, 115, 51)') - return [lcircle, rcircle, rect, ] - - # Vertical obround - else: - rectx = xsize - recty = ysize - xsize - lcircle = ctx.dwg.circle(center=(x * SCALE, - (y - (recty / 2.)) * -SCALE), - r = SCALE * (xsize / 2.), - fill='rgb(184, 115, 51)') - - ucircle = ctx.dwg.circle(center=(x * SCALE, - (y + (recty / 2.)) * -SCALE), - r = SCALE * (xsize / 2.), - fill='rgb(184, 115, 51)') - - rect = ctx.dwg.rect(insert=(SCALE * (x - (xsize / 2.)), - -SCALE * (y + (ysize / 2.))), - size=(SCALE * xsize, SCALE * ysize), - fill='rgb(184, 115, 51)') - return [lcircle, ucircle, rect, ] - - -class GerberSvgContext(GerberContext): - def __init__(self): - GerberContext.__init__(self) - - self.apertures = {} - self.dwg = svgwrite.Drawing() - #self.dwg.add(self.dwg.rect(insert=(0, 0), size=(2000, 2000), fill="black")) - - def set_bounds(self, bounds): - xbounds, ybounds = bounds - size = (SCALE * (xbounds[1] - xbounds[0]), SCALE * (ybounds[1] - ybounds[0])) - self.dwg.add(self.dwg.rect(insert=(SCALE * xbounds[0], -SCALE * ybounds[1]), size=size, fill="black")) - - def define_aperture(self, d, shape, modifiers): - aperture = None - if shape == 'C': - aperture = SvgCircle(diameter=float(modifiers[0][0])) - elif shape == 'R': - aperture = SvgRect(size=modifiers[0][0:2]) - elif shape == 'O': - aperture = SvgObround(size=modifiers[0][0:2]) - self.apertures[d] = aperture - - def stroke(self, x, y): - super(GerberSvgContext, self).stroke(x, y) - - if self.interpolation == 'linear': - self.line(x, y) - elif self.interpolation == 'arc': - self.arc(x, y) - - def line(self, x, y): - super(GerberSvgContext, self).line(x, y) - x, y = self.resolve(x, y) - ap = self.apertures.get(self.aperture, None) - if ap is None: - return - self.dwg.add(ap.draw(self, x, y)) - self.move(x, y, resolve=False) - - def arc(self, x, y): - super(GerberSvgContext, self).arc(x, y) - - def flash(self, x, y): - super(GerberSvgContext, self).flash(x, y) - x, y = self.resolve(x, y) - ap = self.apertures.get(self.aperture, None) - if ap is None: - return - for shape in ap.flash(self, x, y): - self.dwg.add(shape) - self.move(x, y, resolve=False) - - def drill(self, x, y, diameter): - hit = self.dwg.circle(center=(x*SCALE, -y*SCALE), r=SCALE*(diameter/2.0), fill='gray') - self.dwg.add(hit) - - def dump(self, filename): - self.dwg.saveas(filename) diff --git a/gerber/render/svgwrite_backend.py b/gerber/render/svgwrite_backend.py new file mode 100644 index 0000000..7d5c8fd --- /dev/null +++ b/gerber/render/svgwrite_backend.py @@ -0,0 +1,165 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright 2014 Hamilton Kibbe +# Based on render_svg.py by Paulo Henrique Silva + +# 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 .apertures import Circle, Rect, Obround, Polygon +import svgwrite + +SCALE = 300 + + +class SvgCircle(Circle): + def draw(self, ctx, x, y): + return ctx.dwg.line(start=(ctx.x * SCALE, -ctx.y * SCALE), + end=(x * SCALE, -y * SCALE), + stroke="rgb(184, 115, 51)", + stroke_width=SCALE * self.diameter, + stroke_linecap="round") + + def flash(self, ctx, x, y): + return [ctx.dwg.circle(center=(x * SCALE, -y * SCALE), + r = SCALE * (self.diameter / 2.0), + fill='rgb(184, 115, 51)'), ] + + +class SvgRect(Rect): + def draw(self, ctx, x, y): + return ctx.dwg.line(start=(ctx.x * SCALE, -ctx.y * SCALE), + end=(x * SCALE, -y * SCALE), + stroke="rgb(184, 115, 51)", stroke_width=2, + stroke_linecap="butt") + + def flash(self, ctx, x, y): + xsize, ysize = self.size + return [ctx.dwg.rect(insert=(SCALE * (x - (xsize / 2)), + -SCALE * (y + (ysize / 2))), + size=(SCALE * xsize, SCALE * ysize), + fill="rgb(184, 115, 51)"), ] + + +class SvgObround(Obround): + def draw(self, ctx, x, y): + pass + + def flash(self, ctx, x, y): + xsize, ysize = self.size + + # horizontal obround + if xsize == ysize: + return [ctx.dwg.circle(center=(x * SCALE, -y * SCALE), + r = SCALE * (x / 2.0), + fill='rgb(184, 115, 51)'), ] + if xsize > ysize: + rectx = xsize - ysize + recty = ysize + lcircle = ctx.dwg.circle(center=((x - (rectx / 2.0)) * SCALE, + -y * SCALE), + r = SCALE * (ysize / 2.0), + fill='rgb(184, 115, 51)') + + rcircle = ctx.dwg.circle(center=((x + (rectx / 2.0)) * SCALE, + -y * SCALE), + r = SCALE * (ysize / 2.0), + fill='rgb(184, 115, 51)') + + rect = ctx.dwg.rect(insert=(SCALE * (x - (xsize / 2.)), + -SCALE * (y + (ysize / 2.))), + size=(SCALE * xsize, SCALE * ysize), + fill='rgb(184, 115, 51)') + return [lcircle, rcircle, rect, ] + + # Vertical obround + else: + rectx = xsize + recty = ysize - xsize + lcircle = ctx.dwg.circle(center=(x * SCALE, + (y - (recty / 2.)) * -SCALE), + r = SCALE * (xsize / 2.), + fill='rgb(184, 115, 51)') + + ucircle = ctx.dwg.circle(center=(x * SCALE, + (y + (recty / 2.)) * -SCALE), + r = SCALE * (xsize / 2.), + fill='rgb(184, 115, 51)') + + rect = ctx.dwg.rect(insert=(SCALE * (x - (xsize / 2.)), + -SCALE * (y + (ysize / 2.))), + size=(SCALE * xsize, SCALE * ysize), + fill='rgb(184, 115, 51)') + return [lcircle, ucircle, rect, ] + + +class GerberSvgContext(GerberContext): + def __init__(self): + GerberContext.__init__(self) + + self.apertures = {} + self.dwg = svgwrite.Drawing() + #self.dwg.add(self.dwg.rect(insert=(0, 0), size=(2000, 2000), fill="black")) + + def set_bounds(self, bounds): + xbounds, ybounds = bounds + size = (SCALE * (xbounds[1] - xbounds[0]), SCALE * (ybounds[1] - ybounds[0])) + self.dwg.add(self.dwg.rect(insert=(SCALE * xbounds[0], -SCALE * ybounds[1]), size=size, fill="black")) + + def define_aperture(self, d, shape, modifiers): + aperture = None + if shape == 'C': + aperture = SvgCircle(diameter=float(modifiers[0][0])) + elif shape == 'R': + aperture = SvgRect(size=modifiers[0][0:2]) + elif shape == 'O': + aperture = SvgObround(size=modifiers[0][0:2]) + self.apertures[d] = aperture + + def stroke(self, x, y): + super(GerberSvgContext, self).stroke(x, y) + + if self.interpolation == 'linear': + self.line(x, y) + elif self.interpolation == 'arc': + self.arc(x, y) + + def line(self, x, y): + super(GerberSvgContext, self).line(x, y) + x, y = self.resolve(x, y) + ap = self.apertures.get(self.aperture, None) + if ap is None: + return + self.dwg.add(ap.draw(self, x, y)) + self.move(x, y, resolve=False) + + def arc(self, x, y): + super(GerberSvgContext, self).arc(x, y) + + def flash(self, x, y): + super(GerberSvgContext, self).flash(x, y) + x, y = self.resolve(x, y) + ap = self.apertures.get(self.aperture, None) + if ap is None: + return + for shape in ap.flash(self, x, y): + self.dwg.add(shape) + self.move(x, y, resolve=False) + + def drill(self, x, y, diameter): + hit = self.dwg.circle(center=(x*SCALE, -y*SCALE), r=SCALE*(diameter/2.0), fill='gray') + self.dwg.add(hit) + + def dump(self, filename): + self.dwg.saveas(filename) diff --git a/gerber/statements.py b/gerber/statements.py index 418a852..47dbd69 100644 --- a/gerber/statements.py +++ b/gerber/statements.py @@ -48,11 +48,6 @@ class FSParamStmt(ParamStmt): notation = 'absolute' if stmt_dict.get('notation') == 'A' else 'incremental' x = map(int, stmt_dict.get('x').strip()) format = (x[0], x[1]) - if notation == 'incremental': - print('This file uses incremental notation. To quote the gerber \ - file specification:\nIncremental notation is a source of \ - endless confusion. Always use absolute notation.\n\nYou \ - have been warned') return cls(param, zeros, notation, format) def __init__(self, param, zero_suppression='leading', @@ -172,7 +167,7 @@ class IPParamStmt(ParamStmt): self.ip = ip def to_gerber(self): - ip = 'POS' if self.ip == 'positive' else 'negative' + ip = 'POS' if self.ip == 'positive' else 'NEG' return '%IP{0}*%'.format(ip) def __str__(self): diff --git a/gerber/tests/test_cnc.py b/gerber/tests/test_cnc.py new file mode 100644 index 0000000..ace047e --- /dev/null +++ b/gerber/tests/test_cnc.py @@ -0,0 +1,50 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# Author: Hamilton Kibbe + +from ..cnc import CncFile, FileSettings +from tests import * + + +def test_smoke_filesettings(): + """ Smoke test FileSettings class + """ + fs = FileSettings() + + +def test_filesettings_defaults(): + """ Test FileSettings default values + """ + fs = FileSettings() + assert_equal(fs.format, (2, 5)) + assert_equal(fs.notation, 'absolute') + assert_equal(fs.zero_suppression, 'trailing') + assert_equal(fs.units, 'inch') + + +def test_filesettings_dict(): + """ Test FileSettings Dict + """ + fs = FileSettings() + assert_equal(fs['format'], (2, 5)) + assert_equal(fs['notation'], 'absolute') + assert_equal(fs['zero_suppression'], 'trailing') + assert_equal(fs['units'], 'inch') + + +def test_filesettings_assign(): + """ Test FileSettings attribute assignment + """ + fs = FileSettings() + fs.units = 'test' + fs.notation = 'test' + fs.zero_suppression = 'test' + fs.format = 'test' + assert_equal(fs.units, 'test') + assert_equal(fs.notation, 'test') + assert_equal(fs.zero_suppression, 'test') + assert_equal(fs.format, 'test') + + def test_smoke_cncfile(): + pass diff --git a/gerber/tests/test_statements.py b/gerber/tests/test_statements.py new file mode 100644 index 0000000..47fbb48 --- /dev/null +++ b/gerber/tests/test_statements.py @@ -0,0 +1,87 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# Author: Hamilton Kibbe + +from .tests import * +from ..statements import * + + +def test_FSParamStmt_factory(): + """ Test FSParamStruct factory correctly handles parameters + """ + stmt = {'param': 'FS', 'zero': 'L', 'notation': 'A', 'x': '27'} + fs = FSParamStmt.from_dict(stmt) + assert_equal(fs.param, 'FS') + assert_equal(fs.zero_suppression, 'leading') + assert_equal(fs.notation, 'absolute') + assert_equal(fs.format, (2, 7)) + + stmt = {'param': 'FS', 'zero': 'T', 'notation': 'I', 'x': '27'} + fs = FSParamStmt.from_dict(stmt) + assert_equal(fs.param, 'FS') + assert_equal(fs.zero_suppression, 'trailing') + assert_equal(fs.notation, 'incremental') + assert_equal(fs.format, (2, 7)) + + +def test_FSParamStmt_dump(): + """ Test FSParamStmt to_gerber() + """ + stmt = {'param': 'FS', 'zero': 'L', 'notation': 'A', 'x': '27'} + fs = FSParamStmt.from_dict(stmt) + assert_equal(fs.to_gerber(), '%FSLAX27Y27*%') + + stmt = {'param': 'FS', 'zero': 'T', 'notation': 'I', 'x': '25'} + fs = FSParamStmt.from_dict(stmt) + assert_equal(fs.to_gerber(), '%FSTIX25Y25*%') + + +def test_MOParamStmt_factory(): + """ Test MOParamStruct factory correctly handles parameters + """ + stmt = {'param': 'MO', 'mo': 'IN'} + mo = MOParamStmt.from_dict(stmt) + assert_equal(mo.param, 'MO') + assert_equal(mo.mode, 'inch') + + stmt = {'param': 'MO', 'mo': 'MM'} + mo = MOParamStmt.from_dict(stmt) + assert_equal(mo.param, 'MO') + assert_equal(mo.mode, 'metric') + + +def test_MOParamStmt_dump(): + """ Test MOParamStmt to_gerber() + """ + stmt = {'param': 'MO', 'mo': 'IN'} + mo = MOParamStmt.from_dict(stmt) + assert_equal(mo.to_gerber(), '%MOIN*%') + + stmt = {'param': 'MO', 'mo': 'MM'} + mo = MOParamStmt.from_dict(stmt) + assert_equal(mo.to_gerber(), '%MOMM*%') + + +def test_IPParamStmt_factory(): + """ Test IPParamStruct factory correctly handles parameters + """ + stmt = {'param': 'IP', 'ip': 'POS'} + ip = IPParamStmt.from_dict(stmt) + assert_equal(ip.ip, 'positive') + + stmt = {'param': 'IP', 'ip': 'NEG'} + ip = IPParamStmt.from_dict(stmt) + assert_equal(ip.ip, 'negative') + + +def test_IPParamStmt_dump(): + """ Test IPParamStmt to_gerber() + """ + stmt = {'param': 'IP', 'ip': 'POS'} + ip = IPParamStmt.from_dict(stmt) + assert_equal(ip.to_gerber(), '%IPPOS*%') + + stmt = {'param': 'IP', 'ip': 'NEG'} + ip = IPParamStmt.from_dict(stmt) + assert_equal(ip.to_gerber(), '%IPNEG*%') diff --git a/gerber/tests/tests.py b/gerber/tests/tests.py new file mode 100644 index 0000000..29b7899 --- /dev/null +++ b/gerber/tests/tests.py @@ -0,0 +1,18 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# Author: Hamilton Kibbe + +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_true +from nose.tools import assert_false +from nose.tools import assert_raises +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' ] -- cgit From ea0dc8d0c8d0002008a462fbb70e8846f6691253 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Tue, 30 Sep 2014 23:53:57 -0400 Subject: tests --- gerber/statements.py | 4 ++-- gerber/tests/test_statements.py | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) (limited to 'gerber') diff --git a/gerber/statements.py b/gerber/statements.py index 47dbd69..5a9d046 100644 --- a/gerber/statements.py +++ b/gerber/statements.py @@ -209,8 +209,8 @@ class OFParamStmt(ParamStmt): self.a = a self.b = b - def to_gerber(self, settings): - stmt = '%OF' + def to_gerber(self): + ret = '%OF' if self.a: ret += 'A' + decimal_string(self.a, precision=6) if self.b: diff --git a/gerber/tests/test_statements.py b/gerber/tests/test_statements.py index 47fbb48..4560521 100644 --- a/gerber/tests/test_statements.py +++ b/gerber/tests/test_statements.py @@ -85,3 +85,13 @@ def test_IPParamStmt_dump(): stmt = {'param': 'IP', 'ip': 'NEG'} ip = IPParamStmt.from_dict(stmt) assert_equal(ip.to_gerber(), '%IPNEG*%') + + +def test_OFParamStmt_dump(): + """ Test OFParamStmt to_gerber() + """ + stmt = {'param': 'OF', 'a': '0.1234567', 'b': '0.1234567'} + of = OFParamStmt.from_dict(stmt) + assert_equal(of.to_gerber(), '%OFA0.123456B0.123456*%') + + -- cgit From 0b8e2e4b8b552e90d55eabe39aefba0b5b3daef5 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Wed, 1 Oct 2014 14:39:32 -0400 Subject: added numpydoc --- gerber/__init__.py | 5 +++++ gerber/excellon.py | 6 ++++++ gerber/gerber.py | 3 --- 3 files changed, 11 insertions(+), 3 deletions(-) (limited to 'gerber') diff --git a/gerber/__init__.py b/gerber/__init__.py index e31bd6f..3197335 100644 --- a/gerber/__init__.py +++ b/gerber/__init__.py @@ -14,7 +14,12 @@ # 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 module +============ +**Gerber Tools** +""" def read(filename): """ Read a gerber or excellon file and return a representative object. diff --git a/gerber/excellon.py b/gerber/excellon.py index dbd9502..7c7d0c6 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -14,7 +14,13 @@ # 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 module +============ +**Excellon file classes** +This module provides Excellon file classes and parsing utilities +""" import re from .utils import parse_gerber_value from .cnc import CncFile, FileSettings diff --git a/gerber/gerber.py b/gerber/gerber.py index 20409f1..3f93ed4 100644 --- a/gerber/gerber.py +++ b/gerber/gerber.py @@ -59,9 +59,6 @@ class GerberFile(CncFile): comments: list of strings List of comments contained in the gerber file. - units : string - either 'inch' or 'metric'. - size : tuple, (, ) Size in [self.units] of the layer described by the gerber file. -- cgit From 597427d785d6f44348fe15631f2c184504195fb0 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Mon, 6 Oct 2014 08:33:53 -0400 Subject: add excellon statements --- gerber/excellon.py | 128 ++----- gerber/excellon_statements.py | 266 +++++++++++++++ gerber/gerber.py | 2 +- gerber/gerber_statements.py | 597 +++++++++++++++++++++++++++++++++ gerber/statements.py | 597 --------------------------------- gerber/tests/test_gerber_statements.py | 96 ++++++ gerber/tests/test_statements.py | 97 ------ 7 files changed, 980 insertions(+), 803 deletions(-) create mode 100644 gerber/excellon_statements.py create mode 100644 gerber/gerber_statements.py delete mode 100644 gerber/statements.py create mode 100644 gerber/tests/test_gerber_statements.py delete mode 100644 gerber/tests/test_statements.py (limited to 'gerber') diff --git a/gerber/excellon.py b/gerber/excellon.py index 7c7d0c6..6ae182b 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -22,6 +22,7 @@ Excellon module This module provides Excellon file classes and parsing utilities """ import re +from .excellon_statements import * from .utils import parse_gerber_value from .cnc import CncFile, FileSettings @@ -74,108 +75,6 @@ class ExcellonFile(CncFile): ctx.dump(filename) -class ExcellonTool(object): - """ Excellon Tool class - - Parameters - ---------- - settings : FileSettings (dict-like) - File-wide settings. - - kwargs : dict-like - Tool settings from the excellon statement. Valid keys are: - diameter : Tool diameter [expressed in file units] - rpm : Tool RPM - feed_rate : Z-axis tool feed rate - retract_rate : Z-axis tool retraction rate - max_hit_count : Number of hits allowed before a tool change - depth_offset : Offset of tool depth from tip of tool. - - Attributes - ---------- - number : integer - Tool number from the excellon file - - diameter : float - Tool diameter in file units - - rpm : float - Tool RPM - - feed_rate : float - Tool Z-axis feed rate. - - retract_rate : float - Tool Z-axis retract rate - - depth_offset : float - Offset of depth measurement from tip of tool - - max_hit_count : integer - Maximum number of tool hits allowed before a tool change - - hit_count : integer - Number of tool hits in excellon file. - """ - - @classmethod - def from_line(cls, line, settings): - """ Create a Tool from an excellon gile tool definition line. - - Parameters - ---------- - line : string - Tool definition line from an excellon file. - - settings : FileSettings (dict-like) - Excellon file-wide settings - - Returns - ------- - tool : Tool - An ExcellonTool representing the tool defined in `line` - """ - commands = re.split('([BCFHSTZ])', line)[1:] - commands = [(command, value) for command, value in pairwise(commands)] - args = {} - format = settings['format'] - zero_suppression = settings['zero_suppression'] - for cmd, val in commands: - if cmd == 'B': - args['retract_rate'] = parse_gerber_value(val, format, zero_suppression) - elif cmd == 'C': - args['diameter'] = parse_gerber_value(val, format, zero_suppression) - elif cmd == 'F': - args['feed_rate'] = parse_gerber_value(val, format, zero_suppression) - elif cmd == 'H': - args['max_hit_count'] = parse_gerber_value(val, format, zero_suppression) - elif cmd == 'S': - args['rpm'] = 1000 * parse_gerber_value(val, format, zero_suppression) - elif cmd == 'T': - args['number'] = int(val) - elif cmd == 'Z': - args['depth_offset'] = parse_gerber_value(val, format, zero_suppression) - return cls(settings, **args) - - def __init__(self, settings, **kwargs): - self.number = kwargs.get('number') - self.feed_rate = kwargs.get('feed_rate') - self.retract_rate = kwargs.get('retract_rate') - self.rpm = kwargs.get('rpm') - self.diameter = kwargs.get('diameter') - self.max_hit_count = kwargs.get('max_hit_count') - self.depth_offset = kwargs.get('depth_offset') - self.units = settings.get('units', 'inch') - self.hit_count = 0 - - def _hit(self): - self.hit_count += 1 - - def __repr__(self): - unit = 'in.' if self.units == 'inch' else 'mm' - return '' % (self.number, self.diameter, unit) - - class ExcellonParser(object): """ Excellon File Parser """ @@ -186,6 +85,7 @@ class ExcellonParser(object): self.zero_suppression = 'trailing' self.format = (2, 5) self.state = 'INIT' + self.statements = [] self.tools = {} self.hits = [] self.active_tool = None @@ -206,15 +106,23 @@ class ExcellonParser(object): def _parse(self, line): if 'M48' in line: + self.statements.append(HeaderBeginStmt()) self.state = 'HEADER' - if 'G00' in line: - self.state = 'ROUT' + elif line[0] == '%': + self.statements.append(RewindStopStmt()) + if self.state == 'HEADER': + self.state = 'DRILL' - if 'G05' in line: - self.state = 'DRILL' + elif 'M95' in line: + self.statements.append(HeaderEndStmt()) + if self.state == 'HEADER': + self.state = 'DRILL' + + elif 'G00' in line: + self.state = 'ROUT' - elif line[0] == '%' and self.state == 'HEADER': + elif 'G05' in line: self.state = 'DRILL' if 'INCH' in line or line.strip() == 'M72': @@ -279,11 +187,15 @@ class ExcellonParser(object): notation=self.notation) - def pairwise(iterator): + """ Iterate over list taking two elements at a time. + + e.g. [1, 2, 3, 4, 5, 6] ==> [(1, 2), (3, 4), (5, 6)] + """ itr = iter(iterator) while True: yield tuple([itr.next() for i in range(2)]) + if __name__ == '__main__': p = parser() diff --git a/gerber/excellon_statements.py b/gerber/excellon_statements.py new file mode 100644 index 0000000..4544f08 --- /dev/null +++ b/gerber/excellon_statements.py @@ -0,0 +1,266 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# copyright 2014 Hamilton Kibbe +# +# 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 .utils import write_gerber_value + + +__all__ = ['ExcellonTool', 'CommentStmt', 'HeaderBeginStmt', 'HeaderEndStmt', + ] + + +class ExcellonStatement(object): + """ Excellon Statement abstract base class + """ + def to_excellon(self): + pass + + +class ExcellonTool(ExcellonStatement): + """ Excellon Tool class + + Parameters + ---------- + settings : FileSettings (dict-like) + File-wide settings. + + kwargs : dict-like + Tool settings from the excellon statement. Valid keys are: + diameter : Tool diameter [expressed in file units] + rpm : Tool RPM + feed_rate : Z-axis tool feed rate + retract_rate : Z-axis tool retraction rate + max_hit_count : Number of hits allowed before a tool change + depth_offset : Offset of tool depth from tip of tool. + + Attributes + ---------- + number : integer + Tool number from the excellon file + + diameter : float + Tool diameter in file units + + rpm : float + Tool RPM + + feed_rate : float + Tool Z-axis feed rate. + + retract_rate : float + Tool Z-axis retract rate + + depth_offset : float + Offset of depth measurement from tip of tool + + max_hit_count : integer + Maximum number of tool hits allowed before a tool change + + hit_count : integer + Number of tool hits in excellon file. + """ + + @classmethod + def from_line(cls, line, settings): + """ Create a Tool from an excellon file tool definition line. + + Parameters + ---------- + line : string + Tool definition line from an excellon file. + + settings : FileSettings (dict-like) + Excellon file-wide settings + + Returns + ------- + tool : Tool + An ExcellonTool representing the tool defined in `line` + """ + commands = re.split('([BCFHSTZ])', line)[1:] + commands = [(command, value) for command, value in pairwise(commands)] + args = {} + format = settings['format'] + zero_suppression = settings['zero_suppression'] + for cmd, val in commands: + if cmd == 'B': + args['retract_rate'] = parse_gerber_value(val, format, zero_suppression) + elif cmd == 'C': + args['diameter'] = parse_gerber_value(val, format, zero_suppression) + elif cmd == 'F': + args['feed_rate'] = parse_gerber_value(val, format, zero_suppression) + elif cmd == 'H': + args['max_hit_count'] = parse_gerber_value(val, format, zero_suppression) + elif cmd == 'S': + args['rpm'] = 1000 * parse_gerber_value(val, format, zero_suppression) + elif cmd == 'T': + args['number'] = int(val) + elif cmd == 'Z': + args['depth_offset'] = parse_gerber_value(val, format, zero_suppression) + return cls(settings, **args) + + def from_dict(cls, settings, tool_dict): + return cls(settings, tool_dict) + + def __init__(self, settings, **kwargs): + self.settings = settings + self.number = kwargs.get('number') + self.feed_rate = kwargs.get('feed_rate') + self.retract_rate = kwargs.get('retract_rate') + self.rpm = kwargs.get('rpm') + self.diameter = kwargs.get('diameter') + self.max_hit_count = kwargs.get('max_hit_count') + self.depth_offset = kwargs.get('depth_offset') + self.hit_count = 0 + + def to_excellon(self): + fmt = self.settings['format'] + zs = self.settings['zero_suppression'] + stmt = 'T%d' % self.number + if self.retract_rate: + stmt += 'B%s' % write_gerber_value(self.retract_rate, fmt, zs) + if self.diameter: + stmt += 'C%s' % write_gerber_value(self.diameter, fmt, zs) + if self.feed_rate: + stmt += 'F%s' % write_gerber_value(self.feed_rate, fmt, zs) + if self.max_hit_count: + stmt += 'H%s' % write_gerber_value(self.max_hit_count, fmt, zs) + if self.rpm: + if self.rpm < 100000.: + stmt += 'S%s' % write_gerber_value(self.rpm / 1000., fmt, zs) + else: + stmt += 'S%g' % self.rpm / 1000. + if self.depth_offset: + stmt += 'Z%s' % write_gerber_value(self.depth_offset, fmt, zs) + return stmt + + def _hit(self): + self.hit_count += 1 + + def __repr__(self): + unit = 'in.' if self.settings.units == 'inch' else 'mm' + return '' % (self.number, self.diameter, unit) + + +class CommentStmt(ExcellonStatement): + def __init__(self, comment): + self.comment = comment + + def to_excellon(self): + return ';%s' % comment + + +class HeaderBeginStmt(ExcellonStatement): + + def __init__(self): + pass + + def to_excellon(self): + return 'M48' + + +class HeaderEndStmt(ExcellonStatement): + + def __init__(self): + pass + + def to_excellon(self): + return 'M95' + + +class RewindStopStmt(ExcellonStatement): + + def __init__(self): + pass + + def to_excellon(self): + return '%' + + +class EndOfProgramStmt(ExcellonStatement): + + def __init__(self, x=None, y=None): + self.x = x + self.y = y + + def to_excellon(self): + stmt = 'M30' + if self.x is not None: + stmt += 'X%s' % write_gerber_value(self.x) + if self.y is not None: + stmt += 'Y%s' % write_gerber_value(self.y) + + +class UnitStmt(ExcellonStatement): + + def __init__(self, units='inch', zero_suppression='trailing'): + self.units = units.lower() + self.zero_suppression = zero_suppression + + def to_excellon(self): + stmt = '%s,%s' % ('INCH' if self.units == 'inch' else 'METRIC', + 'LZ' if self.zero_suppression == 'trailing' else 'TZ') + + +class IncrementalModeStmt(ExcellonStatement): + + def __init__(self, mode='off'): + if mode.lower() not in ['on', 'off']: + raise ValueError('Mode may be "on" or "off") + self.mode = 'off' + + def to_excellon(self): + return 'ICI,%s' % 'OFF' if self.mode == 'off' else 'ON' + + +class VersionStmt(ExcellonStatement): + + def __init__(self, version=1): + self.version = int(version) + + def to_excellon(self): + return 'VER,%d' % self.version + + +class FormatStmt(ExcellonStatement): + def __init__(self, format=1): + self.format = int(format) + + def to_excellon(self): + return 'FMAT,%d' % self.format + + +class LinkToolStmt(ExcellonStatement): + + def __init__(self, linked_tools): + self.linked_tools = [int(x) for x in linked_tools] + + def to_excellon(self): + return '/'.join([str(x) for x in self.linked_tools]) + + +class MeasuringModeStmt(ExcellonStatement): + + def __init__(self, units='inch'): + units = units.lower() + if units not in ['inch', 'metric']: + raise ValueError('units must be "inch" or "metric"') + self.units = units + + def to_excellon(self): + return 'M72' if self.units == 'inch' else 'M71' + + diff --git a/gerber/gerber.py b/gerber/gerber.py index 3f93ed4..9ad5dc9 100644 --- a/gerber/gerber.py +++ b/gerber/gerber.py @@ -26,7 +26,7 @@ This module provides an RS-274-X class and parser import re import json -from .statements import * +from .gerber_statements import * from .cnc import CncFile, FileSettings diff --git a/gerber/gerber_statements.py b/gerber/gerber_statements.py new file mode 100644 index 0000000..5a9d046 --- /dev/null +++ b/gerber/gerber_statements.py @@ -0,0 +1,597 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- +""" +gerber.statements +================= +**Gerber file statement classes** + +""" +from .utils import parse_gerber_value, write_gerber_value, decimal_string + + +__all__ = ['FSParamStmt', 'MOParamStmt', 'IPParamStmt', 'OFParamStmt', + 'LPParamStmt', 'ADParamStmt', 'AMParamStmt', 'INParamStmt', + 'LNParamStmt', 'CoordStmt', 'ApertureStmt', 'CommentStmt', + 'EofStmt', 'UnknownStmt'] + + +class Statement(object): + def __init__(self, type): + self.type = type + + def __str__(self): + s = "<{0} ".format(self.__class__.__name__) + + for key, value in self.__dict__.items(): + s += "{0}={1} ".format(key, value) + + s = s.rstrip() + ">" + return s + + +class ParamStmt(Statement): + def __init__(self, param): + Statement.__init__(self, "PARAM") + self.param = param + + +class FSParamStmt(ParamStmt): + """ FS - Gerber Format Specification Statement + """ + + @classmethod + def from_dict(cls, stmt_dict): + """ + """ + param = stmt_dict.get('param').strip() + zeros = 'leading' if stmt_dict.get('zero') == 'L' else 'trailing' + notation = 'absolute' if stmt_dict.get('notation') == 'A' else 'incremental' + x = map(int, stmt_dict.get('x').strip()) + format = (x[0], x[1]) + return cls(param, zeros, notation, format) + + def __init__(self, param, zero_suppression='leading', + notation='absolute', format=(2, 4)): + """ Initialize FSParamStmt class + + .. note:: + The FS command specifies the format of the coordinate data. It + must only be used once at the beginning of a file. It must be + specified before the first use of coordinate data. + + Parameters + ---------- + param : string + Parameter. + + zero_suppression : string + Zero-suppression mode. May be either 'leading' or 'trailing' + + notation : string + Notation mode. May be either 'absolute' or 'incremental' + + format : tuple (int, int) + Gerber precision format expressed as a tuple containing: + (number of integer-part digits, number of decimal-part digits) + + Returns + ------- + ParamStmt : FSParamStmt + Initialized FSParamStmt class. + + """ + ParamStmt.__init__(self, param) + self.zero_suppression = zero_suppression + 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' + format = ''.join(map(str, self.format)) + return '%FS{0}{1}X{2}Y{3}*%'.format(zero_suppression, notation, + format, format) + + def __str__(self): + return ('' % + (self.format[0], self.format[1], self.zero_suppression, + self.notation)) + + +class MOParamStmt(ParamStmt): + """ MO - Gerber Mode (measurement units) Statement. + """ + + @classmethod + def from_dict(cls, stmt_dict): + param = stmt_dict.get('param') + mo = 'inch' if stmt_dict.get('mo') == 'IN' else 'metric' + return cls(param, mo) + + def __init__(self, param, mo): + """ Initialize MOParamStmt class + + Parameters + ---------- + param : string + Parameter. + + mo : string + Measurement units. May be either 'inch' or 'metric' + + Returns + ------- + ParamStmt : MOParamStmt + Initialized MOParamStmt class. + + """ + ParamStmt.__init__(self, param) + self.mode = mo + + def to_gerber(self): + 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_str) + + +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 __str__(self): + return ('' % 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_str) + + +class LPParamStmt(ParamStmt): + """ LP - Gerber Level Polarity statement + """ + + @classmethod + def from_dict(cls, stmt_dict): + param = stmt_dict.get('lp') + lp = 'clear' if stmt_dict.get('lp') == 'C' else 'dark' + return cls(param, lp) + + def __init__(self, param, lp): + """ Initialize LPParamStmt class + + Parameters + ---------- + param : string + Parameter + + lp : string + Level polarity. May be either 'clear' or 'dark' + + Returns + ------- + ParamStmt : LPParamStmt + Initialized LPParamStmt class. + + """ + ParamStmt.__init__(self, param) + self.lp = lp + + def to_gerber(self, settings): + lp = 'C' if self.lp == 'clear' else 'dark' + return '%LP{0}*%'.format(self.lp) + + def __str__(self): + return '' % self.lp + + +class ADParamStmt(ParamStmt): + """ AD - Gerber Aperture Definition Statement + """ + + @classmethod + def from_dict(cls, stmt_dict): + param = stmt_dict.get('param') + d = int(stmt_dict.get('d')) + shape = stmt_dict.get('shape') + modifiers = stmt_dict.get('modifiers') + if modifiers is not None: + modifiers = [[float(x) for x in m.split('X')] + for m in modifiers.split(',')] + return cls(param, d, shape, modifiers) + + def __init__(self, param, d, shape, modifiers): + """ Initialize ADParamStmt class + + Parameters + ---------- + param : string + Parameter code + + d : int + Aperture D-code + + shape : string + aperture name + + modifiers : list of lists of floats + Shape modifiers + + Returns + ------- + ParamStmt : LPParamStmt + Initialized LPParamStmt class. + + """ + ParamStmt.__init__(self, param) + self.d = d + self.shape = shape + self.modifiers = 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])) + + def __str__(self): + if self.shape == 'C': + shape = 'circle' + elif self.shape == 'R': + shape = 'rectangle' + elif self.shape == 'O': + shape = 'oblong' + else: + shape = self.shape + + return '' % (self.d, shape) + + +class AMParamStmt(ParamStmt): + """ AM - Aperture Macro Statement + """ + + @classmethod + def from_dict(cls, stmt_dict): + return cls(**stmt_dict) + + def __init__(self, param, name, macro): + """ Initialize AMParamStmt class + + Parameters + ---------- + param : string + Parameter code + + name : string + Aperture macro name + + macro : string + Aperture macro string + + Returns + ------- + ParamStmt : AMParamStmt + Initialized AMParamStmt class. + + """ + ParamStmt.__init__(self, param) + self.name = name + self.macro = macro + + def to_gerber(self): + return '%AM{0}*{1}*%'.format(self.name, self.macro) + + def __str__(self): + return '' % (self.name, macro) + + +class INParamStmt(ParamStmt): + """ IN - Image Name Statement + """ + @classmethod + def from_dict(cls, stmt_dict): + return cls(**stmt_dict) + + def __init__(self, param, name): + """ Initialize INParamStmt class + + Parameters + ---------- + param : string + Parameter code + + name : string + Image name + + Returns + ------- + ParamStmt : INParamStmt + Initialized INParamStmt class. + + """ + ParamStmt.__init__(self, param) + self.name = name + + def to_gerber(self): + return '%IN{0}*%'.format(self.name) + + def __str__(self): + return '' % self.name + + +class LNParamStmt(ParamStmt): + """ LN - Level Name Statement (Deprecated) + """ + @classmethod + def from_dict(cls, stmt_dict): + return cls(**stmt_dict) + + def __init__(self, param, name): + """ Initialize LNParamStmt class + + Parameters + ---------- + param : string + Parameter code + + name : string + Level name + + Returns + ------- + ParamStmt : LNParamStmt + Initialized LNParamStmt class. + + """ + ParamStmt.__init__(self, param) + self.name = name + + def to_gerber(self): + return '%LN{0}*%'.format(self.name) + + def __str__(self): + return '' % self.name + + +class CoordStmt(Statement): + """ Coordinate Data Block + """ + + @classmethod + def from_dict(cls, stmt_dict, settings): + zeros = settings['zero_suppression'] + format = settings['format'] + function = stmt_dict.get('function') + x = stmt_dict.get('x') + y = stmt_dict.get('y') + i = stmt_dict.get('i') + j = stmt_dict.get('j') + op = stmt_dict.get('op') + + if x is not None: + x = parse_gerber_value(stmt_dict.get('x'), + format, zeros) + if y is not None: + y = parse_gerber_value(stmt_dict.get('y'), + format, zeros) + if i is not None: + i = parse_gerber_value(stmt_dict.get('i'), + format, zeros) + if j is not None: + j = parse_gerber_value(stmt_dict.get('j'), + format, zeros) + return cls(function, x, y, i, j, op, settings) + + def __init__(self, function, x, y, i, j, op, settings): + """ Initialize CoordStmt class + + Parameters + ---------- + function : string + function + + x : float + X coordinate + + y : float + Y coordinate + + i : float + Coordinate offset in the X direction + + j : float + Coordinate offset in the Y direction + + op : string + Operation code + + settings : dict {'zero_suppression', 'format'} + Gerber file coordinate format + + Returns + ------- + Statement : CoordStmt + Initialized CoordStmt class. + + """ + Statement.__init__(self, "COORD") + self.zero_suppression = settings['zero_suppression'] + self.format = settings['format'] + self.function = function + self.x = x + self.y = y + self.i = i + self.j = j + self.op = op + + def to_gerber(self): + 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.op: + ret += self.op + return ret + '*' + + 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.op: + if self.op == 'D01': + op = 'Lights On' + elif self.op == 'D02': + op = 'Lights Off' + elif self.op == 'D03': + op = 'Flash' + else: + op = self.op + coord_str += 'Op: %s' % op + + return '' % coord_str + + +class ApertureStmt(Statement): + """ Aperture Statement + """ + def __init__(self, d): + Statement.__init__(self, "APERTURE") + self.d = int(d) + + def to_gerber(self): + return 'G54D{0}*'.format(self.d) + + def __str__(self): + return '' % self.d + + +class CommentStmt(Statement): + """ Comment Statment + """ + def __init__(self, comment): + Statement.__init__(self, "COMMENT") + self.comment = comment + + def to_gerber(self): + return 'G04{0}*'.format(self.comment) + + def __str__(self): + return '' % self.comment + + +class EofStmt(Statement): + """ EOF Statement + """ + def __init__(self): + Statement.__init__(self, "EOF") + + def to_gerber(self): + return 'M02*' + + def __str__(self): + return '' + + +class UnknownStmt(Statement): + """ Unknown Statement + """ + def __init__(self, line): + Statement.__init__(self, "UNKNOWN") + self.line = line diff --git a/gerber/statements.py b/gerber/statements.py deleted file mode 100644 index 5a9d046..0000000 --- a/gerber/statements.py +++ /dev/null @@ -1,597 +0,0 @@ -#! /usr/bin/env python -# -*- coding: utf-8 -*- -""" -gerber.statements -================= -**Gerber file statement classes** - -""" -from .utils import parse_gerber_value, write_gerber_value, decimal_string - - -__all__ = ['FSParamStmt', 'MOParamStmt', 'IPParamStmt', 'OFParamStmt', - 'LPParamStmt', 'ADParamStmt', 'AMParamStmt', 'INParamStmt', - 'LNParamStmt', 'CoordStmt', 'ApertureStmt', 'CommentStmt', - 'EofStmt', 'UnknownStmt'] - - -class Statement(object): - def __init__(self, type): - self.type = type - - def __str__(self): - s = "<{0} ".format(self.__class__.__name__) - - for key, value in self.__dict__.items(): - s += "{0}={1} ".format(key, value) - - s = s.rstrip() + ">" - return s - - -class ParamStmt(Statement): - def __init__(self, param): - Statement.__init__(self, "PARAM") - self.param = param - - -class FSParamStmt(ParamStmt): - """ FS - Gerber Format Specification Statement - """ - - @classmethod - def from_dict(cls, stmt_dict): - """ - """ - param = stmt_dict.get('param').strip() - zeros = 'leading' if stmt_dict.get('zero') == 'L' else 'trailing' - notation = 'absolute' if stmt_dict.get('notation') == 'A' else 'incremental' - x = map(int, stmt_dict.get('x').strip()) - format = (x[0], x[1]) - return cls(param, zeros, notation, format) - - def __init__(self, param, zero_suppression='leading', - notation='absolute', format=(2, 4)): - """ Initialize FSParamStmt class - - .. note:: - The FS command specifies the format of the coordinate data. It - must only be used once at the beginning of a file. It must be - specified before the first use of coordinate data. - - Parameters - ---------- - param : string - Parameter. - - zero_suppression : string - Zero-suppression mode. May be either 'leading' or 'trailing' - - notation : string - Notation mode. May be either 'absolute' or 'incremental' - - format : tuple (int, int) - Gerber precision format expressed as a tuple containing: - (number of integer-part digits, number of decimal-part digits) - - Returns - ------- - ParamStmt : FSParamStmt - Initialized FSParamStmt class. - - """ - ParamStmt.__init__(self, param) - self.zero_suppression = zero_suppression - 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' - format = ''.join(map(str, self.format)) - return '%FS{0}{1}X{2}Y{3}*%'.format(zero_suppression, notation, - format, format) - - def __str__(self): - return ('' % - (self.format[0], self.format[1], self.zero_suppression, - self.notation)) - - -class MOParamStmt(ParamStmt): - """ MO - Gerber Mode (measurement units) Statement. - """ - - @classmethod - def from_dict(cls, stmt_dict): - param = stmt_dict.get('param') - mo = 'inch' if stmt_dict.get('mo') == 'IN' else 'metric' - return cls(param, mo) - - def __init__(self, param, mo): - """ Initialize MOParamStmt class - - Parameters - ---------- - param : string - Parameter. - - mo : string - Measurement units. May be either 'inch' or 'metric' - - Returns - ------- - ParamStmt : MOParamStmt - Initialized MOParamStmt class. - - """ - ParamStmt.__init__(self, param) - self.mode = mo - - def to_gerber(self): - 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_str) - - -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 __str__(self): - return ('' % 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_str) - - -class LPParamStmt(ParamStmt): - """ LP - Gerber Level Polarity statement - """ - - @classmethod - def from_dict(cls, stmt_dict): - param = stmt_dict.get('lp') - lp = 'clear' if stmt_dict.get('lp') == 'C' else 'dark' - return cls(param, lp) - - def __init__(self, param, lp): - """ Initialize LPParamStmt class - - Parameters - ---------- - param : string - Parameter - - lp : string - Level polarity. May be either 'clear' or 'dark' - - Returns - ------- - ParamStmt : LPParamStmt - Initialized LPParamStmt class. - - """ - ParamStmt.__init__(self, param) - self.lp = lp - - def to_gerber(self, settings): - lp = 'C' if self.lp == 'clear' else 'dark' - return '%LP{0}*%'.format(self.lp) - - def __str__(self): - return '' % self.lp - - -class ADParamStmt(ParamStmt): - """ AD - Gerber Aperture Definition Statement - """ - - @classmethod - def from_dict(cls, stmt_dict): - param = stmt_dict.get('param') - d = int(stmt_dict.get('d')) - shape = stmt_dict.get('shape') - modifiers = stmt_dict.get('modifiers') - if modifiers is not None: - modifiers = [[float(x) for x in m.split('X')] - for m in modifiers.split(',')] - return cls(param, d, shape, modifiers) - - def __init__(self, param, d, shape, modifiers): - """ Initialize ADParamStmt class - - Parameters - ---------- - param : string - Parameter code - - d : int - Aperture D-code - - shape : string - aperture name - - modifiers : list of lists of floats - Shape modifiers - - Returns - ------- - ParamStmt : LPParamStmt - Initialized LPParamStmt class. - - """ - ParamStmt.__init__(self, param) - self.d = d - self.shape = shape - self.modifiers = 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])) - - def __str__(self): - if self.shape == 'C': - shape = 'circle' - elif self.shape == 'R': - shape = 'rectangle' - elif self.shape == 'O': - shape = 'oblong' - else: - shape = self.shape - - return '' % (self.d, shape) - - -class AMParamStmt(ParamStmt): - """ AM - Aperture Macro Statement - """ - - @classmethod - def from_dict(cls, stmt_dict): - return cls(**stmt_dict) - - def __init__(self, param, name, macro): - """ Initialize AMParamStmt class - - Parameters - ---------- - param : string - Parameter code - - name : string - Aperture macro name - - macro : string - Aperture macro string - - Returns - ------- - ParamStmt : AMParamStmt - Initialized AMParamStmt class. - - """ - ParamStmt.__init__(self, param) - self.name = name - self.macro = macro - - def to_gerber(self): - return '%AM{0}*{1}*%'.format(self.name, self.macro) - - def __str__(self): - return '' % (self.name, macro) - - -class INParamStmt(ParamStmt): - """ IN - Image Name Statement - """ - @classmethod - def from_dict(cls, stmt_dict): - return cls(**stmt_dict) - - def __init__(self, param, name): - """ Initialize INParamStmt class - - Parameters - ---------- - param : string - Parameter code - - name : string - Image name - - Returns - ------- - ParamStmt : INParamStmt - Initialized INParamStmt class. - - """ - ParamStmt.__init__(self, param) - self.name = name - - def to_gerber(self): - return '%IN{0}*%'.format(self.name) - - def __str__(self): - return '' % self.name - - -class LNParamStmt(ParamStmt): - """ LN - Level Name Statement (Deprecated) - """ - @classmethod - def from_dict(cls, stmt_dict): - return cls(**stmt_dict) - - def __init__(self, param, name): - """ Initialize LNParamStmt class - - Parameters - ---------- - param : string - Parameter code - - name : string - Level name - - Returns - ------- - ParamStmt : LNParamStmt - Initialized LNParamStmt class. - - """ - ParamStmt.__init__(self, param) - self.name = name - - def to_gerber(self): - return '%LN{0}*%'.format(self.name) - - def __str__(self): - return '' % self.name - - -class CoordStmt(Statement): - """ Coordinate Data Block - """ - - @classmethod - def from_dict(cls, stmt_dict, settings): - zeros = settings['zero_suppression'] - format = settings['format'] - function = stmt_dict.get('function') - x = stmt_dict.get('x') - y = stmt_dict.get('y') - i = stmt_dict.get('i') - j = stmt_dict.get('j') - op = stmt_dict.get('op') - - if x is not None: - x = parse_gerber_value(stmt_dict.get('x'), - format, zeros) - if y is not None: - y = parse_gerber_value(stmt_dict.get('y'), - format, zeros) - if i is not None: - i = parse_gerber_value(stmt_dict.get('i'), - format, zeros) - if j is not None: - j = parse_gerber_value(stmt_dict.get('j'), - format, zeros) - return cls(function, x, y, i, j, op, settings) - - def __init__(self, function, x, y, i, j, op, settings): - """ Initialize CoordStmt class - - Parameters - ---------- - function : string - function - - x : float - X coordinate - - y : float - Y coordinate - - i : float - Coordinate offset in the X direction - - j : float - Coordinate offset in the Y direction - - op : string - Operation code - - settings : dict {'zero_suppression', 'format'} - Gerber file coordinate format - - Returns - ------- - Statement : CoordStmt - Initialized CoordStmt class. - - """ - Statement.__init__(self, "COORD") - self.zero_suppression = settings['zero_suppression'] - self.format = settings['format'] - self.function = function - self.x = x - self.y = y - self.i = i - self.j = j - self.op = op - - def to_gerber(self): - 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.op: - ret += self.op - return ret + '*' - - 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.op: - if self.op == 'D01': - op = 'Lights On' - elif self.op == 'D02': - op = 'Lights Off' - elif self.op == 'D03': - op = 'Flash' - else: - op = self.op - coord_str += 'Op: %s' % op - - return '' % coord_str - - -class ApertureStmt(Statement): - """ Aperture Statement - """ - def __init__(self, d): - Statement.__init__(self, "APERTURE") - self.d = int(d) - - def to_gerber(self): - return 'G54D{0}*'.format(self.d) - - def __str__(self): - return '' % self.d - - -class CommentStmt(Statement): - """ Comment Statment - """ - def __init__(self, comment): - Statement.__init__(self, "COMMENT") - self.comment = comment - - def to_gerber(self): - return 'G04{0}*'.format(self.comment) - - def __str__(self): - return '' % self.comment - - -class EofStmt(Statement): - """ EOF Statement - """ - def __init__(self): - Statement.__init__(self, "EOF") - - def to_gerber(self): - return 'M02*' - - def __str__(self): - return '' - - -class UnknownStmt(Statement): - """ Unknown Statement - """ - def __init__(self, line): - Statement.__init__(self, "UNKNOWN") - self.line = line diff --git a/gerber/tests/test_gerber_statements.py b/gerber/tests/test_gerber_statements.py new file mode 100644 index 0000000..121aa42 --- /dev/null +++ b/gerber/tests/test_gerber_statements.py @@ -0,0 +1,96 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# Author: Hamilton Kibbe + +from .tests import * +from ..gerber_statements import * + + +def test_FSParamStmt_factory(): + """ Test FSParamStruct factory correctly handles parameters + """ + stmt = {'param': 'FS', 'zero': 'L', 'notation': 'A', 'x': '27'} + fs = FSParamStmt.from_dict(stmt) + assert_equal(fs.param, 'FS') + assert_equal(fs.zero_suppression, 'leading') + assert_equal(fs.notation, 'absolute') + assert_equal(fs.format, (2, 7)) + + stmt = {'param': 'FS', 'zero': 'T', 'notation': 'I', 'x': '27'} + fs = FSParamStmt.from_dict(stmt) + assert_equal(fs.param, 'FS') + assert_equal(fs.zero_suppression, 'trailing') + assert_equal(fs.notation, 'incremental') + assert_equal(fs.format, (2, 7)) + + +def test_FSParamStmt_dump(): + """ Test FSParamStmt to_gerber() + """ + stmt = {'param': 'FS', 'zero': 'L', 'notation': 'A', 'x': '27'} + fs = FSParamStmt.from_dict(stmt) + assert_equal(fs.to_gerber(), '%FSLAX27Y27*%') + + stmt = {'param': 'FS', 'zero': 'T', 'notation': 'I', 'x': '25'} + fs = FSParamStmt.from_dict(stmt) + assert_equal(fs.to_gerber(), '%FSTIX25Y25*%') + + +def test_MOParamStmt_factory(): + """ Test MOParamStruct factory correctly handles parameters + """ + stmt = {'param': 'MO', 'mo': 'IN'} + mo = MOParamStmt.from_dict(stmt) + assert_equal(mo.param, 'MO') + assert_equal(mo.mode, 'inch') + + stmt = {'param': 'MO', 'mo': 'MM'} + mo = MOParamStmt.from_dict(stmt) + assert_equal(mo.param, 'MO') + assert_equal(mo.mode, 'metric') + + +def test_MOParamStmt_dump(): + """ Test MOParamStmt to_gerber() + """ + stmt = {'param': 'MO', 'mo': 'IN'} + mo = MOParamStmt.from_dict(stmt) + assert_equal(mo.to_gerber(), '%MOIN*%') + + stmt = {'param': 'MO', 'mo': 'MM'} + mo = MOParamStmt.from_dict(stmt) + assert_equal(mo.to_gerber(), '%MOMM*%') + + +def test_IPParamStmt_factory(): + """ Test IPParamStruct factory correctly handles parameters + """ + stmt = {'param': 'IP', 'ip': 'POS'} + ip = IPParamStmt.from_dict(stmt) + assert_equal(ip.ip, 'positive') + + stmt = {'param': 'IP', 'ip': 'NEG'} + ip = IPParamStmt.from_dict(stmt) + assert_equal(ip.ip, 'negative') + + +def test_IPParamStmt_dump(): + """ Test IPParamStmt to_gerber() + """ + stmt = {'param': 'IP', 'ip': 'POS'} + ip = IPParamStmt.from_dict(stmt) + assert_equal(ip.to_gerber(), '%IPPOS*%') + + stmt = {'param': 'IP', 'ip': 'NEG'} + ip = IPParamStmt.from_dict(stmt) + assert_equal(ip.to_gerber(), '%IPNEG*%') + + +def test_OFParamStmt_dump(): + """ Test OFParamStmt to_gerber() + """ + stmt = {'param': 'OF', 'a': '0.1234567', 'b': '0.1234567'} + of = OFParamStmt.from_dict(stmt) + assert_equal(of.to_gerber(), '%OFA0.123456B0.123456*%') + diff --git a/gerber/tests/test_statements.py b/gerber/tests/test_statements.py deleted file mode 100644 index 4560521..0000000 --- a/gerber/tests/test_statements.py +++ /dev/null @@ -1,97 +0,0 @@ -#! /usr/bin/env python -# -*- coding: utf-8 -*- - -# Author: Hamilton Kibbe - -from .tests import * -from ..statements import * - - -def test_FSParamStmt_factory(): - """ Test FSParamStruct factory correctly handles parameters - """ - stmt = {'param': 'FS', 'zero': 'L', 'notation': 'A', 'x': '27'} - fs = FSParamStmt.from_dict(stmt) - assert_equal(fs.param, 'FS') - assert_equal(fs.zero_suppression, 'leading') - assert_equal(fs.notation, 'absolute') - assert_equal(fs.format, (2, 7)) - - stmt = {'param': 'FS', 'zero': 'T', 'notation': 'I', 'x': '27'} - fs = FSParamStmt.from_dict(stmt) - assert_equal(fs.param, 'FS') - assert_equal(fs.zero_suppression, 'trailing') - assert_equal(fs.notation, 'incremental') - assert_equal(fs.format, (2, 7)) - - -def test_FSParamStmt_dump(): - """ Test FSParamStmt to_gerber() - """ - stmt = {'param': 'FS', 'zero': 'L', 'notation': 'A', 'x': '27'} - fs = FSParamStmt.from_dict(stmt) - assert_equal(fs.to_gerber(), '%FSLAX27Y27*%') - - stmt = {'param': 'FS', 'zero': 'T', 'notation': 'I', 'x': '25'} - fs = FSParamStmt.from_dict(stmt) - assert_equal(fs.to_gerber(), '%FSTIX25Y25*%') - - -def test_MOParamStmt_factory(): - """ Test MOParamStruct factory correctly handles parameters - """ - stmt = {'param': 'MO', 'mo': 'IN'} - mo = MOParamStmt.from_dict(stmt) - assert_equal(mo.param, 'MO') - assert_equal(mo.mode, 'inch') - - stmt = {'param': 'MO', 'mo': 'MM'} - mo = MOParamStmt.from_dict(stmt) - assert_equal(mo.param, 'MO') - assert_equal(mo.mode, 'metric') - - -def test_MOParamStmt_dump(): - """ Test MOParamStmt to_gerber() - """ - stmt = {'param': 'MO', 'mo': 'IN'} - mo = MOParamStmt.from_dict(stmt) - assert_equal(mo.to_gerber(), '%MOIN*%') - - stmt = {'param': 'MO', 'mo': 'MM'} - mo = MOParamStmt.from_dict(stmt) - assert_equal(mo.to_gerber(), '%MOMM*%') - - -def test_IPParamStmt_factory(): - """ Test IPParamStruct factory correctly handles parameters - """ - stmt = {'param': 'IP', 'ip': 'POS'} - ip = IPParamStmt.from_dict(stmt) - assert_equal(ip.ip, 'positive') - - stmt = {'param': 'IP', 'ip': 'NEG'} - ip = IPParamStmt.from_dict(stmt) - assert_equal(ip.ip, 'negative') - - -def test_IPParamStmt_dump(): - """ Test IPParamStmt to_gerber() - """ - stmt = {'param': 'IP', 'ip': 'POS'} - ip = IPParamStmt.from_dict(stmt) - assert_equal(ip.to_gerber(), '%IPPOS*%') - - stmt = {'param': 'IP', 'ip': 'NEG'} - ip = IPParamStmt.from_dict(stmt) - assert_equal(ip.to_gerber(), '%IPNEG*%') - - -def test_OFParamStmt_dump(): - """ Test OFParamStmt to_gerber() - """ - stmt = {'param': 'OF', 'a': '0.1234567', 'b': '0.1234567'} - of = OFParamStmt.from_dict(stmt) - assert_equal(of.to_gerber(), '%OFA0.123456B0.123456*%') - - -- cgit From e565624b8181ea0a9dd5ea1585025a4eec72ac18 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Mon, 6 Oct 2014 11:50:38 -0400 Subject: Fix import error --- gerber/render/render.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'gerber') diff --git a/gerber/render/render.py b/gerber/render/render.py index c372783..eab7d33 100644 --- a/gerber/render/render.py +++ b/gerber/render/render.py @@ -16,7 +16,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from ..statements import ( +from ..gerber_statements import ( CommentStmt, UnknownStmt, EofStmt, ParamStmt, CoordStmt, ApertureStmt ) -- cgit From 08253b40f6f677c4edaeb7108177846d8f0d8703 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Mon, 6 Oct 2014 13:11:16 -0400 Subject: Update excellon statements/ExcellonFile --- gerber/cnc.py | 3 +- gerber/excellon.py | 96 ++++++++++++++++++--------------- gerber/excellon_statements.py | 122 +++++++++++++++++++++++++++++++++++++++--- gerber/gerber.py | 3 +- 4 files changed, 173 insertions(+), 51 deletions(-) (limited to 'gerber') diff --git a/gerber/cnc.py b/gerber/cnc.py index aaa1a42..d17517a 100644 --- a/gerber/cnc.py +++ b/gerber/cnc.py @@ -92,7 +92,7 @@ class CncFile(object): decimal digits) """ - def __init__(self, settings=None, filename=None): + def __init__(self, statements=None, settings=None, filename=None): if settings is not None: self.notation = settings['notation'] self.units = settings['units'] @@ -103,6 +103,7 @@ class CncFile(object): self.units = 'inch' self.zero_suppression = 'trailing' self.format = (2, 5) + self.statements = statements if statements is not None else [] self.filename = filename @property diff --git a/gerber/excellon.py b/gerber/excellon.py index 6ae182b..45a8e4b 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -57,8 +57,8 @@ class ExcellonFile(CncFile): either 'inch' or 'metric'. """ - def __init__(self, tools, hits, settings, filename=None): - super(ExcellonFile, self).__init__(settings, filename) + def __init__(self, statements, tools, hits, settings, filename=None): + super(ExcellonFile, self).__init__(statements, settings, filename) self.tools = tools self.hits = hits @@ -98,14 +98,20 @@ class ExcellonParser(object): with open(filename, 'r') as f: for line in f: self._parse(line) - return ExcellonFile(self.tools, self.hits, self._settings(), filename) + return ExcellonFile(self.statements, self.tools, self.hits, self._settings(), filename) def dump(self, filename): if self.ctx is not None: self.ctx.dump(filename) def _parse(self, line): - if 'M48' in line: + zs = self._settings()['zero_suppression'] + fmt = self._settings()['format'] + + if line[0] == ';': + self.statements.append(CommentStmt.from_excellon(line)) + + elif line[:3] == 'M48': self.statements.append(HeaderBeginStmt()) self.state = 'HEADER' @@ -114,56 +120,59 @@ class ExcellonParser(object): if self.state == 'HEADER': self.state = 'DRILL' - elif 'M95' in line: + elif line[:3] == 'M95': self.statements.append(HeaderEndStmt()) if self.state == 'HEADER': self.state = 'DRILL' - elif 'G00' in line: + elif line[:3] == 'G00': self.state = 'ROUT' - elif 'G05' in line: + elif line[:3] == 'G05': self.state = 'DRILL' - - if 'INCH' in line or line.strip() == 'M72': - self.units = 'inch' - - elif 'METRIC' in line or line.strip() == 'M71': - self.units = 'metric' - - if 'LZ' in line: - self.zeros = 'L' - - elif 'TZ' in line: - self.zeros = 'T' - - if 'ICI' in line and 'ON' in line or line.strip() == 'G91': - self.notation = 'incremental' - - if 'ICI' in line and 'OFF' in line or line.strip() == 'G90': - self.notation = 'incremental' - - zs = self._settings()['zero_suppression'] - fmt = self._settings()['format'] + + elif ('INCH' in line or 'METRIC' in line) and ('LZ' in line or 'TZ' in line): + stmt = UnitStmt.from_excellon(line) + self.units = stmt.units + self.zero_suppression = stmt.zero_suppression + self.statements.append(stmt) + + elif line[:3] == 'M71' or line [:3] == 'M72': + stmt = MeasuringModeStmt.from_excellon(line) + self.units = stmt.units + self.statements.append(stmt) + + elif line[:3] == 'ICI': + stmt = IncrementalModeStmt.from_excellon(line) + self.notation = 'incremental' if stmt.mode == 'on' else 'absolute' + self.statements.append(stmt) # tool definition - if line[0] == 'T' and self.state == 'HEADER': - tool = ExcellonTool.from_line(line, self._settings()) + elif line[0] == 'T' and self.state == 'HEADER': + tool = ExcellonTool.from_excellon(line, self._settings()) self.tools[tool.number] = tool + self.statements.append(tool) elif line[0] == 'T' and self.state != 'HEADER': - self.active_tool = self.tools[int(line.strip().split('T')[1])] - - if line[0] in ['X', 'Y']: - x = None - y = None - if line[0] == 'X': - splitline = line.strip('X').split('Y') - x = parse_gerber_value(splitline[0].strip(), fmt, zs) - if len(splitline) == 2: - y = parse_gerber_value(splitline[1].strip(), fmt, zs) - else: - y = parse_gerber_value(line.strip(' Y'), fmt, zs) + stmt = ToolSelectionStmt.from_excellon(line) + self.active_tool self.tools[stmt.tool] + #self.active_tool = self.tools[int(line.strip().split('T')[1])] + self.statements.append(statement) + + elif line[0] in ['X', 'Y']: + stmt = CoordinateStmt.from_excellon(line, fmt, zs) + x = stmt.x + y = stmt.y + self.statements.append(stmt) + #x = None + #y = None + #if line[0] == 'X': + # splitline = line.strip('X').split('Y') + # x = parse_gerber_value(splitline[0].strip(), fmt, zs) + # if len(splitline) == 2: + # y = parse_gerber_value(splitline[1].strip(), fmt, zs) + #else: + # y = parse_gerber_value(line.strip(' Y'), fmt, zs) if self.notation == 'absolute': if x is not None: self.pos[0] = x @@ -180,6 +189,9 @@ class ExcellonParser(object): if self.ctx is not None: self.ctx.drill(self.pos[0], self.pos[1], self.active_tool.diameter) + + else: + self.statements.append(UnknownStmt.from_excellon(line)) def _settings(self): return FileSettings(units=self.units, format=self.format, diff --git a/gerber/excellon_statements.py b/gerber/excellon_statements.py index 4544f08..faadd20 100644 --- a/gerber/excellon_statements.py +++ b/gerber/excellon_statements.py @@ -18,13 +18,21 @@ from .utils import write_gerber_value -__all__ = ['ExcellonTool', 'CommentStmt', 'HeaderBeginStmt', 'HeaderEndStmt', +__all__ = ['ExcellonTool', 'ToolSelectionStatment', 'CoordinateStmt', + 'CommentStmt', 'HeaderBeginStmt', 'HeaderEndStmt', + 'RewindStopStmt', 'EndOfProgramStmt', 'UnitStmt', + 'IncrementalModeStmt', 'VersionStmt', 'FormatStmt', 'LinkToolStmt', + 'MeasuringModeStmt', 'UnknownStmt', ] class ExcellonStatement(object): """ Excellon Statement abstract base class """ + @classmethod + def from_excellon(cls, line): + pass + def to_excellon(self): pass @@ -74,7 +82,7 @@ class ExcellonTool(ExcellonStatement): """ @classmethod - def from_line(cls, line, settings): + def from_excellon(cls, line, settings): """ Create a Tool from an excellon file tool definition line. Parameters @@ -155,7 +163,62 @@ class ExcellonTool(ExcellonStatement): return '' % (self.number, self.diameter, unit) +class ToolSelectionStatment(ExcellonStatement): + + @classmethod + def from_excellon(cls, line): + line = line.strip()[1:] + compensation_index = None + tool = int(line[:2]) + if len(line) > 2: + compensation_index = int(line[2:]) + return cls(tool, compensation_index) + + def __init__(self, tool, compensation_index=None): + tool = int(tool) + compensation_index = int(compensation_index) if compensation_index else None + self.tool = tool + self.compensation_index = compensation_index + + def to_excellon(self): + stmt = 'T%02d' % self.tool + if self.compensation_index is not None: + stmt += '%02d' % self.compensation_index + return stmt + + +class CoordinateStmt(ExcellonStatement): + + def from_excellon(cls, line, format=(2, 5), zero_suppression='trailing'): + x = None + y = None + if line[0] == 'X': + splitline = line.strip('X').split('Y') + x = parse_gerber_value(splitline[0].strip(), format, zero_suppression) + if len(splitline) == 2: + y = parse_gerber_value(splitline[1].strip(), format, zero_suppression) + else: + y = parse_gerber_value(line.strip(' Y'), format, zero_suppression) + return cls(x, y) + + def __init__(self, x=None, y=None): + self.x = x + self.y = y + + def to_excellon(self): + stmt = '' + if self.x is not None: + stmt.append('X%s' % write_gerber_value(self.x)) + if self.y is not None: + stmt.append('Y%s' % write_gerber_value(self.y)) + return stmt + + class CommentStmt(ExcellonStatement): + + def from_excellon(self, line): + return cls(line.strip().lstrip(';')) + def __init__(self, comment): self.comment = comment @@ -206,6 +269,12 @@ class EndOfProgramStmt(ExcellonStatement): class UnitStmt(ExcellonStatement): + @classmethod + def from_excellon(cls, line): + units = 'inch' if 'INCH' in line else 'metric' + zero_suppression = 'trailing' if 'LZ' in line else 'leading' + return cls(units, zero_suppression) + def __init__(self, units='inch', zero_suppression='trailing'): self.units = units.lower() self.zero_suppression = zero_suppression @@ -217,6 +286,10 @@ class UnitStmt(ExcellonStatement): class IncrementalModeStmt(ExcellonStatement): + @classmethod + def from_excellon(cls, line): + return cls('off') if 'OFF' in line else cls('on') + def __init__(self, mode='off'): if mode.lower() not in ['on', 'off']: raise ValueError('Mode may be "on" or "off") @@ -228,16 +301,33 @@ class IncrementalModeStmt(ExcellonStatement): class VersionStmt(ExcellonStatement): + @classmethod + def from_excellon(cls, line): + version = int(line.split(',')[1]) + return cls(version) + def __init__(self, version=1): - self.version = int(version) + version = int(version) + if version not in [1, 2]: + raise ValueError('Valid versions are 1 or 2' + self.version = version def to_excellon(self): return 'VER,%d' % self.version class FormatStmt(ExcellonStatement): + + @classmethod + def from_excellon(cls, line): + fmt = int(line.split(',')[1]) + return cls(fmt) + def __init__(self, format=1): - self.format = int(format) + format = int(format) + if format not in [1, 2]: + raise ValueError('Valid formats are 1 or 2') + self.format = format def to_excellon(self): return 'FMAT,%d' % self.format @@ -245,6 +335,11 @@ class FormatStmt(ExcellonStatement): class LinkToolStmt(ExcellonStatement): + @classmethod + def from_excellon(cls, line): + linked = [int(tool) for tool in line.strip().split('/')] + return cls(linked) + def __init__(self, linked_tools): self.linked_tools = [int(x) for x in linked_tools] @@ -253,14 +348,29 @@ class LinkToolStmt(ExcellonStatement): class MeasuringModeStmt(ExcellonStatement): - + + @classmethod + def from_excellon(cls, line): + return cls('inch') if 'M72' in line else cls('metric') + def __init__(self, units='inch'): units = units.lower() if units not in ['inch', 'metric']: raise ValueError('units must be "inch" or "metric"') self.units = units - + def to_excellon(self): return 'M72' if self.units == 'inch' else 'M71' +class UnknownStmt(ExcellonStatement): + + @classmethod + def from_excellon(cls, line): + return cls(line) + + def __init__(self, stmt): + self.stmt = stmt + + def to_excellon(self): + return self.stmt diff --git a/gerber/gerber.py b/gerber/gerber.py index 9ad5dc9..0278b0d 100644 --- a/gerber/gerber.py +++ b/gerber/gerber.py @@ -68,8 +68,7 @@ class GerberFile(CncFile): """ def __init__(self, statements, settings, filename=None): - super(GerberFile, self).__init__(settings, filename) - self.statements = statements + super(GerberFile, self).__init__(statements, settings, filename) @property def comments(self): -- cgit From 22a6f87e94c1192b277a1353aefc7c0316f41f90 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Mon, 6 Oct 2014 18:28:32 -0400 Subject: add excellon file write --- gerber/excellon.py | 23 +-- gerber/excellon_statements.py | 54 +++--- gerber/utils.py | 385 +++++++++++++++++++++--------------------- 3 files changed, 239 insertions(+), 223 deletions(-) (limited to 'gerber') diff --git a/gerber/excellon.py b/gerber/excellon.py index 45a8e4b..072bc31 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -74,6 +74,11 @@ class ExcellonFile(CncFile): ctx.drill(pos[0], pos[1], tool.diameter) ctx.dump(filename) + def write(self, filename): + with open(filename, 'w') as f: + for statement in self.statements: + f.write(statement.to_excellon() + '\n') + class ExcellonParser(object): """ Excellon File Parser @@ -155,9 +160,9 @@ class ExcellonParser(object): elif line[0] == 'T' and self.state != 'HEADER': stmt = ToolSelectionStmt.from_excellon(line) - self.active_tool self.tools[stmt.tool] + self.active_tool = self.tools[stmt.tool] #self.active_tool = self.tools[int(line.strip().split('T')[1])] - self.statements.append(statement) + self.statements.append(stmt) elif line[0] in ['X', 'Y']: stmt = CoordinateStmt.from_excellon(line, fmt, zs) @@ -197,18 +202,8 @@ class ExcellonParser(object): return FileSettings(units=self.units, format=self.format, zero_suppression=self.zero_suppression, notation=self.notation) - - -def pairwise(iterator): - """ Iterate over list taking two elements at a time. - - e.g. [1, 2, 3, 4, 5, 6] ==> [(1, 2), (3, 4), (5, 6)] - """ - itr = iter(iterator) - while True: - yield tuple([itr.next() for i in range(2)]) if __name__ == '__main__': - p = parser() - p.parse('examples/ncdrill.txt') + p = ExcellonParser() + parsed = p.parse('examples/ncdrill.txt') diff --git a/gerber/excellon_statements.py b/gerber/excellon_statements.py index faadd20..09b72c1 100644 --- a/gerber/excellon_statements.py +++ b/gerber/excellon_statements.py @@ -15,10 +15,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -from .utils import write_gerber_value +from .utils import parse_gerber_value, write_gerber_value, decimal_string +import re -__all__ = ['ExcellonTool', 'ToolSelectionStatment', 'CoordinateStmt', +__all__ = ['ExcellonTool', 'ToolSelectionStmt', 'CoordinateStmt', 'CommentStmt', 'HeaderBeginStmt', 'HeaderEndStmt', 'RewindStopStmt', 'EndOfProgramStmt', 'UnitStmt', 'IncrementalModeStmt', 'VersionStmt', 'FormatStmt', 'LinkToolStmt', @@ -138,20 +139,20 @@ class ExcellonTool(ExcellonStatement): fmt = self.settings['format'] zs = self.settings['zero_suppression'] stmt = 'T%d' % self.number - if self.retract_rate: + if self.retract_rate is not None: stmt += 'B%s' % write_gerber_value(self.retract_rate, fmt, zs) - if self.diameter: - stmt += 'C%s' % write_gerber_value(self.diameter, fmt, zs) - if self.feed_rate: + if self.feed_rate is not None: stmt += 'F%s' % write_gerber_value(self.feed_rate, fmt, zs) - if self.max_hit_count: + if self.max_hit_count is not None: stmt += 'H%s' % write_gerber_value(self.max_hit_count, fmt, zs) - if self.rpm: + if self.rpm is not None: if self.rpm < 100000.: stmt += 'S%s' % write_gerber_value(self.rpm / 1000., fmt, zs) else: stmt += 'S%g' % self.rpm / 1000. - if self.depth_offset: + if self.diameter is not None: + stmt += 'C%s' % decimal_string(self.diameter, 5, True) + if self.depth_offset is not None: stmt += 'Z%s' % write_gerber_value(self.depth_offset, fmt, zs) return stmt @@ -163,7 +164,7 @@ class ExcellonTool(ExcellonStatement): return '' % (self.number, self.diameter, unit) -class ToolSelectionStatment(ExcellonStatement): +class ToolSelectionStmt(ExcellonStatement): @classmethod def from_excellon(cls, line): @@ -189,6 +190,7 @@ class ToolSelectionStatment(ExcellonStatement): class CoordinateStmt(ExcellonStatement): + @classmethod def from_excellon(cls, line, format=(2, 5), zero_suppression='trailing'): x = None y = None @@ -208,22 +210,23 @@ class CoordinateStmt(ExcellonStatement): def to_excellon(self): stmt = '' if self.x is not None: - stmt.append('X%s' % write_gerber_value(self.x)) + stmt += 'X%s' % write_gerber_value(self.x) if self.y is not None: - stmt.append('Y%s' % write_gerber_value(self.y)) + stmt += 'Y%s' % write_gerber_value(self.y) return stmt class CommentStmt(ExcellonStatement): - def from_excellon(self, line): + @classmethod + def from_excellon(cls, line): return cls(line.strip().lstrip(';')) def __init__(self, comment): self.comment = comment def to_excellon(self): - return ';%s' % comment + return ';%s' % self.comment class HeaderBeginStmt(ExcellonStatement): @@ -265,7 +268,7 @@ class EndOfProgramStmt(ExcellonStatement): stmt += 'X%s' % write_gerber_value(self.x) if self.y is not None: stmt += 'Y%s' % write_gerber_value(self.y) - + return stmt class UnitStmt(ExcellonStatement): @@ -281,8 +284,9 @@ class UnitStmt(ExcellonStatement): def to_excellon(self): stmt = '%s,%s' % ('INCH' if self.units == 'inch' else 'METRIC', - 'LZ' if self.zero_suppression == 'trailing' else 'TZ') - + 'LZ' if self.zero_suppression == 'trailing' + else 'TZ') + return stmt class IncrementalModeStmt(ExcellonStatement): @@ -292,7 +296,7 @@ class IncrementalModeStmt(ExcellonStatement): def __init__(self, mode='off'): if mode.lower() not in ['on', 'off']: - raise ValueError('Mode may be "on" or "off") + raise ValueError('Mode may be "on" or "off"') self.mode = 'off' def to_excellon(self): @@ -309,7 +313,7 @@ class VersionStmt(ExcellonStatement): def __init__(self, version=1): version = int(version) if version not in [1, 2]: - raise ValueError('Valid versions are 1 or 2' + raise ValueError('Valid versions are 1 or 2') self.version = version def to_excellon(self): @@ -374,3 +378,15 @@ class UnknownStmt(ExcellonStatement): def to_excellon(self): return self.stmt + + + + +def pairwise(iterator): + """ Iterate over list taking two elements at a time. + + e.g. [1, 2, 3, 4, 5, 6] ==> [(1, 2), (3, 4), (5, 6)] + """ + itr = iter(iterator) + while True: + yield tuple([itr.next() for i in range(2)]) \ No newline at end of file diff --git a/gerber/utils.py b/gerber/utils.py index 625a9e1..1721a7d 100644 --- a/gerber/utils.py +++ b/gerber/utils.py @@ -1,190 +1,195 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -""" -gerber.utils -============ -**Gerber and Excellon file handling utilities** - -This module provides utility functions for working with Gerber and Excellon -files. -""" - -# Author: Hamilton Kibbe -# License: - - -def parse_gerber_value(value, format=(2, 5), zero_suppression='trailing'): - """ Convert gerber/excellon formatted string to floating-point number - - .. note:: - Format and zero suppression are configurable. Note that the Excellon - and Gerber formats use opposite terminology with respect to leading - and trailing zeros. The Gerber format specifies which zeros are - suppressed, while the Excellon format specifies which zeros are - included. This function uses the Gerber-file convention, so an - Excellon file in LZ (leading zeros) mode would use - `zero_suppression='trailing'` - - - Parameters - ---------- - value : string - A Gerber/Excellon-formatted string representing a numerical value. - - format : tuple (int,int) - Gerber/Excellon precision format expressed as a tuple containing: - (number of integer-part digits, number of decimal-part digits) - - zero_suppression : string - Zero-suppression mode. May be 'leading' or 'trailing' - - Returns - ------- - value : float - The specified value as a floating-point number. - - """ - # Format precision - integer_digits, decimal_digits = format - MAX_DIGITS = integer_digits + decimal_digits - - # Absolute maximum number of digits supported. This will handle up to - # 6:7 format, which is somewhat supported, even though the gerber spec - # only allows up to 6:6 - if MAX_DIGITS > 13 or integer_digits > 6 or decimal_digits > 7: - raise ValueError('Parser only supports precision up to 6:7 format') - - # Remove extraneous information - value = value.strip() - value = value.strip(' +') - negative = '-' in value - if negative: - value = value.strip(' -') - - # Handle excellon edge case with explicit decimal. "That was easy!" - if '.' in value: - return float(value) - - digits = [digit for digit in '0' * MAX_DIGITS] - offset = 0 if zero_suppression == 'trailing' else (MAX_DIGITS - len(value)) - for i, digit in enumerate(value): - digits[i + offset] = digit - - result = float(''.join(digits[:integer_digits] + ['.'] + digits[integer_digits:])) - return -1.0 * result if negative else result - - -def write_gerber_value(value, format=(2, 5), zero_suppression='trailing'): - """ Convert a floating point number to a Gerber/Excellon-formatted string. - - .. note:: - Format and zero suppression are configurable. Note that the Excellon - and Gerber formats use opposite terminology with respect to leading - and trailing zeros. The Gerber format specifies which zeros are - suppressed, while the Excellon format specifies which zeros are - included. This function uses the Gerber-file convention, so an - Excellon file in LZ (leading zeros) mode would use - `zero_suppression='trailing'` - - Parameters - ---------- - value : float - A floating point value. - - format : tuple (n=2) - Gerber/Excellon precision format expressed as a tuple containing: - (number of integer-part digits, number of decimal-part digits) - - zero_suppression : string - Zero-suppression mode. May be 'leading' or 'trailing' - - Returns - ------- - value : string - The specified value as a Gerber/Excellon-formatted string. - """ - # Format precision - integer_digits, decimal_digits = format - MAX_DIGITS = integer_digits + decimal_digits - - if MAX_DIGITS > 13 or integer_digits > 6 or decimal_digits > 7: - raise ValueError('Parser only supports precision up to 6:7 format') - - # negative sign affects padding, so deal with it at the end... - negative = value < 0.0 - if negative: - value = -1.0 * value - - # Format string for padding out in both directions - fmtstring = '%%0%d.0%df' % (MAX_DIGITS + 1, decimal_digits) - - digits = [val for val in fmtstring % value if val != '.'] - - # Suppression... - if zero_suppression == 'trailing': - while digits[-1] == '0': - digits.pop() - else: - while digits[0] == '0': - digits.pop(0) - - return ''.join(digits) if not negative else ''.join(['-'] + digits) - - -def decimal_string(value, precision=6): - """ Convert float to string with limited precision - - Parameters - ---------- - value : float - A floating point value. - - precision : - Maximum number of decimal places to print - - Returns - ------- - value : string - The specified value as a string. - - """ - floatstr = '%0.20g' % value - integer = None - decimal = None - if '.' in floatstr: - integer, decimal = floatstr.split('.') - elif ',' in floatstr: - integer, decimal = floatstr.split(',') - if len(decimal) > precision: - decimal = decimal[:precision] - if integer or decimal: - return ''.join([integer, '.', decimal]) - else: - return int(floatstr) - - -def detect_file_format(filename): - """ Determine format of a file - - Parameters - ---------- - filename : string - Filename of the file to read. - - Returns - ------- - format : string - File format. either 'excellon' or 'rs274x' - """ - - # Read the first 20 lines - with open(filename, 'r') as f: - lines = [next(f) for x in xrange(20)] - - # Look for - for line in lines: - if 'M48' in line: - return 'excellon' - elif '%FS' in line: - return'rs274x' - return 'unknown' +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +gerber.utils +============ +**Gerber and Excellon file handling utilities** + +This module provides utility functions for working with Gerber and Excellon +files. +""" + +# Author: Hamilton Kibbe +# License: + + +def parse_gerber_value(value, format=(2, 5), zero_suppression='trailing'): + """ Convert gerber/excellon formatted string to floating-point number + + .. note:: + Format and zero suppression are configurable. Note that the Excellon + and Gerber formats use opposite terminology with respect to leading + and trailing zeros. The Gerber format specifies which zeros are + suppressed, while the Excellon format specifies which zeros are + included. This function uses the Gerber-file convention, so an + Excellon file in LZ (leading zeros) mode would use + `zero_suppression='trailing'` + + + Parameters + ---------- + value : string + A Gerber/Excellon-formatted string representing a numerical value. + + format : tuple (int,int) + Gerber/Excellon precision format expressed as a tuple containing: + (number of integer-part digits, number of decimal-part digits) + + zero_suppression : string + Zero-suppression mode. May be 'leading' or 'trailing' + + Returns + ------- + value : float + The specified value as a floating-point number. + + """ + # Format precision + integer_digits, decimal_digits = format + MAX_DIGITS = integer_digits + decimal_digits + + # Absolute maximum number of digits supported. This will handle up to + # 6:7 format, which is somewhat supported, even though the gerber spec + # only allows up to 6:6 + if MAX_DIGITS > 13 or integer_digits > 6 or decimal_digits > 7: + raise ValueError('Parser only supports precision up to 6:7 format') + + # Remove extraneous information + value = value.strip() + value = value.strip(' +') + negative = '-' in value + if negative: + value = value.strip(' -') + + # Handle excellon edge case with explicit decimal. "That was easy!" + if '.' in value: + return float(value) + + digits = [digit for digit in '0' * MAX_DIGITS] + offset = 0 if zero_suppression == 'trailing' else (MAX_DIGITS - len(value)) + for i, digit in enumerate(value): + digits[i + offset] = digit + + result = float(''.join(digits[:integer_digits] + ['.'] + digits[integer_digits:])) + return -1.0 * result if negative else result + + +def write_gerber_value(value, format=(2, 5), zero_suppression='trailing'): + """ Convert a floating point number to a Gerber/Excellon-formatted string. + + .. note:: + Format and zero suppression are configurable. Note that the Excellon + and Gerber formats use opposite terminology with respect to leading + and trailing zeros. The Gerber format specifies which zeros are + suppressed, while the Excellon format specifies which zeros are + included. This function uses the Gerber-file convention, so an + Excellon file in LZ (leading zeros) mode would use + `zero_suppression='trailing'` + + Parameters + ---------- + value : float + A floating point value. + + format : tuple (n=2) + Gerber/Excellon precision format expressed as a tuple containing: + (number of integer-part digits, number of decimal-part digits) + + zero_suppression : string + Zero-suppression mode. May be 'leading' or 'trailing' + + Returns + ------- + value : string + The specified value as a Gerber/Excellon-formatted string. + """ + # Format precision + integer_digits, decimal_digits = format + MAX_DIGITS = integer_digits + decimal_digits + + 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... + if value == 0: + return '00' + + # negative sign affects padding, so deal with it at the end... + negative = value < 0.0 + if negative: + value = -1.0 * value + + # Format string for padding out in both directions + fmtstring = '%%0%d.0%df' % (MAX_DIGITS + 1, decimal_digits) + digits = [val for val in fmtstring % value if val != '.'] + + # Suppression... + if zero_suppression == 'trailing': + while digits[-1] == '0': + digits.pop() + else: + while digits[0] == '0': + digits.pop(0) + + return ''.join(digits) if not negative else ''.join(['-'] + digits) + + +def decimal_string(value, precision=6, padding=False): + """ Convert float to string with limited precision + + Parameters + ---------- + value : float + A floating point value. + + precision : + Maximum number of decimal places to print + + Returns + ------- + value : string + The specified value as a string. + + """ + floatstr = '%0.10g' % value + integer = None + decimal = None + if '.' in floatstr: + integer, decimal = floatstr.split('.') + elif ',' in floatstr: + integer, decimal = floatstr.split(',') + 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): + """ Determine format of a file + + Parameters + ---------- + filename : string + Filename of the file to read. + + Returns + ------- + format : string + File format. either 'excellon' or 'rs274x' + """ + + # Read the first 20 lines + with open(filename, 'r') as f: + lines = [next(f) for x in xrange(20)] + + # Look for + for line in lines: + if 'M48' in line: + return 'excellon' + elif '%FS' in line: + return'rs274x' + return 'unknown' -- cgit From 2abb7159be80beb0565d35e856f3279d2f1f693b Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Mon, 6 Oct 2014 23:52:57 -0400 Subject: add tests --- gerber/excellon.py | 3 +-- gerber/excellon_statements.py | 2 +- gerber/tests/test_gerber_statements.py | 9 +++++++++ 3 files changed, 11 insertions(+), 3 deletions(-) (limited to 'gerber') diff --git a/gerber/excellon.py b/gerber/excellon.py index 072bc31..9a5ef22 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -163,7 +163,7 @@ class ExcellonParser(object): self.active_tool = self.tools[stmt.tool] #self.active_tool = self.tools[int(line.strip().split('T')[1])] self.statements.append(stmt) - + elif line[0] in ['X', 'Y']: stmt = CoordinateStmt.from_excellon(line, fmt, zs) x = stmt.x @@ -194,7 +194,6 @@ class ExcellonParser(object): if self.ctx is not None: self.ctx.drill(self.pos[0], self.pos[1], self.active_tool.diameter) - else: self.statements.append(UnknownStmt.from_excellon(line)) diff --git a/gerber/excellon_statements.py b/gerber/excellon_statements.py index 09b72c1..caf1626 100644 --- a/gerber/excellon_statements.py +++ b/gerber/excellon_statements.py @@ -151,7 +151,7 @@ class ExcellonTool(ExcellonStatement): else: stmt += 'S%g' % self.rpm / 1000. if self.diameter is not None: - stmt += 'C%s' % decimal_string(self.diameter, 5, True) + 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 diff --git a/gerber/tests/test_gerber_statements.py b/gerber/tests/test_gerber_statements.py index 121aa42..7495ba7 100644 --- a/gerber/tests/test_gerber_statements.py +++ b/gerber/tests/test_gerber_statements.py @@ -87,6 +87,15 @@ def test_IPParamStmt_dump(): assert_equal(ip.to_gerber(), '%IPNEG*%') +def test_OFParamStmt_factory(): + """ Test OFParamStmt factory correctly handles parameters + """ + 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_dump(): """ Test OFParamStmt to_gerber() """ -- cgit From 7f75e8b9e9e338f16f215b2552db9ad9a0a50781 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Mon, 6 Oct 2014 23:54:39 -0400 Subject: add tests --- gerber/tests/test_excellon_statements.py | 33 ++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 gerber/tests/test_excellon_statements.py (limited to 'gerber') diff --git a/gerber/tests/test_excellon_statements.py b/gerber/tests/test_excellon_statements.py new file mode 100644 index 0000000..3a10153 --- /dev/null +++ b/gerber/tests/test_excellon_statements.py @@ -0,0 +1,33 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# Author: Hamilton Kibbe + +from .tests import * +from ..excellon_statements import * + + +def test_ExcellonTool_factory(): + """ Test ExcellonTool factory method + """ + exc_line = 'T8F00S00C0.12500' + settings = {'format': (2, 5), 'zero_suppression': 'trailing', + 'units': 'inch', 'notation': 'absolute'} + tool = ExcellonTool.from_excellon(exc_line, settings) + assert_equal(tool.diameter, 0.125) + assert_equal(tool.feed_rate, 0) + assert_equal(tool.rpm, 0) + + +def test_ExcellonTool_dump(): + """ Test ExcellonTool to_gerber method + """ + exc_lines = ['T1F00S00C0.01200', 'T2F00S00C0.01500', 'T3F00S00C0.01968', + 'T4F00S00C0.02800', 'T5F00S00C0.03300', 'T6F00S00C0.03800', + 'T7F00S00C0.04300', 'T8F00S00C0.12500', 'T9F00S00C0.13000', ] + settings = {'format': (2, 5), 'zero_suppression': 'trailing', + 'units': 'inch', 'notation': 'absolute'} + for line in exc_lines: + tool = ExcellonTool.from_excellon(line, settings) + assert_equal(tool.to_excellon(), line) + \ No newline at end of file -- cgit From f7c19398730d95bd4f34834ebcf66d9a68273055 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Tue, 7 Oct 2014 00:16:39 -0400 Subject: add tests --- gerber/excellon_statements.py | 73 ++++++++++++++++++++++---------- gerber/tests/test_excellon_statements.py | 23 +++++++++- gerber/tests/test_gerber_statements.py | 3 +- 3 files changed, 73 insertions(+), 26 deletions(-) (limited to 'gerber') diff --git a/gerber/excellon_statements.py b/gerber/excellon_statements.py index caf1626..dbd807a 100644 --- a/gerber/excellon_statements.py +++ b/gerber/excellon_statements.py @@ -102,26 +102,42 @@ class ExcellonTool(ExcellonStatement): commands = re.split('([BCFHSTZ])', line)[1:] commands = [(command, value) for command, value in pairwise(commands)] args = {} - format = settings['format'] + nformat = settings['format'] zero_suppression = settings['zero_suppression'] for cmd, val in commands: if cmd == 'B': - args['retract_rate'] = parse_gerber_value(val, format, zero_suppression) + args['retract_rate'] = parse_gerber_value(val, nformat, zero_suppression) elif cmd == 'C': - args['diameter'] = parse_gerber_value(val, format, zero_suppression) + args['diameter'] = parse_gerber_value(val, nformat, zero_suppression) elif cmd == 'F': - args['feed_rate'] = parse_gerber_value(val, format, zero_suppression) + args['feed_rate'] = parse_gerber_value(val, nformat, zero_suppression) elif cmd == 'H': - args['max_hit_count'] = parse_gerber_value(val, format, zero_suppression) + args['max_hit_count'] = parse_gerber_value(val, nformat, zero_suppression) elif cmd == 'S': - args['rpm'] = 1000 * parse_gerber_value(val, format, zero_suppression) + args['rpm'] = 1000 * parse_gerber_value(val, nformat, zero_suppression) elif cmd == 'T': args['number'] = int(val) elif cmd == 'Z': - args['depth_offset'] = parse_gerber_value(val, format, zero_suppression) + args['depth_offset'] = parse_gerber_value(val, nformat, zero_suppression) return cls(settings, **args) + @classmethod def from_dict(cls, settings, tool_dict): + """ Create an ExcellonTool from a dict. + + Parameters + ---------- + settings : FileSettings (dict-like) + Excellon File-wide settings + + tool_dict : dict + Excellon tool parameters as a dict + + Returns + ------- + tool : ExcellonTool + An ExcellonTool initialized with the parameters in tool_dict. + """ return cls(settings, tool_dict) def __init__(self, settings, **kwargs): @@ -168,6 +184,18 @@ class ToolSelectionStmt(ExcellonStatement): @classmethod def from_excellon(cls, line): + """ Create a ToolSelectionStmt from an excellon file line. + + Parameters + ---------- + line : string + Line from an Excellon file + + Returns + ------- + tool_statement : ToolSelectionStmt + ToolSelectionStmt representation of `line.` + """ line = line.strip()[1:] compensation_index = None tool = int(line[:2]) @@ -177,7 +205,8 @@ class ToolSelectionStmt(ExcellonStatement): def __init__(self, tool, compensation_index=None): tool = int(tool) - compensation_index = int(compensation_index) if compensation_index else None + compensation_index = (int(compensation_index) if compensation_index + is not None else None) self.tool = tool self.compensation_index = compensation_index @@ -191,16 +220,16 @@ class ToolSelectionStmt(ExcellonStatement): class CoordinateStmt(ExcellonStatement): @classmethod - def from_excellon(cls, line, format=(2, 5), zero_suppression='trailing'): + def from_excellon(cls, line, nformat=(2, 5), zero_suppression='trailing'): x = None y = None if line[0] == 'X': splitline = line.strip('X').split('Y') - x = parse_gerber_value(splitline[0].strip(), format, zero_suppression) + x = parse_gerber_value(splitline[0].strip(), nformat, zero_suppression) if len(splitline) == 2: - y = parse_gerber_value(splitline[1].strip(), format, zero_suppression) + y = parse_gerber_value(splitline[1].strip(), nformat, zero_suppression) else: - y = parse_gerber_value(line.strip(' Y'), format, zero_suppression) + y = parse_gerber_value(line.strip(' Y'), nformat, zero_suppression) return cls(x, y) def __init__(self, x=None, y=None): @@ -270,6 +299,7 @@ class EndOfProgramStmt(ExcellonStatement): stmt += 'Y%s' % write_gerber_value(self.y) return stmt + class UnitStmt(ExcellonStatement): @classmethod @@ -284,10 +314,11 @@ class UnitStmt(ExcellonStatement): def to_excellon(self): stmt = '%s,%s' % ('INCH' if self.units == 'inch' else 'METRIC', - 'LZ' if self.zero_suppression == 'trailing' + 'LZ' if self.zero_suppression == 'trailing' else 'TZ') return stmt + class IncrementalModeStmt(ExcellonStatement): @classmethod @@ -327,14 +358,14 @@ class FormatStmt(ExcellonStatement): fmt = int(line.split(',')[1]) return cls(fmt) - def __init__(self, format=1): - format = int(format) - if format not in [1, 2]: + def __init__(self, nformat=1): + nformat = int(nformat) + if nformat not in [1, 2]: raise ValueError('Valid formats are 1 or 2') - self.format = format + self.nformat = nformat def to_excellon(self): - return 'FMAT,%d' % self.format + return 'FMAT,%d' % self.n class LinkToolStmt(ExcellonStatement): @@ -379,9 +410,7 @@ class UnknownStmt(ExcellonStatement): def to_excellon(self): return self.stmt - - - + def pairwise(iterator): """ Iterate over list taking two elements at a time. @@ -389,4 +418,4 @@ def pairwise(iterator): """ itr = iter(iterator) while True: - yield tuple([itr.next() for i in range(2)]) \ No newline at end of file + yield tuple([itr.next() for i in range(2)]) diff --git a/gerber/tests/test_excellon_statements.py b/gerber/tests/test_excellon_statements.py index 3a10153..49207d3 100644 --- a/gerber/tests/test_excellon_statements.py +++ b/gerber/tests/test_excellon_statements.py @@ -20,7 +20,7 @@ def test_ExcellonTool_factory(): def test_ExcellonTool_dump(): - """ Test ExcellonTool to_gerber method + """ Test ExcellonTool to_excellon() """ exc_lines = ['T1F00S00C0.01200', 'T2F00S00C0.01500', 'T3F00S00C0.01968', 'T4F00S00C0.02800', 'T5F00S00C0.03300', 'T6F00S00C0.03800', @@ -30,4 +30,23 @@ def test_ExcellonTool_dump(): for line in exc_lines: tool = ExcellonTool.from_excellon(line, settings) assert_equal(tool.to_excellon(), line) - \ No newline at end of file + + +def test_ToolSelectionStmt_factory(): + """ Test ToolSelectionStmt factory method + """ + stmt = ToolSelectionStmt.from_excellon('T01') + assert_equal(stmt.tool, 1) + assert_equal(stmt.compensation_index, None) + stmt = ToolSelectionStmt.from_excellon('T0223') + assert_equal(stmt.tool, 2) + assert_equal(stmt.compensation_index, 23) + + +def test_ToolSelectionStmt_dump(): + """ Test ToolSelectionStmt to_excellon() + """ + lines = ['T01', 'T0223', 'T10', 'T09', 'T0000'] + for line in lines: + stmt = ToolSelectionStmt.from_excellon(line) + assert_equal(stmt.to_excellon(), line) diff --git a/gerber/tests/test_gerber_statements.py b/gerber/tests/test_gerber_statements.py index 7495ba7..7c01130 100644 --- a/gerber/tests/test_gerber_statements.py +++ b/gerber/tests/test_gerber_statements.py @@ -90,7 +90,7 @@ def test_IPParamStmt_dump(): def test_OFParamStmt_factory(): """ Test OFParamStmt factory correctly handles parameters """ - stmt = {'param': 'OF', 'a': '0.1234567', 'b':'0.1234567'} + 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) @@ -102,4 +102,3 @@ def test_OFParamStmt_dump(): stmt = {'param': 'OF', 'a': '0.1234567', 'b': '0.1234567'} of = OFParamStmt.from_dict(stmt) assert_equal(of.to_gerber(), '%OFA0.123456B0.123456*%') - -- cgit From 8ac771db92633fab9aa0ff9ecc7333e6a412e577 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Tue, 7 Oct 2014 09:12:46 -0400 Subject: More tests --- gerber/excellon_statements.py | 15 +++++--- gerber/tests/test_excellon_statements.py | 66 ++++++++++++++++++++++++++++++-- 2 files changed, 71 insertions(+), 10 deletions(-) (limited to 'gerber') diff --git a/gerber/excellon_statements.py b/gerber/excellon_statements.py index dbd807a..13f763e 100644 --- a/gerber/excellon_statements.py +++ b/gerber/excellon_statements.py @@ -221,16 +221,19 @@ class CoordinateStmt(ExcellonStatement): @classmethod def from_excellon(cls, line, nformat=(2, 5), zero_suppression='trailing'): - x = None - y = None + x_coord = None + y_coord = None if line[0] == 'X': splitline = line.strip('X').split('Y') - x = parse_gerber_value(splitline[0].strip(), nformat, zero_suppression) + x_coord = parse_gerber_value(splitline[0].strip(), nformat, + zero_suppression) if len(splitline) == 2: - y = parse_gerber_value(splitline[1].strip(), nformat, zero_suppression) + y_coord = parse_gerber_value(splitline[1].strip(), nformat, + zero_suppression) else: - y = parse_gerber_value(line.strip(' Y'), nformat, zero_suppression) - return cls(x, y) + y_coord = parse_gerber_value(line.strip(' Y'), nformat, + zero_suppression) + return cls(x_coord, y_coord) def __init__(self, x=None, y=None): self.x = x diff --git a/gerber/tests/test_excellon_statements.py b/gerber/tests/test_excellon_statements.py index 49207d3..c728443 100644 --- a/gerber/tests/test_excellon_statements.py +++ b/gerber/tests/test_excellon_statements.py @@ -7,7 +7,7 @@ from .tests import * from ..excellon_statements import * -def test_ExcellonTool_factory(): +def test_excellontool_factory(): """ Test ExcellonTool factory method """ exc_line = 'T8F00S00C0.12500' @@ -19,7 +19,7 @@ def test_ExcellonTool_factory(): assert_equal(tool.rpm, 0) -def test_ExcellonTool_dump(): +def test_excellontool_dump(): """ Test ExcellonTool to_excellon() """ exc_lines = ['T1F00S00C0.01200', 'T2F00S00C0.01500', 'T3F00S00C0.01968', @@ -32,7 +32,19 @@ def test_ExcellonTool_dump(): assert_equal(tool.to_excellon(), line) -def test_ToolSelectionStmt_factory(): +def test_excellontool_order(): + settings = {'format': (2, 5), 'zero_suppression': 'trailing', + 'units': 'inch', 'notation': 'absolute'} + line = 'T8F00S00C0.12500' + tool1 = ExcellonTool.from_excellon(line, settings) + line = 'T8C0.12500F00S00' + tool2 = ExcellonTool.from_excellon(line, settings) + assert_equal(tool1.diameter, tool2.diameter) + assert_equal(tool1.feed_rate, tool2.feed_rate) + assert_equal(tool1.rpm, tool2.rpm) + + +def test_toolselection_factory(): """ Test ToolSelectionStmt factory method """ stmt = ToolSelectionStmt.from_excellon('T01') @@ -43,10 +55,56 @@ def test_ToolSelectionStmt_factory(): assert_equal(stmt.compensation_index, 23) -def test_ToolSelectionStmt_dump(): +def test_toolselection_dump(): """ Test ToolSelectionStmt to_excellon() """ lines = ['T01', 'T0223', 'T10', 'T09', 'T0000'] for line in lines: stmt = ToolSelectionStmt.from_excellon(line) assert_equal(stmt.to_excellon(), line) + + +def test_coordinatestmt_factory(): + line = 'X0278207Y0065293' + stmt = CoordinateStmt.from_excellon(line) + 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 = 'Y00575' + stmt = CoordinateStmt.from_excellon(line) + assert_equal(stmt.y, 0.575) + + +def test_coordinatestmt_dump(): + lines = ['X0278207Y0065293', 'X0243795', 'Y0082528', 'Y0086028', + 'X0251295Y0081528', 'X02525Y0078', 'X0255Y00575', 'Y0052', + 'X02675', 'Y00575', 'X02425', 'Y0052', 'X023', ] + for line in lines: + stmt = CoordinateStmt.from_excellon(line) + assert_equal(stmt.to_excellon(), line) + + +def test_commentstmt_factory(): + line = ';Layer_Color=9474304' + stmt = CommentStmt.from_excellon(line) + assert_equal(stmt.comment, line[1:]) + + line = ';FILE_FORMAT=2:5' + stmt = CommentStmt.from_excellon(line) + assert_equal(stmt.comment, line[1:]) + + line = ';TYPE=PLATED' + stmt = CommentStmt.from_excellon(line) + assert_equal(stmt.comment, line[1:]) + + +def test_commentstmt_dump(): + lines = [';Layer_Color=9474304', ';FILE_FORMAT=2:5', ';TYPE=PLATED', ] + for line in lines: + stmt = CommentStmt.from_excellon(line) + assert_equal(stmt.to_excellon(), line) + -- cgit From b971dacd3f058772326a479a562ceed0a9594deb Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Tue, 7 Oct 2014 18:36:03 -0400 Subject: more tests --- gerber/excellon_statements.py | 16 +++-- gerber/tests/test_excellon_statements.py | 119 +++++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+), 7 deletions(-) (limited to 'gerber') diff --git a/gerber/excellon_statements.py b/gerber/excellon_statements.py index 13f763e..5eba12c 100644 --- a/gerber/excellon_statements.py +++ b/gerber/excellon_statements.py @@ -331,10 +331,10 @@ class IncrementalModeStmt(ExcellonStatement): def __init__(self, mode='off'): if mode.lower() not in ['on', 'off']: raise ValueError('Mode may be "on" or "off"') - self.mode = 'off' + self.mode = mode def to_excellon(self): - return 'ICI,%s' % 'OFF' if self.mode == 'off' else 'ON' + return 'ICI,%s' % ('OFF' if self.mode == 'off' else 'ON') class VersionStmt(ExcellonStatement): @@ -361,14 +361,14 @@ class FormatStmt(ExcellonStatement): fmt = int(line.split(',')[1]) return cls(fmt) - def __init__(self, nformat=1): - nformat = int(nformat) - if nformat not in [1, 2]: + def __init__(self, format=1): + format = int(format) + if format not in [1, 2]: raise ValueError('Valid formats are 1 or 2') - self.nformat = nformat + self.format = format def to_excellon(self): - return 'FMAT,%d' % self.n + return 'FMAT,%d' % self.format class LinkToolStmt(ExcellonStatement): @@ -389,6 +389,8 @@ class MeasuringModeStmt(ExcellonStatement): @classmethod def from_excellon(cls, line): + 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') def __init__(self, units='inch'): diff --git a/gerber/tests/test_excellon_statements.py b/gerber/tests/test_excellon_statements.py index c728443..4fa2b35 100644 --- a/gerber/tests/test_excellon_statements.py +++ b/gerber/tests/test_excellon_statements.py @@ -108,3 +108,122 @@ def test_commentstmt_dump(): stmt = CommentStmt.from_excellon(line) assert_equal(stmt.to_excellon(), line) + +def test_unitstmt_factory(): + line = 'INCH,LZ' + stmt = UnitStmt.from_excellon(line) + assert_equal(stmt.units, 'inch') + assert_equal(stmt.zero_suppression, 'trailing') + + line = 'METRIC,TZ' + stmt = UnitStmt.from_excellon(line) + assert_equal(stmt.units, 'metric') + assert_equal(stmt.zero_suppression, 'leading') + + +def test_unitstmt_dump(): + lines = ['INCH,LZ', 'INCH,TZ', 'METRIC,LZ', 'METRIC,TZ', ] + for line in lines: + stmt = UnitStmt.from_excellon(line) + assert_equal(stmt.to_excellon(), line) + + +def test_incrementalmode_factory(): + line = 'ICI,ON' + stmt = IncrementalModeStmt.from_excellon(line) + assert_equal(stmt.mode, 'on') + + line = 'ICI,OFF' + stmt = IncrementalModeStmt.from_excellon(line) + assert_equal(stmt.mode, 'off') + + +def test_incrementalmode_dump(): + lines = ['ICI,ON', 'ICI,OFF', ] + for line in lines: + stmt = IncrementalModeStmt.from_excellon(line) + assert_equal(stmt.to_excellon(), line) + + +def test_incrementalmode_validation(): + assert_raises(ValueError, IncrementalModeStmt, 'OFF-ISH') + + +def test_versionstmt_factory(): + line = 'VER,1' + stmt = VersionStmt.from_excellon(line) + assert_equal(stmt.version, 1) + + line = 'VER,2' + stmt = VersionStmt.from_excellon(line) + assert_equal(stmt.version, 2) + + +def test_versionstmt_dump(): + lines = ['VER,1', 'VER,2', ] + for line in lines: + stmt = VersionStmt.from_excellon(line) + assert_equal(stmt.to_excellon(), line) + +def test_versionstmt_validation(): + assert_raises(ValueError, VersionStmt, 3) + + +def test_formatstmt_factory(): + line = 'FMAT,1' + stmt = FormatStmt.from_excellon(line) + assert_equal(stmt.format, 1) + + line = 'FMAT,2' + stmt = FormatStmt.from_excellon(line) + assert_equal(stmt.format, 2) + + +def test_formatstmt_dump(): + lines = ['FMAT,1', 'FMAT,2', ] + for line in lines: + stmt = FormatStmt.from_excellon(line) + assert_equal(stmt.to_excellon(), line) + + +def test_formatstmt_validation(): + assert_raises(ValueError, FormatStmt, 3) + + +def test_linktoolstmt_factory(): + line = '1/2/3/4' + stmt = LinkToolStmt.from_excellon(line) + assert_equal(stmt.linked_tools, [1, 2, 3, 4]) + + line = '01/02/03/04' + stmt = LinkToolStmt.from_excellon(line) + assert_equal(stmt.linked_tools, [1, 2, 3, 4]) + + +def test_linktoolstmt_dump(): + lines = ['1/2/3/4', '5/6/7', ] + for line in lines: + stmt = LinkToolStmt.from_excellon(line) + assert_equal(stmt.to_excellon(), line) + + +def test_measuringmodestmt_factory(): + line = 'M72' + stmt = MeasuringModeStmt.from_excellon(line) + assert_equal(stmt.units, 'inch') + + line = 'M71' + stmt = MeasuringModeStmt.from_excellon(line) + assert_equal(stmt.units, 'metric') + + +def test_measuringmodestmt_dump(): + lines = ['M71', 'M72', ] + for line in lines: + stmt = MeasuringModeStmt.from_excellon(line) + assert_equal(stmt.to_excellon(), line) + + +def test_measuringmodestmt_validation(): + assert_raises(ValueError, MeasuringModeStmt.from_excellon, 'M70') + assert_raises(ValueError, MeasuringModeStmt, 'millimeters') -- cgit From 5ff44efbcfca5316796a1ea0191b2a92894a59ee Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Tue, 7 Oct 2014 18:41:14 -0400 Subject: Fix resolve error --- gerber/render/render.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'gerber') diff --git a/gerber/render/render.py b/gerber/render/render.py index eab7d33..e40960d 100644 --- a/gerber/render/render.py +++ b/gerber/render/render.py @@ -63,7 +63,9 @@ class GerberContext(object): self.aperture = d def resolve(self, x, y): - return x or self.x, y or self.y + x = x if x is not None else self.x + y = y if y is not None else self.y + return x, y def define_aperture(self, d, shape, modifiers): pass -- cgit From af97dcf2a8200d9319e20d2789dbb0baa0611ba5 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Tue, 7 Oct 2014 22:44:08 -0400 Subject: fix excellon render --- gerber/__main__.py | 13 +++-- gerber/excellon.py | 62 ++++++++++++------------ gerber/gerber.py | 2 +- gerber/gerber_statements.py | 34 +++++++++++--- gerber/tests/test_excellon_statements.py | 48 +++++++++++++++++-- gerber/tests/test_utils.py | 81 +++++++++++++++++++------------- gerber/utils.py | 2 +- 7 files changed, 157 insertions(+), 85 deletions(-) (limited to 'gerber') diff --git a/gerber/__main__.py b/gerber/__main__.py index 31b70f8..26f36e1 100644 --- a/gerber/__main__.py +++ b/gerber/__main__.py @@ -16,21 +16,20 @@ # the License. if __name__ == '__main__': - from .gerber import GerberFile - from .excellon import ExcellonParser + import gerber + import excellon from .render import GerberSvgContext #import sys # - #if len(sys.argv) < 2: + #if len(sys.argv) < 2:` # print >> sys.stderr, "Usage: python -m gerber ..." # sys.exit(1) # ##for filename in sys.argv[1]: ## print "parsing %s" % filename ctx = GerberSvgContext() - g = GerberFile.read('SCB.GTL') + g = gerber.read('examples/test.gtl') g.render('test.svg', ctx) - p = ExcellonParser(ctx) - p.parse('ncdrill.txt') - p.dump('testwithdrill.svg') + p = excellon.read('ncdrill.txt') + p.render('testwithdrill.svg', ctx) diff --git a/gerber/excellon.py b/gerber/excellon.py index 9a5ef22..66b9ea2 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -21,9 +21,9 @@ Excellon module This module provides Excellon file classes and parsing utilities """ -import re + + from .excellon_statements import * -from .utils import parse_gerber_value from .cnc import CncFile, FileSettings @@ -70,15 +70,18 @@ class ExcellonFile(CncFile): def render(self, filename, ctx): """ Generate image of file """ + count = 0 for tool, pos in self.hits: ctx.drill(pos[0], pos[1], tool.diameter) + count += 1 + print('Drilled %d hits' % count) ctx.dump(filename) def write(self, filename): with open(filename, 'w') as f: for statement in self.statements: f.write(statement.to_excellon() + '\n') - + class ExcellonParser(object): """ Excellon File Parser @@ -95,27 +98,21 @@ class ExcellonParser(object): self.hits = [] self.active_tool = None self.pos = [0., 0.] - if ctx is not None: - self.ctx.set_coord_format(zero_suppression='trailing', - format=(2, 5), notation='absolute') def parse(self, filename): with open(filename, 'r') as f: for line in f: self._parse(line) - return ExcellonFile(self.statements, self.tools, self.hits, self._settings(), filename) - - def dump(self, filename): - if self.ctx is not None: - self.ctx.dump(filename) + 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'] - if line[0] == ';': self.statements.append(CommentStmt.from_excellon(line)) - + elif line[:3] == 'M48': self.statements.append(HeaderBeginStmt()) self.state = 'HEADER' @@ -130,29 +127,41 @@ class ExcellonParser(object): if self.state == 'HEADER': self.state = 'DRILL' + elif line[:3] == 'M30': + stmt = EndOfProgramStmt.from_excellon(line) + self.statements.append(stmt) + elif line[:3] == 'G00': self.state = 'ROUT' elif line[:3] == 'G05': 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) and + ('LZ' in line or 'TZ' in line)): stmt = UnitStmt.from_excellon(line) self.units = stmt.units self.zero_suppression = stmt.zero_suppression self.statements.append(stmt) - + elif line[:3] == 'M71' or line [:3] == 'M72': stmt = MeasuringModeStmt.from_excellon(line) self.units = stmt.units self.statements.append(stmt) - + elif line[:3] == 'ICI': stmt = IncrementalModeStmt.from_excellon(line) self.notation = 'incremental' if stmt.mode == 'on' else 'absolute' self.statements.append(stmt) - # tool definition + elif line[:3] == 'VER': + stmt = VersionStmt.from_excellon(line) + self.statements.append(stmt) + + elif line[:4] == 'FMAT': + stmt = FormatStmt.from_excellon(line) + self.statements.append(stmt) + elif line[0] == 'T' and self.state == 'HEADER': tool = ExcellonTool.from_excellon(line, self._settings()) self.tools[tool.number] = tool @@ -161,7 +170,6 @@ class ExcellonParser(object): elif line[0] == 'T' and self.state != 'HEADER': stmt = ToolSelectionStmt.from_excellon(line) self.active_tool = self.tools[stmt.tool] - #self.active_tool = self.tools[int(line.strip().split('T')[1])] self.statements.append(stmt) elif line[0] in ['X', 'Y']: @@ -169,15 +177,6 @@ class ExcellonParser(object): x = stmt.x y = stmt.y self.statements.append(stmt) - #x = None - #y = None - #if line[0] == 'X': - # splitline = line.strip('X').split('Y') - # x = parse_gerber_value(splitline[0].strip(), fmt, zs) - # if len(splitline) == 2: - # y = parse_gerber_value(splitline[1].strip(), fmt, zs) - #else: - # y = parse_gerber_value(line.strip(' Y'), fmt, zs) if self.notation == 'absolute': if x is not None: self.pos[0] = x @@ -189,11 +188,8 @@ class ExcellonParser(object): if y is not None: self.pos[1] += y if self.state == 'DRILL': - self.hits.append((self.active_tool, self.pos)) + self.hits.append((self.active_tool, tuple(self.pos))) self.active_tool._hit() - if self.ctx is not None: - self.ctx.drill(self.pos[0], self.pos[1], - self.active_tool.diameter) else: self.statements.append(UnknownStmt.from_excellon(line)) @@ -201,7 +197,7 @@ class ExcellonParser(object): return FileSettings(units=self.units, format=self.format, zero_suppression=self.zero_suppression, notation=self.notation) - + if __name__ == '__main__': p = ExcellonParser() diff --git a/gerber/gerber.py b/gerber/gerber.py index 0278b0d..220405c 100644 --- a/gerber/gerber.py +++ b/gerber/gerber.py @@ -256,7 +256,7 @@ class GerberParser(object): elif param["param"] == "IN": yield INParamStmt.from_dict(param) elif param["param"] == "LN": - yield LNParamStmtfrom_dict(param) + yield LNParamStmt.from_dict(param) else: yield UnknownStmt(line) did_something = True diff --git a/gerber/gerber_statements.py b/gerber/gerber_statements.py index 5a9d046..2f58a37 100644 --- a/gerber/gerber_statements.py +++ b/gerber/gerber_statements.py @@ -16,8 +16,8 @@ __all__ = ['FSParamStmt', 'MOParamStmt', 'IPParamStmt', 'OFParamStmt', class Statement(object): - def __init__(self, type): - self.type = type + def __init__(self, stype): + self.type = stype def __str__(self): s = "<{0} ".format(self.__class__.__name__) @@ -47,8 +47,8 @@ class FSParamStmt(ParamStmt): zeros = 'leading' if stmt_dict.get('zero') == 'L' else 'trailing' notation = 'absolute' if stmt_dict.get('notation') == 'A' else 'incremental' x = map(int, stmt_dict.get('x').strip()) - format = (x[0], x[1]) - return cls(param, zeros, notation, format) + fmt = (x[0], x[1]) + return cls(param, zeros, notation, fmt) def __init__(self, param, zero_suppression='leading', notation='absolute', format=(2, 4)): @@ -88,9 +88,9 @@ class FSParamStmt(ParamStmt): def to_gerber(self): zero_suppression = 'L' if self.zero_suppression == 'leading' else 'T' notation = 'A' if self.notation == 'absolute' else 'I' - format = ''.join(map(str, self.format)) + fmt = ''.join(map(str, self.format)) return '%FS{0}{1}X{2}Y{3}*%'.format(zero_suppression, notation, - format, format) + fmt, fmt) def __str__(self): return ('' % @@ -588,6 +588,28 @@ class EofStmt(Statement): def __str__(self): return '' +class QuadrantModeStmt(Statement): + + @classmethod + def from_gerber(cls, line): + line = line.strip() + if 'G74' not in line and 'G75' not in line: + raise ValueError('%s is not a valid quadrant mode statement' + % line) + return (cls('single-quadrant') if line[:3] == 'G74' + else cls('multi-quadrant')) + + def __init__(self, mode): + super(QuadrantModeStmt, self).__init__('Quadrant Mode') + mode = mode.lower + if mode not in ['single-quadrant', 'multi-quadrant']: + raise ValueError('Quadrant mode must be "single-quadrant" \ + or "multi-quadrant"') + self.mode = mode + + def to_gerber(self): + return 'G74*' if self.mode == 'single-quadrant' else 'G75*' + class UnknownStmt(Statement): """ Unknown Statement diff --git a/gerber/tests/test_excellon_statements.py b/gerber/tests/test_excellon_statements.py index 4fa2b35..5e5e8dc 100644 --- a/gerber/tests/test_excellon_statements.py +++ b/gerber/tests/test_excellon_statements.py @@ -3,7 +3,7 @@ # Author: Hamilton Kibbe -from .tests import * +from .tests import assert_equal, assert_raises from ..excellon_statements import * @@ -65,6 +65,8 @@ def test_toolselection_dump(): def test_coordinatestmt_factory(): + """ Test CoordinateStmt factory method + """ line = 'X0278207Y0065293' stmt = CoordinateStmt.from_excellon(line) assert_equal(stmt.x, 2.78207) @@ -80,6 +82,8 @@ def test_coordinatestmt_factory(): def test_coordinatestmt_dump(): + """ Test CoordinateStmt to_excellon() + """ lines = ['X0278207Y0065293', 'X0243795', 'Y0082528', 'Y0086028', 'X0251295Y0081528', 'X02525Y0078', 'X0255Y00575', 'Y0052', 'X02675', 'Y00575', 'X02425', 'Y0052', 'X023', ] @@ -89,6 +93,8 @@ def test_coordinatestmt_dump(): def test_commentstmt_factory(): + """ Test CommentStmt factory method + """ line = ';Layer_Color=9474304' stmt = CommentStmt.from_excellon(line) assert_equal(stmt.comment, line[1:]) @@ -103,6 +109,8 @@ def test_commentstmt_factory(): def test_commentstmt_dump(): + """ Test CommentStmt to_excellon() + """ lines = [';Layer_Color=9474304', ';FILE_FORMAT=2:5', ';TYPE=PLATED', ] for line in lines: stmt = CommentStmt.from_excellon(line) @@ -110,6 +118,8 @@ def test_commentstmt_dump(): def test_unitstmt_factory(): + """ Test UnitStmt factory method + """ line = 'INCH,LZ' stmt = UnitStmt.from_excellon(line) assert_equal(stmt.units, 'inch') @@ -122,6 +132,8 @@ def test_unitstmt_factory(): def test_unitstmt_dump(): + """ Test UnitStmt to_excellon() + """ lines = ['INCH,LZ', 'INCH,TZ', 'METRIC,LZ', 'METRIC,TZ', ] for line in lines: stmt = UnitStmt.from_excellon(line) @@ -129,6 +141,8 @@ def test_unitstmt_dump(): def test_incrementalmode_factory(): + """ Test IncrementalModeStmt factory method + """ line = 'ICI,ON' stmt = IncrementalModeStmt.from_excellon(line) assert_equal(stmt.mode, 'on') @@ -139,6 +153,8 @@ def test_incrementalmode_factory(): def test_incrementalmode_dump(): + """ Test IncrementalModeStmt to_excellon() + """ lines = ['ICI,ON', 'ICI,OFF', ] for line in lines: stmt = IncrementalModeStmt.from_excellon(line) @@ -146,10 +162,14 @@ def test_incrementalmode_dump(): def test_incrementalmode_validation(): + """ Test IncrementalModeStmt input validation + """ assert_raises(ValueError, IncrementalModeStmt, 'OFF-ISH') def test_versionstmt_factory(): + """ Test VersionStmt factory method + """ line = 'VER,1' stmt = VersionStmt.from_excellon(line) assert_equal(stmt.version, 1) @@ -160,16 +180,22 @@ def test_versionstmt_factory(): def test_versionstmt_dump(): + """ Test VersionStmt to_excellon() + """ lines = ['VER,1', 'VER,2', ] for line in lines: stmt = VersionStmt.from_excellon(line) assert_equal(stmt.to_excellon(), line) def test_versionstmt_validation(): + """ Test VersionStmt input validation + """ assert_raises(ValueError, VersionStmt, 3) def test_formatstmt_factory(): + """ Test FormatStmt factory method + """ line = 'FMAT,1' stmt = FormatStmt.from_excellon(line) assert_equal(stmt.format, 1) @@ -180,6 +206,8 @@ def test_formatstmt_factory(): def test_formatstmt_dump(): + """ Test FormatStmt to_excellon() + """ lines = ['FMAT,1', 'FMAT,2', ] for line in lines: stmt = FormatStmt.from_excellon(line) @@ -187,10 +215,14 @@ def test_formatstmt_dump(): def test_formatstmt_validation(): + """ Test FormatStmt input validation + """ assert_raises(ValueError, FormatStmt, 3) def test_linktoolstmt_factory(): + """ Test LinkToolStmt factory method + """ line = '1/2/3/4' stmt = LinkToolStmt.from_excellon(line) assert_equal(stmt.linked_tools, [1, 2, 3, 4]) @@ -201,13 +233,17 @@ def test_linktoolstmt_factory(): def test_linktoolstmt_dump(): + """ Test LinkToolStmt to_excellon() + """ lines = ['1/2/3/4', '5/6/7', ] for line in lines: stmt = LinkToolStmt.from_excellon(line) assert_equal(stmt.to_excellon(), line) -def test_measuringmodestmt_factory(): +def test_measmodestmt_factory(): + """ Test MeasuringModeStmt factory method + """ line = 'M72' stmt = MeasuringModeStmt.from_excellon(line) assert_equal(stmt.units, 'inch') @@ -217,13 +253,17 @@ def test_measuringmodestmt_factory(): assert_equal(stmt.units, 'metric') -def test_measuringmodestmt_dump(): +def test_measmodestmt_dump(): + """ Test MeasuringModeStmt to_excellon() + """ lines = ['M71', 'M72', ] for line in lines: stmt = MeasuringModeStmt.from_excellon(line) assert_equal(stmt.to_excellon(), line) -def test_measuringmodestmt_validation(): +def test_measmodestmt_validation(): + """ Test MeasuringModeStmt input validation + """ assert_raises(ValueError, MeasuringModeStmt.from_excellon, 'M70') assert_raises(ValueError, MeasuringModeStmt, 'millimeters') diff --git a/gerber/tests/test_utils.py b/gerber/tests/test_utils.py index 50e2403..001a32f 100644 --- a/gerber/tests/test_utils.py +++ b/gerber/tests/test_utils.py @@ -3,6 +3,7 @@ # Author: Hamilton Kibbe +from .tests import assert_equal from ..utils import decimal_string, parse_gerber_value, write_gerber_value @@ -10,59 +11,73 @@ def test_zero_suppression(): """ Test gerber value parser and writer handle zero suppression correctly. """ # Default format - format = (2, 5) - + fmt = (2, 5) + # Test leading zero suppression zero_suppression = 'leading' test_cases = [('1', 0.00001), ('10', 0.0001), ('100', 0.001), - ('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),] + ('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), ] for string, value in test_cases: - assert(value == parse_gerber_value(string,format,zero_suppression)) - assert(string == write_gerber_value(value,format,zero_suppression)) - + assert(value == parse_gerber_value(string, fmt, zero_suppression)) + assert(string == write_gerber_value(value, fmt, zero_suppression)) + # Test trailing zero suppression zero_suppression = 'trailing' test_cases = [('1', 10.0), ('01', 1.0), ('001', 0.1), ('0001', 0.01), - ('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)] + ('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)] for string, value in test_cases: - assert(value == parse_gerber_value(string,format,zero_suppression)) - assert(string == write_gerber_value(value,format,zero_suppression)) - + assert(value == parse_gerber_value(string, fmt, zero_suppression)) + assert(string == write_gerber_value(value, fmt, zero_suppression)) def test_format(): """ Test gerber value parser and writer handle format correctly """ zero_suppression = 'leading' - test_cases = [((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,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),] - for format, string, value in test_cases: - assert(value == parse_gerber_value(string,format,zero_suppression)) - assert(string == write_gerber_value(value,format,zero_suppression)) - + test_cases = [((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, 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), ] + 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)) + zero_suppression = 'trailing' - test_cases = [((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), ((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),] - for format, string, value in test_cases: - assert(value == parse_gerber_value(string,format,zero_suppression)) - assert(string == write_gerber_value(value,format,zero_suppression)) + test_cases = [((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), + ((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), ] + 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)) def test_decimal_truncation(): - """ Test decimal string truncates value to the correct precision + """ Test decimal_string truncates value to the correct precision """ 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) \ No newline at end of file + assert(result == calculated) + + +def test_decimal_padding(): + """ Test decimal_string padding + """ + value = 1.123 + assert_equal(decimal_string(value, precision=3, padding=True), '1.123') + 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') diff --git a/gerber/utils.py b/gerber/utils.py index 1721a7d..fce6369 100644 --- a/gerber/utils.py +++ b/gerber/utils.py @@ -113,7 +113,7 @@ def write_gerber_value(value, format=(2, 5), zero_suppression='trailing'): # Edge case... if value == 0: return '00' - + # negative sign affects padding, so deal with it at the end... negative = value < 0.0 if negative: -- cgit From 1653ae5cbe88757e453bccf499dc1b8ccb278e58 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Wed, 8 Oct 2014 09:27:52 -0400 Subject: Update readme and example --- gerber/__init__.py | 24 ---------------------- gerber/__main__.py | 28 +++++++++++++------------- gerber/common.py | 42 +++++++++++++++++++++++++++++++++++++++ gerber/excellon.py | 16 ++++----------- gerber/gerber.py | 5 +++-- gerber/render/svgwrite_backend.py | 6 ++++-- 6 files changed, 67 insertions(+), 54 deletions(-) create mode 100644 gerber/common.py (limited to 'gerber') diff --git a/gerber/__init__.py b/gerber/__init__.py index 3197335..4637713 100644 --- a/gerber/__init__.py +++ b/gerber/__init__.py @@ -21,27 +21,3 @@ gerber module """ -def read(filename): - """ Read a gerber or excellon file and return a representative object. - - Parameters - ---------- - filename : string - Filename of the file to read. - - Returns - ------- - file : CncFile subclass - CncFile object representing the file, either GerberFile or - ExcellonFile. Returns None if file is not an Excellon or Gerber file. - """ - import gerber - import excellon - from utils import detect_file_format - fmt = detect_file_format(filename) - if fmt == 'rs274x': - return gerber.read(filename) - elif fmt == 'excellon': - return excellon.read(filename) - else: - return None diff --git a/gerber/__main__.py b/gerber/__main__.py index 26f36e1..ab0f377 100644 --- a/gerber/__main__.py +++ b/gerber/__main__.py @@ -16,20 +16,20 @@ # the License. if __name__ == '__main__': - import gerber - import excellon + from .common import read from .render import GerberSvgContext + import sys + + if len(sys.argv) < 2: + print >> sys.stderr, "Usage: python -m gerber ..." + sys.exit(1) - #import sys - # - #if len(sys.argv) < 2:` - # print >> sys.stderr, "Usage: python -m gerber ..." - # sys.exit(1) - # - ##for filename in sys.argv[1]: - ## print "parsing %s" % filename ctx = GerberSvgContext() - g = gerber.read('examples/test.gtl') - g.render('test.svg', ctx) - p = excellon.read('ncdrill.txt') - p.render('testwithdrill.svg', ctx) + + for filename in sys.argv[1:]: + print "parsing %s" % filename + gerberfile = read(filename) + gerberfile.render(ctx) + print('Saving image to test.svg') + ctx.dump('test.svg') + diff --git a/gerber/common.py b/gerber/common.py new file mode 100644 index 0000000..0092ec8 --- /dev/null +++ b/gerber/common.py @@ -0,0 +1,42 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright 2014 Hamilton Kibbe + +# 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. + + +def read(filename): + """ Read a gerber or excellon file and return a representative object. + + Parameters + ---------- + filename : string + Filename of the file to read. + + Returns + ------- + file : CncFile subclass + CncFile object representing the file, either GerberFile or + ExcellonFile. Returns None if file is not an Excellon or Gerber file. + """ + import gerber + import excellon + from utils import detect_file_format + fmt = detect_file_format(filename) + if fmt == 'rs274x': + return gerber.read(filename) + elif fmt == 'excellon': + return excellon.read(filename) + else: + return None diff --git a/gerber/excellon.py b/gerber/excellon.py index 66b9ea2..663f791 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -67,15 +67,13 @@ class ExcellonFile(CncFile): """ pass - def render(self, filename, ctx): + def render(self, ctx, filename=None): """ Generate image of file """ - count = 0 for tool, pos in self.hits: ctx.drill(pos[0], pos[1], tool.diameter) - count += 1 - print('Drilled %d hits' % count) - ctx.dump(filename) + if filename is not None: + ctx.dump(filename) def write(self, filename): with open(filename, 'w') as f: @@ -86,8 +84,7 @@ class ExcellonFile(CncFile): class ExcellonParser(object): """ Excellon File Parser """ - def __init__(self, ctx=None): - self.ctx = ctx + def __init__(self): self.notation = 'absolute' self.units = 'inch' self.zero_suppression = 'trailing' @@ -197,8 +194,3 @@ class ExcellonParser(object): return FileSettings(units=self.units, format=self.format, zero_suppression=self.zero_suppression, notation=self.notation) - - -if __name__ == '__main__': - p = ExcellonParser() - parsed = p.parse('examples/ncdrill.txt') diff --git a/gerber/gerber.py b/gerber/gerber.py index 220405c..04203fa 100644 --- a/gerber/gerber.py +++ b/gerber/gerber.py @@ -111,13 +111,14 @@ class GerberFile(CncFile): for statement in self.statements: f.write(statement.to_gerber()) - def render(self, filename, ctx): + def render(self, ctx, filename=None): """ Generate image of layer. """ ctx.set_bounds(self.bounds) for statement in self.statements: ctx.evaluate(statement) - ctx.dump(filename) + if filename is not None: + ctx.dump(filename) class GerberParser(object): diff --git a/gerber/render/svgwrite_backend.py b/gerber/render/svgwrite_backend.py index 7d5c8fd..8d84da1 100644 --- a/gerber/render/svgwrite_backend.py +++ b/gerber/render/svgwrite_backend.py @@ -110,12 +110,14 @@ class GerberSvgContext(GerberContext): self.apertures = {} self.dwg = svgwrite.Drawing() - #self.dwg.add(self.dwg.rect(insert=(0, 0), size=(2000, 2000), fill="black")) + self.background = False def set_bounds(self, bounds): xbounds, ybounds = bounds size = (SCALE * (xbounds[1] - xbounds[0]), SCALE * (ybounds[1] - ybounds[0])) - self.dwg.add(self.dwg.rect(insert=(SCALE * xbounds[0], -SCALE * ybounds[1]), size=size, fill="black")) + if not self.background: + self.dwg.add(self.dwg.rect(insert=(SCALE * xbounds[0], -SCALE * ybounds[1]), size=size, fill="black")) + self.background = True def define_aperture(self, d, shape, modifiers): aperture = None -- cgit From bcb6cbc50dea975954b8a3864690f68ab5e984b7 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Wed, 8 Oct 2014 22:49:49 -0400 Subject: start arc --- gerber/gerber.py | 7 ++++ gerber/gerber_statements.py | 23 +++++++++++-- gerber/render/apertures.py | 21 ++++++++++-- gerber/render/render.py | 42 ++++++++++++++--------- gerber/render/svgwrite_backend.py | 70 ++++++++++++++++++++++++--------------- 5 files changed, 116 insertions(+), 47 deletions(-) (limited to 'gerber') diff --git a/gerber/gerber.py b/gerber/gerber.py index 04203fa..07ecd78 100644 --- a/gerber/gerber.py +++ b/gerber/gerber.py @@ -206,6 +206,13 @@ class GerberParser(object): while did_something and len(line) > 0: did_something = False + # region mode + #if 'G36' in line or 'G37' in line: + # yield RegionModeStmt.from_gerber(line) + # did_something = True + # line = '' + # continue + # coord (coord, r) = self._match_one(self.COORD_STMT, line) if coord: diff --git a/gerber/gerber_statements.py b/gerber/gerber_statements.py index 2f58a37..90952b2 100644 --- a/gerber/gerber_statements.py +++ b/gerber/gerber_statements.py @@ -12,7 +12,7 @@ from .utils import parse_gerber_value, write_gerber_value, decimal_string __all__ = ['FSParamStmt', 'MOParamStmt', 'IPParamStmt', 'OFParamStmt', 'LPParamStmt', 'ADParamStmt', 'AMParamStmt', 'INParamStmt', 'LNParamStmt', 'CoordStmt', 'ApertureStmt', 'CommentStmt', - 'EofStmt', 'UnknownStmt'] + 'EofStmt', 'QuadrantModeStmt', 'RegionModeStmt', 'UnknownStmt'] class Statement(object): @@ -601,7 +601,7 @@ class QuadrantModeStmt(Statement): def __init__(self, mode): super(QuadrantModeStmt, self).__init__('Quadrant Mode') - mode = mode.lower + mode = mode.lower() if mode not in ['single-quadrant', 'multi-quadrant']: raise ValueError('Quadrant mode must be "single-quadrant" \ or "multi-quadrant"') @@ -610,6 +610,25 @@ class QuadrantModeStmt(Statement): def to_gerber(self): return 'G74*' if self.mode == 'single-quadrant' else 'G75*' +class RegionModeStmt(Statement): + + @classmethod + def from_gerber(cls, line): + line = line.strip() + 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')) + + def __init__(self, mode): + super(RegionModeStmt, self).__init__('Region Mode') + mode = mode.lower() + if mode not in ['on', 'off']: + raise ValueError('Valid modes are "on" or "off"') + self.mode = mode + + def to_gerber(self): + return 'G36*' if self.mode == 'on' else 'G37*' + class UnknownStmt(Statement): """ Unknown Statement diff --git a/gerber/render/apertures.py b/gerber/render/apertures.py index f163b1f..52ae50c 100644 --- a/gerber/render/apertures.py +++ b/gerber/render/apertures.py @@ -22,16 +22,31 @@ gerber.render.apertures 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.') + 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.') + 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): diff --git a/gerber/render/render.py b/gerber/render/render.py index e40960d..e7ec6ee 100644 --- a/gerber/render/render.py +++ b/gerber/render/render.py @@ -22,19 +22,20 @@ from ..gerber_statements import ( class GerberContext(object): - settings = {} - - x = 0 - y = 0 - - aperture = 0 - interpolation = 'linear' - - image_polarity = 'positive' - level_polarity = 'dark' def __init__(self): - pass + self.settings = {} + self.x = 0 + self.y = 0 + + self.aperture = 0 + self.interpolation = 'linear' + self.direction = 'clockwise' + self.image_polarity = 'positive' + self.level_polarity = 'dark' + self.region_mode = 'off' + self.color = (0.7215, 0.451, 0.200) + self.drill_color = (0.25, 0.25, 0.25) def set_format(self, settings): self.settings = settings @@ -62,6 +63,12 @@ class GerberContext(object): def set_aperture(self, d): self.aperture = d + def set_color(self, color): + self.color = color + + def set_drill_color(self, color): + self.drill_color = color + def resolve(self, x, y): x = x if x is not None else self.x y = y if y is not None else self.y @@ -76,13 +83,13 @@ class GerberContext(object): else: self.x, self.y = x, y - def stroke(self, x, y): + def stroke(self, x, y, i, j): pass def line(self, x, y): pass - def arc(self, x, y): + def arc(self, x, y, i, j): pass def flash(self, x, y): @@ -109,7 +116,8 @@ class GerberContext(object): def _evaluate_param(self, stmt): if stmt.param == "FS": - self.set_coord_format(stmt.zero_suppression, stmt.format, stmt.notation) + self.set_coord_format(stmt.zero_suppression, stmt.format, + stmt.notation) self.set_coord_notation(stmt.notation) elif stmt.param == "MO:": self.set_coord_unit(stmt.mode) @@ -123,9 +131,11 @@ class GerberContext(object): def _evaluate_coord(self, stmt): if stmt.function in ("G01", "G1", "G02", "G2", "G03", "G3"): self.set_interpolation(stmt.function) - + if stmt.function not in ('G01', 'G1'): + self.direction = ('clockwise' if stmt.function in ('G02', 'G2') + else 'counterclockwise') if stmt.op == "D01": - self.stroke(stmt.x, stmt.y) + self.stroke(stmt.x, stmt.y, stmt.i, stmt.j) elif stmt.op == "D02": self.move(stmt.x, stmt.y) elif stmt.op == "D03": diff --git a/gerber/render/svgwrite_backend.py b/gerber/render/svgwrite_backend.py index 8d84da1..3b2f3c1 100644 --- a/gerber/render/svgwrite_backend.py +++ b/gerber/render/svgwrite_backend.py @@ -23,64 +23,71 @@ import svgwrite SCALE = 300 +def convert_color(color): + color = tuple([int(ch * 255) for ch in color]) + return 'rgb(%d, %d, %d)' % color + class SvgCircle(Circle): - def draw(self, ctx, x, y): + def line(self, ctx, x, y, color='rgb(184, 115, 51)'): return ctx.dwg.line(start=(ctx.x * SCALE, -ctx.y * SCALE), end=(x * SCALE, -y * SCALE), - stroke="rgb(184, 115, 51)", + stroke=color, stroke_width=SCALE * self.diameter, stroke_linecap="round") - def flash(self, ctx, x, y): + def arc(self, ctx, x, y, i, j, direction, color='rgb(184, 115, 51)'): + pass + + def flash(self, ctx, x, y, color='rgb(184, 115, 51)'): return [ctx.dwg.circle(center=(x * SCALE, -y * SCALE), r = SCALE * (self.diameter / 2.0), - fill='rgb(184, 115, 51)'), ] + fill=color), ] class SvgRect(Rect): - def draw(self, ctx, x, y): + def line(self, ctx, x, y, color='rgb(184, 115, 51)'): return ctx.dwg.line(start=(ctx.x * SCALE, -ctx.y * SCALE), end=(x * SCALE, -y * SCALE), - stroke="rgb(184, 115, 51)", stroke_width=2, + stroke=color, stroke_width=2, stroke_linecap="butt") - def flash(self, ctx, x, y): + def flash(self, ctx, x, y, color='rgb(184, 115, 51)'): xsize, ysize = self.size return [ctx.dwg.rect(insert=(SCALE * (x - (xsize / 2)), -SCALE * (y + (ysize / 2))), size=(SCALE * xsize, SCALE * ysize), - fill="rgb(184, 115, 51)"), ] + fill=color), ] class SvgObround(Obround): - def draw(self, ctx, x, y): + def line(self, ctx, x, y, color='rgb(184, 115, 51)'): pass - def flash(self, ctx, x, y): + def flash(self, ctx, x, y, color='rgb(184, 115, 51)'): xsize, ysize = self.size # horizontal obround if xsize == ysize: return [ctx.dwg.circle(center=(x * SCALE, -y * SCALE), r = SCALE * (x / 2.0), - fill='rgb(184, 115, 51)'), ] + fill=color), ] if xsize > ysize: rectx = xsize - ysize recty = ysize lcircle = ctx.dwg.circle(center=((x - (rectx / 2.0)) * SCALE, -y * SCALE), r = SCALE * (ysize / 2.0), - fill='rgb(184, 115, 51)') + fill=color) rcircle = ctx.dwg.circle(center=((x + (rectx / 2.0)) * SCALE, -y * SCALE), r = SCALE * (ysize / 2.0), - fill='rgb(184, 115, 51)') + fill=color) rect = ctx.dwg.rect(insert=(SCALE * (x - (xsize / 2.)), -SCALE * (y + (ysize / 2.))), size=(SCALE * xsize, SCALE * ysize), - fill='rgb(184, 115, 51)') + fill=color) return [lcircle, rcircle, rect, ] # Vertical obround @@ -90,17 +97,17 @@ class SvgObround(Obround): lcircle = ctx.dwg.circle(center=(x * SCALE, (y - (recty / 2.)) * -SCALE), r = SCALE * (xsize / 2.), - fill='rgb(184, 115, 51)') + fill=color) ucircle = ctx.dwg.circle(center=(x * SCALE, (y + (recty / 2.)) * -SCALE), r = SCALE * (xsize / 2.), - fill='rgb(184, 115, 51)') + fill=color) rect = ctx.dwg.rect(insert=(SCALE * (x - (xsize / 2.)), -SCALE * (y + (ysize / 2.))), size=(SCALE * xsize, SCALE * ysize), - fill='rgb(184, 115, 51)') + fill=color) return [lcircle, ucircle, rect, ] @@ -116,7 +123,9 @@ class GerberSvgContext(GerberContext): xbounds, ybounds = bounds size = (SCALE * (xbounds[1] - xbounds[0]), SCALE * (ybounds[1] - ybounds[0])) if not self.background: - self.dwg.add(self.dwg.rect(insert=(SCALE * xbounds[0], -SCALE * ybounds[1]), size=size, fill="black")) + self.dwg.add(self.dwg.rect(insert=(SCALE * xbounds[0], + -SCALE * ybounds[1]), + size=size, fill="black")) self.background = True def define_aperture(self, d, shape, modifiers): @@ -129,13 +138,13 @@ class GerberSvgContext(GerberContext): aperture = SvgObround(size=modifiers[0][0:2]) self.apertures[d] = aperture - def stroke(self, x, y): - super(GerberSvgContext, self).stroke(x, y) + def stroke(self, x, y, i, j): + super(GerberSvgContext, self).stroke(x, y, i, j) if self.interpolation == 'linear': self.line(x, y) elif self.interpolation == 'arc': - self.arc(x, y) + self.arc(x, y, i, j) def line(self, x, y): super(GerberSvgContext, self).line(x, y) @@ -143,11 +152,18 @@ class GerberSvgContext(GerberContext): ap = self.apertures.get(self.aperture, None) if ap is None: return - self.dwg.add(ap.draw(self, x, y)) + self.dwg.add(ap.line(self, x, y, convert_color(self.color))) self.move(x, y, resolve=False) - def arc(self, x, y): - super(GerberSvgContext, self).arc(x, y) + def arc(self, x, y, i, j): + super(GerberSvgContext, self).arc(x, y, i, j) + x, y = self.resolve(x, y) + ap = self.apertures.get(self.aperture, None) + if ap is None: + return + #self.dwg.add(ap.arc(self, x, y, i, j, self.direction, + # convert_color(self.color))) + self.move(x, y, resolve=False) def flash(self, x, y): super(GerberSvgContext, self).flash(x, y) @@ -155,12 +171,14 @@ class GerberSvgContext(GerberContext): ap = self.apertures.get(self.aperture, None) if ap is None: return - for shape in ap.flash(self, x, y): + for shape in ap.flash(self, x, y, convert_color(self.color)): self.dwg.add(shape) self.move(x, y, resolve=False) def drill(self, x, y, diameter): - hit = self.dwg.circle(center=(x*SCALE, -y*SCALE), r=SCALE*(diameter/2.0), fill='gray') + hit = self.dwg.circle(center=(x*SCALE, -y*SCALE), + r=SCALE*(diameter/2.0), + fill=convert_color(self.drill_color)) self.dwg.add(hit) def dump(self, filename): -- cgit From 84bfd34e918251ff82f4b3818bc6268feab72efe Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Thu, 9 Oct 2014 09:51:29 -0400 Subject: Add mode statement parsing --- gerber/gerber.py | 24 ++++++++++++++++++------ gerber/gerber_statements.py | 6 +++--- gerber/render/render.py | 14 ++++++++++++-- 3 files changed, 33 insertions(+), 11 deletions(-) (limited to 'gerber') diff --git a/gerber/gerber.py b/gerber/gerber.py index 07ecd78..c59d871 100644 --- a/gerber/gerber.py +++ b/gerber/gerber.py @@ -164,6 +164,9 @@ class GerberParser(object): EOF_STMT = re.compile(r"(?PM02)\*") + REGION_MODE_STMT = re.compile(r'(?PG3[67])\*') + QUAD_MODE_STMT = re.compile(r'(?PG7[45])\*') + def __init__(self): self.settings = FileSettings() self.statements = [] @@ -206,12 +209,21 @@ class GerberParser(object): while did_something and len(line) > 0: did_something = False - # region mode - #if 'G36' in line or 'G37' in line: - # yield RegionModeStmt.from_gerber(line) - # did_something = True - # line = '' - # continue + # Region Mode + (mode, r) = self._match_one(self.REGION_MODE_STMT, line) + if mode: + yield RegionModeStmt.from_gerber(line) + line = r + did_something = True + continue + + # Quadrant Mode + (mode, r) = self._match_one(self.QUAD_MODE_STMT, line) + if mode: + yield QuadrantModeStmt.from_gerber(line) + line = r + did_something = True + continue # coord (coord, r) = self._match_one(self.COORD_STMT, line) diff --git a/gerber/gerber_statements.py b/gerber/gerber_statements.py index 90952b2..76a6f0c 100644 --- a/gerber/gerber_statements.py +++ b/gerber/gerber_statements.py @@ -600,7 +600,7 @@ class QuadrantModeStmt(Statement): else cls('multi-quadrant')) def __init__(self, mode): - super(QuadrantModeStmt, self).__init__('Quadrant Mode') + super(QuadrantModeStmt, self).__init__('QuadrantMode') mode = mode.lower() if mode not in ['single-quadrant', 'multi-quadrant']: raise ValueError('Quadrant mode must be "single-quadrant" \ @@ -611,7 +611,7 @@ class QuadrantModeStmt(Statement): return 'G74*' if self.mode == 'single-quadrant' else 'G75*' class RegionModeStmt(Statement): - + @classmethod def from_gerber(cls, line): line = line.strip() @@ -620,7 +620,7 @@ class RegionModeStmt(Statement): return (cls('on') if line[:3] == 'G36' else cls('off')) def __init__(self, mode): - super(RegionModeStmt, self).__init__('Region Mode') + super(RegionModeStmt, self).__init__('RegionMode') mode = mode.lower() if mode not in ['on', 'off']: raise ValueError('Valid modes are "on" or "off"') diff --git a/gerber/render/render.py b/gerber/render/render.py index e7ec6ee..e91c71e 100644 --- a/gerber/render/render.py +++ b/gerber/render/render.py @@ -16,8 +16,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -from ..gerber_statements import ( - CommentStmt, UnknownStmt, EofStmt, ParamStmt, CoordStmt, ApertureStmt +from ..gerber_statements import (CommentStmt, UnknownStmt, EofStmt, ParamStmt, + CoordStmt, ApertureStmt, RegionModeStmt, + QuadrantModeStmt, ) @@ -111,9 +112,18 @@ class GerberContext(object): elif isinstance(stmt, ApertureStmt): self._evaluate_aperture(stmt) + elif isinstance(stmt, (RegionModeStmt, QuadrantModeStmt)): + self._evaluate_mode(stmt) + else: raise Exception("Invalid statement to evaluate") + def _evaluate_mode(self, stmt): + if stmt.type == 'RegionMode': + self.region_mode = stmt.mode + elif stmt.type == 'QuadrantMode': + self.quadrant_mode = stmt.mode + def _evaluate_param(self, stmt): if stmt.param == "FS": self.set_coord_format(stmt.zero_suppression, stmt.format, -- cgit From 8851bc17b94a921453b0afd9c2421cb30f8d4425 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Thu, 9 Oct 2014 18:09:17 -0400 Subject: Doc update --- gerber/gerber_statements.py | 28 +++++++++++++ gerber/render/render.py | 86 ++++++++++++++++++++++++++++++++++++++- gerber/render/svgwrite_backend.py | 8 +++- 3 files changed, 119 insertions(+), 3 deletions(-) (limited to 'gerber') diff --git a/gerber/gerber_statements.py b/gerber/gerber_statements.py index 76a6f0c..4c5133a 100644 --- a/gerber/gerber_statements.py +++ b/gerber/gerber_statements.py @@ -16,6 +16,20 @@ __all__ = ['FSParamStmt', 'MOParamStmt', 'IPParamStmt', 'OFParamStmt', class Statement(object): + """ Gerber statement Base class + + The statement class provides a type attribute. + + Parameters + ---------- + type : string + String identifying the statement type. + + Attributes + ---------- + type : string + String identifying the statement type. + """ def __init__(self, stype): self.type = stype @@ -30,6 +44,20 @@ class Statement(object): class ParamStmt(Statement): + """ Gerber parameter statement Base class + + The parameter statement class provides a parameter type attribute. + + Parameters + ---------- + param : string + two-character code identifying the parameter statement type. + + Attributes + ---------- + param : string + Parameter type code + """ def __init__(self, param): Statement.__init__(self, "PARAM") self.param = param diff --git a/gerber/render/render.py b/gerber/render/render.py index e91c71e..8cfc5de 100644 --- a/gerber/render/render.py +++ b/gerber/render/render.py @@ -23,7 +23,60 @@ from ..gerber_statements import (CommentStmt, UnknownStmt, EofStmt, ParamStmt, class GerberContext(object): - + """ Gerber rendering context base class + + Provides basic functionality and API for rendering gerber files. Medium- + specific renderers should subclass GerberContext and implement the drawing + functions. Colors are stored internally as 32-bit RGB and may need to be + converted to a native format in the rendering subclass. + + Attributes + ---------- + settings : FileSettings (dict-like) + Gerber file settings + + x : float + X-coordinate of the "photoplotter" head. + + y : float + Y-coordinate of the "photoplotter" head + + aperture : int + The aperture that is currently in use + + interpolation : str + Current interpolation mode. may be 'linear' or 'arc' + + direction : string + Current arc direction. May be either 'clockwise' or 'counterclockwise' + + image_polarity : string + Current image polarity setting. May be 'positive' or 'negative' + + level_polarity : string + Level polarity. May be 'dark' or 'clear'. Dark polarity indicates the + existance of copper/silkscreen/etc. in the exposed area, whereas clear + polarity indicates material should be removed from the exposed area. + + region_mode : string + Region mode. May be 'on' or 'off'. When region mode is set to 'on' the + following "contours" define the outline of a region. When region mode + is subsequently turned 'off', the defined area is filled. + + quadrant_mode : string + Quadrant mode. May be 'single-quadrant' or 'multi-quadrant'. Defines + how arcs are specified. + + color : tuple (, , ) + Color used for rendering as a tuple of normalized (red, green, blue) values. + + drill_color : tuple (, , ) + Color used for rendering drill hits. Format is the same as for `color`. + + background_color : tuple (, , ) + Color of the background. Used when exposing areas in 'clear' level + polarity mode. Format is the same as for `color`. + """ def __init__(self): self.settings = {} self.x = 0 @@ -35,13 +88,36 @@ class GerberContext(object): self.image_polarity = 'positive' self.level_polarity = 'dark' self.region_mode = 'off' + self.quadrant_mode = 'multi-quadrant' + 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) def set_format(self, settings): + """ Set source file format. + + Parameters + ---------- + settings : FileSettings instance or dict-like + Gerber file settings used in source file. + """ self.settings = settings def set_coord_format(self, zero_suppression, format, notation): + """ Set coordinate format used in source gerber file + + Parameters + ---------- + zero_suppression : string + Zero suppression mode. may be 'leading' or 'trailling' + + format : tuple (, ) + decimal precision format + + notation : string + notation mode. 'absolute' or 'incremental' + """ self.settings['zero_suppression'] = zero_suppression self.settings['format'] = format self.settings['notation'] = notation @@ -69,6 +145,9 @@ class GerberContext(object): def set_drill_color(self, color): self.drill_color = color + + def set_background_color(self, color): + self.background_color = color def resolve(self, x, y): x = x if x is not None else self.x @@ -120,6 +199,8 @@ class GerberContext(object): def _evaluate_mode(self, stmt): if stmt.type == 'RegionMode': + if self.region_mode == 'on' and stmt.mode == 'off': + self._fill_region() self.region_mode = stmt.mode elif stmt.type == 'QuadrantMode': self.quadrant_mode = stmt.mode @@ -153,3 +234,6 @@ class GerberContext(object): def _evaluate_aperture(self, stmt): self.set_aperture(stmt.d) + + def _fill_region(self): + pass diff --git a/gerber/render/svgwrite_backend.py b/gerber/render/svgwrite_backend.py index 3b2f3c1..7570c84 100644 --- a/gerber/render/svgwrite_backend.py +++ b/gerber/render/svgwrite_backend.py @@ -152,7 +152,9 @@ class GerberSvgContext(GerberContext): ap = self.apertures.get(self.aperture, None) if ap is None: return - self.dwg.add(ap.line(self, x, y, convert_color(self.color))) + color = (convert_color(self.color) if self.level_polarity == 'dark' + else convert_color(self.background_color)) + self.dwg.add(ap.line(self, x, y, color)) self.move(x, y, resolve=False) def arc(self, x, y, i, j): @@ -171,7 +173,9 @@ class GerberSvgContext(GerberContext): ap = self.apertures.get(self.aperture, None) if ap is None: return - for shape in ap.flash(self, x, y, convert_color(self.color)): + color = (convert_color(self.color) if self.level_polarity == 'dark' + else convert_color(self.background_color)) + for shape in ap.flash(self, x, y, color): self.dwg.add(shape) self.move(x, y, resolve=False) -- cgit From bf9f9451f555a47651e414faf839d8d83441c737 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Thu, 9 Oct 2014 21:52:04 -0400 Subject: doc update --- gerber/render/render.py | 227 +++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 187 insertions(+), 40 deletions(-) (limited to 'gerber') diff --git a/gerber/render/render.py b/gerber/render/render.py index 8cfc5de..db3c743 100644 --- a/gerber/render/render.py +++ b/gerber/render/render.py @@ -24,57 +24,57 @@ from ..gerber_statements import (CommentStmt, UnknownStmt, EofStmt, ParamStmt, class GerberContext(object): """ Gerber rendering context base class - + Provides basic functionality and API for rendering gerber files. Medium- specific renderers should subclass GerberContext and implement the drawing - functions. Colors are stored internally as 32-bit RGB and may need to be + functions. Colors are stored internally as 32-bit RGB and may need to be converted to a native format in the rendering subclass. - + Attributes ---------- settings : FileSettings (dict-like) Gerber file settings - + x : float - X-coordinate of the "photoplotter" head. - + X-coordinate of the "photoplotter" head. + y : float Y-coordinate of the "photoplotter" head - + aperture : int The aperture that is currently in use - + interpolation : str Current interpolation mode. may be 'linear' or 'arc' - + direction : string Current arc direction. May be either 'clockwise' or 'counterclockwise' - + image_polarity : string Current image polarity setting. May be 'positive' or 'negative' - + level_polarity : string - Level polarity. May be 'dark' or 'clear'. Dark polarity indicates the - existance of copper/silkscreen/etc. in the exposed area, whereas clear - polarity indicates material should be removed from the exposed area. - + Level polarity. May be 'dark' or 'clear'. Dark polarity indicates the + existance of copper/silkscreen/etc. in the exposed area, whereas clear + polarity indicates material should be removed from the exposed area. + region_mode : string Region mode. May be 'on' or 'off'. When region mode is set to 'on' the - following "contours" define the outline of a region. When region mode + following "contours" define the outline of a region. When region mode is subsequently turned 'off', the defined area is filled. - + quadrant_mode : string Quadrant mode. May be 'single-quadrant' or 'multi-quadrant'. Defines how arcs are specified. - + color : tuple (, , ) Color used for rendering as a tuple of normalized (red, green, blue) values. - + drill_color : tuple (, , ) Color used for rendering drill hits. Format is the same as for `color`. - + background_color : tuple (, , ) - Color of the background. Used when exposing areas in 'clear' level + Color of the background. Used when exposing areas in 'clear' level polarity mode. Format is the same as for `color`. """ def __init__(self): @@ -89,14 +89,14 @@ class GerberContext(object): self.level_polarity = 'dark' self.region_mode = 'off' self.quadrant_mode = 'multi-quadrant' - + 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) def set_format(self, settings): """ Set source file format. - + Parameters ---------- settings : FileSettings instance or dict-like @@ -104,52 +104,178 @@ class GerberContext(object): """ self.settings = settings - def set_coord_format(self, zero_suppression, format, notation): + def set_coord_format(self, zero_suppression, decimal_format, notation): """ Set coordinate format used in source gerber file - + Parameters ---------- zero_suppression : string Zero suppression mode. may be 'leading' or 'trailling' - - format : tuple (, ) - decimal precision format - + + decimal_format : tuple (, ) + Decimal precision format specified as (integer digits, decimal digits) + notation : string - notation mode. 'absolute' or 'incremental' + Notation mode. 'absolute' or 'incremental' """ + if zero_suppression not in ('leading', 'trailling'): + raise ValueError('Zero suppression must be "leading" or "trailing"') self.settings['zero_suppression'] = zero_suppression - self.settings['format'] = format + self.settings['format'] = decimal_format self.settings['notation'] = notation def set_coord_notation(self, notation): + """ Set context notation mode + + Parameters + ---------- + notation : string + Notation mode. may be 'absolute' or 'incremental' + + Raises + ------ + ValueError + If `notation` is not either "absolute" or "incremental" + + """ + if notation not in ('absolute', 'incremental'): + raise ValueError('Notation may be "absolute" or "incremental"') self.settings['notation'] = notation def set_coord_unit(self, unit): + """ Set context measurement units + + Parameters + ---------- + unit : string + Measurement units. may be 'inch' or 'metric' + + Raises + ------ + ValueError + If `unit` is not 'inch' or 'metric' + """ + if unit not in ('inch', 'metric'): + raise ValueError('Unit may be "inch" or "metric"') self.settings['units'] = unit def set_image_polarity(self, polarity): + """ Set context image polarity + + Parameters + ---------- + polarity : string + Image polarity. May be "positive" or "negative" + + Raises + ------ + ValueError + If polarity is not 'positive' or 'negative' + """ + if polarity not in ('positive', 'negative'): + raise ValueError('Polarity may be "positive" or "negative"') self.image_polarity = polarity def set_level_polarity(self, polarity): + """ Set context level polarity + + Parameters + ---------- + polarity : string + Level polarity. May be "dark" or "clear" + + Raises + ------ + ValueError + If polarity is not 'dark' or 'clear' + """ + if polarity not in ('dark', 'clear'): + raise ValueError('Polarity may be "dark" or "clear"') self.level_polarity = polarity def set_interpolation(self, interpolation): - self.interpolation = 'linear' if interpolation in ("G01", "G1") else 'arc' + """ Set arc interpolation mode + + Parameters + ---------- + interpolation : string + Interpolation mode. May be 'linear' or 'arc' + + Raises + ------ + ValueError + If `interpolation` is not 'linear' or 'arc' + """ + if interpolation not in ('linear', 'arc'): + raise ValueError('Interpolation may be "linear" or "arc"') + self.interpolation = interpolation def set_aperture(self, d): + """ Set active aperture + + Parameters + ---------- + aperture : int + Aperture number to activate. + """ self.aperture = d def set_color(self, color): + """ Set rendering color. + + Parameters + ---------- + color : tuple (, , ) + 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): - self.drill_color = color - + """ Set color used for rendering drill hits. + + Parameters + ---------- + color : tuple (, , ) + 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 (, , ) + 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 resolve(self, x, y): + """ Resolve missing x or y coordinates in a coordinate command. + + Replace missing x or y values with the current x or y position. This + is the default method for handling coordinate pairs pulled from gerber + file statments, as a move/line/arc involving a change in only one axis + will drop the redundant axis coordinate to reduce file size. + + Parameters + ---------- + x : float + X-coordinate. If `None`, will be replaced with current + "photoplotter" head x-coordinate + + y : float + Y-coordinate. If `None`, will be replaced with current + "photoplotter" head y-coordinate + + Returns + ------- + coordinates : tuple (, ) + Coordinates in absolute notation + """ x = x if x is not None else self.x y = y if y is not None else self.y return x, y @@ -158,6 +284,26 @@ class GerberContext(object): pass def move(self, x, y, resolve=True): + """ Lights-off move. + + Move the "photoplotter" head to (x, y) without drawing a line. If x or + y is `None`, remain at the same point in that axis. + + Parameters + ----------- + x : float + X-coordinate to move to. If x is `None`, do not move in the X + direction + + y : float + Y-coordinate to move to. if y is `None`, do not move in the Y + direction + + resolve : bool + If resolve is `True` the context will replace missing x or y + coordinates with the current plotter head position. This is the + default behavior. + """ if resolve: self.x, self.y = self.resolve(x, y) else: @@ -220,11 +366,12 @@ class GerberContext(object): self.define_aperture(stmt.d, stmt.shape, stmt.modifiers) def _evaluate_coord(self, stmt): - if stmt.function in ("G01", "G1", "G02", "G2", "G03", "G3"): - self.set_interpolation(stmt.function) - if stmt.function not in ('G01', 'G1'): - self.direction = ('clockwise' if stmt.function in ('G02', 'G2') - else 'counterclockwise') + if stmt.function in ("G01", "G1"): + self.set_interpolation('linear') + elif stmt.function in ('G02', 'G2', 'G03', 'G3'): + self.set_interpolation('arc') + self.direction = ('clockwise' if stmt.function in ('G02', 'G2') + else 'counterclockwise') if stmt.op == "D01": self.stroke(stmt.x, stmt.y, stmt.i, stmt.j) elif stmt.op == "D02": @@ -234,6 +381,6 @@ class GerberContext(object): def _evaluate_aperture(self, stmt): self.set_aperture(stmt.d) - + def _fill_region(self): pass -- cgit From f2f411493ea303075d5dbdd7656c572dda61cf67 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Thu, 9 Oct 2014 22:10:28 -0400 Subject: doc update --- gerber/__init__.py | 4 +++- gerber/excellon.py | 4 ++-- gerber/excellon_statements.py | 5 +++++ gerber/gerber.py | 4 ++-- gerber/gerber_statements.py | 6 +++--- gerber/render/render.py | 6 ++++++ 6 files changed, 21 insertions(+), 8 deletions(-) (limited to 'gerber') diff --git a/gerber/__init__.py b/gerber/__init__.py index 4637713..fce6483 100644 --- a/gerber/__init__.py +++ b/gerber/__init__.py @@ -15,9 +15,11 @@ # See the License for the specific language governing permissions and # limitations under the License. """ -gerber module +Gerber Tools ============ **Gerber Tools** +gerber-tools provides utilities for working with Gerber (RS-274X) and Excellon +files in python. """ diff --git a/gerber/excellon.py b/gerber/excellon.py index 663f791..4166de6 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -15,8 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. """ -Excellon module -============ +Excellon File module +==================== **Excellon file classes** This module provides Excellon file classes and parsing utilities diff --git a/gerber/excellon_statements.py b/gerber/excellon_statements.py index 5eba12c..7300b60 100644 --- a/gerber/excellon_statements.py +++ b/gerber/excellon_statements.py @@ -14,7 +14,12 @@ # 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 Statements +==================== +**Excellon file statement classes** +""" from .utils import parse_gerber_value, write_gerber_value, decimal_string import re diff --git a/gerber/gerber.py b/gerber/gerber.py index c59d871..4ce261d 100644 --- a/gerber/gerber.py +++ b/gerber/gerber.py @@ -16,8 +16,8 @@ # See the License for the specific language governing permissions and # limitations under the License. """ -gerber.gerber -============ +Gerber File module +================== **Gerber File module** This module provides an RS-274-X class and parser diff --git a/gerber/gerber_statements.py b/gerber/gerber_statements.py index 4c5133a..2caced5 100644 --- a/gerber/gerber_statements.py +++ b/gerber/gerber_statements.py @@ -1,9 +1,9 @@ #! /usr/bin/env python # -*- coding: utf-8 -*- """ -gerber.statements -================= -**Gerber file statement classes** +Gerber (RS-274X) Statements +=========================== +**Gerber RS-274X file statement classes** """ from .utils import parse_gerber_value, write_gerber_value, decimal_string diff --git a/gerber/render/render.py b/gerber/render/render.py index db3c743..f2d23b4 100644 --- a/gerber/render/render.py +++ b/gerber/render/render.py @@ -15,7 +15,13 @@ # 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. +""" +Rendering +============ +**Gerber (RS-274X) and Excellon file rendering** +Render Gerber and Excellon files to a variety of formats. +""" from ..gerber_statements import (CommentStmt, UnknownStmt, EofStmt, ParamStmt, CoordStmt, ApertureStmt, RegionModeStmt, QuadrantModeStmt, -- cgit From a9059df190be0238ce0e6fca8c59700e92ddf205 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Fri, 10 Oct 2014 09:35:06 -0400 Subject: doc update --- gerber/excellon_statements.py | 12 ++--- gerber/render/render.py | 100 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 105 insertions(+), 7 deletions(-) (limited to 'gerber') diff --git a/gerber/excellon_statements.py b/gerber/excellon_statements.py index 7300b60..4b92f07 100644 --- a/gerber/excellon_statements.py +++ b/gerber/excellon_statements.py @@ -53,12 +53,12 @@ class ExcellonTool(ExcellonStatement): kwargs : dict-like Tool settings from the excellon statement. Valid keys are: - diameter : Tool diameter [expressed in file units] - rpm : Tool RPM - feed_rate : Z-axis tool feed rate - retract_rate : Z-axis tool retraction rate - max_hit_count : Number of hits allowed before a tool change - depth_offset : Offset of tool depth from tip of tool. + - `diameter` : Tool diameter [expressed in file units] + - `rpm` : Tool RPM + - `feed_rate` : Z-axis tool feed rate + - `retract_rate` : Z-axis tool retraction rate + - `max_hit_count` : Number of hits allowed before a tool change + - `depth_offset` : Offset of tool depth from tip of tool. Attributes ---------- diff --git a/gerber/render/render.py b/gerber/render/render.py index f2d23b4..e76aed1 100644 --- a/gerber/render/render.py +++ b/gerber/render/render.py @@ -20,7 +20,8 @@ Rendering ============ **Gerber (RS-274X) and Excellon file rendering** -Render Gerber and Excellon files to a variety of formats. +Render Gerber and Excellon files to a variety of formats. The render module +currently supports SVG rendering using the `svgwrite` library. """ from ..gerber_statements import (CommentStmt, UnknownStmt, EofStmt, ParamStmt, CoordStmt, ApertureStmt, RegionModeStmt, @@ -316,21 +317,118 @@ class GerberContext(object): self.x, self.y = x, y def stroke(self, x, y, i, j): + """ Lights-on move. (draws a line or arc) + + The stroke method is called when a Lights-on move statement is + encountered. This will call the `line` or `arc` method as necessary + based on the move statement's parameters. The `stroke` method should + be overridden in `GerberContext` subclasses. + + Parameters + ---------- + x : float + X coordinate of target position + + y : float + Y coordinate of target position + + i : float + Offset in X-direction from current position of arc center. + + j : float + Offset in Y-direction from current position of arc center. + """ pass def line(self, x, y): + """ Draw a line + + Draws a line from the current position to (x, y) using the currently + selected aperture. The `line` method should be overridden in + `GerberContext` subclasses. + + Parameters + ---------- + x : float + X coordinate of target position + + y : float + Y coordinate of target position + """ pass def arc(self, x, y, i, j): + """ Draw an arc + + Draw an arc from the current position to (x, y) using the currently + selected aperture. `i` and `j` specify the offset from the starting + position to the center of the arc.The `arc` method should be + overridden in `GerberContext` subclasses. + + Parameters + ---------- + x : float + X coordinate of target position + + y : float + Y coordinate of target position + + i : float + Offset in X-direction from current position of arc center. + + j : float + Offset in Y-direction from current position of arc center. + """ pass def flash(self, x, y): + """ Flash the current aperture + + Draw a filled shape defined by the currently selected aperture. + + Parameters + ---------- + x : float + X coordinate of the position at which to flash + + y : float + Y coordinate of the position at which to flash + """ pass def drill(self, x, y, diameter): + """ Draw a drill hit + + Draw a filled circle representing a drill hit at the specified + position and with the specified diameter. + + Parameters + ---------- + x : float + X coordinate of the drill hit + + y : float + Y coordinate of the drill hit + + diameter : float + Finished hole diameter to draw. + """ pass def evaluate(self, stmt): + """ Evaluate Gerber statement and update image accordingly. + + This method is called once for each statement in a Gerber/Excellon + file when the file's `render` method is called. The evaluate method + should forward the statement on to the relevant handling method based + on the statement type. + + Parameters + ---------- + statement : Statement + Gerber/Excellon statement to evaluate. + + """ if isinstance(stmt, (CommentStmt, UnknownStmt, EofStmt)): return -- cgit From 1750c3c60aeffc813dad8191ceabcdb90dd2e0a6 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Fri, 10 Oct 2014 13:07:54 -0400 Subject: Add tests --- gerber/gerber_statements.py | 12 ++- gerber/tests/test_gerber_statements.py | 155 +++++++++++++++++++++++++++++++++ 2 files changed, 164 insertions(+), 3 deletions(-) (limited to 'gerber') diff --git a/gerber/gerber_statements.py b/gerber/gerber_statements.py index 2caced5..9072b58 100644 --- a/gerber/gerber_statements.py +++ b/gerber/gerber_statements.py @@ -284,9 +284,9 @@ class LPParamStmt(ParamStmt): ParamStmt.__init__(self, param) self.lp = lp - def to_gerber(self, settings): - lp = 'C' if self.lp == 'clear' else 'dark' - return '%LP{0}*%'.format(self.lp) + def to_gerber(self): + lp = 'C' if self.lp == 'clear' else 'D' + return '%LP{0}*%'.format(lp) def __str__(self): return '' % self.lp @@ -593,6 +593,7 @@ class ApertureStmt(Statement): class CommentStmt(Statement): """ Comment Statment """ + def __init__(self, comment): Statement.__init__(self, "COMMENT") self.comment = comment @@ -616,6 +617,7 @@ class EofStmt(Statement): def __str__(self): return '' + class QuadrantModeStmt(Statement): @classmethod @@ -638,6 +640,7 @@ class QuadrantModeStmt(Statement): def to_gerber(self): return 'G74*' if self.mode == 'single-quadrant' else 'G75*' + class RegionModeStmt(Statement): @classmethod @@ -664,3 +667,6 @@ class UnknownStmt(Statement): def __init__(self, line): Statement.__init__(self, "UNKNOWN") self.line = line + + def to_gerber(self): + return self.line diff --git a/gerber/tests/test_gerber_statements.py b/gerber/tests/test_gerber_statements.py index 7c01130..9e73fd4 100644 --- a/gerber/tests/test_gerber_statements.py +++ b/gerber/tests/test_gerber_statements.py @@ -102,3 +102,158 @@ def test_OFParamStmt_dump(): stmt = {'param': 'OF', 'a': '0.1234567', 'b': '0.1234567'} of = OFParamStmt.from_dict(stmt) assert_equal(of.to_gerber(), '%OFA0.123456B0.123456*%') + + +def test_LPParamStmt_factory(): + """ Test LPParamStmt factory correctly handles parameters + """ + stmt = {'param': 'LP', 'lp': 'C'} + lp = LPParamStmt.from_dict(stmt) + assert_equal(lp.lp, 'clear') + + stmt = {'param': 'LP', 'lp': 'D'} + lp = LPParamStmt.from_dict(stmt) + assert_equal(lp.lp, 'dark') + +def test_LPParamStmt_dump(): + """ Test LPParamStmt to_gerber() + """ + stmt = {'param': 'LP', 'lp': 'C'} + lp = LPParamStmt.from_dict(stmt) + assert_equal(lp.to_gerber(), '%LPC*%') + + stmt = {'param': 'LP', 'lp': 'D'} + lp = LPParamStmt.from_dict(stmt) + assert_equal(lp.to_gerber(), '%LPD*%') + + +def test_INParamStmt_factory(): + """ Test INParamStmt factory correctly handles parameters + """ + stmt = {'param': 'IN', 'name': 'test'} + inp = INParamStmt.from_dict(stmt) + assert_equal(inp.name, 'test') + +def test_INParamStmt_dump(): + """ Test INParamStmt to_gerber() + """ + stmt = {'param': 'IN', 'name': 'test'} + inp = INParamStmt.from_dict(stmt) + assert_equal(inp.to_gerber(), '%INtest*%') + + +def test_LNParamStmt_factory(): + """ Test LNParamStmt factory correctly handles parameters + """ + stmt = {'param': 'LN', 'name': 'test'} + lnp = LNParamStmt.from_dict(stmt) + assert_equal(lnp.name, 'test') + +def test_LNParamStmt_dump(): + """ Test LNParamStmt to_gerber() + """ + stmt = {'param': 'LN', 'name': 'test'} + lnp = LNParamStmt.from_dict(stmt) + assert_equal(lnp.to_gerber(), '%LNtest*%') + +def test_comment_stmt(): + """ Test comment statement + """ + stmt = CommentStmt('A comment') + assert_equal(stmt.type, 'COMMENT') + assert_equal(stmt.comment, 'A comment') + +def test_comment_stmt_dump(): + """ Test CommentStmt to_gerber() + """ + stmt = CommentStmt('A comment') + assert_equal(stmt.to_gerber(), 'G04A comment*') + + +def test_eofstmt(): + """ Test EofStmt + """ + stmt = EofStmt() + assert_equal(stmt.type, 'EOF') + +def test_eofstmt_dump(): + """ Test EofStmt to_gerber() + """ + stmt = EofStmt() + assert_equal(stmt.to_gerber(), 'M02*') + + +def test_quadmodestmt_factory(): + """ Test QuadrantModeStmt.from_gerber() + """ + line = 'G74*' + stmt = QuadrantModeStmt.from_gerber(line) + assert_equal(stmt.type, 'QuadrantMode') + assert_equal(stmt.mode, 'single-quadrant') + + line = 'G75*' + stmt = QuadrantModeStmt.from_gerber(line) + assert_equal(stmt.mode, 'multi-quadrant') + +def test_quadmodestmt_validation(): + """ Test QuadrantModeStmt input validation + """ + line = 'G76*' + assert_raises(ValueError, QuadrantModeStmt.from_gerber, line) + assert_raises(ValueError, QuadrantModeStmt, 'quadrant-ful') + + +def test_quadmodestmt_dump(): + """ Test QuadrantModeStmt.to_gerber() + """ + for line in ('G74*', 'G75*',): + stmt = QuadrantModeStmt.from_gerber(line) + assert_equal(stmt.to_gerber(), line) + + +def test_regionmodestmt_factory(): + """ Test RegionModeStmt.from_gerber() + """ + line = 'G36*' + stmt = RegionModeStmt.from_gerber(line) + assert_equal(stmt.type, 'RegionMode') + assert_equal(stmt.mode, 'on') + + line = 'G37*' + stmt = RegionModeStmt.from_gerber(line) + assert_equal(stmt.mode, 'off') + + +def test_regionmodestmt_validation(): + """ Test RegionModeStmt input validation + """ + line = 'G38*' + assert_raises(ValueError, RegionModeStmt.from_gerber, line) + assert_raises(ValueError, RegionModeStmt, 'off-ish') + + +def test_regionmodestmt_dump(): + """ Test RegionModeStmt.to_gerber() + """ + for line in ('G36*', 'G37*',): + stmt = RegionModeStmt.from_gerber(line) + assert_equal(stmt.to_gerber(), line) + + +def test_unknownstmt(): + """ Test UnknownStmt + """ + line = 'G696969*' + stmt = UnknownStmt(line) + assert_equal(stmt.type, 'UNKNOWN') + assert_equal(stmt.line, line) + + +def test_unknownstmt_dump(): + """ Test UnknownStmt.to_gerber() + """ + lines = ('G696969*', 'M03*',) + for line in lines: + stmt = UnknownStmt(line) + assert_equal(stmt.to_gerber(), line) + -- cgit From 152fca07685d6f96f5e5bad723f1f62de99d8b7d Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Fri, 10 Oct 2014 17:56:15 -0400 Subject: Add layer names file --- gerber/layer_names.py | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 gerber/layer_names.py (limited to 'gerber') diff --git a/gerber/layer_names.py b/gerber/layer_names.py new file mode 100644 index 0000000..372c40d --- /dev/null +++ b/gerber/layer_names.py @@ -0,0 +1,51 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# copyright 2014 Hamilton Kibbe +# +# 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. + +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', 'ts', 'skt', ] +top_silk_name = ['sst01', 'topsilk, 'silk', 'slk', 'sst', ] + +bottom_silk_ext = ['gbo, 'bs', 'skb', ] +bottom_silk_name = ['sst', 'bsilk', 'ssb', 'botsilk', ] + +top_mask_ext = ['gts', 'tmk', 'smt', 'tr', ] +top_mask_name = ['sm01', 'cmask', 'tmask', 'mask1', 'maskcom', 'topmask', + 'mst', ] + +bottom_mask_ext = ['gbs', 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', ] -- cgit From 76c03a55c91addff71339d80cf17560926f1580b Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Fri, 10 Oct 2014 20:36:38 -0400 Subject: Working region fills and level polarity. Renders Altium-generated gerbers like a champ! --- gerber/gerber_statements.py | 16 ++++++++-------- gerber/render/render.py | 23 +++++++++++++++-------- gerber/render/svgwrite_backend.py | 22 ++++++++++++++++++++-- 3 files changed, 43 insertions(+), 18 deletions(-) (limited to 'gerber') diff --git a/gerber/gerber_statements.py b/gerber/gerber_statements.py index 9072b58..a22eae2 100644 --- a/gerber/gerber_statements.py +++ b/gerber/gerber_statements.py @@ -17,9 +17,9 @@ __all__ = ['FSParamStmt', 'MOParamStmt', 'IPParamStmt', 'OFParamStmt', class Statement(object): """ Gerber statement Base class - + The statement class provides a type attribute. - + Parameters ---------- type : string @@ -27,7 +27,7 @@ class Statement(object): Attributes ---------- - type : string + type : string String identifying the statement type. """ def __init__(self, stype): @@ -45,9 +45,9 @@ class Statement(object): class ParamStmt(Statement): """ Gerber parameter statement Base class - + The parameter statement class provides a parameter type attribute. - + Parameters ---------- param : string @@ -55,7 +55,7 @@ class ParamStmt(Statement): Attributes ---------- - param : string + param : string Parameter type code """ def __init__(self, param): @@ -260,7 +260,7 @@ class LPParamStmt(ParamStmt): @classmethod def from_dict(cls, stmt_dict): - param = stmt_dict.get('lp') + param = stmt_dict['param'] lp = 'clear' if stmt_dict.get('lp') == 'C' else 'dark' return cls(param, lp) @@ -667,6 +667,6 @@ class UnknownStmt(Statement): def __init__(self, line): Statement.__init__(self, "UNKNOWN") self.line = line - + def to_gerber(self): return self.line diff --git a/gerber/render/render.py b/gerber/render/render.py index e76aed1..48a53f8 100644 --- a/gerber/render/render.py +++ b/gerber/render/render.py @@ -96,7 +96,7 @@ class GerberContext(object): self.level_polarity = 'dark' self.region_mode = 'off' self.quadrant_mode = 'multi-quadrant' - + self.step_and_repeat = (1, 1, 0, 0) 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) @@ -415,6 +415,12 @@ class GerberContext(object): """ pass + def region_contour(self, x, y): + pass + + def fill_region(self): + pass + def evaluate(self, stmt): """ Evaluate Gerber statement and update image accordingly. @@ -450,7 +456,7 @@ class GerberContext(object): def _evaluate_mode(self, stmt): if stmt.type == 'RegionMode': if self.region_mode == 'on' and stmt.mode == 'off': - self._fill_region() + self.fill_region() self.region_mode = stmt.mode elif stmt.type == 'QuadrantMode': self.quadrant_mode = stmt.mode @@ -460,11 +466,11 @@ class GerberContext(object): self.set_coord_format(stmt.zero_suppression, stmt.format, stmt.notation) self.set_coord_notation(stmt.notation) - elif stmt.param == "MO:": + elif stmt.param == "MO": self.set_coord_unit(stmt.mode) - elif stmt.param == "IP:": + elif stmt.param == "IP": self.set_image_polarity(stmt.ip) - elif stmt.param == "LP:": + elif stmt.param == "LP": self.set_level_polarity(stmt.lp) elif stmt.param == "AD": self.define_aperture(stmt.d, stmt.shape, stmt.modifiers) @@ -477,7 +483,10 @@ class GerberContext(object): self.direction = ('clockwise' if stmt.function in ('G02', 'G2') else 'counterclockwise') if stmt.op == "D01": - self.stroke(stmt.x, stmt.y, stmt.i, stmt.j) + if self.region_mode == 'on': + self.region_contour(stmt.x, stmt.y) + else: + self.stroke(stmt.x, stmt.y, stmt.i, stmt.j) elif stmt.op == "D02": self.move(stmt.x, stmt.y) elif stmt.op == "D03": @@ -486,5 +495,3 @@ class GerberContext(object): def _evaluate_aperture(self, stmt): self.set_aperture(stmt.d) - def _fill_region(self): - pass diff --git a/gerber/render/svgwrite_backend.py b/gerber/render/svgwrite_backend.py index 7570c84..886b4f8 100644 --- a/gerber/render/svgwrite_backend.py +++ b/gerber/render/svgwrite_backend.py @@ -118,6 +118,7 @@ class GerberSvgContext(GerberContext): self.apertures = {} self.dwg = svgwrite.Drawing() self.background = False + self.region_path = None def set_bounds(self, bounds): xbounds, ybounds = bounds @@ -125,7 +126,7 @@ class GerberSvgContext(GerberContext): if not self.background: self.dwg.add(self.dwg.rect(insert=(SCALE * xbounds[0], -SCALE * ybounds[1]), - size=size, fill="black")) + size=size, fill=convert_color(self.background_color))) self.background = True def define_aperture(self, d, shape, modifiers): @@ -173,7 +174,8 @@ class GerberSvgContext(GerberContext): ap = self.apertures.get(self.aperture, None) if ap is None: return - color = (convert_color(self.color) if self.level_polarity == 'dark' + + color = (convert_color(self.color) if self.level_polarity == 'dark' else convert_color(self.background_color)) for shape in ap.flash(self, x, y, color): self.dwg.add(shape) @@ -185,5 +187,21 @@ class GerberSvgContext(GerberContext): fill=convert_color(self.drill_color)) self.dwg.add(hit) + def region_contour(self, x, y): + super(GerberSvgContext, self).region_contour(x, y) + x, y = self.resolve(x, y) + color = (convert_color(self.color) if self.level_polarity == 'dark' + else convert_color(self.background_color)) + if self.region_path is None: + self.region_path = self.dwg.path(d = 'M %f, %f' % + (self.x*SCALE, -self.y*SCALE), + fill = color, stroke = 'none') + self.region_path.push('L %f, %f' % (x*SCALE, -y*SCALE)) + self.move(x, y, resolve=False) + + def fill_region(self): + self.dwg.add(self.region_path) + self.region_path = None + def dump(self, filename): self.dwg.saveas(filename) -- cgit From ae3bbff8b0849e0b49dc139396d7f8c57334a7b8 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Fri, 10 Oct 2014 23:07:51 -0400 Subject: Added excellon format detection --- gerber/cam.py | 124 ++++++++++++++++++++++++++ gerber/cnc.py | 119 ------------------------- gerber/excellon.py | 155 +++++++++++++++++++++++++++++++-- gerber/gerber.py | 4 +- gerber/gerber_statements.py | 7 +- gerber/tests/test_cam.py | 50 +++++++++++ gerber/tests/test_cnc.py | 50 ----------- gerber/tests/test_gerber_statements.py | 76 ++++++++++++---- 8 files changed, 392 insertions(+), 193 deletions(-) create mode 100644 gerber/cam.py delete mode 100644 gerber/cnc.py create mode 100644 gerber/tests/test_cam.py delete mode 100644 gerber/tests/test_cnc.py (limited to 'gerber') diff --git a/gerber/cam.py b/gerber/cam.py new file mode 100644 index 0000000..e7a49d1 --- /dev/null +++ b/gerber/cam.py @@ -0,0 +1,124 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# copyright 2014 Hamilton Kibbe +# +# 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 +============ +**AM file classes** + +This module provides common base classes for Excellon/Gerber CNC files +""" + + +class FileSettings(object): + """ CAM File Settings + + Provides a common representation of gerber/excellon file settings + """ + def __init__(self, notation='absolute', units='inch', + zero_suppression='trailing', format=(2, 5)): + if notation not in ['absolute', 'incremental']: + raise ValueError('Notation must be either absolute or incremental') + self.notation = notation + + if units not in ['inch', 'metric']: + 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 len(format) != 2: + raise ValueError('Format must be a tuple(n=2) of integers') + self.format = format + + def __getitem__(self, key): + if key == 'notation': + return self.notation + elif key == 'units': + return self.units + elif key == 'zero_suppression': + return self.zero_suppression + elif key == 'format': + return self.format + else: + raise KeyError() + + +class CamFile(object): + """ Base class for Gerber/Excellon files. + + Provides a common set of settings parameters. + + Parameters + ---------- + settings : FileSettings + The current file configuration. + + filename : string + Name of the file that this CamFile represents. + + layer_name : string + Name of the PCB layer that the file represents + + Attributes + ---------- + settings : FileSettings + File settings as a FileSettings object + + notation : string + File notation setting. May be either 'absolute' or 'incremental' + + units : string + File units setting. May be 'inch' or 'metric' + + zero_suppression : string + File zero-suppression setting. May be either 'leading' or 'trailling' + + format : tuple (, ) + File decimal representation format as a tuple of (integer digits, + decimal digits) + """ + + def __init__(self, statements=None, settings=None, filename=None, + layer_name=None): + if settings is not None: + self.notation = settings['notation'] + self.units = settings['units'] + self.zero_suppression = settings['zero_suppression'] + self.format = settings['format'] + else: + self.notation = 'absolute' + self.units = 'inch' + self.zero_suppression = 'trailing' + self.format = (2, 5) + self.statements = statements if statements is not None else [] + self.filename = filename + self.layer_name = layer_name + + @property + def settings(self): + """ File settings + + Returns + ------- + settings : FileSettings (dict-like) + A FileSettings object with the specified configuration. + """ + return FileSettings(self.notation, self.units, self.zero_suppression, + self.format) diff --git a/gerber/cnc.py b/gerber/cnc.py deleted file mode 100644 index d17517a..0000000 --- a/gerber/cnc.py +++ /dev/null @@ -1,119 +0,0 @@ -#! /usr/bin/env python -# -*- coding: utf-8 -*- - -# copyright 2014 Hamilton Kibbe -# -# 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.cnc -============ -**CNC file classes** - -This module provides common base classes for Excellon/Gerber CNC files -""" - - -class FileSettings(object): - """ CNC File Settings - - Provides a common representation of gerber/excellon file settings - """ - def __init__(self, notation='absolute', units='inch', - zero_suppression='trailing', format=(2, 5)): - if notation not in ['absolute', 'incremental']: - raise ValueError('Notation must be either absolute or incremental') - self.notation = notation - - if units not in ['inch', 'metric']: - 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 len(format) != 2: - raise ValueError('Format must be a tuple(n=2) of integers') - self.format = format - - def __getitem__(self, key): - if key == 'notation': - return self.notation - elif key == 'units': - return self.units - elif key == 'zero_suppression': - return self.zero_suppression - elif key == 'format': - return self.format - else: - raise KeyError() - - -class CncFile(object): - """ Base class for Gerber/Excellon files. - - Provides a common set of settings parameters. - - Parameters - ---------- - settings : FileSettings - The current file configuration. - - filename : string - Name of the file that this CncFile represents. - - Attributes - ---------- - settings : FileSettings - File settings as a FileSettings object - - notation : string - File notation setting. May be either 'absolute' or 'incremental' - - units : string - File units setting. May be 'inch' or 'metric' - - zero_suppression : string - File zero-suppression setting. May be either 'leading' or 'trailling' - - format : tuple (, ) - File decimal representation format as a tuple of (integer digits, - decimal digits) - """ - - def __init__(self, statements=None, settings=None, filename=None): - if settings is not None: - self.notation = settings['notation'] - self.units = settings['units'] - self.zero_suppression = settings['zero_suppression'] - self.format = settings['format'] - else: - self.notation = 'absolute' - self.units = 'inch' - self.zero_suppression = 'trailing' - self.format = (2, 5) - self.statements = statements if statements is not None else [] - self.filename = filename - - @property - def settings(self): - """ File settings - - Returns - ------- - settings : FileSettings (dict-like) - A FileSettings object with the specified configuration. - """ - return FileSettings(self.notation, self.units, self.zero_suppression, - self.format) diff --git a/gerber/excellon.py b/gerber/excellon.py index 4166de6..1a498dc 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -24,16 +24,22 @@ This module provides Excellon file classes and parsing utilities from .excellon_statements import * -from .cnc import CncFile, FileSettings +from .cam import CamFile, FileSettings +import math def read(filename): """ Read data from filename and return an ExcellonFile """ - return ExcellonParser().parse(filename) + detected_settings = detect_excellon_format(filename) + settings = FileSettings(**detected_settings) + zeros = '' + print('Detected %d:%d format with %s zero suppression' % + (settings.format[0], settings.format[1], settings.zero_suppression)) + return ExcellonParser(settings).parse(filename) -class ExcellonFile(CncFile): +class ExcellonFile(CamFile): """ A class representing a single excellon file The ExcellonFile class represents a single excellon file. @@ -83,8 +89,13 @@ class ExcellonFile(CncFile): class ExcellonParser(object): """ Excellon File Parser + + Parameters + ---------- + settings : FileSettings or dict-like + Excellon file settings to use when interpreting the excellon file. """ - def __init__(self): + def __init__(self, settings=None): self.notation = 'absolute' self.units = 'inch' self.zero_suppression = 'trailing' @@ -95,7 +106,38 @@ class ExcellonParser(object): self.hits = [] self.active_tool = None self.pos = [0., 0.] - + if settings is not None: + self.units = settings['units'] + self.zero_suppression = settings['zero_suppression'] + 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)] + + @property + def bounds(self): + xmin = ymin = 100000000000 + xmax = ymax = -100000000000 + for x, y in self.coordinates: + if x is not None: + xmin = x if x < xmin else xmin + xmax = x if x > xmax else xmax + if y is not None: + ymin = y if y < ymin else ymin + ymax = y if y > ymax else ymax + return ((xmin, xmax), (ymin, ymax)) + + @property + def hole_sizes(self): + return [stmt.diameter for stmt in self.statements if isinstance(stmt, ExcellonTool)] + + @property + def hole_count(self): + return len(self.hits) + def parse(self, filename): with open(filename, 'r') as f: for line in f: @@ -194,3 +236,106 @@ class ExcellonParser(object): return FileSettings(units=self.units, format=self.format, zero_suppression=self.zero_suppression, notation=self.notation) + + +def detect_excellon_format(filename): + """ 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. + + Returns + ------- + settings : dict + Detected excellon file settings. Keys are + - `format`: decimal format as tuple (, ) + - `zero_suppression`: zero suppression, 'leading' or 'trailing' + """ + results = {} + detected_zeros = None + detected_format = None + zs_options = ('leading', 'trailing', ) + format_options = ((2, 4), (2, 5), (3, 3),) + + # Check for obvious clues: + p = ExcellonParser() + p.parse(filename) + + # Get zero_suppression from a unit statement + zero_statements = [stmt.zero_suppression for stmt in p.statements + if isinstance(stmt, UnitStmt)] + + # get format from altium comment + format_comment = [stmt.comment for stmt in p.statements + if isinstance(stmt, CommentStmt) + and 'FILE_FORMAT' in stmt.comment] + + detected_format = (tuple([int(val) for val in + 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} + + # 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,) + + # Brute force all remaining options, and pick the best looking one... + for zs in zs_options: + for fmt in format_options: + key = (fmt, zs) + settings = FileSettings(zero_suppression=zs, format=fmt) + try: + p = ExcellonParser(settings) + p.parse(filename) + size = tuple([t[1] - t[0] for t in p.bounds]) + hole_area = 0.0 + for hit in p.hits: + tool = hit[0] + hole_area += math.pow(math.pi * tool.diameter, 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()) + if len(formats) == 1: + detected_format = formats.pop() + if len(zeros) == 1: + detected_zeros = zeros.pop() + + # 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} + + # Otherwise score each option and pick the best candidate + else: + scores = {} + for key in results.keys(): + size, count, diameter = results[key] + scores[key] = _layer_size_score(size, count, diameter) + minscore = min(scores.values()) + for key in scores.iterkeys(): + if scores[key] == minscore: + return {'format': key[0], 'zero_suppression': key[1]} + + +def _layer_size_score(size, hole_count, hole_area): + """ Heuristic used for determining the correct file number interpretation. + Lower is better. + """ + board_area = size[0] * size[1] + hole_percentage = hole_area / board_area + hole_score = (hole_percentage - 0.25) ** 2 + size_score = (board_area - 8) **2 + return hole_score * size_score + \ No newline at end of file diff --git a/gerber/gerber.py b/gerber/gerber.py index 4ce261d..215b970 100644 --- a/gerber/gerber.py +++ b/gerber/gerber.py @@ -27,7 +27,7 @@ This module provides an RS-274-X class and parser import re import json from .gerber_statements import * -from .cnc import CncFile, FileSettings +from .cam import CamFile, FileSettings @@ -38,7 +38,7 @@ def read(filename): return GerberParser().parse(filename) -class GerberFile(CncFile): +class GerberFile(CamFile): """ A class representing a single gerber file The GerberFile class represents a single gerber file. diff --git a/gerber/gerber_statements.py b/gerber/gerber_statements.py index a22eae2..218074f 100644 --- a/gerber/gerber_statements.py +++ b/gerber/gerber_statements.py @@ -133,7 +133,12 @@ class MOParamStmt(ParamStmt): @classmethod def from_dict(cls, stmt_dict): param = stmt_dict.get('param') - mo = 'inch' if stmt_dict.get('mo') == 'IN' else 'metric' + if stmt_dict.get('mo').lower() == 'in': + mo = 'inch' + elif stmt_dict.get('mo').lower() == 'mm': + mo = 'metric' + else: + mo = None return cls(param, mo) def __init__(self, param, mo): diff --git a/gerber/tests/test_cam.py b/gerber/tests/test_cam.py new file mode 100644 index 0000000..4af1984 --- /dev/null +++ b/gerber/tests/test_cam.py @@ -0,0 +1,50 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# Author: Hamilton Kibbe + +from ..cam import CamFile, FileSettings +from tests import * + + +def test_smoke_filesettings(): + """ Smoke test FileSettings class + """ + fs = FileSettings() + + +def test_filesettings_defaults(): + """ Test FileSettings default values + """ + fs = FileSettings() + assert_equal(fs.format, (2, 5)) + assert_equal(fs.notation, 'absolute') + assert_equal(fs.zero_suppression, 'trailing') + assert_equal(fs.units, 'inch') + + +def test_filesettings_dict(): + """ Test FileSettings Dict + """ + fs = FileSettings() + assert_equal(fs['format'], (2, 5)) + assert_equal(fs['notation'], 'absolute') + assert_equal(fs['zero_suppression'], 'trailing') + assert_equal(fs['units'], 'inch') + + +def test_filesettings_assign(): + """ Test FileSettings attribute assignment + """ + fs = FileSettings() + fs.units = 'test' + fs.notation = 'test' + fs.zero_suppression = 'test' + fs.format = 'test' + assert_equal(fs.units, 'test') + assert_equal(fs.notation, 'test') + assert_equal(fs.zero_suppression, 'test') + assert_equal(fs.format, 'test') + +def test_smoke_camfile(): + cf = CamFile diff --git a/gerber/tests/test_cnc.py b/gerber/tests/test_cnc.py deleted file mode 100644 index ace047e..0000000 --- a/gerber/tests/test_cnc.py +++ /dev/null @@ -1,50 +0,0 @@ -#! /usr/bin/env python -# -*- coding: utf-8 -*- - -# Author: Hamilton Kibbe - -from ..cnc import CncFile, FileSettings -from tests import * - - -def test_smoke_filesettings(): - """ Smoke test FileSettings class - """ - fs = FileSettings() - - -def test_filesettings_defaults(): - """ Test FileSettings default values - """ - fs = FileSettings() - assert_equal(fs.format, (2, 5)) - assert_equal(fs.notation, 'absolute') - assert_equal(fs.zero_suppression, 'trailing') - assert_equal(fs.units, 'inch') - - -def test_filesettings_dict(): - """ Test FileSettings Dict - """ - fs = FileSettings() - assert_equal(fs['format'], (2, 5)) - assert_equal(fs['notation'], 'absolute') - assert_equal(fs['zero_suppression'], 'trailing') - assert_equal(fs['units'], 'inch') - - -def test_filesettings_assign(): - """ Test FileSettings attribute assignment - """ - fs = FileSettings() - fs.units = 'test' - fs.notation = 'test' - fs.zero_suppression = 'test' - fs.format = 'test' - assert_equal(fs.units, 'test') - assert_equal(fs.notation, 'test') - assert_equal(fs.zero_suppression, 'test') - assert_equal(fs.format, 'test') - - def test_smoke_cncfile(): - pass diff --git a/gerber/tests/test_gerber_statements.py b/gerber/tests/test_gerber_statements.py index 9e73fd4..a463c9d 100644 --- a/gerber/tests/test_gerber_statements.py +++ b/gerber/tests/test_gerber_statements.py @@ -8,7 +8,7 @@ from ..gerber_statements import * def test_FSParamStmt_factory(): - """ Test FSParamStruct factory correctly handles parameters + """ Test FSParamStruct factory """ stmt = {'param': 'FS', 'zero': 'L', 'notation': 'A', 'x': '27'} fs = FSParamStmt.from_dict(stmt) @@ -24,6 +24,18 @@ def test_FSParamStmt_factory(): assert_equal(fs.notation, 'incremental') assert_equal(fs.format, (2, 7)) +def test_FSParamStmt(): + """ Test FSParamStmt initialization + """ + param = 'FS' + zeros = 'trailing' + notation = 'absolute' + fmt = (2, 5) + stmt = FSParamStmt(param, zeros, notation, fmt) + assert_equal(stmt.param, param) + assert_equal(stmt.zero_suppression, zeros) + assert_equal(stmt.notation, notation) + assert_equal(stmt.format, fmt) def test_FSParamStmt_dump(): """ Test FSParamStmt to_gerber() @@ -38,17 +50,31 @@ def test_FSParamStmt_dump(): def test_MOParamStmt_factory(): - """ Test MOParamStruct factory correctly handles parameters + """ Test MOParamStruct factory """ - stmt = {'param': 'MO', 'mo': 'IN'} - mo = MOParamStmt.from_dict(stmt) - assert_equal(mo.param, 'MO') - assert_equal(mo.mode, 'inch') + stmts = [{'param': 'MO', 'mo': 'IN'}, {'param': 'MO', 'mo': 'in'}, ] + for stmt in stmts: + mo = MOParamStmt.from_dict(stmt) + assert_equal(mo.param, 'MO') + assert_equal(mo.mode, 'inch') + + stmts = [{'param': 'MO', 'mo': 'MM'}, {'param': 'MO', 'mo': 'mm'}, ] + for stmt in stmts: + mo = MOParamStmt.from_dict(stmt) + assert_equal(mo.param, 'MO') + assert_equal(mo.mode, 'metric') + +def test_MOParamStmt(): + """ Test MOParamStmt initialization + """ + param = 'MO' + mode = 'inch' + stmt = MOParamStmt(param, mode) + assert_equal(stmt.param, param) - stmt = {'param': 'MO', 'mo': 'MM'} - mo = MOParamStmt.from_dict(stmt) - assert_equal(mo.param, 'MO') - assert_equal(mo.mode, 'metric') + for mode in ['inch', 'metric']: + stmt = MOParamStmt(param, mode) + assert_equal(stmt.mode, mode) def test_MOParamStmt_dump(): @@ -64,7 +90,7 @@ def test_MOParamStmt_dump(): def test_IPParamStmt_factory(): - """ Test IPParamStruct factory correctly handles parameters + """ Test IPParamStruct factory """ stmt = {'param': 'IP', 'ip': 'POS'} ip = IPParamStmt.from_dict(stmt) @@ -74,6 +100,15 @@ def test_IPParamStmt_factory(): ip = IPParamStmt.from_dict(stmt) assert_equal(ip.ip, 'negative') +def test_IPParamStmt(): + """ Test IPParamStmt initialization + """ + param = 'IP' + for ip in ['positive', 'negative']: + stmt = IPParamStmt(param, ip) + assert_equal(stmt.param, param) + assert_equal(stmt.ip, ip) + def test_IPParamStmt_dump(): """ Test IPParamStmt to_gerber() @@ -88,14 +123,23 @@ def test_IPParamStmt_dump(): def test_OFParamStmt_factory(): - """ Test OFParamStmt factory correctly handles parameters + """ 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 + """ + param = 'OF' + for val in [0.0, -3.4567]: + stmt = OFParamStmt(param, val, val) + assert_equal(stmt.param, param) + assert_equal(stmt.a, val) + assert_equal(stmt.b, val) + def test_OFParamStmt_dump(): """ Test OFParamStmt to_gerber() """ @@ -105,7 +149,7 @@ def test_OFParamStmt_dump(): def test_LPParamStmt_factory(): - """ Test LPParamStmt factory correctly handles parameters + """ Test LPParamStmt factory """ stmt = {'param': 'LP', 'lp': 'C'} lp = LPParamStmt.from_dict(stmt) @@ -128,7 +172,7 @@ def test_LPParamStmt_dump(): def test_INParamStmt_factory(): - """ Test INParamStmt factory correctly handles parameters + """ Test INParamStmt factory """ stmt = {'param': 'IN', 'name': 'test'} inp = INParamStmt.from_dict(stmt) @@ -143,7 +187,7 @@ def test_INParamStmt_dump(): def test_LNParamStmt_factory(): - """ Test LNParamStmt factory correctly handles parameters + """ Test LNParamStmt factory """ stmt = {'param': 'LN', 'name': 'test'} lnp = LNParamStmt.from_dict(stmt) -- cgit From 62c689be172a7a06d76fd4b69c3443f3ec053765 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Sat, 11 Oct 2014 13:12:21 -0400 Subject: Doc update --- gerber/__main__.py | 1 + gerber/excellon.py | 28 ++++++++++++++++++++++------ gerber/gerber.py | 25 +++++++++++++++++++------ 3 files changed, 42 insertions(+), 12 deletions(-) (limited to 'gerber') diff --git a/gerber/__main__.py b/gerber/__main__.py index ab0f377..1af4c0f 100644 --- a/gerber/__main__.py +++ b/gerber/__main__.py @@ -30,6 +30,7 @@ if __name__ == '__main__': print "parsing %s" % filename gerberfile = read(filename) gerberfile.render(ctx) + ctx.set_color(tuple([color * 0.4 for color in ctx.color])) print('Saving image to test.svg') ctx.dump('test.svg') diff --git a/gerber/excellon.py b/gerber/excellon.py index 1a498dc..f5d6c29 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -30,6 +30,15 @@ import math def read(filename): """ Read data from filename and return an ExcellonFile + Parameters + ---------- + filename : string + Filename of file to parse + + Returns + ------- + file : :class:`gerber.excellon.ExcellonFile` + An ExcellonFile created from the specified file. """ detected_settings = detect_excellon_format(filename) settings = FileSettings(**detected_settings) @@ -75,6 +84,14 @@ class ExcellonFile(CamFile): def render(self, ctx, filename=None): """ Generate image of file + + Parameters + ---------- + ctx : :class:`gerber.render.GerberContext` + GerberContext subclass used for rendering the image + + filename : string + If provided, the rendered image will be saved to `filename` """ for tool, pos in self.hits: ctx.drill(pos[0], pos[1], tool.diameter) @@ -89,7 +106,7 @@ class ExcellonFile(CamFile): class ExcellonParser(object): """ Excellon File Parser - + Parameters ---------- settings : FileSettings or dict-like @@ -129,15 +146,15 @@ class ExcellonParser(object): ymin = y if y < ymin else ymin ymax = y if y > ymax else ymax return ((xmin, xmax), (ymin, ymax)) - + @property def hole_sizes(self): return [stmt.diameter for stmt in self.statements if isinstance(stmt, ExcellonTool)] - + @property def hole_count(self): return len(self.hits) - + def parse(self, filename): with open(filename, 'r') as f: for line in f: @@ -316,7 +333,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} - + # Otherwise score each option and pick the best candidate else: scores = {} @@ -338,4 +355,3 @@ def _layer_size_score(size, hole_count, hole_area): hole_score = (hole_percentage - 0.25) ** 2 size_score = (board_area - 8) **2 return hole_score * size_score - \ No newline at end of file diff --git a/gerber/gerber.py b/gerber/gerber.py index 215b970..335b443 100644 --- a/gerber/gerber.py +++ b/gerber/gerber.py @@ -15,12 +15,7 @@ # 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 File module -================== -**Gerber File module** - -This module provides an RS-274-X class and parser +""" This module provides an RS-274-X class and parser. """ @@ -34,6 +29,16 @@ from .cam import CamFile, FileSettings def read(filename): """ Read data from filename and return a GerberFile + + Parameters + ---------- + filename : string + Filename of file to parse + + Returns + ------- + file : :class:`gerber.gerber.GerberFile` + A GerberFile created from the specified file. """ return GerberParser().parse(filename) @@ -113,6 +118,14 @@ class GerberFile(CamFile): def render(self, ctx, filename=None): """ Generate image of layer. + + Parameters + ---------- + ctx : :class:`GerberContext` + GerberContext subclass used for rendering the image + + filename : string + If provided, the rendered image will be saved to `filename` """ ctx.set_bounds(self.bounds) for statement in self.statements: -- cgit From 8c5c7ec8bbc8a074884ef04b566f9c0ecd6e78bb Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Sun, 12 Oct 2014 12:38:40 -0400 Subject: update docs and example images --- gerber/__main__.py | 1 - gerber/common.py | 4 +- gerber/excellon.py | 3 +- gerber/gerber.py | 332 -------------------------------------- gerber/render/render.py | 19 ++- gerber/render/svgwrite_backend.py | 7 + gerber/rs274x.py | 327 +++++++++++++++++++++++++++++++++++++ 7 files changed, 356 insertions(+), 337 deletions(-) delete mode 100644 gerber/gerber.py create mode 100644 gerber/rs274x.py (limited to 'gerber') diff --git a/gerber/__main__.py b/gerber/__main__.py index 1af4c0f..ab0f377 100644 --- a/gerber/__main__.py +++ b/gerber/__main__.py @@ -30,7 +30,6 @@ if __name__ == '__main__': print "parsing %s" % filename gerberfile = read(filename) gerberfile.render(ctx) - ctx.set_color(tuple([color * 0.4 for color in ctx.color])) print('Saving image to test.svg') ctx.dump('test.svg') diff --git a/gerber/common.py b/gerber/common.py index 0092ec8..6e8c862 100644 --- a/gerber/common.py +++ b/gerber/common.py @@ -30,12 +30,12 @@ def read(filename): CncFile object representing the file, either GerberFile or ExcellonFile. Returns None if file is not an Excellon or Gerber file. """ - import gerber + import rs274x import excellon from utils import detect_file_format fmt = detect_file_format(filename) if fmt == 'rs274x': - return gerber.read(filename) + return rs274x.read(filename) elif fmt == 'excellon': return excellon.read(filename) else: diff --git a/gerber/excellon.py b/gerber/excellon.py index f5d6c29..13aacc6 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -39,6 +39,7 @@ def read(filename): ------- file : :class:`gerber.excellon.ExcellonFile` An ExcellonFile created from the specified file. + """ detected_settings = detect_excellon_format(filename) settings = FileSettings(**detected_settings) @@ -317,7 +318,7 @@ def detect_excellon_format(filename): hole_area = 0.0 for hit in p.hits: tool = hit[0] - hole_area += math.pow(math.pi * tool.diameter, 2) + hole_area += math.pow(math.pi * tool.diameter / 2., 2) results[key] = (size, p.hole_count, hole_area) except: pass diff --git a/gerber/gerber.py b/gerber/gerber.py deleted file mode 100644 index 335b443..0000000 --- a/gerber/gerber.py +++ /dev/null @@ -1,332 +0,0 @@ -#! /usr/bin/env python -# -*- coding: utf-8 -*- - -# copyright 2014 Hamilton Kibbe -# Modified from parser.py by Paulo Henrique Silva -# -# 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 an RS-274-X class and parser. -""" - - -import re -import json -from .gerber_statements import * -from .cam import CamFile, FileSettings - - - - -def read(filename): - """ Read data from filename and return a GerberFile - - Parameters - ---------- - filename : string - Filename of file to parse - - Returns - ------- - file : :class:`gerber.gerber.GerberFile` - A GerberFile created from the specified file. - """ - return GerberParser().parse(filename) - - -class GerberFile(CamFile): - """ A class representing a single gerber file - - The GerberFile class represents a single gerber file. - - Parameters - ---------- - statements : list - list of gerber file statements - - settings : dict - Dictionary of gerber file settings - - filename : string - Filename of the source gerber file - - Attributes - ---------- - comments: list of strings - List of comments contained in the gerber file. - - size : tuple, (, ) - Size in [self.units] of the layer described by the gerber file. - - bounds: tuple, ((, ), (, )) - boundaries of the layer described by the gerber file. - `bounds` is stored as ((min x, max x), (min y, max y)) - - """ - def __init__(self, statements, settings, filename=None): - super(GerberFile, self).__init__(statements, settings, filename) - - @property - def comments(self): - return [comment.comment for comment in self.statements - if isinstance(comment, CommentStmt)] - - @property - def size(self): - xbounds, ybounds = self.bounds - return (xbounds[1] - xbounds[0], ybounds[1] - ybounds[0]) - - @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)]: - if stmt.x is not None and stmt.x < xbounds[0]: - xbounds[0] = stmt.x - if stmt.x is not None and stmt.x > xbounds[1]: - xbounds[1] = stmt.x - if stmt.i is not None and stmt.i < xbounds[0]: - xbounds[0] = stmt.i - if stmt.i is not None and stmt.i > xbounds[1]: - xbounds[1] = stmt.i - if stmt.y is not None and stmt.y < ybounds[0]: - ybounds[0] = stmt.y - if stmt.y is not None and stmt.y > ybounds[1]: - ybounds[1] = stmt.y - if stmt.j is not None and stmt.j < ybounds[0]: - ybounds[0] = stmt.j - if stmt.j is not None and stmt.j > ybounds[1]: - ybounds[1] = stmt.j - return (xbounds, ybounds) - - def write(self, filename): - """ Write data out to a gerber file - """ - with open(filename, 'w') as f: - for statement in self.statements: - f.write(statement.to_gerber()) - - def render(self, ctx, filename=None): - """ Generate image of layer. - - Parameters - ---------- - ctx : :class:`GerberContext` - GerberContext subclass used for rendering the image - - filename : string - If provided, the rendered image will be saved to `filename` - """ - ctx.set_bounds(self.bounds) - for statement in self.statements: - ctx.evaluate(statement) - if filename is not None: - ctx.dump(filename) - - -class GerberParser(object): - """ GerberParser - """ - NUMBER = r"[\+-]?\d+" - DECIMAL = r"[\+-]?\d+([.]?\d+)?" - STRING = r"[a-zA-Z0-9_+\-/!?<>”’(){}.\|&@# :]+" - NAME = "[a-zA-Z_$][a-zA-Z_$0-9]+" - FUNCTION = r"G\d{2}" - - COORD_OP = r"D[0]?[123]" - - FS = r"(?PFS)(?P(L|T))?(?P(A|I))X(?P[0-7][0-7])Y(?P[0-7][0-7])" - MO = r"(?PMO)(?P(MM|IN))" - IP = r"(?PIP)(?P(POS|NEG))" - LP = r"(?PLP)(?P(D|C))" - AD_CIRCLE = r"(?PAD)D(?P\d+)(?PC)[,](?P[^,]*)" - AD_RECT = r"(?PAD)D(?P\d+)(?PR)[,](?P[^,]*)" - AD_OBROUND = r"(?PAD)D(?P\d+)(?PO)[,](?P[^,]*)" - AD_POLY = r"(?PAD)D(?P\d+)(?PP)[,](?P[^,]*)" - AD_MACRO = r"(?PAD)D(?P\d+)+(?P{name})[,](?P[^,]*)".format(name=NAME) - AM = r"(?PAM)(?P{name})\*(?P.*)".format(name=NAME) - - # begin deprecated - OF = r"(?POF)(A(?P{decimal}))?(B(?P{decimal}))?".format(decimal=DECIMAL) - IN = r"(?PIN)(?P.*)" - LN = r"(?PLN)(?P.*)" - # end deprecated - - PARAMS = (FS, MO, IP, LP, AD_CIRCLE, AD_RECT, AD_OBROUND, AD_MACRO, AD_POLY, AM, OF, IN, LN) - PARAM_STMT = [re.compile(r"%{0}\*%".format(p)) for p in PARAMS] - - COORD_STMT = re.compile(( - r"(?P{function})?" - r"(X(?P{number}))?(Y(?P{number}))?" - r"(I(?P{number}))?(J(?P{number}))?" - r"(?P{op})?\*".format(number=NUMBER, function=FUNCTION, op=COORD_OP))) - - APERTURE_STMT = re.compile(r"(G54)?D(?P\d+)\*") - - COMMENT_STMT = re.compile(r"G04(?P[^*]*)(\*)?") - - EOF_STMT = re.compile(r"(?PM02)\*") - - REGION_MODE_STMT = re.compile(r'(?PG3[67])\*') - QUAD_MODE_STMT = re.compile(r'(?PG7[45])\*') - - def __init__(self): - self.settings = FileSettings() - self.statements = [] - - def parse(self, filename): - fp = open(filename, "r") - data = fp.readlines() - - for stmt in self._parse(data): - self.statements.append(stmt) - - return GerberFile(self.statements, self.settings, filename) - - def dump_json(self): - stmts = {"statements": [stmt.__dict__ for stmt in self.statements]} - return json.dumps(stmts) - - def dump_str(self): - s = "" - for stmt in self.statements: - s += str(stmt) + "\n" - return s - - def _parse(self, data): - oldline = '' - - for i, line in enumerate(data): - line = oldline + line.strip() - - # skip empty lines - if not len(line): - continue - - # deal with multi-line parameters - if line.startswith("%") and not line.endswith("%"): - 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) = self._match_one(self.REGION_MODE_STMT, line) - if mode: - yield RegionModeStmt.from_gerber(line) - line = r - did_something = True - continue - - # Quadrant Mode - (mode, r) = self._match_one(self.QUAD_MODE_STMT, line) - if mode: - yield QuadrantModeStmt.from_gerber(line) - line = r - did_something = True - continue - - # coord - (coord, r) = self._match_one(self.COORD_STMT, line) - if coord: - yield CoordStmt.from_dict(coord, self.settings) - line = r - did_something = True - continue - - # aperture selection - (aperture, r) = self._match_one(self.APERTURE_STMT, line) - if aperture: - yield ApertureStmt(**aperture) - - did_something = True - line = r - continue - - # comment - (comment, r) = self._match_one(self.COMMENT_STMT, line) - if comment: - yield CommentStmt(comment["comment"]) - did_something = True - line = r - continue - - # parameter - (param, r) = self._match_one_from_many(self.PARAM_STMT, line) - if param: - if param["param"] == "FS": - stmt = FSParamStmt.from_dict(param) - self.settings.zero_suppression = stmt.zero_suppression - self.settings.format = stmt.format - self.settings.notation = stmt.notation - yield stmt - elif param["param"] == "MO": - 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) - 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) - else: - yield UnknownStmt(line) - did_something = True - line = r - continue - - # eof - (eof, r) = self._match_one(self.EOF_STMT, line) - if eof: - yield EofStmt() - did_something = True - 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) - oldline = line - - def _match_one(self, expr, data): - match = expr.match(data) - if match is None: - return ({}, None) - else: - return (match.groupdict(), data[match.end(0):]) - - def _match_one_from_many(self, exprs, data): - for expr in exprs: - match = expr.match(data) - if match: - return (match.groupdict(), data[match.end(0):]) - - return ({}, None) diff --git a/gerber/render/render.py b/gerber/render/render.py index 48a53f8..f7e4485 100644 --- a/gerber/render/render.py +++ b/gerber/render/render.py @@ -83,6 +83,9 @@ class GerberContext(object): background_color : tuple (, , ) Color of the background. Used when exposing areas in 'clear' level polarity mode. Format is the same as for `color`. + + alpha : float + Rendering opacity. Between 0.0 (transparent) and 1.0 (opaque.) """ def __init__(self): self.settings = {} @@ -100,7 +103,8 @@ class GerberContext(object): 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_format(self, settings): """ Set source file format. @@ -260,6 +264,19 @@ class GerberContext(object): """ 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 + def resolve(self, x, y): """ Resolve missing x or y coordinates in a coordinate command. diff --git a/gerber/render/svgwrite_backend.py b/gerber/render/svgwrite_backend.py index 886b4f8..78961da 100644 --- a/gerber/render/svgwrite_backend.py +++ b/gerber/render/svgwrite_backend.py @@ -117,6 +117,7 @@ class GerberSvgContext(GerberContext): self.apertures = {} self.dwg = svgwrite.Drawing() + self.dwg.transform = 'scale 1 -1' self.background = False self.region_path = None @@ -124,11 +125,17 @@ class GerberSvgContext(GerberContext): xbounds, ybounds = bounds size = (SCALE * (xbounds[1] - xbounds[0]), SCALE * (ybounds[1] - ybounds[0])) if not self.background: + self.dwg = svgwrite.Drawing(viewBox='%f, %f, %f, %f' % (SCALE*xbounds[0], -SCALE*ybounds[1],size[0], size[1])) self.dwg.add(self.dwg.rect(insert=(SCALE * xbounds[0], -SCALE * ybounds[1]), size=size, fill=convert_color(self.background_color))) self.background = True + def set_alpha(self, alpha): + super(GerberSvgContext, self).set_alpha(alpha) + import warnings + warnings.warn('SVG output does not support transparency') + def define_aperture(self, d, shape, modifiers): aperture = None if shape == 'C': diff --git a/gerber/rs274x.py b/gerber/rs274x.py new file mode 100644 index 0000000..4076f77 --- /dev/null +++ b/gerber/rs274x.py @@ -0,0 +1,327 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# copyright 2014 Hamilton Kibbe +# Modified from parser.py by Paulo Henrique Silva +# +# 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 an RS-274-X class and parser. +""" + + +import re +import json +from .gerber_statements import * +from .cam import CamFile, FileSettings + + + + +def read(filename): + """ Read data from filename and return a GerberFile + + Parameters + ---------- + filename : string + Filename of file to parse + + Returns + ------- + file : :class:`gerber.rs274x.GerberFile` + A GerberFile created from the specified file. + """ + return GerberParser().parse(filename) + + +class GerberFile(CamFile): + """ A class representing a single gerber file + + The GerberFile class represents a single gerber file. + + Parameters + ---------- + statements : list + list of gerber file statements + + settings : dict + Dictionary of gerber file settings + + filename : string + Filename of the source gerber file + + Attributes + ---------- + comments: list of strings + List of comments contained in the gerber file. + + size : tuple, (, ) + Size in [self.units] of the layer described by the gerber file. + + bounds: tuple, ((, ), (, )) + boundaries of the layer described by the gerber file. + `bounds` is stored as ((min x, max x), (min y, max y)) + + """ + def __init__(self, statements, settings, filename=None): + super(GerberFile, self).__init__(statements, settings, filename) + + @property + def comments(self): + return [comment.comment for comment in self.statements + if isinstance(comment, CommentStmt)] + + @property + def size(self): + xbounds, ybounds = self.bounds + return (xbounds[1] - xbounds[0], ybounds[1] - ybounds[0]) + + @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)]: + if stmt.x is not None: + if stmt.x < xbounds[0]: + xbounds[0] = stmt.x + elif stmt.x > xbounds[1]: + xbounds[1] = stmt.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) + + + def write(self, filename): + """ Write data out to a gerber file + """ + with open(filename, 'w') as f: + for statement in self.statements: + f.write(statement.to_gerber()) + + def render(self, ctx, filename=None): + """ Generate image of layer. + + Parameters + ---------- + ctx : :class:`GerberContext` + GerberContext subclass used for rendering the image + + filename : string + If provided, the rendered image will be saved to `filename` + """ + ctx.set_bounds(self.bounds) + for statement in self.statements: + ctx.evaluate(statement) + if filename is not None: + ctx.dump(filename) + + +class GerberParser(object): + """ GerberParser + """ + NUMBER = r"[\+-]?\d+" + DECIMAL = r"[\+-]?\d+([.]?\d+)?" + STRING = r"[a-zA-Z0-9_+\-/!?<>”’(){}.\|&@# :]+" + NAME = "[a-zA-Z_$][a-zA-Z_$0-9]+" + FUNCTION = r"G\d{2}" + + COORD_OP = r"D[0]?[123]" + + FS = r"(?PFS)(?P(L|T))?(?P(A|I))X(?P[0-7][0-7])Y(?P[0-7][0-7])" + MO = r"(?PMO)(?P(MM|IN))" + IP = r"(?PIP)(?P(POS|NEG))" + LP = r"(?PLP)(?P(D|C))" + AD_CIRCLE = r"(?PAD)D(?P\d+)(?PC)[,](?P[^,]*)" + AD_RECT = r"(?PAD)D(?P\d+)(?PR)[,](?P[^,]*)" + AD_OBROUND = r"(?PAD)D(?P\d+)(?PO)[,](?P[^,]*)" + AD_POLY = r"(?PAD)D(?P\d+)(?PP)[,](?P[^,]*)" + AD_MACRO = r"(?PAD)D(?P\d+)+(?P{name})[,](?P[^,]*)".format(name=NAME) + AM = r"(?PAM)(?P{name})\*(?P.*)".format(name=NAME) + + # begin deprecated + OF = r"(?POF)(A(?P{decimal}))?(B(?P{decimal}))?".format(decimal=DECIMAL) + IN = r"(?PIN)(?P.*)" + LN = r"(?PLN)(?P.*)" + # end deprecated + + PARAMS = (FS, MO, IP, LP, AD_CIRCLE, AD_RECT, AD_OBROUND, AD_MACRO, AD_POLY, AM, OF, IN, LN) + PARAM_STMT = [re.compile(r"%{0}\*%".format(p)) for p in PARAMS] + + COORD_STMT = re.compile(( + r"(?P{function})?" + r"(X(?P{number}))?(Y(?P{number}))?" + r"(I(?P{number}))?(J(?P{number}))?" + r"(?P{op})?\*".format(number=NUMBER, function=FUNCTION, op=COORD_OP))) + + APERTURE_STMT = re.compile(r"(G54)?D(?P\d+)\*") + + COMMENT_STMT = re.compile(r"G04(?P[^*]*)(\*)?") + + EOF_STMT = re.compile(r"(?PM02)\*") + + REGION_MODE_STMT = re.compile(r'(?PG3[67])\*') + QUAD_MODE_STMT = re.compile(r'(?PG7[45])\*') + + def __init__(self): + self.settings = FileSettings() + self.statements = [] + + def parse(self, filename): + fp = open(filename, "r") + data = fp.readlines() + + for stmt in self._parse(data): + self.statements.append(stmt) + + return GerberFile(self.statements, self.settings, filename) + + def dump_json(self): + stmts = {"statements": [stmt.__dict__ for stmt in self.statements]} + return json.dumps(stmts) + + def dump_str(self): + s = "" + for stmt in self.statements: + s += str(stmt) + "\n" + return s + + def _parse(self, data): + oldline = '' + + for i, line in enumerate(data): + line = oldline + line.strip() + + # skip empty lines + if not len(line): + continue + + # deal with multi-line parameters + if line.startswith("%") and not line.endswith("%"): + 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) = self._match_one(self.REGION_MODE_STMT, line) + if mode: + yield RegionModeStmt.from_gerber(line) + line = r + did_something = True + continue + + # Quadrant Mode + (mode, r) = self._match_one(self.QUAD_MODE_STMT, line) + if mode: + yield QuadrantModeStmt.from_gerber(line) + line = r + did_something = True + continue + + # coord + (coord, r) = self._match_one(self.COORD_STMT, line) + if coord: + yield CoordStmt.from_dict(coord, self.settings) + line = r + did_something = True + continue + + # aperture selection + (aperture, r) = self._match_one(self.APERTURE_STMT, line) + if aperture: + yield ApertureStmt(**aperture) + + did_something = True + line = r + continue + + # comment + (comment, r) = self._match_one(self.COMMENT_STMT, line) + if comment: + yield CommentStmt(comment["comment"]) + did_something = True + line = r + continue + + # parameter + (param, r) = self._match_one_from_many(self.PARAM_STMT, line) + if param: + if param["param"] == "FS": + stmt = FSParamStmt.from_dict(param) + self.settings.zero_suppression = stmt.zero_suppression + self.settings.format = stmt.format + self.settings.notation = stmt.notation + yield stmt + elif param["param"] == "MO": + 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) + 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) + else: + yield UnknownStmt(line) + did_something = True + line = r + continue + + # eof + (eof, r) = self._match_one(self.EOF_STMT, line) + if eof: + yield EofStmt() + did_something = True + 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) + oldline = line + + def _match_one(self, expr, data): + match = expr.match(data) + if match is None: + return ({}, None) + else: + return (match.groupdict(), data[match.end(0):]) + + def _match_one_from_many(self, exprs, data): + for expr in exprs: + match = expr.match(data) + if match: + return (match.groupdict(), data[match.end(0):]) + + return ({}, None) -- cgit From c50949e15a839ecd27a6da273ccaf1dc3a7d7853 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Mon, 13 Oct 2014 13:26:32 -0400 Subject: Add SVG transparency --- gerber/__main__.py | 9 ++++- gerber/render/svgwrite_backend.py | 72 ++++++++++++++++++++++++--------------- 2 files changed, 52 insertions(+), 29 deletions(-) (limited to 'gerber') diff --git a/gerber/__main__.py b/gerber/__main__.py index ab0f377..10da12e 100644 --- a/gerber/__main__.py +++ b/gerber/__main__.py @@ -25,11 +25,18 @@ if __name__ == '__main__': sys.exit(1) ctx = GerberSvgContext() - + ctx.set_alpha(0.95) for filename in sys.argv[1:]: print "parsing %s" % filename + if 'GTO' in filename or 'GBO' in filename: + ctx.set_color((1,1,1)) + ctx.set_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) gerberfile = read(filename) gerberfile.render(ctx) + print('Saving image to test.svg') ctx.dump('test.svg') diff --git a/gerber/render/svgwrite_backend.py b/gerber/render/svgwrite_backend.py index 78961da..15d7bd3 100644 --- a/gerber/render/svgwrite_backend.py +++ b/gerber/render/svgwrite_backend.py @@ -28,49 +28,59 @@ def convert_color(color): return 'rgb(%d, %d, %d)' % color class SvgCircle(Circle): - def line(self, ctx, x, y, color='rgb(184, 115, 51)'): - return ctx.dwg.line(start=(ctx.x * SCALE, -ctx.y * SCALE), - end=(x * SCALE, -y * SCALE), - stroke=color, - stroke_width=SCALE * self.diameter, - stroke_linecap="round") - - def arc(self, ctx, x, y, i, j, direction, color='rgb(184, 115, 51)'): + def line(self, ctx, x, y, color='rgb(184, 115, 51)', alpha=1.0): + aline = ctx.dwg.line(start=(ctx.x * SCALE, -ctx.y * SCALE), + end=(x * SCALE, -y * SCALE), + stroke=color, + stroke_width=SCALE * self.diameter, + stroke_linecap="round") + aline.stroke(opacity=alpha) + return aline + + def arc(self, ctx, x, y, i, j, direction, color='rgb(184, 115, 51)', alpha=1.0): pass - def flash(self, ctx, x, y, color='rgb(184, 115, 51)'): - return [ctx.dwg.circle(center=(x * SCALE, -y * SCALE), + def flash(self, ctx, x, y, color='rgb(184, 115, 51)', alpha=1.0): + circle = ctx.dwg.circle(center=(x * SCALE, -y * SCALE), r = SCALE * (self.diameter / 2.0), - fill=color), ] + fill=color) + circle.fill(opacity=alpha) + return [circle, ] class SvgRect(Rect): - def line(self, ctx, x, y, color='rgb(184, 115, 51)'): - return ctx.dwg.line(start=(ctx.x * SCALE, -ctx.y * SCALE), + def line(self, ctx, x, y, color='rgb(184, 115, 51)', alpha=1.0): + aline = ctx.dwg.line(start=(ctx.x * SCALE, -ctx.y * SCALE), end=(x * SCALE, -y * SCALE), stroke=color, stroke_width=2, stroke_linecap="butt") + aline.stroke(opacity=alpha) + return aline - def flash(self, ctx, x, y, color='rgb(184, 115, 51)'): + def flash(self, ctx, x, y, color='rgb(184, 115, 51)', alpha=1.0): xsize, ysize = self.size - return [ctx.dwg.rect(insert=(SCALE * (x - (xsize / 2)), + rectangle = ctx.dwg.rect(insert=(SCALE * (x - (xsize / 2)), -SCALE * (y + (ysize / 2))), size=(SCALE * xsize, SCALE * ysize), - fill=color), ] + fill=color) + rectangle.fill(opacity=alpha) + return [rectangle, ] class SvgObround(Obround): - def line(self, ctx, x, y, color='rgb(184, 115, 51)'): + def line(self, ctx, x, y, color='rgb(184, 115, 51)', alpha=1.0): pass - def flash(self, ctx, x, y, color='rgb(184, 115, 51)'): + def flash(self, ctx, x, y, color='rgb(184, 115, 51)', alpha=1.0): xsize, ysize = self.size # horizontal obround if xsize == ysize: - return [ctx.dwg.circle(center=(x * SCALE, -y * SCALE), + circle = ctx.dwg.circle(center=(x * SCALE, -y * SCALE), r = SCALE * (x / 2.0), - fill=color), ] + fill=color) + circle.fill(opacity=alpha) + return [circle, ] if xsize > ysize: rectx = xsize - ysize recty = ysize @@ -88,6 +98,9 @@ class SvgObround(Obround): -SCALE * (y + (ysize / 2.))), size=(SCALE * xsize, SCALE * ysize), fill=color) + lcircle.fill(opacity=alpha) + rcircle.fill(opacity=alpha) + rect.fill(opacity=alpha) return [lcircle, rcircle, rect, ] # Vertical obround @@ -108,6 +121,9 @@ class SvgObround(Obround): -SCALE * (y + (ysize / 2.))), size=(SCALE * xsize, SCALE * ysize), fill=color) + lcircle.fill(opacity=alpha) + ucircle.fill(opacity=alpha) + rect.fill(opacity=alpha) return [lcircle, ucircle, rect, ] @@ -131,11 +147,6 @@ class GerberSvgContext(GerberContext): size=size, fill=convert_color(self.background_color))) self.background = True - def set_alpha(self, alpha): - super(GerberSvgContext, self).set_alpha(alpha) - import warnings - warnings.warn('SVG output does not support transparency') - def define_aperture(self, d, shape, modifiers): aperture = None if shape == 'C': @@ -162,7 +173,8 @@ class GerberSvgContext(GerberContext): return color = (convert_color(self.color) if self.level_polarity == 'dark' else convert_color(self.background_color)) - self.dwg.add(ap.line(self, x, y, color)) + alpha = self.alpha if self.level_polarity == 'dark' else 1.0 + self.dwg.add(ap.line(self, x, y, color, alpha)) self.move(x, y, resolve=False) def arc(self, x, y, i, j): @@ -172,7 +184,7 @@ class GerberSvgContext(GerberContext): if ap is None: return #self.dwg.add(ap.arc(self, x, y, i, j, self.direction, - # convert_color(self.color))) + # convert_color(self.color), self.alpha)) self.move(x, y, resolve=False) def flash(self, x, y): @@ -184,7 +196,8 @@ class GerberSvgContext(GerberContext): color = (convert_color(self.color) if self.level_polarity == 'dark' else convert_color(self.background_color)) - for shape in ap.flash(self, x, y, color): + alpha = self.alpha if self.level_polarity == 'dark' else 1.0 + for shape in ap.flash(self, x, y, color, alpha): self.dwg.add(shape) self.move(x, y, resolve=False) @@ -192,6 +205,7 @@ class GerberSvgContext(GerberContext): hit = self.dwg.circle(center=(x*SCALE, -y*SCALE), r=SCALE*(diameter/2.0), fill=convert_color(self.drill_color)) + #hit.fill(opacity=self.alpha) self.dwg.add(hit) def region_contour(self, x, y): @@ -199,10 +213,12 @@ class GerberSvgContext(GerberContext): x, y = self.resolve(x, y) color = (convert_color(self.color) if self.level_polarity == 'dark' else convert_color(self.background_color)) + alpha = self.alpha if self.level_polarity == 'dark' else 1.0 if self.region_path is None: self.region_path = self.dwg.path(d = 'M %f, %f' % (self.x*SCALE, -self.y*SCALE), fill = color, stroke = 'none') + self.region_path.fill(opacity=alpha) self.region_path.push('L %f, %f' % (x*SCALE, -y*SCALE)) self.move(x, y, resolve=False) -- cgit From d90da4000f3fd542da1896e705d3db43fd48ea4b Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Thu, 16 Oct 2014 18:13:43 -0400 Subject: Add primitive definitions and bounding box calcs for DRC --- gerber/primitives.py | 193 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 gerber/primitives.py (limited to 'gerber') diff --git a/gerber/primitives.py b/gerber/primitives.py new file mode 100644 index 0000000..366c397 --- /dev/null +++ b/gerber/primitives.py @@ -0,0 +1,193 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# copyright 2014 Hamilton Kibbe + +# 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 +from operator import sub + + +class Primitive(object): + def bounding_box(self): + """ Calculate bounding box + + will be helpful for sweep & prune during DRC clearance checks. + + Return ((min x, max x), (min y, max y)) + """ + pass + + +class Line(Primitive): + """ + """ + def __init__(self, start, end, width): + self.start = start + self.end = end + self.width = width + + @property + def angle(self): + dx, dy = tuple(map(sub, end, start)) + angle = degrees(math.tan(dy/dx)) + + 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)) + + +class Arc(Primitive): + """ + """ + def __init__(self, start, end, center, direction, width): + self.start = start + self.end = end + self.center = center + self.direction = direction + self.width = width + + @property + def start_angle(self): + dy, dx = map(sub, self.start, self.center) + return math.atan2(dy, dx) + + @property + def end_angle(self): + dy, dx = map(sub, self.end, self.center) + return math.atan2(dy, dx) + + def bounding_box(self): + pass + +class Circle(Primitive): + """ + """ + def __init__(self, position, diameter): + self.position = position + self.diameter = diameter + self.radius = diameter / 2. + + 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)) + + +class Rectangle(Primitive): + """ + """ + def __init__(self, position, width, height): + self.position = position + self.width = width + self.height = height + + @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.)) + + 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)) + + +class Obround(Primitive): + """ + """ + def __init__(self, position, width, height) + self.position = position + self.width = width + self.height = height + + @property + def orientation(self): + return 'vertical' if self.height > self.width else 'horizontal' + + @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.)) + + 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)) + + +class Polygon(Primitive): + """ + """ + def __init__(self, position, sides, radius): + self.position = position + self.sides = sides + self.radius = radius + + 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)) + + +class Region(Primitive): + """ + """ + def __init__(self, points): + self.points = points + + 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)) + + +class Drill(Primitive): + """ + """ + def __init__(self, position, diameter): + self.position = position + self.diameter = diameter + self.radius = diameter / 2. + + 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)) -- cgit From 6d2db67e6d0973ce26ce3a6700ca44295f73fea7 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Sat, 18 Oct 2014 01:44:51 -0400 Subject: Refactor rendering --- gerber/cam.py | 25 ++- gerber/excellon.py | 38 ++-- gerber/gerber_statements.py | 3 +- gerber/layer_names.py | 51 ----- gerber/layers.py | 54 +++++ gerber/primitives.py | 67 ++++-- gerber/render/render.py | 418 ++++---------------------------------- gerber/render/svgwrite_backend.py | 305 +++++++++++---------------- gerber/rs274x.py | 189 +++++++++++++---- 9 files changed, 454 insertions(+), 696 deletions(-) delete mode 100644 gerber/layer_names.py create mode 100644 gerber/layers.py (limited to 'gerber') diff --git a/gerber/cam.py b/gerber/cam.py index e7a49d1..4c19588 100644 --- a/gerber/cam.py +++ b/gerber/cam.py @@ -70,6 +70,9 @@ class CamFile(object): settings : FileSettings The current file configuration. + primitives : iterable + List of primitives in the file. + filename : string Name of the file that this CamFile represents. @@ -95,8 +98,8 @@ class CamFile(object): decimal digits) """ - def __init__(self, statements=None, settings=None, filename=None, - layer_name=None): + def __init__(self, statements=None, settings=None, primitives=None, + filename=None, layer_name=None): if settings is not None: self.notation = settings['notation'] self.units = settings['units'] @@ -108,6 +111,7 @@ class CamFile(object): self.zero_suppression = 'trailing' self.format = (2, 5) self.statements = statements if statements is not None else [] + self.primitives = primitives self.filename = filename self.layer_name = layer_name @@ -122,3 +126,20 @@ class CamFile(object): """ return FileSettings(self.notation, self.units, self.zero_suppression, self.format) + + def render(self, ctx, filename=None): + """ Generate image of layer. + + Parameters + ---------- + ctx : :class:`GerberContext` + GerberContext subclass used for rendering the image + + filename : string + If provided, save the rendered image to `filename` + """ + ctx.set_bounds(self.bounds) + for p in self.primitives: + ctx.render(p) + if filename is not None: + ctx.dump(filename) diff --git a/gerber/excellon.py b/gerber/excellon.py index 13aacc6..ca2f7c8 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -25,7 +25,7 @@ This module provides Excellon file classes and parsing utilities from .excellon_statements import * from .cam import CamFile, FileSettings - +from .primitives import Drill import math def read(filename): @@ -74,30 +74,33 @@ class ExcellonFile(CamFile): """ def __init__(self, statements, tools, hits, settings, filename=None): - super(ExcellonFile, self).__init__(statements, settings, filename) + 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 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) + return ((xmin, xmax), (ymin, ymax)) def report(self): """ Print drill report """ pass - def render(self, ctx, filename=None): - """ Generate image of file - - Parameters - ---------- - ctx : :class:`gerber.render.GerberContext` - GerberContext subclass used for rendering the image - - filename : string - If provided, the rendered image will be saved to `filename` - """ - for tool, pos in self.hits: - ctx.drill(pos[0], pos[1], tool.diameter) - if filename is not None: - ctx.dump(filename) def write(self, filename): with open(filename, 'w') as f: @@ -105,6 +108,7 @@ class ExcellonFile(CamFile): f.write(statement.to_excellon() + '\n') + class ExcellonParser(object): """ Excellon File Parser diff --git a/gerber/gerber_statements.py b/gerber/gerber_statements.py index 218074f..6f7b73d 100644 --- a/gerber/gerber_statements.py +++ b/gerber/gerber_statements.py @@ -12,7 +12,8 @@ from .utils import parse_gerber_value, write_gerber_value, decimal_string __all__ = ['FSParamStmt', 'MOParamStmt', 'IPParamStmt', 'OFParamStmt', 'LPParamStmt', 'ADParamStmt', 'AMParamStmt', 'INParamStmt', 'LNParamStmt', 'CoordStmt', 'ApertureStmt', 'CommentStmt', - 'EofStmt', 'QuadrantModeStmt', 'RegionModeStmt', 'UnknownStmt'] + 'EofStmt', 'QuadrantModeStmt', 'RegionModeStmt', 'UnknownStmt', + 'ParamStmt'] class Statement(object): diff --git a/gerber/layer_names.py b/gerber/layer_names.py deleted file mode 100644 index 372c40d..0000000 --- a/gerber/layer_names.py +++ /dev/null @@ -1,51 +0,0 @@ -#! /usr/bin/env python -# -*- coding: utf-8 -*- - -# copyright 2014 Hamilton Kibbe -# -# 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. - -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', 'ts', 'skt', ] -top_silk_name = ['sst01', 'topsilk, 'silk', 'slk', 'sst', ] - -bottom_silk_ext = ['gbo, 'bs', 'skb', ] -bottom_silk_name = ['sst', 'bsilk', 'ssb', 'botsilk', ] - -top_mask_ext = ['gts', 'tmk', 'smt', 'tr', ] -top_mask_name = ['sm01', 'cmask', 'tmask', 'mask1', 'maskcom', 'topmask', - 'mst', ] - -bottom_mask_ext = ['gbs', 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', ] diff --git a/gerber/layers.py b/gerber/layers.py new file mode 100644 index 0000000..b10cf16 --- /dev/null +++ b/gerber/layers.py @@ -0,0 +1,54 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# copyright 2014 Hamilton Kibbe +# +# 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. + +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', ] + + diff --git a/gerber/primitives.py b/gerber/primitives.py index 366c397..670b758 100644 --- a/gerber/primitives.py +++ b/gerber/primitives.py @@ -19,11 +19,15 @@ from operator import sub class Primitive(object): + + def __init__(self, level_polarity='dark'): + self.level_polarity = level_polarity + def bounding_box(self): """ Calculate bounding box will be helpful for sweep & prune during DRC clearance checks. - + Return ((min x, max x), (min y, max y)) """ pass @@ -32,16 +36,19 @@ class Primitive(object): class Line(Primitive): """ """ - def __init__(self, start, end, width): + def __init__(self, start, end, width, level_polarity='dark'): + super(Line, self).__init__(level_polarity) self.start = start self.end = end self.width = width - + @property def angle(self): - dx, dy = tuple(map(sub, end, start)) - angle = degrees(math.tan(dy/dx)) + delta_x, delta_y = tuple(map(sub, end, start)) + angle = degrees(math.tan(delta_y/delta_x)) + return angle + @property def bounding_box(self): width_2 = self.width / 2. min_x = min(self.start[0], self.end[0]) - width_2 @@ -54,7 +61,8 @@ class Line(Primitive): class Arc(Primitive): """ """ - def __init__(self, start, end, center, direction, width): + 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 @@ -71,17 +79,23 @@ class Arc(Primitive): dy, dx = map(sub, self.end, self.center) return math.atan2(dy, dx) + @property def bounding_box(self): pass class Circle(Primitive): """ """ - def __init__(self, position, diameter): + def __init__(self, position, diameter, level_polarity='dark'): + super(Circle, self).__init__(level_polarity) self.position = position self.diameter = diameter - self.radius = diameter / 2. + @property + def radius(self): + return self.diameter / 2. + + @property def bounding_box(self): min_x = self.position[0] - self.radius max_x = self.position[0] + self.radius @@ -89,11 +103,16 @@ class Circle(Primitive): max_y = self.position[1] + self.radius return ((min_x, max_x), (min_y, max_y)) + @property + def stroke_width(self): + return self.diameter + class Rectangle(Primitive): """ """ - def __init__(self, position, width, height): + def __init__(self, position, width, height, level_polarity='dark'): + super(Rectangle, self).__init__(level_polarity) self.position = position self.width = width self.height = height @@ -108,6 +127,7 @@ class Rectangle(Primitive): return (self.position[0] + (self.width / 2.), self.position[1] + (self.height / 2.)) + @property def bounding_box(self): min_x = self.lower_left[0] max_x = self.upper_right[0] @@ -115,11 +135,16 @@ class Rectangle(Primitive): max_y = self.upper_right[1] return ((min_x, max_x), (min_y, max_y)) + @property + def stroke_width(self): + return max((self.width, self.height)) + class Obround(Primitive): """ """ - def __init__(self, position, width, height) + def __init__(self, position, width, height, level_polarity='dark'): + super(Obround, self).__init__(level_polarity) self.position = position self.width = width self.height = height @@ -138,6 +163,7 @@ class Obround(Primitive): return (self.position[0] + (self.width / 2.), self.position[1] + (self.height / 2.)) + @property def bounding_box(self): min_x = self.lower_left[0] max_x = self.upper_right[0] @@ -149,11 +175,13 @@ class Obround(Primitive): class Polygon(Primitive): """ """ - def __init__(self, position, sides, radius): + def __init__(self, position, sides, radius, level_polarity='dark'): + super(Polygon, self).__init__(level_polarity) self.position = position self.sides = sides self.radius = radius - + + @property def bounding_box(self): min_x = self.position[0] - self.radius max_x = self.position[0] + self.radius @@ -165,9 +193,11 @@ class Polygon(Primitive): class Region(Primitive): """ """ - def __init__(self, points): + def __init__(self, points, level_polarity='dark'): + super(Region, self).__init__(level_polarity) self.points = points - + + @property def bounding_box(self): x_list, y_list = zip(*self.points) min_x = min(x_list) @@ -181,10 +211,15 @@ class Drill(Primitive): """ """ def __init__(self, position, diameter): + super(Drill, self).__init__('dark') self.position = position self.diameter = diameter - self.radius = diameter / 2. - + + @property + def radius(self): + return self.diameter / 2. + + @property def bounding_box(self): min_x = self.position[0] - self.radius max_x = self.position[0] + self.radius diff --git a/gerber/render/render.py b/gerber/render/render.py index f7e4485..f5c58d8 100644 --- a/gerber/render/render.py +++ b/gerber/render/render.py @@ -28,6 +28,7 @@ from ..gerber_statements import (CommentStmt, UnknownStmt, EofStmt, ParamStmt, QuadrantModeStmt, ) +from ..primitives import * class GerberContext(object): """ Gerber rendering context base class @@ -39,40 +40,8 @@ class GerberContext(object): Attributes ---------- - settings : FileSettings (dict-like) - Gerber file settings - - x : float - X-coordinate of the "photoplotter" head. - - y : float - Y-coordinate of the "photoplotter" head - - aperture : int - The aperture that is currently in use - - interpolation : str - Current interpolation mode. may be 'linear' or 'arc' - - direction : string - Current arc direction. May be either 'clockwise' or 'counterclockwise' - - image_polarity : string - Current image polarity setting. May be 'positive' or 'negative' - - level_polarity : string - Level polarity. May be 'dark' or 'clear'. Dark polarity indicates the - existance of copper/silkscreen/etc. in the exposed area, whereas clear - polarity indicates material should be removed from the exposed area. - - region_mode : string - Region mode. May be 'on' or 'off'. When region mode is set to 'on' the - following "contours" define the outline of a region. When region mode - is subsequently turned 'off', the defined area is filled. - - quadrant_mode : string - Quadrant mode. May be 'single-quadrant' or 'multi-quadrant'. Defines - how arcs are specified. + units : string + Measurement units color : tuple (, , ) Color used for rendering as a tuple of normalized (red, green, blue) values. @@ -87,73 +56,14 @@ class GerberContext(object): alpha : float Rendering opacity. Between 0.0 (transparent) and 1.0 (opaque.) """ - def __init__(self): - self.settings = {} - self.x = 0 - self.y = 0 - - self.aperture = 0 - self.interpolation = 'linear' - self.direction = 'clockwise' - self.image_polarity = 'positive' - self.level_polarity = 'dark' - self.region_mode = 'off' - self.quadrant_mode = 'multi-quadrant' - self.step_and_repeat = (1, 1, 0, 0) + 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_format(self, settings): - """ Set source file format. - - Parameters - ---------- - settings : FileSettings instance or dict-like - Gerber file settings used in source file. - """ - self.settings = settings - - def set_coord_format(self, zero_suppression, decimal_format, notation): - """ Set coordinate format used in source gerber file - Parameters - ---------- - zero_suppression : string - Zero suppression mode. may be 'leading' or 'trailling' - - decimal_format : tuple (, ) - Decimal precision format specified as (integer digits, decimal digits) - - notation : string - Notation mode. 'absolute' or 'incremental' - """ - if zero_suppression not in ('leading', 'trailling'): - raise ValueError('Zero suppression must be "leading" or "trailing"') - self.settings['zero_suppression'] = zero_suppression - self.settings['format'] = decimal_format - self.settings['notation'] = notation - - def set_coord_notation(self, notation): - """ Set context notation mode - - Parameters - ---------- - notation : string - Notation mode. may be 'absolute' or 'incremental' - - Raises - ------ - ValueError - If `notation` is not either "absolute" or "incremental" - - """ - if notation not in ('absolute', 'incremental'): - raise ValueError('Notation may be "absolute" or "incremental"') - self.settings['notation'] = notation - - def set_coord_unit(self, unit): + def set_units(self, units): """ Set context measurement units Parameters @@ -166,70 +76,9 @@ class GerberContext(object): ValueError If `unit` is not 'inch' or 'metric' """ - if unit not in ('inch', 'metric'): - raise ValueError('Unit may be "inch" or "metric"') - self.settings['units'] = unit - - def set_image_polarity(self, polarity): - """ Set context image polarity - - Parameters - ---------- - polarity : string - Image polarity. May be "positive" or "negative" - - Raises - ------ - ValueError - If polarity is not 'positive' or 'negative' - """ - if polarity not in ('positive', 'negative'): - raise ValueError('Polarity may be "positive" or "negative"') - self.image_polarity = polarity - - def set_level_polarity(self, polarity): - """ Set context level polarity - - Parameters - ---------- - polarity : string - Level polarity. May be "dark" or "clear" - - Raises - ------ - ValueError - If polarity is not 'dark' or 'clear' - """ - if polarity not in ('dark', 'clear'): - raise ValueError('Polarity may be "dark" or "clear"') - self.level_polarity = polarity - - def set_interpolation(self, interpolation): - """ Set arc interpolation mode - - Parameters - ---------- - interpolation : string - Interpolation mode. May be 'linear' or 'arc' - - Raises - ------ - ValueError - If `interpolation` is not 'linear' or 'arc' - """ - if interpolation not in ('linear', 'arc'): - raise ValueError('Interpolation may be "linear" or "arc"') - self.interpolation = interpolation - - def set_aperture(self, d): - """ Set active aperture - - Parameters - ---------- - aperture : int - Aperture number to activate. - """ - self.aperture = d + 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. @@ -277,238 +126,49 @@ class GerberContext(object): """ self.alpha = alpha - def resolve(self, x, y): - """ Resolve missing x or y coordinates in a coordinate command. - - Replace missing x or y values with the current x or y position. This - is the default method for handling coordinate pairs pulled from gerber - file statments, as a move/line/arc involving a change in only one axis - will drop the redundant axis coordinate to reduce file size. - - Parameters - ---------- - x : float - X-coordinate. If `None`, will be replaced with current - "photoplotter" head x-coordinate - - y : float - Y-coordinate. If `None`, will be replaced with current - "photoplotter" head y-coordinate - - Returns - ------- - coordinates : tuple (, ) - Coordinates in absolute notation - """ - x = x if x is not None else self.x - y = y if y is not None else self.y - return x, y - - def define_aperture(self, d, shape, modifiers): - pass - - def move(self, x, y, resolve=True): - """ Lights-off move. - - Move the "photoplotter" head to (x, y) without drawing a line. If x or - y is `None`, remain at the same point in that axis. - - Parameters - ----------- - x : float - X-coordinate to move to. If x is `None`, do not move in the X - direction - - y : float - Y-coordinate to move to. if y is `None`, do not move in the Y - direction - - resolve : bool - If resolve is `True` the context will replace missing x or y - coordinates with the current plotter head position. This is the - default behavior. - """ - if resolve: - self.x, self.y = self.resolve(x, y) + def render(self, primitive): + color = (self.color if primitive.level_polarity == 'dark' + else self.background_color) + if isinstance(primitive, Line): + self._render_line(primitive, color) + elif isinstance(primitive, Arc): + self._render_arc(primitive, color) + elif isinstance(primitive, Region): + self._render_region(primitive, color) + elif isinstance(primitive, Circle): + self._render_circle(primitive, color) + elif isinstance(primitive, Rectangle): + self._render_rectangle(primitive, color) + elif isinstance(primitive, Obround): + self._render_obround(primitive, color) + elif isinstance(primitive, Polygon): + self._render_polygon(Polygon, color) + elif isinstance(primitive, Drill): + self._render_drill(primitive, self.drill_color) else: - self.x, self.y = x, y - - def stroke(self, x, y, i, j): - """ Lights-on move. (draws a line or arc) - - The stroke method is called when a Lights-on move statement is - encountered. This will call the `line` or `arc` method as necessary - based on the move statement's parameters. The `stroke` method should - be overridden in `GerberContext` subclasses. - - Parameters - ---------- - x : float - X coordinate of target position - - y : float - Y coordinate of target position - - i : float - Offset in X-direction from current position of arc center. + return - j : float - Offset in Y-direction from current position of arc center. - """ + def _render_line(self, primitive, color): pass - def line(self, x, y): - """ Draw a line - - Draws a line from the current position to (x, y) using the currently - selected aperture. The `line` method should be overridden in - `GerberContext` subclasses. - - Parameters - ---------- - x : float - X coordinate of target position - - y : float - Y coordinate of target position - """ + def _render_arc(self, primitive, color): pass - def arc(self, x, y, i, j): - """ Draw an arc - - Draw an arc from the current position to (x, y) using the currently - selected aperture. `i` and `j` specify the offset from the starting - position to the center of the arc.The `arc` method should be - overridden in `GerberContext` subclasses. - - Parameters - ---------- - x : float - X coordinate of target position - - y : float - Y coordinate of target position - - i : float - Offset in X-direction from current position of arc center. - - j : float - Offset in Y-direction from current position of arc center. - """ + def _render_region(self, primitive, color): pass - def flash(self, x, y): - """ Flash the current aperture - - Draw a filled shape defined by the currently selected aperture. - - Parameters - ---------- - x : float - X coordinate of the position at which to flash - - y : float - Y coordinate of the position at which to flash - """ + def _render_circle(self, primitive, color): pass - def drill(self, x, y, diameter): - """ Draw a drill hit - - Draw a filled circle representing a drill hit at the specified - position and with the specified diameter. - - Parameters - ---------- - x : float - X coordinate of the drill hit - - y : float - Y coordinate of the drill hit - - diameter : float - Finished hole diameter to draw. - """ + def _render_rectangle(self, primitive, color): pass - def region_contour(self, x, y): - pass - - def fill_region(self): + def _render_obround(self, primitive, color): pass - - def evaluate(self, stmt): - """ Evaluate Gerber statement and update image accordingly. - - This method is called once for each statement in a Gerber/Excellon - file when the file's `render` method is called. The evaluate method - should forward the statement on to the relevant handling method based - on the statement type. - - Parameters - ---------- - statement : Statement - Gerber/Excellon statement to evaluate. - """ - if isinstance(stmt, (CommentStmt, UnknownStmt, EofStmt)): - return - - elif isinstance(stmt, ParamStmt): - self._evaluate_param(stmt) - - elif isinstance(stmt, CoordStmt): - self._evaluate_coord(stmt) - - elif isinstance(stmt, ApertureStmt): - self._evaluate_aperture(stmt) - - elif isinstance(stmt, (RegionModeStmt, QuadrantModeStmt)): - self._evaluate_mode(stmt) + def _render_polygon(self, primitive, color): + pass - else: - raise Exception("Invalid statement to evaluate") - - def _evaluate_mode(self, stmt): - if stmt.type == 'RegionMode': - if self.region_mode == 'on' and stmt.mode == 'off': - self.fill_region() - self.region_mode = stmt.mode - elif stmt.type == 'QuadrantMode': - self.quadrant_mode = stmt.mode - - def _evaluate_param(self, stmt): - if stmt.param == "FS": - self.set_coord_format(stmt.zero_suppression, stmt.format, - stmt.notation) - self.set_coord_notation(stmt.notation) - elif stmt.param == "MO": - self.set_coord_unit(stmt.mode) - elif stmt.param == "IP": - self.set_image_polarity(stmt.ip) - elif stmt.param == "LP": - self.set_level_polarity(stmt.lp) - elif stmt.param == "AD": - self.define_aperture(stmt.d, stmt.shape, stmt.modifiers) - - def _evaluate_coord(self, stmt): - if stmt.function in ("G01", "G1"): - self.set_interpolation('linear') - elif stmt.function in ('G02', 'G2', 'G03', 'G3'): - self.set_interpolation('arc') - self.direction = ('clockwise' if stmt.function in ('G02', 'G2') - else 'counterclockwise') - if stmt.op == "D01": - if self.region_mode == 'on': - self.region_contour(stmt.x, stmt.y) - else: - self.stroke(stmt.x, stmt.y, stmt.i, stmt.j) - elif stmt.op == "D02": - self.move(stmt.x, stmt.y) - elif stmt.op == "D03": - self.flash(stmt.x, stmt.y) - - def _evaluate_aperture(self, stmt): - self.set_aperture(stmt.d) + def _render_drill(self, primitive, color): + pass diff --git a/gerber/render/svgwrite_backend.py b/gerber/render/svgwrite_backend.py index 15d7bd3..d9456a5 100644 --- a/gerber/render/svgwrite_backend.py +++ b/gerber/render/svgwrite_backend.py @@ -17,214 +17,139 @@ # limitations under the License. from .render import GerberContext -from .apertures import Circle, Rect, Obround, Polygon +from operator import mul import svgwrite SCALE = 300 -def convert_color(color): +def svg_color(color): color = tuple([int(ch * 255) for ch in color]) return 'rgb(%d, %d, %d)' % color -class SvgCircle(Circle): - def line(self, ctx, x, y, color='rgb(184, 115, 51)', alpha=1.0): - aline = ctx.dwg.line(start=(ctx.x * SCALE, -ctx.y * SCALE), - end=(x * SCALE, -y * SCALE), - stroke=color, - stroke_width=SCALE * self.diameter, - stroke_linecap="round") - aline.stroke(opacity=alpha) - return aline - - def arc(self, ctx, x, y, i, j, direction, color='rgb(184, 115, 51)', alpha=1.0): - pass - - def flash(self, ctx, x, y, color='rgb(184, 115, 51)', alpha=1.0): - circle = ctx.dwg.circle(center=(x * SCALE, -y * SCALE), - r = SCALE * (self.diameter / 2.0), - fill=color) - circle.fill(opacity=alpha) - return [circle, ] - - -class SvgRect(Rect): - def line(self, ctx, x, y, color='rgb(184, 115, 51)', alpha=1.0): - aline = ctx.dwg.line(start=(ctx.x * SCALE, -ctx.y * SCALE), - end=(x * SCALE, -y * SCALE), - stroke=color, stroke_width=2, - stroke_linecap="butt") - aline.stroke(opacity=alpha) - return aline - - def flash(self, ctx, x, y, color='rgb(184, 115, 51)', alpha=1.0): - xsize, ysize = self.size - rectangle = ctx.dwg.rect(insert=(SCALE * (x - (xsize / 2)), - -SCALE * (y + (ysize / 2))), - size=(SCALE * xsize, SCALE * ysize), - fill=color) - rectangle.fill(opacity=alpha) - return [rectangle, ] - - -class SvgObround(Obround): - def line(self, ctx, x, y, color='rgb(184, 115, 51)', alpha=1.0): - pass - - def flash(self, ctx, x, y, color='rgb(184, 115, 51)', alpha=1.0): - xsize, ysize = self.size - - # horizontal obround - if xsize == ysize: - circle = ctx.dwg.circle(center=(x * SCALE, -y * SCALE), - r = SCALE * (x / 2.0), - fill=color) - circle.fill(opacity=alpha) - return [circle, ] - if xsize > ysize: - rectx = xsize - ysize - recty = ysize - lcircle = ctx.dwg.circle(center=((x - (rectx / 2.0)) * SCALE, - -y * SCALE), - r = SCALE * (ysize / 2.0), - fill=color) - - rcircle = ctx.dwg.circle(center=((x + (rectx / 2.0)) * SCALE, - -y * SCALE), - r = SCALE * (ysize / 2.0), - fill=color) - - rect = ctx.dwg.rect(insert=(SCALE * (x - (xsize / 2.)), - -SCALE * (y + (ysize / 2.))), - size=(SCALE * xsize, SCALE * ysize), - fill=color) - lcircle.fill(opacity=alpha) - rcircle.fill(opacity=alpha) - rect.fill(opacity=alpha) - return [lcircle, rcircle, rect, ] - - # Vertical obround - else: - rectx = xsize - recty = ysize - xsize - lcircle = ctx.dwg.circle(center=(x * SCALE, - (y - (recty / 2.)) * -SCALE), - r = SCALE * (xsize / 2.), - fill=color) - - ucircle = ctx.dwg.circle(center=(x * SCALE, - (y + (recty / 2.)) * -SCALE), - r = SCALE * (xsize / 2.), - fill=color) - - rect = ctx.dwg.rect(insert=(SCALE * (x - (xsize / 2.)), - -SCALE * (y + (ysize / 2.))), - size=(SCALE * xsize, SCALE * ysize), - fill=color) - lcircle.fill(opacity=alpha) - ucircle.fill(opacity=alpha) - rect.fill(opacity=alpha) - return [lcircle, ucircle, rect, ] - class GerberSvgContext(GerberContext): def __init__(self): GerberContext.__init__(self) - - self.apertures = {} + self.scale = (SCALE, -SCALE) self.dwg = svgwrite.Drawing() - self.dwg.transform = 'scale 1 -1' self.background = False - self.region_path = None + + 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])) + size = (SCALE * (xbounds[1] - xbounds[0]), + SCALE * (ybounds[1] - ybounds[0])) if not self.background: - self.dwg = svgwrite.Drawing(viewBox='%f, %f, %f, %f' % (SCALE*xbounds[0], -SCALE*ybounds[1],size[0], size[1])) - self.dwg.add(self.dwg.rect(insert=(SCALE * xbounds[0], - -SCALE * ybounds[1]), - size=size, fill=convert_color(self.background_color))) + 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 define_aperture(self, d, shape, modifiers): - aperture = None - if shape == 'C': - aperture = SvgCircle(diameter=float(modifiers[0][0])) - elif shape == 'R': - aperture = SvgRect(size=modifiers[0][0:2]) - elif shape == 'O': - aperture = SvgObround(size=modifiers[0][0:2]) - self.apertures[d] = aperture - - def stroke(self, x, y, i, j): - super(GerberSvgContext, self).stroke(x, y, i, j) - - if self.interpolation == 'linear': - self.line(x, y) - elif self.interpolation == 'arc': - self.arc(x, y, i, j) - - def line(self, x, y): - super(GerberSvgContext, self).line(x, y) - x, y = self.resolve(x, y) - ap = self.apertures.get(self.aperture, None) - if ap is None: - return - color = (convert_color(self.color) if self.level_polarity == 'dark' - else convert_color(self.background_color)) - alpha = self.alpha if self.level_polarity == 'dark' else 1.0 - self.dwg.add(ap.line(self, x, y, color, alpha)) - self.move(x, y, resolve=False) - - def arc(self, x, y, i, j): - super(GerberSvgContext, self).arc(x, y, i, j) - x, y = self.resolve(x, y) - ap = self.apertures.get(self.aperture, None) - if ap is None: - return - #self.dwg.add(ap.arc(self, x, y, i, j, self.direction, - # convert_color(self.color), self.alpha)) - self.move(x, y, resolve=False) - - def flash(self, x, y): - super(GerberSvgContext, self).flash(x, y) - x, y = self.resolve(x, y) - ap = self.apertures.get(self.aperture, None) - if ap is None: - return - - color = (convert_color(self.color) if self.level_polarity == 'dark' - else convert_color(self.background_color)) - alpha = self.alpha if self.level_polarity == 'dark' else 1.0 - for shape in ap.flash(self, x, y, color, alpha): - self.dwg.add(shape) - self.move(x, y, resolve=False) - - def drill(self, x, y, diameter): - hit = self.dwg.circle(center=(x*SCALE, -y*SCALE), - r=SCALE*(diameter/2.0), - fill=convert_color(self.drill_color)) - #hit.fill(opacity=self.alpha) - self.dwg.add(hit) - - def region_contour(self, x, y): - super(GerberSvgContext, self).region_contour(x, y) - x, y = self.resolve(x, y) - color = (convert_color(self.color) if self.level_polarity == 'dark' - else convert_color(self.background_color)) - alpha = self.alpha if self.level_polarity == 'dark' else 1.0 - if self.region_path is None: - self.region_path = self.dwg.path(d = 'M %f, %f' % - (self.x*SCALE, -self.y*SCALE), - fill = color, stroke = 'none') - self.region_path.fill(opacity=alpha) - self.region_path.push('L %f, %f' % (x*SCALE, -y*SCALE)) - self.move(x, y, resolve=False) - - def fill_region(self): - self.dwg.add(self.region_path) - self.region_path = None + 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) - def dump(self, filename): - self.dwg.saveas(filename) + # 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, primitive, color): + center = map(mul, primitive.position, self.scale) + hit = self.dwg.circle(center=center, r=SCALE * primitive.radius, + fill=svg_color(color)) + self.dwg.add(hit) diff --git a/gerber/rs274x.py b/gerber/rs274x.py index 4076f77..39693c9 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -19,14 +19,13 @@ """ -import re +import copy import json +import re from .gerber_statements import * +from .primitives import * from .cam import CamFile, FileSettings - - - def read(filename): """ Read data from filename and return a GerberFile @@ -72,8 +71,9 @@ class GerberFile(CamFile): `bounds` is stored as ((min x, max x), (min y, max y)) """ - def __init__(self, statements, settings, filename=None): - super(GerberFile, self).__init__(statements, settings, filename) + def __init__(self, statements, settings, primitives, filename=None): + super(GerberFile, self).__init__(statements, settings, primitives, filename) + @property def comments(self): @@ -111,22 +111,7 @@ class GerberFile(CamFile): for statement in self.statements: f.write(statement.to_gerber()) - def render(self, ctx, filename=None): - """ Generate image of layer. - Parameters - ---------- - ctx : :class:`GerberContext` - GerberContext subclass used for rendering the image - - filename : string - If provided, the rendered image will be saved to `filename` - """ - ctx.set_bounds(self.bounds) - for statement in self.statements: - ctx.evaluate(statement) - if filename is not None: - ctx.dump(filename) class GerberParser(object): @@ -178,15 +163,31 @@ class GerberParser(object): def __init__(self): self.settings = FileSettings() self.statements = [] + self.primitives = [] + self.apertures = {} + self.current_region = None + self.x = 0 + self.y = 0 + + self.aperture = 0 + self.interpolation = 'linear' + self.direction = 'clockwise' + self.image_polarity = 'positive' + self.level_polarity = 'dark' + self.region_mode = 'off' + self.quadrant_mode = 'multi-quadrant' + self.step_and_repeat = (1, 1, 0, 0) + def parse(self, filename): fp = open(filename, "r") data = fp.readlines() for stmt in self._parse(data): + self.evaluate(stmt) self.statements.append(stmt) - return GerberFile(self.statements, self.settings, filename) + return GerberFile(self.statements, self.settings, self.primitives, filename) def dump_json(self): stmts = {"statements": [stmt.__dict__ for stmt in self.statements]} @@ -218,7 +219,7 @@ class GerberParser(object): did_something = False # Region Mode - (mode, r) = self._match_one(self.REGION_MODE_STMT, line) + (mode, r) = _match_one(self.REGION_MODE_STMT, line) if mode: yield RegionModeStmt.from_gerber(line) line = r @@ -226,7 +227,7 @@ class GerberParser(object): continue # Quadrant Mode - (mode, r) = self._match_one(self.QUAD_MODE_STMT, line) + (mode, r) = _match_one(self.QUAD_MODE_STMT, line) if mode: yield QuadrantModeStmt.from_gerber(line) line = r @@ -234,7 +235,7 @@ class GerberParser(object): continue # coord - (coord, r) = self._match_one(self.COORD_STMT, line) + (coord, r) = _match_one(self.COORD_STMT, line) if coord: yield CoordStmt.from_dict(coord, self.settings) line = r @@ -242,7 +243,7 @@ class GerberParser(object): continue # aperture selection - (aperture, r) = self._match_one(self.APERTURE_STMT, line) + (aperture, r) = _match_one(self.APERTURE_STMT, line) if aperture: yield ApertureStmt(**aperture) @@ -251,7 +252,7 @@ class GerberParser(object): continue # comment - (comment, r) = self._match_one(self.COMMENT_STMT, line) + (comment, r) = _match_one(self.COMMENT_STMT, line) if comment: yield CommentStmt(comment["comment"]) did_something = True @@ -259,7 +260,7 @@ class GerberParser(object): continue # parameter - (param, r) = self._match_one_from_many(self.PARAM_STMT, line) + (param, r) = _match_one_from_many(self.PARAM_STMT, line) if param: if param["param"] == "FS": stmt = FSParamStmt.from_dict(param) @@ -292,7 +293,7 @@ class GerberParser(object): continue # eof - (eof, r) = self._match_one(self.EOF_STMT, line) + (eof, r) = _match_one(self.EOF_STMT, line) if eof: yield EofStmt() did_something = True @@ -311,17 +312,125 @@ class GerberParser(object): yield UnknownStmt(line) oldline = line - def _match_one(self, expr, data): - match = expr.match(data) - if match is None: - return ({}, None) - else: - return (match.groupdict(), data[match.end(0):]) + def evaluate(self, stmt): + """ Evaluate Gerber statement and update image accordingly. - def _match_one_from_many(self, exprs, data): - for expr in exprs: - match = expr.match(data) - if match: - return (match.groupdict(), data[match.end(0):]) + This method is called once for each statement in the file as it + is parsed. + Parameters + ---------- + statement : Statement + Gerber/Excellon statement to evaluate. + + """ + if isinstance(stmt, (CommentStmt, UnknownStmt, EofStmt)): + return + + elif isinstance(stmt, ParamStmt): + self._evaluate_param(stmt) + + elif isinstance(stmt, CoordStmt): + self._evaluate_coord(stmt) + + elif isinstance(stmt, ApertureStmt): + self._evaluate_aperture(stmt) + + elif isinstance(stmt, (RegionModeStmt, QuadrantModeStmt)): + self._evaluate_mode(stmt) + + 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) + elif shape == 'R': + width = float(modifiers[0][0]) + height = float(modifiers[0][1]) + aperture = Rectangle(position=None, width=width, height=height) + elif shape == 'O': + width = float(modifiers[0][0]) + height = float(modifiers[0][1]) + aperture = Obround(position=None, width=width, height=height) + 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)) + self.current_region = None + self.region_mode = stmt.mode + elif stmt.type == 'QuadrantMode': + self.quadrant_mode = stmt.mode + + def _evaluate_param(self, stmt): + if stmt.param == "FS": + self.settings.zero_suppression = stmt.zero_suppression + self.settings.format = stmt.format + self.settings.notation = stmt.notation + elif stmt.param == "MO": + self.settings.units = stmt.mode + elif stmt.param == "IP": + self.image_polarity = stmt.ip + elif stmt.param == "LP": + self.level_polarity = stmt.lp + elif stmt.param == "AD": + self._define_aperture(stmt.d, stmt.shape, stmt.modifiers) + + def _evaluate_coord(self, stmt): + x = self.x if stmt.x is None else stmt.x + y = self.y if stmt.y is None else stmt.y + if stmt.function in ("G01", "G1"): + 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') + 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,)) + 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)) + 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 + + elif stmt.op == "D03": + primitive = copy.deepcopy(self.apertures[self.aperture]) + primitive.position = (x, y) + primitive.level_polarity = self.level_polarity + self.primitives.append(primitive) + self.x, self.y = x, y + + def _evaluate_aperture(self, stmt): + self.aperture = stmt.d + + +def _match_one(expr, data): + match = expr.match(data) + if match is None: return ({}, None) + else: + return (match.groupdict(), data[match.end(0):]) + + +def _match_one_from_many(exprs, data): + for expr in exprs: + match = expr.match(data) + if match: + return (match.groupdict(), data[match.end(0):]) + + return ({}, None) -- cgit From 18e3b87625ddb739faeddffcaed48e12db6c7e8b Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Sun, 19 Oct 2014 22:23:00 -0400 Subject: Test update --- gerber/__init__.py | 1 + gerber/__main__.py | 5 +- gerber/cam.py | 33 + gerber/excellon.py | 19 +- gerber/excellon_statements.py | 18 +- gerber/gerber_statements.py | 16 +- gerber/rs274x.py | 12 +- gerber/tests/resources/board_outline.GKO | 503 +++ gerber/tests/resources/bottom_copper.GBL | 1811 +++++++++ gerber/tests/resources/bottom_mask.GBS | 66 + gerber/tests/resources/bottom_silk.GBO | 6007 ++++++++++++++++++++++++++++++ gerber/tests/resources/ncdrill.DRD | 51 + gerber/tests/resources/top_copper.GTL | 3457 +++++++++++++++++ gerber/tests/resources/top_mask.GTS | 162 + gerber/tests/resources/top_silk.GTO | 2099 +++++++++++ gerber/tests/test_cam.py | 52 +- gerber/tests/test_common.py | 24 + gerber/tests/test_excellon.py | 32 + gerber/tests/test_excellon_statements.py | 13 +- gerber/tests/test_rs274x.py | 16 + gerber/utils.py | 17 +- 21 files changed, 14344 insertions(+), 70 deletions(-) create mode 100644 gerber/tests/resources/board_outline.GKO create mode 100644 gerber/tests/resources/bottom_copper.GBL create mode 100644 gerber/tests/resources/bottom_mask.GBS create mode 100644 gerber/tests/resources/bottom_silk.GBO create mode 100644 gerber/tests/resources/ncdrill.DRD create mode 100644 gerber/tests/resources/top_copper.GTL create mode 100644 gerber/tests/resources/top_mask.GTS create mode 100644 gerber/tests/resources/top_silk.GTO create mode 100644 gerber/tests/test_common.py create mode 100644 gerber/tests/test_excellon.py create mode 100644 gerber/tests/test_rs274x.py (limited to 'gerber') diff --git a/gerber/__init__.py b/gerber/__init__.py index fce6483..1a11159 100644 --- a/gerber/__init__.py +++ b/gerber/__init__.py @@ -23,3 +23,4 @@ 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 diff --git a/gerber/__main__.py b/gerber/__main__.py index 10da12e..71e3bfc 100644 --- a/gerber/__main__.py +++ b/gerber/__main__.py @@ -29,14 +29,13 @@ if __name__ == '__main__': for filename in sys.argv[1:]: print "parsing %s" % filename if 'GTO' in filename or 'GBO' in filename: - ctx.set_color((1,1,1)) + ctx.set_color((1, 1, 1)) ctx.set_alpha(0.8) elif 'GTS' in filename or 'GBS' in filename: - ctx.set_color((0.2,0.2,0.75)) + ctx.set_color((0.2, 0.2, 0.75)) ctx.set_alpha(0.8) gerberfile = read(filename) gerberfile.render(ctx) print('Saving image to test.svg') ctx.dump('test.svg') - diff --git a/gerber/cam.py b/gerber/cam.py index 4c19588..051c3b5 100644 --- a/gerber/cam.py +++ b/gerber/cam.py @@ -59,6 +59,33 @@ class FileSettings(object): else: raise KeyError() + def __setitem__(self, key, value): + if key == 'notation': + if value not in ['absolute', 'incremental']: + raise ValueError('Notation must be either \ + absolute or incremental') + self.notation = value + elif key == 'units': + 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 == 'format': + if len(value) != 2: + raise ValueError('Format must be a tuple(n=2) of integers') + self.format = value + + 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) + + class CamFile(object): """ Base class for Gerber/Excellon files. @@ -127,6 +154,12 @@ class CamFile(object): return FileSettings(self.notation, self.units, self.zero_suppression, self.format) + @property + def bounds(self): + """ File baundaries + """ + pass + def render(self, ctx, filename=None): """ Generate image of layer. diff --git a/gerber/excellon.py b/gerber/excellon.py index ca2f7c8..780d08f 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -44,8 +44,6 @@ def read(filename): detected_settings = detect_excellon_format(filename) settings = FileSettings(**detected_settings) zeros = '' - print('Detected %d:%d format with %s zero suppression' % - (settings.format[0], settings.format[1], settings.zero_suppression)) return ExcellonParser(settings).parse(filename) @@ -108,7 +106,6 @@ class ExcellonFile(CamFile): f.write(statement.to_excellon() + '\n') - class ExcellonParser(object): """ Excellon File Parser @@ -129,10 +126,10 @@ class ExcellonParser(object): self.active_tool = None self.pos = [0., 0.] if settings is not None: - self.units = settings['units'] - self.zero_suppression = settings['zero_suppression'] - self.notation = settings['notation'] - self.format = settings['format'] + self.units = settings.units + self.zero_suppression = settings.zero_suppression + self.notation = settings.notation + self.format = settings.format @property @@ -163,14 +160,14 @@ class ExcellonParser(object): def parse(self, filename): with open(filename, 'r') as f: for line in f: - self._parse(line) + self._parse(line.strip()) 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'] + #line = line.strip() + zs = self._settings().zero_suppression + fmt = self._settings().format if line[0] == ';': self.statements.append(CommentStmt.from_excellon(line)) diff --git a/gerber/excellon_statements.py b/gerber/excellon_statements.py index 4b92f07..feeda44 100644 --- a/gerber/excellon_statements.py +++ b/gerber/excellon_statements.py @@ -107,8 +107,8 @@ class ExcellonTool(ExcellonStatement): commands = re.split('([BCFHSTZ])', line)[1:] commands = [(command, value) for command, value in pairwise(commands)] args = {} - nformat = settings['format'] - zero_suppression = settings['zero_suppression'] + nformat = settings.format + zero_suppression = settings.zero_suppression for cmd, val in commands: if cmd == 'B': args['retract_rate'] = parse_gerber_value(val, nformat, zero_suppression) @@ -157,8 +157,8 @@ class ExcellonTool(ExcellonStatement): self.hit_count = 0 def to_excellon(self): - fmt = self.settings['format'] - zs = self.settings['zero_suppression'] + fmt = self.settings.format + zs = self.settings.format stmt = 'T%d' % self.number if self.retract_rate is not None: stmt += 'B%s' % write_gerber_value(self.retract_rate, fmt, zs) @@ -201,7 +201,7 @@ class ToolSelectionStmt(ExcellonStatement): tool_statement : ToolSelectionStmt ToolSelectionStmt representation of `line.` """ - line = line.strip()[1:] + line = line[1:] compensation_index = None tool = int(line[:2]) if len(line) > 2: @@ -230,10 +230,10 @@ class CoordinateStmt(ExcellonStatement): y_coord = None if line[0] == 'X': splitline = line.strip('X').split('Y') - x_coord = parse_gerber_value(splitline[0].strip(), nformat, + x_coord = parse_gerber_value(splitline[0], nformat, zero_suppression) if len(splitline) == 2: - y_coord = parse_gerber_value(splitline[1].strip(), nformat, + y_coord = parse_gerber_value(splitline[1], nformat, zero_suppression) else: y_coord = parse_gerber_value(line.strip(' Y'), nformat, @@ -257,7 +257,7 @@ class CommentStmt(ExcellonStatement): @classmethod def from_excellon(cls, line): - return cls(line.strip().lstrip(';')) + return cls(line.lstrip(';')) def __init__(self, comment): self.comment = comment @@ -380,7 +380,7 @@ class LinkToolStmt(ExcellonStatement): @classmethod def from_excellon(cls, line): - linked = [int(tool) for tool in line.strip().split('/')] + linked = [int(tool) for tool in line.split('/')] return cls(linked) def __init__(self, linked_tools): diff --git a/gerber/gerber_statements.py b/gerber/gerber_statements.py index 6f7b73d..44eeee0 100644 --- a/gerber/gerber_statements.py +++ b/gerber/gerber_statements.py @@ -72,10 +72,10 @@ class FSParamStmt(ParamStmt): def from_dict(cls, stmt_dict): """ """ - param = stmt_dict.get('param').strip() + param = stmt_dict.get('param') zeros = 'leading' if stmt_dict.get('zero') == 'L' else 'trailing' notation = 'absolute' if stmt_dict.get('notation') == 'A' else 'incremental' - x = map(int, stmt_dict.get('x').strip()) + x = map(int, stmt_dict.get('x')) fmt = (x[0], x[1]) return cls(param, zeros, notation, fmt) @@ -471,9 +471,9 @@ class CoordStmt(Statement): @classmethod def from_dict(cls, stmt_dict, settings): - zeros = settings['zero_suppression'] - format = settings['format'] - function = stmt_dict.get('function') + zeros = settings.zero_suppression + format = settings.format + function = stmt_dict['function'] x = stmt_dict.get('x') y = stmt_dict.get('y') i = stmt_dict.get('i') @@ -527,8 +527,8 @@ class CoordStmt(Statement): """ Statement.__init__(self, "COORD") - self.zero_suppression = settings['zero_suppression'] - self.format = settings['format'] + self.zero_suppression = settings.zero_suppression + self.format = settings.format self.function = function self.x = x self.y = y @@ -628,7 +628,6 @@ class QuadrantModeStmt(Statement): @classmethod def from_gerber(cls, line): - line = line.strip() if 'G74' not in line and 'G75' not in line: raise ValueError('%s is not a valid quadrant mode statement' % line) @@ -651,7 +650,6 @@ class RegionModeStmt(Statement): @classmethod def from_gerber(cls, line): - line = line.strip() 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')) diff --git a/gerber/rs274x.py b/gerber/rs274x.py index 39693c9..739c253 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -112,8 +112,6 @@ class GerberFile(CamFile): f.write(statement.to_gerber()) - - class GerberParser(object): """ GerberParser """ @@ -324,21 +322,21 @@ class GerberParser(object): Gerber/Excellon statement to evaluate. """ - if isinstance(stmt, (CommentStmt, UnknownStmt, EofStmt)): - return + if isinstance(stmt, CoordStmt): + self._evaluate_coord(stmt) elif isinstance(stmt, ParamStmt): self._evaluate_param(stmt) - elif isinstance(stmt, CoordStmt): - self._evaluate_coord(stmt) - elif isinstance(stmt, ApertureStmt): self._evaluate_aperture(stmt) elif isinstance(stmt, (RegionModeStmt, QuadrantModeStmt)): self._evaluate_mode(stmt) + elif isinstance(stmt, (CommentStmt, UnknownStmt, EofStmt)): + return + else: raise Exception("Invalid statement to evaluate") diff --git a/gerber/tests/resources/board_outline.GKO b/gerber/tests/resources/board_outline.GKO new file mode 100644 index 0000000..40b8c7d --- /dev/null +++ b/gerber/tests/resources/board_outline.GKO @@ -0,0 +1,503 @@ +G75* +%MOIN*% +%OFA0B0*% +%FSLAX24Y24*% +%IPPOS*% +%LPD*% +%AMOC8* +5,1,8,0,0,1.08239X$1,22.5* +% +%ADD10C,0.0000*% +%ADD11C,0.0004*% +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* +X022869Y007639D02* +X022869Y013789D01* +M02* diff --git a/gerber/tests/resources/bottom_copper.GBL b/gerber/tests/resources/bottom_copper.GBL new file mode 100644 index 0000000..0d98da3 --- /dev/null +++ b/gerber/tests/resources/bottom_copper.GBL @@ -0,0 +1,1811 @@ +G75* +%MOIN*% +%OFA0B0*% +%FSLAX24Y24*% +%IPPOS*% +%LPD*% +%AMOC8* +5,1,8,0,0,1.08239X$1,22.5* +% +%ADD10C,0.0000*% +%ADD11C,0.0110*% +%ADD12C,0.0004*% +%ADD13C,0.0554*% +%ADD14C,0.0600*% +%ADD15C,0.0160*% +%ADD16C,0.0396*% +%ADD17C,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* +X019495Y004010D02* +X019298Y003813D01* +X019101Y004010D01* +X019101Y003419D01* +X018850Y003419D02* +X018654Y003419D01* +X018752Y003419D02* +X018752Y004010D01* +X018850Y004010D02* +X018654Y004010D01* +X018421Y004010D02* +X018125Y004010D01* +X018027Y003911D01* +X018027Y003518D01* +X018125Y003419D01* +X018421Y003419D01* +X018421Y004010D01* +X017776Y004010D02* +X017579Y004010D01* +X017678Y004010D02* +X017678Y003419D01* +X017776Y003419D02* +X017579Y003419D01* +X016702Y003715D02* +X016308Y003715D01* +X015413Y004010D02* +X015413Y003419D01* +X015118Y003419D01* +X015019Y003518D01* +X015019Y003911D01* +X015118Y004010D01* +X015413Y004010D01* +X014768Y004010D02* +X014768Y003419D01* +X014375Y003419D02* +X014375Y004010D01* +X014571Y003813D01* +X014768Y004010D01* +X014124Y004010D02* +X013730Y003419D01* +X014124Y003419D02* +X013730Y004010D01* +X012835Y004010D02* +X012835Y003419D01* +X012539Y003419D01* +X012441Y003518D01* +X012441Y003616D01* +X012539Y003715D01* +X012835Y003715D01* +X012835Y004010D02* +X012539Y004010D01* +X012441Y003911D01* +X012441Y003813D01* +X012539Y003715D01* +X012190Y003813D02* +X012190Y003419D01* +X012190Y003616D02* +X011993Y003813D01* +X011895Y003813D01* +X011653Y003813D02* +X011555Y003813D01* +X011555Y003419D01* +X011653Y003419D02* +X011456Y003419D01* +X011223Y003518D02* +X011223Y003715D01* +X011125Y003813D01* +X010830Y003813D01* +X010830Y004010D02* +X010830Y003419D01* +X011125Y003419D01* +X011223Y003518D01* +X011555Y004010D02* +X011555Y004108D01* +X010579Y003715D02* +X010579Y003518D01* +X010480Y003419D01* +X010185Y003419D01* +X010185Y003321D02* +X010185Y003813D01* +X010480Y003813D01* +X010579Y003715D01* +X010185Y003321D02* +X010283Y003222D01* +X010382Y003222D01* +X009934Y003518D02* +X009934Y003715D01* +X009836Y003813D01* +X009639Y003813D01* +X009541Y003715D01* +X009541Y003616D01* +X009934Y003616D01* +X009934Y003518D02* +X009836Y003419D01* +X009639Y003419D01* +X019495Y003419D02* +X019495Y004010D01* +D12* +X022869Y007639D02* +X022869Y013789D01* +D13* +X018200Y011964D03* +X017200Y011464D03* +X017200Y010464D03* +X018200Y009964D03* +X018200Y010964D03* +X017200Y009464D03* +D14* +X017350Y016514D02* +X017350Y017114D01* +X018350Y017114D02* +X018350Y016514D01* +X007350Y016664D02* +X007350Y017264D01* +X006350Y017264D02* +X006350Y016664D01* +X005350Y016664D02* +X005350Y017264D01* +X001800Y012564D02* +X001200Y012564D01* +X001200Y011564D02* +X001800Y011564D01* +X001800Y010564D02* +X001200Y010564D01* +X001200Y009564D02* +X001800Y009564D01* +X001800Y008564D02* +X001200Y008564D01* +D15* +X001031Y008136D02* +X000780Y008136D01* +X000780Y007978D02* +X019853Y007978D01* +X019804Y008027D02* +X020012Y007818D01* +X020268Y007671D01* +X020553Y007594D01* +X020847Y007594D01* +X021132Y007671D01* +X021388Y007818D01* +X021596Y008027D01* +X021744Y008282D01* +X021820Y008567D01* +X021820Y008862D01* +X021744Y009147D01* +X021596Y009402D01* +X021388Y009611D01* +X021132Y009758D01* +X020847Y009834D01* +X020553Y009834D01* +X020268Y009758D01* +X020012Y009611D01* +X019804Y009402D01* +X019656Y009147D01* +X019580Y008862D01* +X019580Y008567D01* +X019656Y008282D01* +X019804Y008027D01* +X019740Y008136D02* +X001969Y008136D01* +X001891Y008104D02* +X002061Y008174D01* +X002190Y008304D01* +X002260Y008473D01* +X002260Y008656D01* +X002190Y008825D01* +X002061Y008954D01* +X001891Y009024D01* +X001108Y009024D01* +X000939Y008954D01* +X000810Y008825D01* +X000780Y008752D01* +X000780Y009376D01* +X000810Y009304D01* +X000939Y009174D01* +X001108Y009104D01* +X001891Y009104D01* +X002061Y009174D01* +X002190Y009304D01* +X002260Y009473D01* +X002260Y009656D01* +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* +X005258Y016204D01* +X005441Y016204D01* +X005611Y016274D01* +X005740Y016404D01* +X005810Y016573D01* +X005810Y017356D01* +X005740Y017525D01* +X005681Y017584D01* +X006019Y017584D01* +X005960Y017525D01* +X005890Y017356D01* +X005890Y016573D01* +X005960Y016404D01* +X006089Y016274D01* +X006258Y016204D01* +X006441Y016204D01* +X006611Y016274D01* +X006740Y016404D01* +X006810Y016573D01* +X006810Y017356D01* +X006740Y017525D01* +X006681Y017584D01* +X006991Y017584D01* +X006984Y017577D01* +X006939Y017516D01* +X006905Y017449D01* +X006882Y017377D01* +X006870Y017302D01* +X006870Y016984D01* +X007330Y016984D01* +X007330Y016944D01* +X007370Y016944D01* +X007370Y016184D01* +X007388Y016184D01* +X007462Y016196D01* +X007534Y016219D01* +X007602Y016254D01* +X007663Y016298D01* +X007716Y016352D01* +X007761Y016413D01* +X007795Y016480D01* +X007818Y016552D01* +X007830Y016627D01* +X007830Y016944D01* +X007370Y016944D01* +X007370Y016984D01* +X007830Y016984D01* +X007830Y017302D01* +X007818Y017377D01* +X007795Y017449D01* +X007761Y017516D01* +X007716Y017577D01* +X007709Y017584D01* +X018249Y017584D01* +X018238Y017583D01* +X018166Y017559D01* +X018098Y017525D01* +X018037Y017480D01* +X017984Y017427D01* +X017939Y017366D01* +X017905Y017299D01* +X017882Y017227D01* +X017870Y017152D01* +X017870Y016834D01* +X018330Y016834D01* +X018330Y016794D01* +X018370Y016794D01* +X018370Y016034D01* +X018388Y016034D01* +X018462Y016046D01* +X018534Y016069D01* +X018602Y016104D01* +X018663Y016148D01* +X018716Y016202D01* +X018761Y016263D01* +X018795Y016330D01* +X018818Y016402D01* +X018830Y016477D01* +X018830Y016794D01* +X018370Y016794D01* +X018370Y016834D01* +X018830Y016834D01* +X018830Y017152D01* +X018818Y017227D01* +X018795Y017299D01* +X018761Y017366D01* +X018716Y017427D01* +X018663Y017480D01* +X018602Y017525D01* +X018534Y017559D01* +X018462Y017583D01* +X018451Y017584D01* +X020126Y017584D01* +X019960Y017519D01* +X019568Y017207D01* +X019286Y016793D01* +X019139Y016315D01* +X019139Y015814D01* +X019286Y015335D01* +X019568Y014922D01* +X019568Y014922D01* +X019568Y014922D01* +X019960Y014609D01* +X020426Y014426D01* +X020926Y014389D01* +X021414Y014500D01* +X021847Y014751D01* +X021847Y014751D01* +X022188Y015118D01* +X022320Y015392D01* +X022320Y005737D01* +X022188Y006011D01* +X021847Y006378D01* +X021414Y006628D01* +X021414Y006628D01* +X020926Y006740D01* +X020926Y006740D01* +X020426Y006702D01* +X019960Y006519D01* +X019568Y006207D01* +X019286Y005793D01* +X019139Y005315D01* +X019139Y004814D01* +X019231Y004514D01* +X009450Y004514D01* +X009450Y003928D01* +X009326Y003804D01* +X009326Y003544D01* +X002937Y003544D01* +X002964Y003550D01* +X003397Y003801D01* +X003397Y003801D01* +X003738Y004168D01* +X003955Y004619D01* +X004030Y005114D01* +X003955Y005610D01* +X003738Y006061D01* +X003397Y006428D01* +X002964Y006678D01* +X002964Y006678D01* +X002476Y006790D01* +X002476Y006790D01* +X001976Y006752D01* +X001510Y006569D01* +X001118Y006257D01* +X000836Y005843D01* +X000780Y005660D01* +X000780Y008376D01* +X000810Y008304D01* +X000939Y008174D01* +X001108Y008104D01* +X001891Y008104D01* +X002181Y008295D02* +X019653Y008295D01* +X019610Y008453D02* +X013735Y008453D01* +X013753Y008461D02* +X013854Y008561D01* +X013908Y008693D01* +X013908Y008836D01* +X013854Y008967D01* +X013753Y009068D01* +X013621Y009122D01* +X013588Y009122D01* +X011930Y010780D01* +X011930Y012938D01* +X011954Y012961D01* +X012008Y013093D01* +X012008Y013236D01* +X011954Y013367D01* +X019783Y013367D01* +X019804Y013402D02* +X019656Y013147D01* +X019580Y012862D01* +X019580Y012567D01* +X019656Y012282D01* +X019804Y012027D01* +X020012Y011818D01* +X020268Y011671D01* +X020553Y011594D01* +X020847Y011594D01* +X021132Y011671D01* +X021388Y011818D01* +X021596Y012027D01* +X021744Y012282D01* +X021820Y012567D01* +X021820Y012862D01* +X021744Y013147D01* +X021596Y013402D01* +X021388Y013611D01* +X021132Y013758D01* +X020847Y013834D01* +X020553Y013834D01* +X020268Y013758D01* +X020012Y013611D01* +X019804Y013402D01* +X019927Y013525D02* +X000780Y013525D01* +X000780Y013367D02* +X011346Y013367D01* +X011292Y013236D01* +X011292Y013093D01* +X011346Y012961D01* +X011370Y012938D01* +X011370Y010609D01* +X011413Y010506D01* +X013192Y008726D01* +X013192Y008693D01* +X013246Y008561D01* +X013347Y008461D01* +X013479Y008406D01* +X013621Y008406D01* +X013753Y008461D01* +X013874Y008612D02* +X019580Y008612D01* +X019580Y008770D02* +X013908Y008770D01* +X013869Y008929D02* +X019598Y008929D01* +X019640Y009087D02* +X017432Y009087D01* +X017448Y009094D02* +X017571Y009217D01* +X017637Y009377D01* +X017637Y009551D01* +X017571Y009712D01* +X017558Y009724D01* +X017826Y009724D01* +X017829Y009717D01* +X017952Y009594D01* +X018113Y009527D01* +X018287Y009527D01* +X018448Y009594D01* +X018571Y009717D01* +X018637Y009877D01* +X018637Y010051D01* +X018571Y010212D01* +X018448Y010335D01* +X018287Y010401D01* +X018113Y010401D01* +X017952Y010335D01* +X017829Y010212D01* +X017826Y010204D01* +X017576Y010204D01* +X017591Y010225D01* +X017624Y010289D01* +X017646Y010357D01* +X017657Y010428D01* +X017657Y010456D01* +X017209Y010456D01* +X017209Y010473D01* +X017657Y010473D01* +X017657Y010500D01* +X017646Y010571D01* +X017624Y010640D01* +X017591Y010704D01* +X017549Y010762D01* +X017498Y010813D01* +X017440Y010855D01* +X017375Y010888D01* +X017307Y010910D01* +X017236Y010921D01* +X017209Y010921D01* +X017209Y010473D01* +X017191Y010473D01* +X017191Y010456D01* +X016743Y010456D01* +X016743Y010428D01* +X016754Y010357D01* +X016776Y010289D01* +X016809Y010225D01* +X016824Y010204D01* +X016066Y010204D01* +X016053Y010218D01* +X015921Y010272D01* +X015779Y010272D01* +X015647Y010218D01* +X015546Y010117D01* +X015492Y009986D01* +X015492Y009843D01* +X015546Y009711D01* +X015647Y009611D01* +X015779Y009556D01* +X015921Y009556D01* +X016053Y009611D01* +X016154Y009711D01* +X016159Y009724D01* +X016842Y009724D01* +X016829Y009712D01* +X016763Y009551D01* +X016763Y009377D01* +X016829Y009217D01* +X016952Y009094D01* +X017113Y009027D01* +X017287Y009027D01* +X017448Y009094D01* +X017583Y009246D02* +X019714Y009246D01* +X019806Y009404D02* +X017637Y009404D01* +X017632Y009563D02* +X018027Y009563D01* +X017827Y009721D02* +X017561Y009721D01* +X017645Y010355D02* +X018002Y010355D01* +X018113Y010527D02* +X018287Y010527D01* +X018448Y010594D01* +X018571Y010717D01* +X018637Y010877D01* +X018637Y011051D01* +X018571Y011212D01* +X018448Y011335D01* +X018287Y011401D01* +X018113Y011401D01* +X017952Y011335D01* +X017829Y011212D01* +X017763Y011051D01* +X017763Y010877D01* +X017829Y010717D01* +X017952Y010594D01* +X018113Y010527D01* +X017874Y010672D02* +X017607Y010672D01* +X017655Y010514D02* +X022320Y010514D01* +X022320Y010672D02* +X018526Y010672D01* +X018618Y010831D02* +X022320Y010831D01* +X022320Y010989D02* +X018637Y010989D01* +X018597Y011148D02* +X022320Y011148D01* +X022320Y011306D02* +X018476Y011306D01* +X018448Y011594D02* +X018287Y011527D01* +X018113Y011527D01* +X017952Y011594D01* +X017829Y011717D01* +X017763Y011877D01* +X017763Y012051D01* +X017829Y012212D01* +X017952Y012335D01* +X018113Y012401D01* +X018287Y012401D01* +X018448Y012335D01* +X018571Y012212D01* +X018637Y012051D01* +X018637Y011877D01* +X018571Y011717D01* +X018448Y011594D01* +X018477Y011623D02* +X020444Y011623D01* +X020075Y011782D02* +X018598Y011782D01* +X018637Y011940D02* +X019890Y011940D01* +X019762Y012099D02* +X018617Y012099D01* +X018525Y012257D02* +X019671Y012257D01* +X019620Y012416D02* +X011930Y012416D01* +X011930Y012574D02* +X019580Y012574D01* +X019580Y012733D02* +X011930Y012733D01* +X011930Y012891D02* +X019588Y012891D01* +X019630Y013050D02* +X011990Y013050D01* +X012008Y013208D02* +X019692Y013208D01* +X020139Y013684D02* +X000780Y013684D01* +X000780Y013842D02* +X022320Y013842D01* +X022320Y013684D02* +X021261Y013684D01* +X021473Y013525D02* +X022320Y013525D01* +X022320Y013367D02* +X021617Y013367D01* +X021708Y013208D02* +X022320Y013208D01* +X022320Y013050D02* +X021770Y013050D01* +X021812Y012891D02* +X022320Y012891D01* +X022320Y012733D02* +X021820Y012733D01* +X021820Y012574D02* +X022320Y012574D01* +X022320Y012416D02* +X021780Y012416D01* +X021729Y012257D02* +X022320Y012257D01* +X022320Y012099D02* +X021638Y012099D01* +X021510Y011940D02* +X022320Y011940D01* +X022320Y011782D02* +X021325Y011782D01* +X020956Y011623D02* +X022320Y011623D01* +X022320Y011465D02* +X017637Y011465D01* +X017637Y011551D02* +X017637Y011377D01* +X017571Y011217D01* +X017448Y011094D01* +X017287Y011027D01* +X017113Y011027D01* +X016952Y011094D01* +X016829Y011217D01* +X016763Y011377D01* +X016763Y011551D01* +X016829Y011712D01* +X016952Y011835D01* +X017113Y011901D01* +X017287Y011901D01* +X017448Y011835D01* +X017571Y011712D01* +X017637Y011551D01* +X017607Y011623D02* +X017923Y011623D01* +X017802Y011782D02* +X017501Y011782D01* +X017763Y011940D02* +X011930Y011940D01* +X011930Y011782D02* +X016899Y011782D01* +X016793Y011623D02* +X011930Y011623D01* +X011930Y011465D02* +X016763Y011465D01* +X016792Y011306D02* +X011930Y011306D01* +X011930Y011148D02* +X016898Y011148D01* +X017025Y010888D02* +X016960Y010855D01* +X016902Y010813D01* +X016851Y010762D01* +X016809Y010704D01* +X016776Y010640D01* +X016754Y010571D01* +X016743Y010500D01* +X016743Y010473D01* +X017191Y010473D01* +X017191Y010921D01* +X017164Y010921D01* +X017093Y010910D01* +X017025Y010888D01* +X016927Y010831D02* +X011930Y010831D01* +X011930Y010989D02* +X017763Y010989D01* +X017782Y010831D02* +X017473Y010831D01* +X017502Y011148D02* +X017803Y011148D01* +X017924Y011306D02* +X017608Y011306D01* +X017209Y010831D02* +X017191Y010831D01* +X017191Y010672D02* +X017209Y010672D01* +X017209Y010514D02* +X017191Y010514D01* +X016793Y010672D02* +X012038Y010672D01* +X012196Y010514D02* +X016745Y010514D01* +X016755Y010355D02* +X012355Y010355D01* +X012513Y010197D02* +X015626Y010197D01* +X015514Y010038D02* +X012672Y010038D01* +X012830Y009880D02* +X015492Y009880D01* +X015542Y009721D02* +X012989Y009721D01* +X013147Y009563D02* +X015763Y009563D01* +X015937Y009563D02* +X016768Y009563D01* +X016763Y009404D02* +X013306Y009404D01* +X013464Y009246D02* +X016817Y009246D01* +X016968Y009087D02* +X013706Y009087D01* +X013148Y008770D02* +X002213Y008770D01* +X002260Y008612D02* +X013226Y008612D01* +X013365Y008453D02* +X002252Y008453D01* +X002086Y008929D02* +X012990Y008929D01* +X012831Y009087D02* +X000780Y009087D01* +X000780Y008929D02* +X000914Y008929D01* +X000787Y008770D02* +X000780Y008770D01* +X000780Y008295D02* +X000819Y008295D01* +X000780Y007819D02* +X020011Y007819D01* +X020304Y007661D02* +X000780Y007661D01* +X000780Y007502D02* +X022320Y007502D01* +X022320Y007344D02* +X000780Y007344D01* +X000780Y007185D02* +X022320Y007185D01* +X022320Y007027D02* +X000780Y007027D01* +X000780Y006868D02* +X022320Y006868D01* +X022320Y006710D02* +X021056Y006710D01* +X021547Y006551D02* +X022320Y006551D01* +X022320Y006393D02* +X021821Y006393D01* +X021847Y006378D02* +X021847Y006378D01* +X021981Y006234D02* +X022320Y006234D01* +X022320Y006076D02* +X022128Y006076D01* +X022188Y006011D02* +X022188Y006011D01* +X022233Y005917D02* +X022320Y005917D01* +X022309Y005759D02* +X022320Y005759D01* +X020528Y006710D02* +X002825Y006710D01* +X003184Y006551D02* +X020042Y006551D01* +X019960Y006519D02* +X019960Y006519D01* +X019801Y006393D02* +X003430Y006393D01* +X003397Y006428D02* +X003397Y006428D01* +X003577Y006234D02* +X019603Y006234D01* +X019568Y006207D02* +X019568Y006207D01* +X019479Y006076D02* +X003724Y006076D01* +X003738Y006061D02* +X003738Y006061D01* +X003807Y005917D02* +X019371Y005917D01* +X019286Y005793D02* +X019286Y005793D01* +X019276Y005759D02* +X003883Y005759D01* +X003955Y005610D02* +X003955Y005610D01* +X003957Y005600D02* +X019227Y005600D01* +X019178Y005442D02* +X003981Y005442D01* +X004005Y005283D02* +X019139Y005283D01* +X019139Y005125D02* +X004028Y005125D01* +X004008Y004966D02* +X019139Y004966D01* +X019141Y004808D02* +X003984Y004808D01* +X003960Y004649D02* +X019190Y004649D01* +X020426Y006702D02* +X020426Y006702D01* +X021096Y007661D02* +X022320Y007661D01* +X022320Y007819D02* +X021389Y007819D01* +X021547Y007978D02* +X022320Y007978D01* +X022320Y008136D02* +X021660Y008136D01* +X021747Y008295D02* +X022320Y008295D01* +X022320Y008453D02* +X021790Y008453D01* +X021820Y008612D02* +X022320Y008612D01* +X022320Y008770D02* +X021820Y008770D01* +X021802Y008929D02* +X022320Y008929D01* +X022320Y009087D02* +X021760Y009087D01* +X021686Y009246D02* +X022320Y009246D01* +X022320Y009404D02* +X021594Y009404D01* +X021435Y009563D02* +X022320Y009563D01* +X022320Y009721D02* +X021196Y009721D01* +X020204Y009721D02* +X018573Y009721D01* +X018637Y009880D02* +X022320Y009880D01* +X022320Y010038D02* +X018637Y010038D01* +X018577Y010197D02* +X022320Y010197D01* +X022320Y010355D02* +X018398Y010355D01* +X018200Y009964D02* +X015900Y009964D01* +X015850Y009914D01* +X016158Y009721D02* +X016839Y009721D01* +X018373Y009563D02* +X019965Y009563D01* +X017783Y012099D02* +X011930Y012099D01* +X011930Y012257D02* +X017875Y012257D01* +X020426Y014426D02* +X020426Y014426D01* +X020299Y014476D02* +X002808Y014476D01* +X002914Y014500D02* +X002914Y014500D01* +X003147Y014635D02* +X019928Y014635D01* +X019960Y014609D02* +X019960Y014609D01* +X019729Y014793D02* +X003387Y014793D01* +X003534Y014952D02* +X019548Y014952D01* +X019440Y015110D02* +X003681Y015110D01* +X003688Y015118D02* +X003688Y015118D01* +X003761Y015269D02* +X019332Y015269D01* +X019286Y015335D02* +X019286Y015335D01* +X019258Y015427D02* +X003837Y015427D01* +X003905Y015569D02* +X003905Y015569D01* +X003908Y015586D02* +X019209Y015586D01* +X019160Y015744D02* +X003932Y015744D01* +X003956Y015903D02* +X019139Y015903D01* +X019139Y016061D02* +X018509Y016061D01* +X018370Y016061D02* +X018330Y016061D01* +X018330Y016034D02* +X018330Y016794D01* +X017870Y016794D01* +X017870Y016477D01* +X017882Y016402D01* +X017905Y016330D01* +X017939Y016263D01* +X017984Y016202D01* +X018037Y016148D01* +X018098Y016104D01* +X018166Y016069D01* +X018238Y016046D01* +X018312Y016034D01* +X018330Y016034D01* +X018191Y016061D02* +X017458Y016061D01* +X017441Y016054D02* +X017611Y016124D01* +X017740Y016254D01* +X017810Y016423D01* +X017810Y017206D01* +X017740Y017375D01* +X017611Y017504D01* +X017441Y017574D01* +X017258Y017574D01* +X017089Y017504D01* +X016960Y017375D01* +X016890Y017206D01* +X016890Y016423D01* +X016960Y016254D01* +X017089Y016124D01* +X017258Y016054D01* +X017441Y016054D01* +X017242Y016061D02* +X003980Y016061D01* +X003980Y016064D02* +X003980Y016064D01* +X003957Y016220D02* +X005221Y016220D01* +X005479Y016220D02* +X006221Y016220D01* +X006479Y016220D02* +X007165Y016220D01* +X007166Y016219D02* +X007238Y016196D01* +X007312Y016184D01* +X007330Y016184D01* +X007330Y016944D01* +X006870Y016944D01* +X006870Y016627D01* +X006882Y016552D01* +X006905Y016480D01* +X006939Y016413D01* +X006984Y016352D01* +X007037Y016298D01* +X007098Y016254D01* +X007166Y016219D01* +X007330Y016220D02* +X007370Y016220D01* +X007370Y016378D02* +X007330Y016378D01* +X007330Y016537D02* +X007370Y016537D01* +X007370Y016695D02* +X007330Y016695D01* +X007330Y016854D02* +X007370Y016854D01* +X007830Y016854D02* +X016890Y016854D01* +X016890Y017012D02* +X007830Y017012D01* +X007830Y017171D02* +X016890Y017171D01* +X016941Y017329D02* +X007826Y017329D01* +X007775Y017488D02* +X017073Y017488D01* +X017627Y017488D02* +X018047Y017488D01* +X017921Y017329D02* +X017759Y017329D01* +X017810Y017171D02* +X017873Y017171D01* +X017870Y017012D02* +X017810Y017012D01* +X017810Y016854D02* +X017870Y016854D01* +X017870Y016695D02* +X017810Y016695D01* +X017810Y016537D02* +X017870Y016537D01* +X017889Y016378D02* +X017792Y016378D01* +X017706Y016220D02* +X017971Y016220D01* +X018330Y016220D02* +X018370Y016220D01* +X018370Y016378D02* +X018330Y016378D01* +X018330Y016537D02* +X018370Y016537D01* +X018370Y016695D02* +X018330Y016695D01* +X018830Y016695D02* +X019256Y016695D01* +X019286Y016793D02* +X019286Y016793D01* +X019328Y016854D02* +X018830Y016854D01* +X018830Y017012D02* +X019436Y017012D01* +X019544Y017171D02* +X018827Y017171D01* +X018779Y017329D02* +X019722Y017329D01* +X019568Y017207D02* +X019568Y017207D01* +X019921Y017488D02* +X018653Y017488D01* +X018830Y016537D02* +X019207Y016537D01* +X019158Y016378D02* +X018811Y016378D01* +X018729Y016220D02* +X019139Y016220D01* +X019960Y017519D02* +X019960Y017519D01* +X022261Y015269D02* +X022320Y015269D01* +X022320Y015110D02* +X022181Y015110D01* +X022188Y015118D02* +X022188Y015118D01* +X022320Y014952D02* +X022034Y014952D01* +X021887Y014793D02* +X022320Y014793D01* +X022320Y014635D02* +X021647Y014635D01* +X021414Y014500D02* +X021414Y014500D01* +X021308Y014476D02* +X022320Y014476D01* +X022320Y014318D02* +X000780Y014318D01* +X000780Y014476D02* +X001799Y014476D01* +X001926Y014426D02* +X001926Y014426D01* +X001460Y014609D02* +X001460Y014609D01* +X001428Y014635D02* +X000780Y014635D01* +X000780Y014793D02* +X001229Y014793D01* +X001048Y014952D02* +X000780Y014952D01* +X000780Y015110D02* +X000940Y015110D01* +X000832Y015269D02* +X000780Y015269D01* +X000786Y015335D02* +X000786Y015335D01* +X000780Y014159D02* +X022320Y014159D01* +X022320Y014001D02* +X000780Y014001D01* +X000780Y013208D02* +X011292Y013208D01* +X011310Y013050D02* +X000780Y013050D01* +X000780Y012891D02* +X000876Y012891D01* +X000856Y012257D02* +X000780Y012257D01* +X000780Y012099D02* +X011370Y012099D01* +X011370Y012257D02* +X002144Y012257D01* +X002236Y012416D02* +X011370Y012416D01* +X011370Y012574D02* +X002260Y012574D01* +X002228Y012733D02* +X011370Y012733D01* +X011370Y012891D02* +X002124Y012891D01* +X002075Y011940D02* +X011370Y011940D01* +X011370Y011782D02* +X002208Y011782D01* +X002260Y011623D02* +X011370Y011623D01* +X011370Y011465D02* +X002257Y011465D01* +X002191Y011306D02* +X011370Y011306D01* +X011370Y011148D02* +X001997Y011148D01* +X001976Y010989D02* +X011370Y010989D01* +X011370Y010831D02* +X002184Y010831D01* +X002253Y010672D02* +X011370Y010672D01* +X011409Y010514D02* +X002260Y010514D01* +X002211Y010355D02* +X011563Y010355D01* +X011722Y010197D02* +X002083Y010197D01* +X002135Y009880D02* +X012039Y009880D01* +X012197Y009721D02* +X002233Y009721D01* +X002260Y009563D02* +X012356Y009563D01* +X012514Y009404D02* +X002232Y009404D01* +X002132Y009246D02* +X012673Y009246D01* +X011880Y010038D02* +X000780Y010038D01* +X000780Y009880D02* +X000865Y009880D01* +X000917Y010197D02* +X000780Y010197D01* +X000780Y010355D02* +X000789Y010355D01* +X000780Y010831D02* +X000816Y010831D01* +X000780Y010989D02* +X001024Y010989D01* +X001003Y011148D02* +X000780Y011148D01* +X000780Y011306D02* +X000809Y011306D01* +X000780Y011782D02* +X000792Y011782D01* +X000780Y011940D02* +X000925Y011940D01* +X002426Y014389D02* +X002426Y014389D01* +X003933Y016378D02* +X004985Y016378D01* +X004905Y016537D02* +X003909Y016537D01* +X003840Y016695D02* +X004890Y016695D01* +X004890Y016854D02* +X003764Y016854D01* +X003688Y017011D02* +X003688Y017011D01* +X003687Y017012D02* +X004890Y017012D01* +X004890Y017171D02* +X003539Y017171D01* +X003392Y017329D02* +X004890Y017329D01* +X004945Y017488D02* +X003157Y017488D01* +X003347Y017378D02* +X003347Y017378D01* +X005715Y016378D02* +X005985Y016378D01* +X005905Y016537D02* +X005795Y016537D01* +X005810Y016695D02* +X005890Y016695D01* +X005890Y016854D02* +X005810Y016854D01* +X005810Y017012D02* +X005890Y017012D01* +X005890Y017171D02* +X005810Y017171D01* +X005810Y017329D02* +X005890Y017329D01* +X005945Y017488D02* +X005755Y017488D01* +X006755Y017488D02* +X006925Y017488D01* +X006874Y017329D02* +X006810Y017329D01* +X006810Y017171D02* +X006870Y017171D01* +X006870Y017012D02* +X006810Y017012D01* +X006810Y016854D02* +X006870Y016854D01* +X006870Y016695D02* +X006810Y016695D01* +X006795Y016537D02* +X006887Y016537D01* +X006964Y016378D02* +X006715Y016378D01* +X007535Y016220D02* +X016994Y016220D01* +X016908Y016378D02* +X007736Y016378D01* +X007813Y016537D02* +X016890Y016537D01* +X016890Y016695D02* +X007830Y016695D01* +X011346Y013367D02* +X011447Y013468D01* +X011579Y013522D01* +X011721Y013522D01* +X011853Y013468D01* +X011954Y013367D01* +X020926Y014389D02* +X020926Y014389D01* +X009450Y004491D02* +X003894Y004491D01* +X003955Y004619D02* +X003955Y004619D01* +X003817Y004332D02* +X009450Y004332D01* +X009450Y004174D02* +X003741Y004174D01* +X003738Y004168D02* +X003738Y004168D01* +X003596Y004015D02* +X009450Y004015D01* +X009379Y003857D02* +X003449Y003857D01* +X003220Y003698D02* +X009326Y003698D01* +X002964Y003550D02* +X002964Y003550D01* +X000810Y005759D02* +X000780Y005759D01* +X000836Y005843D02* +X000836Y005843D01* +X000887Y005917D02* +X000780Y005917D01* +X000780Y006076D02* +X000995Y006076D01* +X001103Y006234D02* +X000780Y006234D01* +X000780Y006393D02* +X001289Y006393D01* +X001118Y006257D02* +X001118Y006257D01* +X000780Y006551D02* +X001488Y006551D01* +X001510Y006569D02* +X001510Y006569D01* +X001868Y006710D02* +X000780Y006710D01* +X001976Y006752D02* +X001976Y006752D01* +X000868Y009246D02* +X000780Y009246D01* +D16* +X004150Y011564D03* +X006500Y013714D03* +X010000Y015114D03* +X011650Y013164D03* +X013300Y011464D03* +X013350Y010114D03* +X013550Y008764D03* +X013500Y006864D03* +X012100Y005314D03* +X009250Y004064D03* +X015200Y004514D03* +X015650Y006264D03* +X015850Y009914D03* +X014250Y014964D03* +D17* +X011650Y013164D02* +X011650Y010664D01* +X013550Y008764D01* +M02* diff --git a/gerber/tests/resources/bottom_mask.GBS b/gerber/tests/resources/bottom_mask.GBS new file mode 100644 index 0000000..b06654f --- /dev/null +++ b/gerber/tests/resources/bottom_mask.GBS @@ -0,0 +1,66 @@ +G75* +%MOIN*% +%OFA0B0*% +%FSLAX24Y24*% +%IPPOS*% +%LPD*% +%AMOC8* +5,1,8,0,0,1.08239X$1,22.5* +% +%ADD10C,0.0634*% +%ADD11C,0.1360*% +%ADD12C,0.0680*% +%ADD13C,0.1340*% +%ADD14C,0.0476*% +D10* +X017200Y009464D03* +X018200Y009964D03* +X018200Y010964D03* +X017200Y010464D03* +X017200Y011464D03* +X018200Y011964D03* +D11* +X020700Y012714D03* +X020700Y008714D03* +D12* +X018350Y016514D02* +X018350Y017114D01* +X017350Y017114D02* +X017350Y016514D01* +X007350Y016664D02* +X007350Y017264D01* +X006350Y017264D02* +X006350Y016664D01* +X005350Y016664D02* +X005350Y017264D01* +X001800Y012564D02* +X001200Y012564D01* +X001200Y011564D02* +X001800Y011564D01* +X001800Y010564D02* +X001200Y010564D01* +X001200Y009564D02* +X001800Y009564D01* +X001800Y008564D02* +X001200Y008564D01* +D13* +X002350Y005114D03* +X002300Y016064D03* +X020800Y016064D03* +X020800Y005064D03* +D14* +X015650Y006264D03* +X013500Y006864D03* +X012100Y005314D03* +X009250Y004064D03* +X015200Y004514D03* +X013550Y008764D03* +X013350Y010114D03* +X013300Y011464D03* +X011650Y013164D03* +X010000Y015114D03* +X006500Y013714D03* +X004150Y011564D03* +X014250Y014964D03* +X015850Y009914D03* +M02* diff --git a/gerber/tests/resources/bottom_silk.GBO b/gerber/tests/resources/bottom_silk.GBO new file mode 100644 index 0000000..0e19197 --- /dev/null +++ b/gerber/tests/resources/bottom_silk.GBO @@ -0,0 +1,6007 @@ +G75* +%MOIN*% +%OFA0B0*% +%FSLAX24Y24*% +%IPPOS*% +%LPD*% +%AMOC8* +5,1,8,0,0,1.08239X$1,22.5* +% +%ADD10C,0.0000*% +%ADD11R,0.0470X0.0010*% +%ADD12R,0.0560X0.0010*% +%ADD13R,0.0570X0.0010*% +%ADD14R,0.0580X0.0010*% +%ADD15R,0.0300X0.0010*% +%ADD16R,0.0450X0.0010*% +%ADD17R,0.0670X0.0010*% +%ADD18R,0.0510X0.0010*% +%ADD19R,0.0760X0.0010*% +%ADD20R,0.0520X0.0010*% +%ADD21R,0.1900X0.0010*% +%ADD22R,0.0820X0.0010*% +%ADD23R,0.0880X0.0010*% +%ADD24R,0.0530X0.0010*% +%ADD25R,0.0940X0.0010*% +%ADD26R,0.1000X0.0010*% +%ADD27R,0.0540X0.0010*% +%ADD28R,0.1050X0.0010*% +%ADD29R,0.0550X0.0010*% +%ADD30R,0.1100X0.0010*% +%ADD31R,0.1140X0.0010*% +%ADD32R,0.1180X0.0010*% +%ADD33R,0.1220X0.0010*% +%ADD34R,0.1260X0.0010*% +%ADD35R,0.1300X0.0010*% +%ADD36R,0.1320X0.0010*% +%ADD37R,0.0590X0.0010*% +%ADD38R,0.1360X0.0010*% +%ADD39R,0.0600X0.0010*% +%ADD40R,0.1400X0.0010*% +%ADD41R,0.1420X0.0010*% +%ADD42R,0.0610X0.0010*% +%ADD43R,0.1460X0.0010*% +%ADD44R,0.1480X0.0010*% +%ADD45R,0.0620X0.0010*% +%ADD46R,0.1500X0.0010*% +%ADD47R,0.0630X0.0010*% +%ADD48R,0.1540X0.0010*% +%ADD49R,0.1560X0.0010*% +%ADD50R,0.0640X0.0010*% +%ADD51R,0.1580X0.0010*% +%ADD52R,0.0650X0.0010*% +%ADD53R,0.1600X0.0010*% +%ADD54R,0.1640X0.0010*% +%ADD55R,0.0660X0.0010*% +%ADD56R,0.1660X0.0010*% +%ADD57R,0.1680X0.0010*% +%ADD58R,0.1700X0.0010*% +%ADD59R,0.0680X0.0010*% +%ADD60R,0.1720X0.0010*% +%ADD61R,0.1740X0.0010*% +%ADD62R,0.0690X0.0010*% +%ADD63R,0.1760X0.0010*% +%ADD64R,0.1780X0.0010*% +%ADD65R,0.0700X0.0010*% +%ADD66R,0.1800X0.0010*% +%ADD67R,0.0710X0.0010*% +%ADD68R,0.1820X0.0010*% +%ADD69R,0.0720X0.0010*% +%ADD70R,0.1840X0.0010*% +%ADD71R,0.0730X0.0010*% +%ADD72R,0.1860X0.0010*% +%ADD73R,0.1880X0.0010*% +%ADD74R,0.0740X0.0010*% +%ADD75R,0.1920X0.0010*% +%ADD76R,0.0750X0.0010*% +%ADD77R,0.1940X0.0010*% +%ADD78R,0.0860X0.0010*% +%ADD79R,0.0850X0.0010*% +%ADD80R,0.0810X0.0010*% +%ADD81R,0.0770X0.0010*% +%ADD82R,0.0790X0.0010*% +%ADD83R,0.0780X0.0010*% +%ADD84R,0.0800X0.0010*% +%ADD85R,0.0830X0.0010*% +%ADD86R,0.0840X0.0010*% +%ADD87R,0.0870X0.0010*% +%ADD88R,0.0890X0.0010*% +%ADD89R,0.0900X0.0010*% +%ADD90R,0.0910X0.0010*% +%ADD91R,0.0920X0.0010*% +%ADD92R,0.0930X0.0010*% +%ADD93R,0.0950X0.0010*% +%ADD94R,0.0960X0.0010*% +%ADD95R,0.0970X0.0010*% +%ADD96R,0.0980X0.0010*% +%ADD97R,0.0990X0.0010*% +%ADD98R,0.1010X0.0010*% +%ADD99R,0.1020X0.0010*% +%ADD100R,0.1030X0.0010*% +%ADD101R,0.1040X0.0010*% +%ADD102R,0.0480X0.0010*% +%ADD103R,0.1990X0.0010*% +%ADD104R,0.1850X0.0010*% +%ADD105R,0.1620X0.0010*% +%ADD106R,0.1570X0.0010*% +%ADD107R,0.1550X0.0010*% +%ADD108R,0.1520X0.0010*% +%ADD109R,0.1490X0.0010*% +%ADD110R,0.1470X0.0010*% +%ADD111R,0.1430X0.0010*% +%ADD112R,0.1410X0.0010*% +%ADD113R,0.1380X0.0010*% +%ADD114R,0.1350X0.0010*% +%ADD115R,0.1310X0.0010*% +%ADD116R,0.1280X0.0010*% +%ADD117R,0.1250X0.0010*% +%ADD118R,0.1210X0.0010*% +%ADD119R,0.1170X0.0010*% +%ADD120R,0.1120X0.0010*% +%ADD121R,0.1080X0.0010*% +%ADD122R,0.0500X0.0010*% +%ADD123R,0.0370X0.0010*% +%ADD124R,0.0070X0.0010*% +%ADD125R,0.2950X0.0010*% +%ADD126R,0.0490X0.0010*% +%ADD127R,0.1290X0.0010*% +%ADD128R,0.1610X0.0010*% +%ADD129R,0.1690X0.0010*% +%ADD130R,0.1710X0.0010*% +%ADD131R,0.1730X0.0010*% +%ADD132R,0.1750X0.0010*% +%ADD133R,0.1810X0.0010*% +%ADD134R,0.1830X0.0010*% +%ADD135R,0.1870X0.0010*% +%ADD136R,0.1890X0.0010*% +%ADD137R,0.1910X0.0010*% +%ADD138R,0.1930X0.0010*% +%ADD139R,0.1950X0.0010*% +%ADD140R,0.1960X0.0010*% +%ADD141R,0.1970X0.0010*% +%ADD142R,0.1980X0.0010*% +%ADD143R,0.2000X0.0010*% +%ADD144R,0.2010X0.0010*% +%ADD145R,0.2020X0.0010*% +%ADD146R,0.2060X0.0010*% +%ADD147R,0.2050X0.0010*% +%ADD148R,0.2030X0.0010*% +%ADD149R,0.1790X0.0010*% +%ADD150R,0.1770X0.0010*% +%ADD151R,0.1450X0.0010*% +%ADD152R,0.1440X0.0010*% +%ADD153R,0.1670X0.0010*% +%ADD154R,0.1650X0.0010*% +%ADD155R,0.1630X0.0010*% +%ADD156R,0.1390X0.0010*% +%ADD157R,0.1370X0.0010*% +%ADD158R,0.3140X0.0010*% +%ADD159R,0.1240X0.0010*% +%ADD160C,0.0004*% +D10* +X000303Y003014D02* +X000310Y003014D01* +X000313Y003018D01* +X000318Y003018D02* +X000318Y003014D01* +X000322Y003014D01* +X000322Y003018D01* +X000318Y003018D01* +X000318Y003024D02* +X000318Y003028D01* +X000322Y003028D01* +X000322Y003024D01* +X000318Y003024D01* +X000313Y003031D02* +X000310Y003034D01* +X000303Y003034D01* +X000300Y003031D01* +X000300Y003018D01* +X000303Y003014D01* +X000328Y003014D02* +X000341Y003034D01* +X000346Y003034D02* +X000346Y003018D01* +X000349Y003014D01* +X000356Y003014D01* +X000359Y003018D01* +X000359Y003034D01* +X000368Y003028D02* +X000378Y003028D01* +X000383Y003024D02* +X000386Y003028D01* +X000393Y003028D01* +X000396Y003024D01* +X000396Y003021D01* +X000383Y003021D01* +X000383Y003018D02* +X000383Y003024D01* +X000383Y003018D02* +X000386Y003014D01* +X000393Y003014D01* +X000401Y003014D02* +X000401Y003028D01* +X000408Y003028D02* +X000411Y003028D01* +X000408Y003028D02* +X000401Y003021D01* +X000417Y003024D02* +X000420Y003028D01* +X000430Y003028D01* +X000427Y003021D02* +X000420Y003021D01* +X000417Y003024D01* +X000417Y003014D02* +X000427Y003014D01* +X000430Y003018D01* +X000427Y003021D01* +X000435Y003014D02* +X000448Y003034D01* +X000453Y003034D02* +X000453Y003014D01* +X000453Y003024D02* +X000457Y003028D01* +X000463Y003028D01* +X000467Y003024D01* +X000467Y003014D01* +X000472Y003018D02* +X000475Y003021D01* +X000485Y003021D01* +X000485Y003024D02* +X000485Y003014D01* +X000475Y003014D01* +X000472Y003018D01* +X000475Y003028D02* +X000482Y003028D01* +X000485Y003024D01* +X000490Y003028D02* +X000494Y003028D01* +X000497Y003024D01* +X000500Y003028D01* +X000504Y003024D01* +X000504Y003014D01* +X000509Y003014D02* +X000515Y003014D01* +X000512Y003014D02* +X000512Y003028D01* +X000509Y003028D01* +X000512Y003034D02* +X000512Y003038D01* +X000521Y003034D02* +X000524Y003034D01* +X000524Y003014D01* +X000521Y003014D02* +X000528Y003014D01* +X000537Y003018D02* +X000540Y003014D01* +X000537Y003018D02* +X000537Y003031D01* +X000540Y003028D02* +X000533Y003028D01* +X000546Y003024D02* +X000546Y003018D01* +X000549Y003014D01* +X000556Y003014D01* +X000559Y003018D01* +X000559Y003024D01* +X000556Y003028D01* +X000549Y003028D01* +X000546Y003024D01* +X000564Y003028D02* +X000574Y003028D01* +X000577Y003024D01* +X000577Y003014D01* +X000582Y003014D02* +X000586Y003014D01* +X000586Y003018D01* +X000582Y003018D01* +X000582Y003014D01* +X000592Y003014D02* +X000592Y003034D01* +X000602Y003028D02* +X000592Y003021D01* +X000602Y003014D01* +X000607Y003014D02* +X000614Y003014D01* +X000610Y003014D02* +X000610Y003028D01* +X000607Y003028D01* +X000610Y003034D02* +X000610Y003038D01* +X000619Y003034D02* +X000619Y003014D01* +X000629Y003014D01* +X000633Y003018D01* +X000633Y003024D01* +X000629Y003028D01* +X000619Y003028D01* +X000638Y003028D02* +X000648Y003028D01* +X000651Y003024D01* +X000651Y003018D01* +X000648Y003014D01* +X000638Y003014D01* +X000638Y003034D01* +X000656Y003024D02* +X000659Y003028D01* +X000666Y003028D01* +X000669Y003024D01* +X000669Y003021D01* +X000656Y003021D01* +X000656Y003018D02* +X000656Y003024D01* +X000656Y003018D02* +X000659Y003014D01* +X000666Y003014D01* +X000674Y003014D02* +X000688Y003034D01* +X000693Y003034D02* +X000703Y003034D01* +X000706Y003031D01* +X000706Y003018D01* +X000703Y003014D01* +X000693Y003014D01* +X000693Y003034D01* +X000711Y003024D02* +X000715Y003028D01* +X000721Y003028D01* +X000725Y003024D01* +X000725Y003021D01* +X000711Y003021D01* +X000711Y003018D02* +X000711Y003024D01* +X000711Y003018D02* +X000715Y003014D01* +X000721Y003014D01* +X000730Y003014D02* +X000740Y003014D01* +X000743Y003018D01* +X000740Y003021D01* +X000733Y003021D01* +X000730Y003024D01* +X000733Y003028D01* +X000743Y003028D01* +X000748Y003034D02* +X000748Y003014D01* +X000748Y003021D02* +X000758Y003028D01* +X000763Y003028D02* +X000770Y003028D01* +X000767Y003031D02* +X000767Y003018D01* +X000770Y003014D01* +X000776Y003018D02* +X000779Y003014D01* +X000786Y003014D01* +X000789Y003018D01* +X000789Y003024D01* +X000786Y003028D01* +X000779Y003028D01* +X000776Y003024D01* +X000776Y003018D01* +X000758Y003014D02* +X000748Y003021D01* +X000794Y003014D02* +X000804Y003014D01* +X000807Y003018D01* +X000807Y003024D01* +X000804Y003028D01* +X000794Y003028D01* +X000794Y003008D01* +X000813Y003014D02* +X000826Y003034D01* +X000831Y003034D02* +X000831Y003014D01* +X000841Y003014D01* +X000844Y003018D01* +X000844Y003024D01* +X000841Y003028D01* +X000831Y003028D01* +X000849Y003028D02* +X000856Y003028D01* +X000853Y003031D02* +X000853Y003018D01* +X000856Y003014D01* +X000865Y003014D02* +X000865Y003031D01* +X000868Y003034D01* +X000874Y003028D02* +X000887Y003014D01* +X000892Y003014D02* +X000896Y003014D01* +X000896Y003018D01* +X000892Y003018D01* +X000892Y003014D01* +X000902Y003014D02* +X000912Y003014D01* +X000915Y003018D01* +X000915Y003021D01* +X000912Y003024D01* +X000902Y003024D01* +X000912Y003024D02* +X000915Y003028D01* +X000915Y003031D01* +X000912Y003034D01* +X000902Y003034D01* +X000902Y003014D01* +X000920Y003014D02* +X000920Y003034D01* +X000927Y003028D01* +X000933Y003034D01* +X000933Y003014D01* +X000938Y003014D02* +X000938Y003034D01* +X000948Y003034D01* +X000952Y003031D01* +X000952Y003024D01* +X000948Y003021D01* +X000938Y003021D01* +X000887Y003028D02* +X000874Y003014D01* +X000868Y003024D02* +X000862Y003024D01* +X000564Y003014D02* +X000564Y003028D01* +X000497Y003024D02* +X000497Y003014D01* +X000490Y003014D02* +X000490Y003028D01* +X000378Y003018D02* +X000374Y003021D01* +X000368Y003021D01* +X000364Y003024D01* +X000368Y003028D01* +X000364Y003014D02* +X000374Y003014D01* +X000378Y003018D01* +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* +X015810Y007044D03* +X015810Y007054D03* +X015810Y007064D03* +X015810Y007074D03* +X015810Y007084D03* +X015810Y007094D03* +X015810Y007104D03* +X015810Y007114D03* +X015810Y007124D03* +X015810Y007134D03* +X015810Y007144D03* +X015810Y007154D03* +X015810Y007164D03* +X015810Y007174D03* +X015810Y007184D03* +X015810Y007194D03* +X015810Y007204D03* +X015810Y007214D03* +X015810Y007224D03* +X015810Y007234D03* +X015810Y007244D03* +X015810Y007254D03* +X015810Y007264D03* +X015810Y007274D03* +X015810Y007284D03* +X015810Y007294D03* +X015810Y007304D03* +X015810Y007314D03* +X015810Y007324D03* +X015810Y007334D03* +X015810Y007344D03* +X015810Y007354D03* +X015810Y007364D03* +X015810Y007374D03* +X015810Y007384D03* +X015810Y007394D03* +X015810Y007404D03* +X015810Y007414D03* +X015810Y007424D03* +X015810Y007434D03* +X015810Y007444D03* +X015810Y007454D03* +X015810Y007464D03* +X015810Y007474D03* +X015810Y007484D03* +X015810Y007494D03* +X015810Y007504D03* +X015810Y007514D03* +X015810Y007524D03* +X015810Y007534D03* +X015810Y007544D03* +X015810Y007554D03* +X015810Y007564D03* +X015810Y007574D03* +X015810Y007584D03* +X015810Y007594D03* +X015810Y007604D03* +X015810Y007614D03* +X015810Y007624D03* +X015810Y007634D03* +X015810Y007644D03* +X015810Y007654D03* +X015810Y007664D03* +X015810Y007674D03* +X015810Y007684D03* +X015810Y007694D03* +X015810Y007704D03* +X015810Y007714D03* +X015810Y007724D03* +X015810Y007734D03* +X015810Y007744D03* +X015810Y007754D03* +X015810Y007764D03* +X015810Y007774D03* +X015810Y007784D03* +X015810Y007794D03* +X015810Y007804D03* +X015810Y007814D03* +X015810Y007824D03* +X015810Y007834D03* +X015810Y007844D03* +X015810Y007854D03* +X015810Y007864D03* +X015810Y007874D03* +X015810Y007884D03* +X015810Y007894D03* +X015810Y007904D03* +X015810Y007914D03* +X015810Y007924D03* +X015810Y007934D03* +X015810Y007944D03* +X015810Y007954D03* +X015810Y007964D03* +X015810Y007974D03* +X015810Y007984D03* +X015810Y007994D03* +X015810Y008004D03* +X015810Y008014D03* +X015810Y008024D03* +X015810Y008034D03* +X015810Y008044D03* +X015810Y008054D03* +X015810Y008064D03* +X015810Y008074D03* +X015810Y008084D03* +X015810Y008094D03* +X015810Y008104D03* +X015810Y008114D03* +X015810Y008124D03* +X015810Y008134D03* +X015810Y008144D03* +X015810Y008154D03* +X015810Y008164D03* +X015810Y008174D03* +X015810Y008184D03* +X015810Y008194D03* +X015810Y008204D03* +X015810Y008214D03* +X015810Y008224D03* +X015810Y008234D03* +X015810Y008244D03* +X015810Y008254D03* +X015810Y008264D03* +X015810Y008274D03* +X015810Y008284D03* +X015810Y008294D03* +X015810Y008304D03* +X015810Y008314D03* +X015810Y008324D03* +X015810Y008334D03* +X015810Y008344D03* +X015810Y008354D03* +X015810Y008364D03* +X015810Y008374D03* +X015810Y008384D03* +X015810Y008394D03* +X015810Y008404D03* +X015810Y008414D03* +X015810Y008424D03* +X015810Y008434D03* +X015810Y008444D03* +X015810Y008454D03* +X015810Y008464D03* +X015810Y008474D03* +X015810Y008484D03* +X015810Y008494D03* +X015810Y008504D03* +X015810Y008514D03* +X015810Y008524D03* +X015810Y008534D03* +X015810Y008544D03* +X015810Y008554D03* +X015810Y008564D03* +X015810Y008574D03* +X015810Y008584D03* +X015810Y008594D03* +X015810Y008604D03* +X015810Y008614D03* +X015810Y008624D03* +X015810Y008634D03* +X010930Y008634D03* +X010930Y008624D03* +X010930Y008614D03* +X010930Y008604D03* +X010930Y008594D03* +X010930Y008584D03* +X010930Y008574D03* +X010930Y008564D03* +X010930Y008554D03* +X010930Y008544D03* +X010930Y008534D03* +X010930Y008524D03* +X010930Y008514D03* +X010930Y008504D03* +X010930Y008494D03* +X010930Y008484D03* +X010930Y008474D03* +X010930Y008464D03* +X010930Y008454D03* +X010930Y008444D03* +X010930Y008434D03* +X010930Y008424D03* +X010930Y008414D03* +X010930Y008404D03* +X010930Y008394D03* +X010930Y008384D03* +X010930Y008374D03* +X010930Y008364D03* +X010930Y008354D03* +X010930Y008344D03* +X010930Y008334D03* +X010930Y008324D03* +X010930Y008314D03* +X010930Y008304D03* +X010930Y008294D03* +X010930Y008284D03* +X010930Y008274D03* +X010930Y008264D03* +X010930Y008254D03* +X010930Y008244D03* +X010930Y008234D03* +X010930Y008224D03* +X010930Y008214D03* +X010930Y008204D03* +X010930Y008194D03* +X010930Y008184D03* +X010930Y008174D03* +X010930Y008164D03* +X010930Y008154D03* +X010930Y008144D03* +X010930Y008134D03* +X010930Y008124D03* +X010930Y008114D03* +X010930Y008104D03* +X010930Y008094D03* +X010930Y008084D03* +X010930Y008074D03* +X010930Y008064D03* +X010930Y008054D03* +X010930Y008044D03* +X010930Y008034D03* +X010930Y008024D03* +X010930Y008014D03* +X010930Y008004D03* +X010930Y007994D03* +X010930Y007984D03* +X010930Y007974D03* +X010930Y007964D03* +X010930Y007954D03* +X010930Y007944D03* +X010930Y007934D03* +X010930Y007924D03* +X010930Y007914D03* +X010930Y007904D03* +X010930Y007894D03* +X010930Y007884D03* +X010930Y007874D03* +X010930Y007864D03* +X010930Y007854D03* +X010930Y007844D03* +X010930Y007834D03* +X010930Y007824D03* +X010930Y007814D03* +X010930Y007804D03* +X010930Y007794D03* +X010930Y007784D03* +X010930Y007774D03* +X010930Y007764D03* +X010930Y007754D03* +X010930Y007744D03* +X010930Y007734D03* +X010930Y007724D03* +X010930Y007714D03* +X010930Y007704D03* +X010930Y007694D03* +X010930Y007684D03* +X010930Y007674D03* +X010930Y007664D03* +X010930Y007654D03* +X010930Y007644D03* +X010930Y007634D03* +X010930Y007624D03* +X010930Y007614D03* +X010930Y007604D03* +X010930Y007594D03* +X010930Y007584D03* +X010930Y007574D03* +X010930Y007564D03* +X010930Y007554D03* +X010930Y007544D03* +X010930Y007534D03* +X010930Y007524D03* +X010930Y007514D03* +X010930Y007504D03* +X010930Y007494D03* +X010930Y007484D03* +X010930Y007474D03* +X010930Y007464D03* +X010930Y007454D03* +X010930Y007444D03* +X010930Y007434D03* +X010930Y007424D03* +X010930Y007414D03* +X010930Y007404D03* +X010930Y007394D03* +X010930Y007384D03* +X010930Y007374D03* +X010930Y007364D03* +X010930Y007354D03* +X010930Y007344D03* +X010930Y007334D03* +X010930Y007324D03* +X010930Y007314D03* +X010930Y007304D03* +X010930Y007294D03* +X010930Y007284D03* +X010930Y007274D03* +X010930Y007264D03* +X010930Y007254D03* +X010930Y007244D03* +X010930Y007234D03* +X010930Y007224D03* +X010930Y007214D03* +X010930Y007204D03* +X010930Y007194D03* +X010930Y007184D03* +X010930Y007174D03* +X010930Y007164D03* +X010930Y007154D03* +X010930Y007144D03* +X010930Y007134D03* +X010930Y007124D03* +X010930Y007114D03* +X010930Y007104D03* +X010930Y007094D03* +X010930Y007084D03* +X010930Y007074D03* +X010930Y007064D03* +X010930Y007054D03* +X010930Y007044D03* +X010930Y007034D03* +X010930Y007024D03* +X010930Y007014D03* +X010930Y007004D03* +X010930Y006994D03* +X010930Y006984D03* +X010930Y006974D03* +X010930Y008644D03* +X010930Y008654D03* +X010930Y008664D03* +X010930Y008674D03* +X010930Y008684D03* +X010930Y008694D03* +X010930Y008704D03* +X010930Y008714D03* +X010930Y008724D03* +X010930Y008734D03* +X010930Y008744D03* +X010930Y008754D03* +X010930Y008764D03* +X010930Y008774D03* +X010930Y008784D03* +X010930Y008794D03* +X010930Y008804D03* +X010930Y008814D03* +X010930Y008824D03* +X010930Y008834D03* +X010930Y008844D03* +X010930Y008854D03* +X010930Y008864D03* +X010930Y008874D03* +X010930Y008884D03* +X010930Y008894D03* +X010930Y008904D03* +X010930Y008914D03* +X010930Y008924D03* +X010930Y008934D03* +X010930Y008944D03* +X010930Y008954D03* +X010930Y008964D03* +X010930Y008974D03* +X010930Y008984D03* +X010930Y008994D03* +X010930Y009004D03* +X010930Y009014D03* +X010930Y009024D03* +X010930Y009034D03* +X010930Y009044D03* +X010930Y009054D03* +X010930Y009064D03* +X010930Y009074D03* +X010930Y009084D03* +X010930Y009094D03* +X010930Y009104D03* +X010930Y009114D03* +X010930Y009124D03* +X010930Y009134D03* +X010930Y009144D03* +X010930Y009154D03* +X010930Y009164D03* +X010930Y009174D03* +X010930Y009184D03* +X010930Y009194D03* +X010930Y009204D03* +X010930Y009214D03* +X010930Y009224D03* +X010930Y009234D03* +X010930Y009244D03* +X010930Y009254D03* +X010930Y009264D03* +X010930Y009274D03* +X010930Y009284D03* +X010930Y009294D03* +X010930Y009304D03* +X010930Y009314D03* +X010930Y009324D03* +X010930Y009334D03* +X010930Y009344D03* +X010930Y009354D03* +X010930Y009364D03* +X010930Y009374D03* +X010930Y009384D03* +X010930Y009394D03* +X010930Y009404D03* +X010930Y009414D03* +X010930Y009424D03* +X010930Y009434D03* +X010930Y009444D03* +X010930Y009454D03* +X010930Y009464D03* +X010930Y009474D03* +X010930Y009484D03* +X010930Y009494D03* +X010930Y009504D03* +X010930Y009514D03* +X010930Y009524D03* +X010930Y009534D03* +X010930Y009544D03* +X010930Y009554D03* +X010930Y009564D03* +X010930Y009574D03* +X010930Y009584D03* +X010930Y009594D03* +X010930Y009604D03* +X010930Y009614D03* +X010930Y009624D03* +X010930Y009634D03* +X010930Y009644D03* +X010930Y009654D03* +X010930Y009664D03* +X010930Y009674D03* +X010930Y009684D03* +X010930Y009694D03* +X010930Y009704D03* +X010930Y009714D03* +X010930Y009724D03* +X010930Y009734D03* +X010930Y009744D03* +X010930Y009754D03* +X010930Y009764D03* +X010930Y009774D03* +X010930Y009784D03* +X010930Y009794D03* +X010930Y009804D03* +X010930Y009814D03* +X010930Y009824D03* +X010930Y009834D03* +X010930Y009844D03* +X010930Y009854D03* +X010930Y009864D03* +X010930Y009874D03* +X010930Y009884D03* +X010930Y009894D03* +X010930Y009904D03* +X010930Y009914D03* +X010930Y009924D03* +X010930Y009934D03* +X010930Y009944D03* +X010930Y009954D03* +X010930Y009964D03* +X010930Y009974D03* +X010930Y009984D03* +X010930Y009994D03* +X010930Y010004D03* +X010930Y010014D03* +X010930Y010024D03* +X010930Y010034D03* +X010930Y010044D03* +X010930Y010054D03* +X010930Y010064D03* +X010930Y010074D03* +X010930Y010084D03* +X010930Y010094D03* +X010930Y010534D03* +X010930Y010544D03* +X010930Y010554D03* +X010930Y010564D03* +X010930Y010574D03* +X010930Y010584D03* +X010930Y010594D03* +X010930Y010604D03* +X010930Y010614D03* +X010930Y010624D03* +X010930Y010634D03* +X010930Y010644D03* +X010930Y010654D03* +X010930Y010664D03* +X010930Y010674D03* +X010930Y010684D03* +X010930Y010694D03* +X010930Y010704D03* +X010930Y010714D03* +X010930Y010724D03* +X010930Y010734D03* +X010930Y010744D03* +X010930Y010754D03* +X010930Y010764D03* +X010930Y010774D03* +X010930Y010784D03* +X010930Y010794D03* +X010930Y010804D03* +X010930Y010814D03* +X010930Y010824D03* +X010930Y010834D03* +X010930Y010844D03* +X010930Y010854D03* +X010930Y010864D03* +X010930Y010874D03* +X010930Y010884D03* +X010930Y010894D03* +X010930Y010904D03* +X010930Y010914D03* +X010930Y010924D03* +X010930Y010934D03* +X010930Y010944D03* +X010930Y010954D03* +X010930Y010964D03* +X010930Y010974D03* +X010930Y010984D03* +X010930Y010994D03* +X010930Y011004D03* +X010930Y011014D03* +X010930Y011024D03* +X010930Y011034D03* +X010930Y011044D03* +X010930Y011054D03* +X010930Y011064D03* +X010930Y011074D03* +X010930Y011084D03* +X010930Y011094D03* +X010930Y011104D03* +X010930Y011114D03* +X010930Y011124D03* +X010930Y011134D03* +X010930Y011144D03* +X010930Y011154D03* +X010930Y011164D03* +X010930Y011174D03* +X010930Y011184D03* +X010930Y011194D03* +X010930Y011204D03* +X010930Y011214D03* +X010930Y011224D03* +X010930Y011234D03* +X010930Y011244D03* +X010930Y011254D03* +X010930Y011264D03* +X010930Y011274D03* +X010930Y011284D03* +X010930Y011294D03* +X010930Y011304D03* +X010930Y011314D03* +X010930Y011324D03* +X010930Y011334D03* +X010930Y011344D03* +X010930Y011354D03* +X010930Y011364D03* +X010930Y011374D03* +X010930Y011384D03* +X010930Y011394D03* +X010930Y011404D03* +X010930Y011414D03* +X010930Y011424D03* +X010930Y011434D03* +X010930Y011444D03* +X010930Y011454D03* +X010930Y011464D03* +X010930Y011474D03* +X010930Y011484D03* +X010930Y011494D03* +X010930Y011504D03* +X010930Y011514D03* +X010930Y011524D03* +X010930Y011534D03* +X010930Y011544D03* +X010930Y011554D03* +X010930Y011564D03* +X010930Y011574D03* +X010930Y011584D03* +X010930Y011594D03* +X010930Y011604D03* +X010930Y011614D03* +X010930Y011624D03* +X010930Y011634D03* +X010930Y011644D03* +X010930Y011654D03* +X010930Y011664D03* +X010930Y011674D03* +X010930Y011684D03* +X010930Y011694D03* +X010930Y011704D03* +X010930Y011714D03* +X010930Y011724D03* +X010930Y011734D03* +X010930Y011744D03* +X010930Y011754D03* +X010930Y011764D03* +X010930Y011774D03* +X010930Y011784D03* +X010930Y011794D03* +X010930Y011804D03* +X010930Y011814D03* +X010930Y011824D03* +X010930Y011834D03* +X010930Y011844D03* +X010930Y011854D03* +X010930Y011864D03* +X010930Y011874D03* +X010930Y011884D03* +X010930Y011894D03* +X010930Y011904D03* +X010930Y011914D03* +X010930Y011924D03* +X010930Y011934D03* +X010930Y011944D03* +X010930Y011954D03* +X010930Y011964D03* +X010930Y011974D03* +X010930Y011984D03* +X010930Y011994D03* +X010930Y012004D03* +X010930Y012014D03* +X010930Y012024D03* +X010930Y012034D03* +X010930Y012044D03* +X010930Y012054D03* +X010930Y012064D03* +X010930Y012074D03* +X010930Y012084D03* +X010930Y012094D03* +X010930Y012104D03* +X010930Y012114D03* +X010930Y012124D03* +X010930Y012134D03* +X010930Y012144D03* +X010930Y012154D03* +X010930Y012164D03* +X010930Y012174D03* +X010930Y012184D03* +X010930Y012194D03* +X010930Y012204D03* +X010930Y012214D03* +X010930Y012224D03* +X010930Y012234D03* +X010930Y012244D03* +X010930Y012254D03* +X010930Y012264D03* +X010930Y012274D03* +X010930Y012284D03* +X010930Y012294D03* +X010930Y012304D03* +X010930Y012314D03* +X010930Y012324D03* +X010930Y012334D03* +X010930Y012344D03* +X010930Y012354D03* +X010930Y012364D03* +X010930Y012374D03* +X010930Y012384D03* +X010930Y012394D03* +X010930Y012404D03* +X010930Y012414D03* +X010930Y012424D03* +X010930Y012434D03* +X010930Y012444D03* +X010930Y012454D03* +X010930Y012464D03* +X010930Y012474D03* +X010930Y012484D03* +X010930Y012494D03* +X010930Y012504D03* +X010930Y012514D03* +X010930Y012524D03* +X010930Y012534D03* +X010930Y012544D03* +X010930Y012554D03* +X010930Y012564D03* +X010930Y012574D03* +X010930Y012584D03* +X010930Y012594D03* +X010930Y012604D03* +X010930Y012614D03* +X010930Y012624D03* +X010930Y012634D03* +X010930Y012644D03* +X010930Y012654D03* +X010930Y012664D03* +X010930Y012674D03* +X010930Y012684D03* +X010930Y012694D03* +X010930Y012704D03* +X010930Y012714D03* +X010930Y012724D03* +X010930Y012734D03* +X010930Y012744D03* +X010930Y012754D03* +X010930Y012764D03* +X010930Y012774D03* +X010930Y012784D03* +X010930Y012794D03* +X010930Y012804D03* +X010930Y012814D03* +X010930Y012824D03* +X010930Y012834D03* +X010930Y012844D03* +X010930Y012854D03* +X010930Y012864D03* +X010930Y012874D03* +X010930Y012884D03* +X010930Y012894D03* +X010930Y012904D03* +X010930Y012914D03* +X010930Y012924D03* +X010930Y012934D03* +X010930Y012944D03* +X010930Y012954D03* +X010930Y012964D03* +X010930Y012974D03* +X010930Y012984D03* +X010930Y012994D03* +X010930Y013004D03* +X010930Y013014D03* +X010930Y013024D03* +X010930Y013034D03* +X010930Y013044D03* +X010930Y013054D03* +X010930Y013064D03* +X010930Y013074D03* +X010930Y013084D03* +X010930Y013094D03* +X010930Y013104D03* +X010930Y013114D03* +X010930Y013124D03* +X010930Y013134D03* +X010930Y013144D03* +X010930Y013154D03* +X010930Y013164D03* +X010930Y013174D03* +X010930Y013184D03* +X010930Y013194D03* +X010930Y013204D03* +X010930Y013214D03* +X010930Y013224D03* +X010930Y013234D03* +X010930Y013244D03* +X010930Y013254D03* +D12* +X013355Y012004D03* +X014305Y011544D03* +X014285Y011494D03* +X014275Y011464D03* +X014265Y011444D03* +X014255Y011414D03* +X014245Y011394D03* +X014235Y011364D03* +X014225Y011344D03* +X014225Y011334D03* +X014215Y011314D03* +X014205Y011294D03* +X014205Y011284D03* +X014195Y011264D03* +X014185Y011244D03* +X014185Y011234D03* +X014175Y011214D03* +X015765Y009434D03* +X015765Y009424D03* +X015275Y008624D03* +X015265Y008604D03* +X015255Y008594D03* +X015245Y008574D03* +X015225Y008544D03* +X015215Y008524D03* +X015205Y008514D03* +X015195Y008494D03* +X015185Y008474D03* +X015175Y008464D03* +X015165Y008444D03* +X015145Y008414D03* +X015135Y008394D03* +X015125Y008384D03* +X015115Y008364D03* +X015105Y008344D03* +X015095Y008334D03* +X015085Y008314D03* +X015075Y008304D03* +X015075Y008294D03* +X015065Y008284D03* +X015055Y008264D03* +X015045Y008254D03* +X015035Y008234D03* +X015025Y008214D03* +X015015Y008204D03* +X015005Y008184D03* +X014995Y008174D03* +X014995Y008164D03* +X014985Y008154D03* +X014975Y008134D03* +X014965Y008124D03* +X014955Y008104D03* +X014945Y008094D03* +X014945Y008084D03* +X014935Y008074D03* +X014925Y008054D03* +X014915Y008044D03* +X014915Y008034D03* +X014905Y008024D03* +X014895Y008004D03* +X014885Y007994D03* +X014885Y007984D03* +X014875Y007974D03* +X014865Y007964D03* +X014865Y007954D03* +X014855Y007944D03* +X014845Y007924D03* +X014835Y007914D03* +X014835Y007904D03* +X014345Y007124D03* +X014345Y007114D03* +X016765Y007834D03* +X016775Y007814D03* +X016775Y007804D03* +X016785Y007784D03* +X016785Y007774D03* +X016795Y007764D03* +X016785Y008794D03* +X016795Y008814D03* +X016795Y008824D03* +X016805Y008844D03* +X018495Y008824D03* +X018495Y008814D03* +X018505Y008804D03* +X018505Y008794D03* +X018515Y008774D03* +X018525Y007834D03* +X018515Y007814D03* +X018515Y007804D03* +X018505Y007794D03* +X018505Y007784D03* +X019175Y011774D03* +X019195Y012154D03* +X019195Y012164D03* +X019205Y012174D03* +X019295Y013194D03* +X015165Y013674D03* +X006695Y007474D03* +X006705Y007454D03* +X006715Y007444D03* +X006725Y007424D03* +X006745Y007394D03* +X006755Y007374D03* +X006765Y007364D03* +X006775Y007344D03* +X006795Y007314D03* +X006825Y007264D03* +X006845Y007234D03* +X006875Y007184D03* +X006925Y007104D03* +X007005Y006974D03* +X006685Y007494D03* +X006675Y007504D03* +X006665Y007524D03* +X006655Y007534D03* +X006655Y007544D03* +X006645Y007554D03* +X006635Y007574D03* +X006625Y007584D03* +X006615Y007604D03* +X006605Y007614D03* +X006605Y007624D03* +X006595Y007634D03* +X006585Y007654D03* +X006575Y007664D03* +X006575Y007674D03* +X006565Y007684D03* +X006555Y007694D03* +X006555Y007704D03* +X006545Y007714D03* +X006535Y007734D03* +X006525Y007744D03* +X006525Y007754D03* +X006515Y007764D03* +X006505Y007774D03* +X006505Y007784D03* +X006495Y007794D03* +X006495Y007804D03* +X006485Y007814D03* +X006475Y007824D03* +X006475Y007834D03* +X006465Y007844D03* +X006455Y007854D03* +X006455Y007864D03* +X006445Y007874D03* +X006445Y007884D03* +X006435Y007894D03* +X006425Y007904D03* +X006425Y007914D03* +X006415Y007924D03* +X006405Y007944D03* +X006395Y007954D03* +X006395Y007964D03* +X006385Y007974D03* +X006375Y007984D03* +X006375Y007994D03* +X006365Y008004D03* +X006355Y008024D03* +X006345Y008034D03* +X006345Y008044D03* +X006335Y008054D03* +X006325Y008074D03* +X006315Y008084D03* +X006305Y008104D03* +X006295Y008124D03* +X006285Y008134D03* +X006275Y008154D03* +X006265Y008164D03* +X006255Y008184D03* +X006235Y008214D03* +X006225Y008234D03* +X006205Y008264D03* +X006185Y008294D03* +X006175Y008314D03* +X006155Y008344D03* +X006125Y008394D03* +X006105Y008424D03* +X006075Y008474D03* +X006025Y008554D03* +X004355Y009554D03* +X004345Y009544D03* +X004335Y009524D03* +X004315Y009494D03* +X004305Y009474D03* +X004285Y009444D03* +X004275Y009424D03* +X004255Y009394D03* +X004245Y009374D03* +X004225Y009344D03* +X004215Y009324D03* +X004195Y009294D03* +X004185Y009274D03* +X004165Y009244D03* +X004155Y009224D03* +X004135Y009194D03* +X004105Y009144D03* +X004075Y009094D03* +X004045Y009044D03* +X004015Y008994D03* +X003995Y008964D03* +X003985Y008944D03* +X003965Y008914D03* +X003935Y008864D03* +X003905Y008814D03* +X003875Y008764D03* +X003845Y008714D03* +X003815Y008664D03* +X003645Y008384D03* +X004365Y009574D03* +X004375Y009594D03* +X004385Y009604D03* +X004395Y009624D03* +X004405Y009644D03* +X004415Y009654D03* +X004415Y009664D03* +X004425Y009674D03* +X004435Y009694D03* +X004445Y009704D03* +X004445Y009714D03* +X004455Y009724D03* +X004455Y009734D03* +X004465Y009744D03* +X004475Y009764D03* +X004485Y009774D03* +X004485Y009784D03* +X004495Y009794D03* +X004505Y009814D03* +X005235Y010924D03* +X005245Y010944D03* +X005265Y010974D03* +X005275Y010994D03* +X005285Y011014D03* +X005295Y011024D03* +X005295Y011034D03* +X005305Y011044D03* +X005305Y011054D03* +X005315Y011064D03* +X005325Y011074D03* +X005325Y011084D03* +X005335Y011094D03* +X005335Y011104D03* +X005345Y011114D03* +X005355Y011124D03* +X005355Y011134D03* +X005365Y011144D03* +X005365Y011154D03* +X005375Y011164D03* +X005385Y011184D03* +X005395Y011194D03* +X005395Y011204D03* +X005405Y011214D03* +X005415Y011234D03* +X005425Y011244D03* +X005425Y011254D03* +X005435Y011264D03* +X005445Y011284D03* +X005455Y011294D03* +X005455Y011304D03* +X005465Y011314D03* +X005475Y011334D03* +X005485Y011344D03* +X005485Y011354D03* +X005495Y011364D03* +X005505Y011384D03* +X005515Y011394D03* +X005515Y011404D03* +X005525Y011414D03* +X005535Y011434D03* +X005545Y011444D03* +X005545Y011454D03* +X005555Y011464D03* +X005565Y011484D03* +X005575Y011494D03* +X005575Y011504D03* +X005585Y011514D03* +X005595Y011534D03* +X005605Y011554D03* +X005615Y011564D03* +X005625Y011584D03* +X005635Y011604D03* +X005645Y011614D03* +X005645Y011624D03* +X005655Y011634D03* +X005665Y011654D03* +X005675Y011664D03* +X005675Y011674D03* +X005685Y011684D03* +X005695Y011704D03* +X005705Y011714D03* +X005705Y011724D03* +X005715Y011734D03* +X005725Y011754D03* +X005735Y011764D03* +X005735Y011774D03* +X005745Y011784D03* +X005755Y011804D03* +X005765Y011814D03* +X005765Y011824D03* +X005775Y011834D03* +X005785Y011854D03* +X005795Y011864D03* +X005795Y011874D03* +X005805Y011884D03* +X005815Y011904D03* +X005825Y011914D03* +X005825Y011924D03* +X005835Y011934D03* +X005845Y011954D03* +X005855Y011974D03* +X005865Y011984D03* +X005875Y012004D03* +X005885Y012024D03* +X005895Y012034D03* +X005905Y012054D03* +X005915Y012074D03* +X005925Y012084D03* +X005935Y012104D03* +X005945Y012124D03* +X005955Y012134D03* +X005965Y012154D03* +X005975Y012174D03* +X005985Y012184D03* +X005995Y012204D03* +X006005Y012224D03* +X006015Y012234D03* +X006025Y012254D03* +X006035Y012274D03* +X006045Y012284D03* +X006055Y012304D03* +X006065Y012324D03* +X006075Y012334D03* +X006085Y012354D03* +X006095Y012374D03* +X006105Y012384D03* +X006115Y012404D03* +X006125Y012424D03* +X006145Y012454D03* +X006155Y012474D03* +X006175Y012504D03* +X006185Y012524D03* +X006205Y012554D03* +X006215Y012574D03* +X006235Y012604D03* +X006245Y012624D03* +X006265Y012654D03* +X006275Y012674D03* +X006295Y012704D03* +X006305Y012724D03* +X006325Y012754D03* +X006335Y012774D03* +X006355Y012804D03* +X006365Y012824D03* +X006395Y012874D03* +X006425Y012924D03* +X006455Y012974D03* +X006485Y013024D03* +X006515Y013074D03* +X006545Y013124D03* +X006575Y013174D03* +X006605Y013224D03* +D13* +X006600Y013214D03* +X006590Y013204D03* +X006590Y013194D03* +X006580Y013184D03* +X006570Y013164D03* +X006560Y013154D03* +X006560Y013144D03* +X006550Y013134D03* +X006540Y013114D03* +X006530Y013104D03* +X006530Y013094D03* +X006520Y013084D03* +X006510Y013064D03* +X006500Y013054D03* +X006500Y013044D03* +X006490Y013034D03* +X006480Y013014D03* +X006470Y013004D03* +X006470Y012994D03* +X006460Y012984D03* +X006450Y012964D03* +X006440Y012954D03* +X006440Y012944D03* +X006430Y012934D03* +X006420Y012914D03* +X006410Y012904D03* +X006410Y012894D03* +X006400Y012884D03* +X006390Y012864D03* +X006380Y012854D03* +X006380Y012844D03* +X006370Y012834D03* +X006360Y012814D03* +X006350Y012794D03* +X006340Y012784D03* +X006330Y012764D03* +X006320Y012744D03* +X006310Y012734D03* +X006300Y012714D03* +X006290Y012694D03* +X006280Y012684D03* +X006270Y012664D03* +X006260Y012644D03* +X006250Y012634D03* +X006240Y012614D03* +X006230Y012594D03* +X006220Y012584D03* +X006210Y012564D03* +X006200Y012544D03* +X006190Y012534D03* +X006180Y012514D03* +X006170Y012494D03* +X006160Y012484D03* +X006150Y012464D03* +X006140Y012444D03* +X006130Y012434D03* +X006120Y012414D03* +X006110Y012394D03* +X006090Y012364D03* +X006080Y012344D03* +X006060Y012314D03* +X006050Y012294D03* +X006030Y012264D03* +X006020Y012244D03* +X006000Y012214D03* +X005990Y012194D03* +X005970Y012164D03* +X005960Y012144D03* +X005940Y012114D03* +X005930Y012094D03* +X005910Y012064D03* +X005900Y012044D03* +X005880Y012014D03* +X005870Y011994D03* +X005850Y011964D03* +X005840Y011944D03* +X005810Y011894D03* +X005780Y011844D03* +X005750Y011794D03* +X005720Y011744D03* +X005690Y011694D03* +X005660Y011644D03* +X005630Y011594D03* +X005620Y011574D03* +X005600Y011544D03* +X005590Y011524D03* +X005560Y011474D03* +X005530Y011424D03* +X005500Y011374D03* +X005470Y011324D03* +X005440Y011274D03* +X005410Y011224D03* +X005380Y011174D03* +X004430Y009684D03* +X004400Y009634D03* +X004390Y009614D03* +X004370Y009584D03* +X004360Y009564D03* +X004340Y009534D03* +X004330Y009514D03* +X004320Y009504D03* +X004310Y009484D03* +X004300Y009464D03* +X004290Y009454D03* +X004280Y009434D03* +X004270Y009414D03* +X004260Y009404D03* +X004250Y009384D03* +X004240Y009364D03* +X004230Y009354D03* +X004220Y009334D03* +X004210Y009314D03* +X004200Y009304D03* +X004190Y009284D03* +X004180Y009264D03* +X004170Y009254D03* +X004160Y009234D03* +X004150Y009214D03* +X004140Y009204D03* +X004130Y009184D03* +X004120Y009174D03* +X004120Y009164D03* +X004110Y009154D03* +X004100Y009134D03* +X004090Y009124D03* +X004090Y009114D03* +X004080Y009104D03* +X004070Y009084D03* +X004060Y009074D03* +X004060Y009064D03* +X004050Y009054D03* +X004040Y009034D03* +X004030Y009024D03* +X004030Y009014D03* +X004020Y009004D03* +X004010Y008984D03* +X004000Y008974D03* +X003990Y008954D03* +X003980Y008934D03* +X003970Y008924D03* +X003960Y008904D03* +X003950Y008894D03* +X003950Y008884D03* +X003940Y008874D03* +X003930Y008854D03* +X003920Y008844D03* +X003920Y008834D03* +X003910Y008824D03* +X003900Y008804D03* +X003890Y008794D03* +X003890Y008784D03* +X003880Y008774D03* +X003870Y008754D03* +X003860Y008744D03* +X003860Y008734D03* +X003850Y008724D03* +X003840Y008704D03* +X003830Y008694D03* +X003830Y008684D03* +X003820Y008674D03* +X003810Y008654D03* +X003800Y008644D03* +X003800Y008634D03* +X003790Y008624D03* +X003780Y008614D03* +X003780Y008604D03* +X003770Y008594D03* +X003770Y008584D03* +X003760Y008574D03* +X003750Y008564D03* +X003750Y008554D03* +X003740Y008544D03* +X003740Y008534D03* +X003730Y008524D03* +X003720Y008514D03* +X003720Y008504D03* +X003710Y008494D03* +X003710Y008484D03* +X003700Y008474D03* +X003690Y008464D03* +X003690Y008454D03* +X003680Y008444D03* +X003680Y008434D03* +X003670Y008424D03* +X003660Y008414D03* +X003660Y008404D03* +X003650Y008394D03* +X003640Y008374D03* +X003630Y008364D03* +X003630Y008354D03* +X003620Y008344D03* +X003610Y008334D03* +X003610Y008324D03* +X003600Y008314D03* +X003600Y008304D03* +X003590Y008294D03* +X003580Y008284D03* +X003580Y008274D03* +X003570Y008264D03* +X003570Y008254D03* +X003560Y008244D03* +X003550Y008234D03* +X003550Y008224D03* +X003540Y008214D03* +X003540Y008204D03* +X003530Y008194D03* +X003520Y008184D03* +X003520Y008174D03* +X003510Y008164D03* +X003510Y008154D03* +X003500Y008144D03* +X003490Y008134D03* +X003490Y008124D03* +X003480Y008114D03* +X003480Y008104D03* +X003470Y008094D03* +X003460Y008084D03* +X003460Y008074D03* +X003450Y008064D03* +X003450Y008054D03* +X003440Y008044D03* +X003430Y008034D03* +X003430Y008024D03* +X003420Y008014D03* +X003410Y007994D03* +X003400Y007984D03* +X003400Y007974D03* +X003390Y007964D03* +X003380Y007944D03* +X003370Y007934D03* +X003370Y007924D03* +X003360Y007914D03* +X003350Y007894D03* +X003340Y007884D03* +X003340Y007874D03* +X003330Y007864D03* +X003320Y007844D03* +X003310Y007834D03* +X003310Y007824D03* +X003300Y007814D03* +X003290Y007804D03* +X003290Y007794D03* +X003280Y007784D03* +X003280Y007774D03* +X003270Y007764D03* +X003260Y007754D03* +X003260Y007744D03* +X003250Y007734D03* +X003240Y007714D03* +X003230Y007704D03* +X003230Y007694D03* +X003220Y007684D03* +X003210Y007664D03* +X003200Y007654D03* +X003200Y007644D03* +X003190Y007634D03* +X003180Y007614D03* +X003170Y007604D03* +X003170Y007594D03* +X003160Y007584D03* +X003150Y007564D03* +X003140Y007554D03* +X003140Y007544D03* +X003130Y007534D03* +X003120Y007514D03* +X003110Y007504D03* +X003110Y007494D03* +X003100Y007484D03* +X003090Y007464D03* +X003080Y007454D03* +X003070Y007434D03* +X003060Y007414D03* +X003050Y007404D03* +X003040Y007384D03* +X003030Y007364D03* +X003020Y007354D03* +X003010Y007334D03* +X003000Y007314D03* +X002990Y007304D03* +X002980Y007284D03* +X002970Y007264D03* +X002960Y007254D03* +X002950Y007234D03* +X002940Y007224D03* +X002940Y007214D03* +X002930Y007204D03* +X002920Y007184D03* +X002910Y007174D03* +X002900Y007154D03* +X002890Y007134D03* +X002880Y007124D03* +X002870Y007104D03* +X002860Y007084D03* +X002850Y007074D03* +X002840Y007054D03* +X002830Y007034D03* +X002820Y007024D03* +X002810Y007004D03* +X002800Y006984D03* +X002790Y006974D03* +X006540Y007724D03* +X006590Y007644D03* +X006620Y007594D03* +X006640Y007564D03* +X006670Y007514D03* +X006690Y007484D03* +X006700Y007464D03* +X006720Y007434D03* +X006730Y007414D03* +X006740Y007404D03* +X006750Y007384D03* +X006770Y007354D03* +X006780Y007334D03* +X006790Y007324D03* +X006800Y007304D03* +X006810Y007294D03* +X006810Y007284D03* +X006820Y007274D03* +X006830Y007254D03* +X006840Y007244D03* +X006850Y007224D03* +X006860Y007214D03* +X006860Y007204D03* +X006870Y007194D03* +X006880Y007174D03* +X006890Y007164D03* +X006890Y007154D03* +X006900Y007144D03* +X006910Y007134D03* +X006910Y007124D03* +X006920Y007114D03* +X006930Y007094D03* +X006940Y007084D03* +X006940Y007074D03* +X006950Y007064D03* +X006960Y007054D03* +X006960Y007044D03* +X006970Y007034D03* +X006970Y007024D03* +X006980Y007014D03* +X006990Y007004D03* +X006990Y006994D03* +X007000Y006984D03* +X013350Y012014D03* +X015160Y013654D03* +X015160Y013664D03* +X019300Y013204D03* +X019370Y012754D03* +X019220Y012194D03* +X019210Y012184D03* +X019180Y011764D03* +X019190Y011744D03* +X015760Y009414D03* +X015760Y009404D03* +X015280Y008634D03* +X015270Y008614D03* +X015250Y008584D03* +X015240Y008564D03* +X015230Y008554D03* +X015220Y008534D03* +X015200Y008504D03* +X015190Y008484D03* +X015170Y008454D03* +X015160Y008434D03* +X015150Y008424D03* +X015140Y008404D03* +X015120Y008374D03* +X015110Y008354D03* +X015090Y008324D03* +X015060Y008274D03* +X015040Y008244D03* +X015030Y008224D03* +X015010Y008194D03* +X014980Y008144D03* +X014960Y008114D03* +X014930Y008064D03* +X014900Y008014D03* +X014850Y007934D03* +X014350Y007134D03* +X016800Y007744D03* +X016800Y007754D03* +X016810Y007724D03* +X016800Y008834D03* +X016810Y008854D03* +X016820Y008864D03* +X016820Y008874D03* +X018480Y008854D03* +X018480Y008844D03* +X018490Y008834D03* +X018500Y007774D03* +X018500Y007764D03* +X018490Y007754D03* +X018490Y007744D03* +X006860Y013644D03* +X006860Y013654D03* +X006870Y013664D03* +X006870Y013674D03* +X006880Y013684D03* +X006850Y013634D03* +X006840Y013624D03* +X006840Y013614D03* +X006830Y013604D03* +X006830Y013594D03* +X006820Y013584D03* +X006810Y013574D03* +X006810Y013564D03* +X006800Y013554D03* +X006800Y013544D03* +X006790Y013534D03* +X006780Y013524D03* +X006780Y013514D03* +X006770Y013504D03* +X006770Y013494D03* +X006760Y013484D03* +X006750Y013474D03* +X006750Y013464D03* +X006740Y013454D03* +X006740Y013444D03* +X006730Y013434D03* +X006720Y013424D03* +X006720Y013414D03* +X006710Y013404D03* +X006710Y013394D03* +X006700Y013384D03* +X006690Y013374D03* +X006690Y013364D03* +X006680Y013354D03* +X006680Y013344D03* +X006670Y013334D03* +X006660Y013324D03* +X006660Y013314D03* +X006650Y013304D03* +X006650Y013294D03* +X006640Y013284D03* +X006630Y013274D03* +X006630Y013264D03* +X006620Y013254D03* +X006620Y013244D03* +X006610Y013234D03* +D14* +X013345Y012024D03* +X015755Y009394D03* +X016825Y008884D03* +X016835Y008894D03* +X016845Y008914D03* +X016805Y007734D03* +X016815Y007714D03* +X016825Y007704D03* +X016835Y007684D03* +X017645Y007024D03* +X018465Y007704D03* +X018475Y007714D03* +X018475Y007724D03* +X018485Y007734D03* +X018475Y008864D03* +X018465Y008874D03* +X018465Y008884D03* +X018455Y008894D03* +X019195Y011734D03* +X019185Y011754D03* +X019385Y012744D03* +X019305Y013214D03* +X014355Y007154D03* +X014355Y007144D03* +X003415Y008004D03* +X003385Y007954D03* +X003355Y007904D03* +X003325Y007854D03* +X003245Y007724D03* +X003215Y007674D03* +X003185Y007624D03* +X003155Y007574D03* +X003125Y007524D03* +X003095Y007474D03* +X003075Y007444D03* +X003065Y007424D03* +X003045Y007394D03* +X003035Y007374D03* +X003015Y007344D03* +X003005Y007324D03* +X002985Y007294D03* +X002975Y007274D03* +X002955Y007244D03* +X002925Y007194D03* +X002905Y007164D03* +X002895Y007144D03* +X002875Y007114D03* +X002865Y007094D03* +X002845Y007064D03* +X002835Y007044D03* +X002815Y007014D03* +X002805Y006994D03* +D15* +X017645Y007004D03* +D16* +X017640Y007014D03* +D17* +X017640Y007034D03* +X016950Y007534D03* +X016940Y007544D03* +X016950Y009044D03* +X015710Y009244D03* +X015710Y009254D03* +X014400Y007294D03* +X018330Y009044D03* +X013300Y012114D03* +X012690Y012714D03* +X012260Y013184D03* +X012250Y013194D03* +X012240Y013204D03* +X012230Y013214D03* +X012220Y013224D03* +X012210Y013234D03* +X012200Y013244D03* +X012190Y013254D03* +X012180Y013264D03* +X012170Y013274D03* +X012160Y013284D03* +X012150Y013294D03* +X012090Y013364D03* +X012080Y013374D03* +X012070Y013384D03* +X012060Y013394D03* +X012050Y013404D03* +X012040Y013414D03* +X011720Y011224D03* +X015160Y013524D03* +X015160Y013534D03* +X015160Y013544D03* +X004870Y010414D03* +D18* +X005140Y009994D03* +X005150Y009984D03* +X005160Y009964D03* +X005170Y009944D03* +X004470Y010934D03* +X004440Y010984D03* +X004430Y011004D03* +X004420Y011024D03* +X004410Y011034D03* +X004410Y011044D03* +X004400Y011054D03* +X004390Y011074D03* +X004380Y011084D03* +X004380Y011094D03* +X004370Y011104D03* +X004360Y011124D03* +X004350Y011144D03* +X004340Y011154D03* +X004330Y011174D03* +X004320Y011194D03* +X004300Y011224D03* +X004290Y011244D03* +X004270Y011274D03* +X004260Y011294D03* +X004240Y011324D03* +X004230Y011344D03* +X004210Y011374D03* +X004200Y011394D03* +X004180Y011424D03* +X013380Y011424D03* +X013380Y011414D03* +X013380Y011404D03* +X013380Y011394D03* +X013380Y011384D03* +X013380Y011374D03* +X013380Y011364D03* +X013380Y011354D03* +X013380Y011344D03* +X013380Y011334D03* +X013380Y011324D03* +X013380Y011314D03* +X013380Y011304D03* +X013380Y011294D03* +X013380Y011284D03* +X013380Y011274D03* +X013380Y011264D03* +X013380Y011254D03* +X013380Y011244D03* +X013380Y011234D03* +X013380Y011224D03* +X013380Y011214D03* +X013380Y011434D03* +X013380Y011444D03* +X013380Y011454D03* +X013380Y011464D03* +X013380Y011474D03* +X013380Y011484D03* +X013380Y011494D03* +X013380Y011504D03* +X013380Y011514D03* +X013380Y011524D03* +X013380Y011534D03* +X013380Y011544D03* +X013380Y011554D03* +X013380Y011564D03* +X013380Y011574D03* +X013380Y011584D03* +X013380Y011594D03* +X013380Y011604D03* +X013380Y011614D03* +X013380Y011624D03* +X013380Y011634D03* +X013380Y011644D03* +X013380Y011654D03* +X013380Y011664D03* +X013380Y011674D03* +X013380Y011684D03* +X013380Y011694D03* +X013380Y011704D03* +X013380Y011714D03* +X013380Y011724D03* +X013380Y011734D03* +X013380Y011744D03* +X013380Y011754D03* +X013380Y011764D03* +X013380Y011774D03* +X013380Y011784D03* +X013380Y011794D03* +X013380Y011804D03* +X013380Y011814D03* +X013380Y011824D03* +X013380Y011834D03* +X013380Y011844D03* +X013380Y011854D03* +X013380Y011864D03* +X013380Y011874D03* +X013380Y011884D03* +X013380Y011894D03* +X013380Y011904D03* +X013380Y011914D03* +X013380Y011924D03* +X013380Y011934D03* +X013380Y011944D03* +X013380Y011954D03* +X013380Y012614D03* +X013380Y012624D03* +X013380Y012634D03* +X013380Y012644D03* +X013380Y012654D03* +X013380Y012664D03* +X013380Y012674D03* +X013380Y012684D03* +X013380Y012694D03* +X013380Y012704D03* +X013380Y012714D03* +X013380Y012724D03* +X013380Y012734D03* +X013380Y012744D03* +X013380Y012754D03* +X013380Y012764D03* +X013380Y012774D03* +X013380Y012784D03* +X013380Y012794D03* +X013380Y012804D03* +X013380Y012814D03* +X013380Y012824D03* +X013380Y012834D03* +X013380Y012844D03* +X013380Y012854D03* +X013380Y012864D03* +X013380Y012874D03* +X013380Y012884D03* +X013380Y012894D03* +X013380Y012904D03* +X013380Y012914D03* +X013380Y012924D03* +X013380Y012934D03* +X013380Y012944D03* +X013380Y012954D03* +X013380Y012964D03* +X013380Y012974D03* +X013380Y012984D03* +X013380Y012994D03* +X013380Y013004D03* +X013380Y013014D03* +X013380Y013024D03* +X013380Y013034D03* +X013380Y013044D03* +X013380Y013054D03* +X013380Y013064D03* +X013380Y013074D03* +X013380Y013084D03* +X013380Y013094D03* +X013380Y013104D03* +X013380Y013114D03* +X013380Y013124D03* +X013380Y013134D03* +X013380Y013144D03* +X013380Y013154D03* +X013380Y013164D03* +X013380Y013174D03* +X013380Y013184D03* +X013380Y013194D03* +X013380Y013204D03* +X013380Y013214D03* +X013380Y013224D03* +X013380Y013234D03* +X013380Y013244D03* +X013380Y013254D03* +X013380Y013264D03* +X013380Y013274D03* +X013380Y013284D03* +X013380Y013294D03* +X013380Y013304D03* +X013380Y013314D03* +X013380Y013324D03* +X013380Y013334D03* +X013380Y013344D03* +X013380Y013354D03* +X013380Y013364D03* +X013380Y013374D03* +X013380Y013384D03* +X013380Y013394D03* +X013380Y013404D03* +X013380Y013414D03* +X013380Y013424D03* +X013380Y013434D03* +X013380Y013444D03* +X013380Y013454D03* +X013380Y013464D03* +X013380Y013474D03* +X013380Y013484D03* +X013380Y013494D03* +X013380Y013504D03* +X013380Y013514D03* +X013380Y013524D03* +X013380Y013534D03* +X013380Y013544D03* +X013380Y013554D03* +X013380Y013564D03* +X013380Y013574D03* +X013380Y013584D03* +X013380Y013594D03* +X013380Y013604D03* +X013380Y013614D03* +X013380Y013624D03* +X013380Y013634D03* +X013380Y013644D03* +X013380Y013654D03* +X013380Y013664D03* +X013380Y013674D03* +X013380Y013684D03* +X013380Y013694D03* +X014900Y013074D03* +X014880Y013024D03* +X015490Y012904D03* +X015500Y012874D03* +X015510Y012854D03* +X015520Y012824D03* +X015530Y012804D03* +X015530Y012794D03* +X015540Y012774D03* +X015540Y012764D03* +X015550Y012744D03* +X015560Y012724D03* +X015560Y012714D03* +X015570Y012694D03* +X015570Y012684D03* +X015580Y012674D03* +X015580Y012664D03* +X015580Y012654D03* +X015590Y012644D03* +X015590Y012634D03* +X015600Y012624D03* +X015600Y012614D03* +X015600Y012604D03* +X015610Y012594D03* +X015610Y012584D03* +X015610Y012574D03* +X015620Y012564D03* +X015620Y012554D03* +X015630Y012534D03* +X015630Y012524D03* +X015640Y012514D03* +X015640Y012504D03* +X015650Y012484D03* +X015650Y012474D03* +X015660Y012454D03* +X015670Y012434D03* +X015670Y012424D03* +X015680Y012404D03* +X015680Y012394D03* +X015690Y012374D03* +X015700Y012344D03* +X015710Y012324D03* +X015720Y012294D03* +X015750Y012214D03* +X018210Y012214D03* +X018210Y012204D03* +X018210Y012194D03* +X018210Y012184D03* +X018210Y012174D03* +X018210Y012164D03* +X018210Y012154D03* +X018210Y012144D03* +X018210Y012134D03* +X018210Y012124D03* +X018210Y012114D03* +X018210Y012104D03* +X018210Y012094D03* +X018210Y012084D03* +X018210Y012074D03* +X018210Y012064D03* +X018210Y012054D03* +X018210Y012044D03* +X018210Y012034D03* +X018210Y012024D03* +X018210Y012014D03* +X018210Y012004D03* +X018210Y011994D03* +X018210Y011984D03* +X018210Y011974D03* +X018210Y011964D03* +X018210Y011954D03* +X018210Y011944D03* +X018210Y011934D03* +X018210Y011924D03* +X018210Y011914D03* +X018210Y011904D03* +X018210Y011894D03* +X018210Y011884D03* +X018210Y011874D03* +X018210Y011864D03* +X018210Y011854D03* +X018210Y011844D03* +X018210Y011834D03* +X018210Y011824D03* +X018210Y011814D03* +X018210Y011804D03* +X018210Y011794D03* +X018210Y011784D03* +X018210Y011774D03* +X018210Y011764D03* +X018210Y011754D03* +X018210Y011744D03* +X018210Y011734D03* +X018210Y011724D03* +X018210Y011714D03* +X018210Y011704D03* +X018210Y011694D03* +X018210Y011684D03* +X018210Y011674D03* +X018210Y011664D03* +X018210Y011654D03* +X018210Y011644D03* +X018210Y011634D03* +X018210Y012224D03* +X018210Y012234D03* +X018210Y012244D03* +X018210Y012254D03* +X018210Y012264D03* +X018210Y012274D03* +X018210Y012284D03* +X018210Y012294D03* +X018210Y012734D03* +X018210Y012744D03* +X018210Y012754D03* +X018210Y012764D03* +X018210Y012774D03* +X018210Y012784D03* +X018210Y012794D03* +X018210Y012804D03* +X018210Y012814D03* +X018210Y012824D03* +X018210Y012834D03* +X018210Y012844D03* +X018210Y012854D03* +X018210Y012864D03* +X018210Y012874D03* +X018210Y012884D03* +X018210Y012894D03* +X018210Y012904D03* +X018210Y012914D03* +X018210Y012924D03* +X018210Y012934D03* +X018210Y012944D03* +X018210Y012954D03* +X018210Y012964D03* +X018210Y012974D03* +X018210Y012984D03* +X018210Y012994D03* +X018210Y013004D03* +X018210Y013014D03* +X018210Y013024D03* +X018210Y013034D03* +X018210Y013044D03* +X018210Y013054D03* +X018210Y013064D03* +X018210Y013074D03* +X018210Y013084D03* +X018210Y013094D03* +X018210Y013104D03* +X018210Y013114D03* +X018210Y013124D03* +X018210Y013134D03* +X018210Y013144D03* +X018210Y013154D03* +X018210Y013164D03* +X018210Y013174D03* +X018210Y013184D03* +X018210Y013194D03* +X018210Y013204D03* +X018210Y013214D03* +X018210Y013224D03* +X018210Y013234D03* +X018210Y013244D03* +X018210Y013254D03* +X018210Y013264D03* +X019260Y013114D03* +X019260Y013104D03* +X019260Y013094D03* +X019310Y012814D03* +X019320Y012804D03* +X020710Y012804D03* +X020710Y012794D03* +X020710Y012784D03* +X020710Y012774D03* +X020710Y012764D03* +X020710Y012754D03* +X020710Y012744D03* +X020710Y012734D03* +X020710Y012724D03* +X020710Y012714D03* +X020710Y012814D03* +X020710Y012824D03* +X020710Y012834D03* +X020710Y012844D03* +X020710Y012854D03* +X020710Y012864D03* +X020710Y012874D03* +X020710Y012884D03* +X020710Y012894D03* +X020710Y012904D03* +X020710Y012914D03* +X020710Y012924D03* +X020710Y012934D03* +X020710Y012944D03* +X020710Y012954D03* +X020710Y012964D03* +X020710Y012974D03* +X020710Y012984D03* +X020710Y012994D03* +X020710Y013004D03* +X020710Y013014D03* +X020710Y013024D03* +X020710Y013034D03* +X020710Y013044D03* +X020710Y013054D03* +X020710Y013064D03* +X020710Y013074D03* +X020710Y013084D03* +X020710Y013094D03* +X020710Y013104D03* +X020710Y013114D03* +X020710Y013124D03* +X020710Y013134D03* +X020710Y013144D03* +X020710Y013154D03* +X020710Y013164D03* +X020710Y013174D03* +X020710Y013184D03* +X020710Y013194D03* +X020710Y013204D03* +X020710Y013214D03* +X020710Y013224D03* +X020710Y013234D03* +X020710Y013244D03* +X020710Y013254D03* +X020710Y013264D03* +X020710Y013274D03* +X020710Y012284D03* +X020710Y012274D03* +X020710Y012264D03* +X020710Y012254D03* +X020710Y012244D03* +X020710Y012234D03* +X020710Y012224D03* +X020710Y012214D03* +X020710Y012204D03* +X020710Y012194D03* +X020710Y012184D03* +X020710Y012174D03* +X020710Y012164D03* +X020710Y012154D03* +X020710Y012144D03* +X020710Y012134D03* +X020710Y012124D03* +X020710Y012114D03* +X020710Y012104D03* +X020710Y012094D03* +X020710Y012084D03* +X020710Y012074D03* +X020710Y012064D03* +X020710Y012054D03* +X020710Y012044D03* +X020710Y012034D03* +X020710Y012024D03* +X020710Y012014D03* +X020710Y012004D03* +X020710Y011994D03* +X020710Y011984D03* +X020710Y011974D03* +X020710Y011964D03* +X020710Y011954D03* +X020710Y011944D03* +X020710Y011934D03* +X020710Y011924D03* +X020710Y011914D03* +X020710Y011904D03* +X020710Y011894D03* +X020710Y011884D03* +X020710Y011874D03* +X020710Y011864D03* +X020710Y011854D03* +X020710Y011844D03* +X020710Y011834D03* +X020710Y011824D03* +X020710Y011814D03* +X020710Y011804D03* +X020710Y011794D03* +X020710Y011784D03* +X020710Y011774D03* +X020710Y011764D03* +X020710Y011754D03* +X020710Y011744D03* +X020710Y011734D03* +X020710Y011724D03* +X020710Y011714D03* +X020710Y011704D03* +X020710Y011694D03* +X020710Y011684D03* +X020710Y011674D03* +X020710Y011664D03* +X020710Y011654D03* +X020710Y011644D03* +X020710Y011634D03* +X020060Y009104D03* +X020060Y009094D03* +X020060Y009084D03* +X020060Y009074D03* +X020060Y009064D03* +X020060Y009054D03* +X020060Y009044D03* +X020060Y009034D03* +X020060Y009024D03* +X020060Y009014D03* +X020060Y009004D03* +X020060Y008994D03* +X020060Y008984D03* +X020060Y008974D03* +X020060Y008964D03* +X020060Y008954D03* +X020060Y008944D03* +X020060Y008934D03* +X020060Y008924D03* +X020060Y008914D03* +X020060Y008904D03* +X020060Y008894D03* +X020060Y008884D03* +X020060Y008874D03* +X020060Y008864D03* +X020060Y008854D03* +X020060Y008844D03* +X020060Y008834D03* +X020060Y008824D03* +X020060Y008814D03* +X020060Y008804D03* +X020060Y008794D03* +X020060Y008784D03* +X020060Y008774D03* +X020060Y008764D03* +X020060Y008754D03* +X020060Y008744D03* +X020060Y008734D03* +X020060Y008724D03* +X020060Y008714D03* +X020060Y008704D03* +X020060Y008694D03* +X020060Y008684D03* +X020060Y008674D03* +X020060Y008664D03* +X020060Y008654D03* +X020060Y008644D03* +X020060Y008634D03* +X020060Y008624D03* +X020060Y008614D03* +X020060Y008604D03* +X020060Y008594D03* +X020060Y008584D03* +X020060Y008574D03* +X020060Y008564D03* +X020060Y008554D03* +X020060Y008544D03* +X020060Y008534D03* +X020060Y008524D03* +X020060Y008514D03* +X020060Y008504D03* +X020060Y008494D03* +X020060Y008484D03* +X020060Y008474D03* +X020060Y008464D03* +X020060Y008454D03* +X020060Y008444D03* +X020060Y008434D03* +X020060Y008424D03* +X020060Y008414D03* +X020060Y008404D03* +X020060Y008394D03* +X020060Y008384D03* +X020060Y008374D03* +X020060Y008364D03* +X020060Y008354D03* +X020060Y008344D03* +X020060Y008334D03* +X020060Y008324D03* +X020060Y008314D03* +X020060Y008304D03* +X020060Y008294D03* +X020060Y008284D03* +X020060Y008274D03* +X020060Y008264D03* +X020060Y008254D03* +X020060Y008244D03* +X020060Y008234D03* +X020060Y008224D03* +X020060Y008214D03* +X020060Y008204D03* +X020060Y008194D03* +X020060Y008184D03* +X020060Y008174D03* +X020060Y008164D03* +X020060Y008154D03* +X020060Y008144D03* +X020060Y008134D03* +X020060Y008124D03* +X020060Y008114D03* +X020060Y008104D03* +X020060Y008094D03* +X020060Y008084D03* +X020060Y008074D03* +X020060Y008064D03* +X020060Y008054D03* +X020060Y008044D03* +X020060Y008034D03* +X020060Y008024D03* +X020060Y008014D03* +X020060Y008004D03* +X020060Y007994D03* +X020060Y007984D03* +X020060Y007974D03* +X020060Y007964D03* +X020060Y007954D03* +X020060Y007944D03* +X020060Y007934D03* +X020060Y007924D03* +X020060Y007914D03* +X020060Y007904D03* +X020060Y007894D03* +X020060Y007884D03* +X020060Y007874D03* +X020060Y007864D03* +X020060Y007854D03* +X020060Y007844D03* +X020060Y007834D03* +X020060Y007824D03* +X020060Y007814D03* +X020060Y007804D03* +X020060Y007794D03* +X020060Y007784D03* +X020060Y007774D03* +X020060Y007764D03* +X020060Y007754D03* +X020060Y007744D03* +X020060Y007734D03* +X020060Y007724D03* +X020060Y007714D03* +X020060Y007704D03* +X020060Y007694D03* +X020060Y007684D03* +X020060Y007674D03* +X020060Y007664D03* +X020060Y007654D03* +X020060Y007644D03* +X020060Y007634D03* +X020060Y007624D03* +X020060Y007614D03* +X020060Y007604D03* +X020060Y007594D03* +X020060Y007584D03* +X020060Y007574D03* +X020060Y007564D03* +X020060Y007554D03* +X020060Y007544D03* +X020060Y007534D03* +X020060Y007524D03* +X020060Y007514D03* +X020060Y007504D03* +X020060Y007494D03* +X020060Y007484D03* +X020060Y007474D03* +X020060Y007464D03* +X020060Y007454D03* +X020060Y007444D03* +X020060Y007434D03* +X020060Y007424D03* +X020060Y007414D03* +X020060Y007404D03* +X020060Y007394D03* +X020060Y007384D03* +X020060Y007374D03* +X020060Y007364D03* +X020060Y007354D03* +X020060Y007344D03* +X020060Y007334D03* +X020060Y007324D03* +X020060Y007314D03* +X020060Y007304D03* +X020060Y007294D03* +X020060Y007284D03* +X020060Y007274D03* +X020060Y007264D03* +X020060Y007254D03* +X020060Y007244D03* +X020060Y007234D03* +X020060Y007224D03* +X020060Y007214D03* +X020060Y007204D03* +X020060Y007194D03* +X020060Y007184D03* +X020060Y007174D03* +X020060Y007164D03* +X020060Y007154D03* +X020060Y007144D03* +X020060Y007134D03* +X020060Y007124D03* +X020060Y007114D03* +X020060Y007104D03* +X020060Y007094D03* +X020060Y007084D03* +X020060Y007074D03* +X020060Y007064D03* +X020060Y007054D03* +X020060Y007044D03* +X015790Y009504D03* +X015790Y009514D03* +X013290Y009104D03* +X013290Y009094D03* +X013290Y009084D03* +X013290Y009074D03* +X013290Y009064D03* +X013290Y009054D03* +X013290Y009044D03* +X013290Y009034D03* +X013290Y009024D03* +X013290Y009014D03* +X013290Y009004D03* +X013290Y008994D03* +X013290Y008984D03* +X013290Y008974D03* +X013290Y008964D03* +X013290Y008954D03* +X013290Y008944D03* +X013290Y008934D03* +X013290Y008924D03* +X013290Y008914D03* +X013290Y008904D03* +X013290Y008894D03* +X013290Y008884D03* +X013290Y008874D03* +X013290Y008864D03* +X013290Y008854D03* +X013290Y008844D03* +X013290Y008834D03* +X013290Y008824D03* +X013290Y008814D03* +X013290Y008804D03* +X013290Y008794D03* +X013290Y008784D03* +X013290Y008774D03* +X013290Y008764D03* +X013290Y008754D03* +X013290Y008744D03* +X013290Y008734D03* +X013290Y008724D03* +X013290Y008714D03* +X013290Y008704D03* +X013290Y008694D03* +X013290Y008684D03* +X013290Y008674D03* +X013290Y008664D03* +X013290Y008654D03* +X013290Y008644D03* +X013290Y008634D03* +X013290Y008624D03* +X013290Y008614D03* +X013290Y008604D03* +X013290Y008594D03* +X013290Y008584D03* +X013290Y008574D03* +X013290Y008564D03* +X013290Y008134D03* +X013290Y008124D03* +X013290Y008114D03* +X013290Y008104D03* +X013290Y008094D03* +X013290Y008084D03* +X013290Y008074D03* +X013290Y008064D03* +X013290Y008054D03* +X013290Y008044D03* +X013290Y008034D03* +X013290Y008024D03* +X013290Y008014D03* +X013290Y008004D03* +X013290Y007994D03* +X013290Y007984D03* +X013290Y007974D03* +X013290Y007964D03* +X013290Y007954D03* +X013290Y007944D03* +X013290Y007934D03* +X013290Y007924D03* +X013290Y007914D03* +X013290Y007904D03* +X013290Y007894D03* +X013290Y007884D03* +X013290Y007874D03* +X013290Y007864D03* +X013290Y007854D03* +X013290Y007844D03* +X013290Y007834D03* +X013290Y007824D03* +X013290Y007814D03* +X013290Y007804D03* +X013290Y007794D03* +X013290Y007784D03* +X013290Y007774D03* +X013290Y007764D03* +X013290Y007754D03* +X013290Y007744D03* +X013290Y007734D03* +X013290Y007724D03* +X013290Y007714D03* +X013290Y007704D03* +X013290Y007694D03* +X013290Y007684D03* +X013290Y007674D03* +X013290Y007664D03* +X013290Y007654D03* +X013290Y007644D03* +X013290Y007634D03* +X013290Y007624D03* +X013290Y007614D03* +X013290Y007604D03* +X013290Y007594D03* +X013290Y007584D03* +X013290Y007574D03* +X013290Y007564D03* +X013290Y007554D03* +X013290Y007544D03* +X013290Y007534D03* +X013290Y007524D03* +X013290Y007514D03* +X013290Y007504D03* +X013290Y007494D03* +X013290Y007484D03* +X013290Y007474D03* +D19* +X014445Y007444D03* +X014445Y007434D03* +X015665Y009094D03* +X015665Y009104D03* +X017035Y009104D03* +X017035Y007474D03* +X017645Y007044D03* +X018255Y007474D03* +X013255Y012214D03* +X012695Y012654D03* +X019495Y012714D03* +X004865Y010244D03* +X004865Y010234D03* +D20* +X005155Y009974D03* +X005165Y009954D03* +X005175Y009934D03* +X005185Y009924D03* +X005185Y009914D03* +X005195Y009904D03* +X005205Y009884D03* +X004365Y011114D03* +X004355Y011134D03* +X004335Y011164D03* +X004325Y011184D03* +X004315Y011204D03* +X004305Y011214D03* +X004295Y011234D03* +X004285Y011254D03* +X004275Y011264D03* +X004265Y011284D03* +X004255Y011304D03* +X004245Y011314D03* +X004235Y011334D03* +X004225Y011354D03* +X004215Y011364D03* +X004205Y011384D03* +X004195Y011404D03* +X004185Y011414D03* +X004175Y011434D03* +X004165Y011444D03* +X004165Y011454D03* +X004155Y011464D03* +X004155Y011474D03* +X004145Y011484D03* +X004135Y011494D03* +X004135Y011504D03* +X004125Y011514D03* +X004125Y011524D03* +X004115Y011534D03* +X004105Y011544D03* +X004105Y011554D03* +X004095Y011564D03* +X004095Y011574D03* +X004085Y011584D03* +X004075Y011594D03* +X004075Y011604D03* +X004065Y011614D03* +X004065Y011624D03* +X004055Y011634D03* +X004045Y011644D03* +X004045Y011654D03* +X004035Y011664D03* +X004035Y011674D03* +X004025Y011684D03* +X004015Y011704D03* +X004005Y011714D03* +X003995Y011734D03* +X003985Y011754D03* +X003975Y011764D03* +X003965Y011784D03* +X003955Y011804D03* +X003945Y011814D03* +X003935Y011834D03* +X003925Y011854D03* +X003915Y011864D03* +X003905Y011884D03* +X003895Y011904D03* +X003885Y011914D03* +X003875Y011934D03* +X003865Y011954D03* +X003845Y011984D03* +X003815Y012034D03* +X003785Y012084D03* +X003755Y012134D03* +X013375Y011964D03* +X014745Y012674D03* +X014765Y012724D03* +X014775Y012754D03* +X014785Y012774D03* +X014785Y012784D03* +X014795Y012804D03* +X014805Y012824D03* +X014805Y012834D03* +X014815Y012854D03* +X014815Y012864D03* +X014825Y012874D03* +X014825Y012884D03* +X014835Y012904D03* +X014835Y012914D03* +X014845Y012924D03* +X014845Y012934D03* +X014845Y012944D03* +X014855Y012954D03* +X014855Y012964D03* +X014865Y012974D03* +X014865Y012984D03* +X014865Y012994D03* +X014875Y013004D03* +X014875Y013014D03* +X014885Y013034D03* +X014885Y013044D03* +X014895Y013054D03* +X014895Y013064D03* +X014905Y013084D03* +X015625Y012544D03* +X015645Y012494D03* +X015655Y012464D03* +X015665Y012444D03* +X015675Y012414D03* +X015685Y012384D03* +X015695Y012364D03* +X015695Y012354D03* +X015705Y012334D03* +X015715Y012314D03* +X015715Y012304D03* +X015725Y012284D03* +X015725Y012274D03* +X015735Y012264D03* +X015735Y012254D03* +X015735Y012244D03* +X015745Y012234D03* +X015745Y012224D03* +X015755Y012204D03* +X015785Y009494D03* +X016705Y008444D03* +X018585Y008434D03* +X018585Y008424D03* +X018585Y008414D03* +X014325Y007054D03* +X014325Y007044D03* +X019145Y011994D03* +X019145Y012004D03* +X019325Y012794D03* +X019265Y013124D03* +X019265Y013134D03* +D21* +X020015Y013334D03* +X020015Y013344D03* +X020015Y012384D03* +X020015Y011434D03* +X017515Y011434D03* +X017515Y011424D03* +X017515Y011414D03* +X017515Y011404D03* +X017515Y011394D03* +X017515Y011384D03* +X017515Y011374D03* +X017515Y011364D03* +X017515Y011354D03* +X017515Y011344D03* +X017515Y011334D03* +X017515Y011324D03* +X017515Y011314D03* +X017515Y011304D03* +X017515Y011294D03* +X017515Y011284D03* +X017515Y011274D03* +X017515Y011264D03* +X017515Y011254D03* +X017515Y011244D03* +X017515Y011234D03* +X017515Y011224D03* +X017515Y011214D03* +X017515Y011444D03* +X017515Y011454D03* +X017515Y011464D03* +X017515Y011474D03* +X017515Y011484D03* +X017515Y011494D03* +X017515Y011504D03* +X017515Y011514D03* +X017515Y011524D03* +X017515Y011534D03* +X017515Y011544D03* +X017515Y011554D03* +X017515Y011564D03* +X017515Y011574D03* +X017515Y011584D03* +X017515Y011594D03* +X017515Y011604D03* +X017515Y011614D03* +X017515Y011624D03* +X015155Y011974D03* +X017645Y009154D03* +X017645Y007414D03* +X012595Y007414D03* +X012595Y007404D03* +X012595Y007394D03* +X012595Y007384D03* +X012595Y007374D03* +X012595Y007364D03* +X012595Y007354D03* +X012595Y007344D03* +X012595Y007334D03* +X012595Y007324D03* +X012595Y007314D03* +X012595Y007304D03* +X012595Y007294D03* +X012595Y007284D03* +X012595Y007274D03* +X012595Y007264D03* +X012595Y007254D03* +X012595Y007244D03* +X012595Y007234D03* +X012595Y007224D03* +X012595Y007214D03* +X012595Y007204D03* +X012595Y007194D03* +X012595Y007184D03* +X012595Y007174D03* +X012595Y007164D03* +X012595Y007154D03* +X012595Y007144D03* +X012595Y007134D03* +X012595Y007124D03* +X012595Y007114D03* +X012595Y007104D03* +X012595Y007094D03* +X012595Y007084D03* +X012595Y007074D03* +X012595Y007064D03* +X012595Y007054D03* +X012595Y007044D03* +X012595Y007424D03* +X012595Y007434D03* +X012595Y007444D03* +X012595Y007454D03* +X012595Y007464D03* +D22* +X014475Y007534D03* +X014475Y007544D03* +X015635Y009004D03* +X017085Y009124D03* +X018205Y009124D03* +X018215Y007454D03* +X017645Y007054D03* +X013225Y012274D03* +X012705Y012614D03* +X015155Y013344D03* +X004865Y010194D03* +X004865Y010184D03* +D23* +X004865Y010144D03* +X013195Y012334D03* +X017115Y009134D03* +X018165Y009134D03* +X015605Y008904D03* +X014505Y007634D03* +X017645Y007064D03* +D24* +X016730Y007964D03* +X016720Y008004D03* +X016720Y008014D03* +X016720Y008024D03* +X016720Y008034D03* +X016710Y008064D03* +X016710Y008074D03* +X016710Y008084D03* +X016710Y008094D03* +X016710Y008104D03* +X016710Y008114D03* +X016710Y008124D03* +X016700Y008144D03* +X016700Y008154D03* +X016700Y008164D03* +X016700Y008174D03* +X016700Y008184D03* +X016700Y008194D03* +X016700Y008204D03* +X016700Y008214D03* +X016700Y008224D03* +X016700Y008234D03* +X016700Y008244D03* +X016700Y008254D03* +X016700Y008264D03* +X016700Y008274D03* +X016700Y008284D03* +X016700Y008294D03* +X016700Y008304D03* +X016700Y008314D03* +X016700Y008324D03* +X016700Y008334D03* +X016700Y008344D03* +X016700Y008354D03* +X016700Y008364D03* +X016700Y008374D03* +X016700Y008384D03* +X016700Y008394D03* +X016700Y008404D03* +X016700Y008414D03* +X016700Y008424D03* +X016700Y008434D03* +X016710Y008454D03* +X016710Y008464D03* +X016710Y008474D03* +X016710Y008484D03* +X016710Y008494D03* +X016710Y008504D03* +X016710Y008514D03* +X016710Y008524D03* +X016720Y008534D03* +X016720Y008544D03* +X016720Y008554D03* +X016720Y008564D03* +X016720Y008574D03* +X016720Y008584D03* +X016730Y008604D03* +X016730Y008614D03* +X016730Y008624D03* +X016740Y008654D03* +X016740Y008664D03* +X015780Y009474D03* +X015780Y009484D03* +X017650Y009554D03* +X018550Y008654D03* +X018560Y008614D03* +X018560Y008604D03* +X018570Y008574D03* +X018570Y008564D03* +X018570Y008554D03* +X018570Y008544D03* +X018570Y008534D03* +X018580Y008514D03* +X018580Y008504D03* +X018580Y008494D03* +X018580Y008484D03* +X018580Y008474D03* +X018580Y008464D03* +X018580Y008454D03* +X018580Y008444D03* +X018590Y008404D03* +X018590Y008394D03* +X018590Y008384D03* +X018590Y008374D03* +X018590Y008364D03* +X018590Y008354D03* +X018590Y008344D03* +X018590Y008334D03* +X018590Y008324D03* +X018590Y008314D03* +X018590Y008304D03* +X018590Y008294D03* +X018590Y008284D03* +X018590Y008274D03* +X018590Y008264D03* +X018590Y008254D03* +X018590Y008244D03* +X018590Y008234D03* +X018590Y008224D03* +X018590Y008214D03* +X018590Y008204D03* +X018590Y008194D03* +X018590Y008184D03* +X018590Y008174D03* +X018590Y008164D03* +X018590Y008154D03* +X018590Y008144D03* +X018580Y008134D03* +X018580Y008124D03* +X018580Y008114D03* +X018580Y008104D03* +X018580Y008094D03* +X018580Y008084D03* +X018580Y008074D03* +X018580Y008064D03* +X018570Y008034D03* +X018570Y008024D03* +X018570Y008014D03* +X018570Y008004D03* +X018560Y007964D03* +X014330Y007074D03* +X014330Y007064D03* +X016060Y011394D03* +X016040Y011444D03* +X016030Y011474D03* +X016020Y011504D03* +X016010Y011524D03* +X016000Y011554D03* +X015990Y011574D03* +X015990Y011584D03* +X015980Y011604D03* +X015980Y011614D03* +X015970Y011624D03* +X015970Y011634D03* +X015960Y011654D03* +X015960Y011664D03* +X015950Y011684D03* +X015950Y011694D03* +X015940Y011704D03* +X015940Y011714D03* +X015940Y011724D03* +X015930Y011734D03* +X015930Y011744D03* +X015920Y011754D03* +X015920Y011764D03* +X014610Y012324D03* +X014630Y012374D03* +X014640Y012404D03* +X014650Y012424D03* +X014650Y012434D03* +X014660Y012454D03* +X014670Y012474D03* +X014670Y012484D03* +X014680Y012504D03* +X014680Y012514D03* +X014690Y012524D03* +X014690Y012534D03* +X014700Y012554D03* +X014700Y012564D03* +X014710Y012574D03* +X014710Y012584D03* +X014710Y012594D03* +X014720Y012604D03* +X014720Y012614D03* +X014730Y012624D03* +X014730Y012634D03* +X014730Y012644D03* +X014740Y012654D03* +X014740Y012664D03* +X014750Y012684D03* +X014750Y012694D03* +X014760Y012704D03* +X014760Y012714D03* +X014770Y012734D03* +X014770Y012744D03* +X014780Y012764D03* +X014790Y012794D03* +X014800Y012814D03* +X014810Y012844D03* +X014830Y012894D03* +X013370Y011974D03* +X019140Y011974D03* +X019140Y011964D03* +X019140Y011954D03* +X019140Y011944D03* +X019140Y011934D03* +X019140Y011924D03* +X019140Y011914D03* +X019140Y011904D03* +X019140Y011894D03* +X019150Y011884D03* +X019150Y011874D03* +X019150Y011864D03* +X019150Y011854D03* +X019140Y011984D03* +X019150Y012014D03* +X019150Y012024D03* +X019150Y012034D03* +X019150Y012044D03* +X019150Y012054D03* +X019160Y012074D03* +X019160Y012084D03* +X019340Y012784D03* +X019270Y013144D03* +X019270Y013154D03* +X005430Y009514D03* +X005300Y009724D03* +X005280Y009754D03* +X005270Y009774D03* +X005260Y009794D03* +X005250Y009804D03* +X005240Y009824D03* +X005230Y009834D03* +X005230Y009844D03* +X005220Y009854D03* +X005220Y009864D03* +X005210Y009874D03* +X005200Y009894D03* +X004600Y009984D03* +X004600Y009994D03* +X004590Y009974D03* +X004580Y009954D03* +X004570Y009934D03* +X005130Y010734D03* +X005140Y010754D03* +X004020Y011694D03* +X004000Y011724D03* +X003990Y011744D03* +X003970Y011774D03* +X003960Y011794D03* +X003940Y011824D03* +X003930Y011844D03* +X003910Y011874D03* +X003900Y011894D03* +X003880Y011924D03* +X003870Y011944D03* +X003860Y011964D03* +X003850Y011974D03* +X003840Y011994D03* +X003830Y012004D03* +X003830Y012014D03* +X003820Y012024D03* +X003810Y012044D03* +X003800Y012054D03* +X003800Y012064D03* +X003790Y012074D03* +X003780Y012094D03* +X003770Y012104D03* +X003770Y012114D03* +X003760Y012124D03* +X003750Y012144D03* +X003740Y012154D03* +X003740Y012164D03* +X003730Y012174D03* +X003730Y012184D03* +X003720Y012194D03* +X003710Y012204D03* +X003710Y012214D03* +X003700Y012224D03* +X003700Y012234D03* +X003690Y012244D03* +X003680Y012254D03* +X003680Y012264D03* +X003670Y012274D03* +X003670Y012284D03* +X003660Y012294D03* +X003650Y012304D03* +X003650Y012314D03* +X003640Y012324D03* +X003630Y012344D03* +X003620Y012354D03* +X003620Y012364D03* +X003610Y012374D03* +X003600Y012394D03* +X003590Y012404D03* +X003590Y012414D03* +X003580Y012424D03* +X003570Y012444D03* +X003560Y012464D03* +X003550Y012474D03* +X003540Y012494D03* +X003530Y012514D03* +X003520Y012524D03* +X003510Y012544D03* +X003500Y012564D03* +X003490Y012574D03* +X003480Y012594D03* +X003460Y012624D03* +X003450Y012644D03* +X003420Y012694D03* +X003390Y012744D03* +X003360Y012794D03* +X003330Y012844D03* +X003300Y012894D03* +D25* +X004865Y010094D03* +X014535Y007734D03* +X014535Y007724D03* +X015575Y008804D03* +X015575Y008814D03* +X017645Y007074D03* +X015155Y013194D03* +D26* +X015155Y013114D03* +X015545Y008714D03* +X015545Y008704D03* +X014565Y007834D03* +X014565Y007824D03* +X017645Y007084D03* +X004865Y010044D03* +D27* +X004585Y009964D03* +X004575Y009944D03* +X004565Y009924D03* +X004555Y009914D03* +X004555Y009904D03* +X004545Y009884D03* +X005245Y009814D03* +X005265Y009784D03* +X005275Y009764D03* +X005285Y009744D03* +X005295Y009734D03* +X005305Y009714D03* +X005315Y009704D03* +X005315Y009694D03* +X005325Y009684D03* +X005325Y009674D03* +X005335Y009664D03* +X005345Y009654D03* +X005345Y009644D03* +X005355Y009634D03* +X005365Y009624D03* +X005365Y009614D03* +X005375Y009604D03* +X005375Y009594D03* +X005385Y009584D03* +X005395Y009574D03* +X005395Y009564D03* +X005405Y009554D03* +X005415Y009544D03* +X005415Y009534D03* +X005425Y009524D03* +X005435Y009504D03* +X005445Y009494D03* +X005445Y009484D03* +X005455Y009474D03* +X005465Y009464D03* +X005465Y009454D03* +X005475Y009444D03* +X005475Y009434D03* +X005485Y009424D03* +X005495Y009414D03* +X005495Y009404D03* +X005505Y009394D03* +X005515Y009384D03* +X005515Y009374D03* +X005525Y009364D03* +X005525Y009354D03* +X005535Y009344D03* +X005545Y009334D03* +X005545Y009324D03* +X005555Y009314D03* +X005565Y009294D03* +X005575Y009284D03* +X005575Y009274D03* +X005585Y009264D03* +X005595Y009244D03* +X005605Y009234D03* +X005615Y009214D03* +X005625Y009204D03* +X005625Y009194D03* +X005635Y009184D03* +X005645Y009164D03* +X005655Y009154D03* +X005665Y009134D03* +X005675Y009114D03* +X005685Y009104D03* +X005695Y009084D03* +X005705Y009074D03* +X005715Y009054D03* +X005725Y009034D03* +X005735Y009024D03* +X005745Y009004D03* +X005765Y008974D03* +X005785Y008944D03* +X005795Y008924D03* +X005815Y008894D03* +X005845Y008844D03* +X005865Y008814D03* +X005895Y008764D03* +X005125Y010724D03* +X005135Y010744D03* +X005145Y010764D03* +X005155Y010774D03* +X005155Y010784D03* +X005165Y010794D03* +X005165Y010804D03* +X005175Y010814D03* +X005185Y010834D03* +X005195Y010854D03* +X003635Y012334D03* +X003605Y012384D03* +X003575Y012434D03* +X003565Y012454D03* +X003545Y012484D03* +X003535Y012504D03* +X003515Y012534D03* +X003505Y012554D03* +X003485Y012584D03* +X003475Y012604D03* +X003465Y012614D03* +X003455Y012634D03* +X003445Y012654D03* +X003435Y012664D03* +X003435Y012674D03* +X003425Y012684D03* +X003415Y012704D03* +X003405Y012714D03* +X003405Y012724D03* +X003395Y012734D03* +X003385Y012754D03* +X003375Y012764D03* +X003375Y012774D03* +X003365Y012784D03* +X003355Y012804D03* +X003345Y012814D03* +X003345Y012824D03* +X003335Y012834D03* +X003325Y012854D03* +X003315Y012864D03* +X003315Y012874D03* +X003305Y012884D03* +X003295Y012904D03* +X003285Y012914D03* +X003285Y012924D03* +X003275Y012934D03* +X003265Y012954D03* +X003255Y012964D03* +X003255Y012974D03* +X003245Y012984D03* +X003235Y013004D03* +X003225Y013014D03* +X003225Y013024D03* +X003215Y013034D03* +X003205Y013054D03* +X003195Y013064D03* +X003195Y013074D03* +X003185Y013084D03* +X003175Y013104D03* +X003165Y013114D03* +X003165Y013124D03* +X003155Y013134D03* +X003145Y013154D03* +X003135Y013174D03* +X003125Y013184D03* +X003115Y013204D03* +X003095Y013234D03* +X003085Y013254D03* +X003065Y013284D03* +X003055Y013304D03* +X003035Y013334D03* +X003025Y013354D03* +X003005Y013384D03* +X002995Y013404D03* +X002965Y013454D03* +X002935Y013504D03* +X013365Y011984D03* +X014375Y011724D03* +X014565Y012204D03* +X014565Y012214D03* +X014575Y012224D03* +X014575Y012234D03* +X014575Y012244D03* +X014585Y012254D03* +X014585Y012264D03* +X014595Y012274D03* +X014595Y012284D03* +X014595Y012294D03* +X014605Y012304D03* +X014605Y012314D03* +X014615Y012334D03* +X014615Y012344D03* +X014625Y012354D03* +X014625Y012364D03* +X014635Y012384D03* +X014635Y012394D03* +X014645Y012414D03* +X014655Y012444D03* +X014665Y012464D03* +X014675Y012494D03* +X014695Y012544D03* +X015955Y011674D03* +X015965Y011644D03* +X015985Y011594D03* +X015995Y011564D03* +X016005Y011544D03* +X016005Y011534D03* +X016015Y011514D03* +X016025Y011494D03* +X016025Y011484D03* +X016035Y011464D03* +X016035Y011454D03* +X016045Y011434D03* +X016045Y011424D03* +X016055Y011414D03* +X016055Y011404D03* +X016065Y011384D03* +X016065Y011374D03* +X016075Y011364D03* +X016075Y011354D03* +X016075Y011344D03* +X016085Y011334D03* +X016085Y011324D03* +X016095Y011304D03* +X016095Y011294D03* +X016105Y011284D03* +X016105Y011274D03* +X016105Y011264D03* +X016115Y011254D03* +X016115Y011244D03* +X016125Y011224D03* +X016125Y011214D03* +X015775Y009464D03* +X015775Y009454D03* +X016765Y008744D03* +X016755Y008724D03* +X016755Y008714D03* +X016755Y008704D03* +X016745Y008694D03* +X016745Y008684D03* +X016745Y008674D03* +X016735Y008644D03* +X016735Y008634D03* +X016725Y008594D03* +X016705Y008134D03* +X016715Y008054D03* +X016715Y008044D03* +X016725Y007994D03* +X016725Y007984D03* +X016725Y007974D03* +X016735Y007954D03* +X016735Y007944D03* +X016735Y007934D03* +X016735Y007924D03* +X016745Y007914D03* +X016745Y007904D03* +X016745Y007894D03* +X016755Y007864D03* +X018545Y007904D03* +X018545Y007914D03* +X018555Y007924D03* +X018555Y007934D03* +X018555Y007944D03* +X018555Y007954D03* +X018565Y007974D03* +X018565Y007984D03* +X018565Y007994D03* +X018575Y008044D03* +X018575Y008054D03* +X018575Y008524D03* +X018565Y008584D03* +X018565Y008594D03* +X018555Y008624D03* +X018555Y008634D03* +X018555Y008644D03* +X018545Y008664D03* +X018545Y008674D03* +X018545Y008684D03* +X018535Y008704D03* +X018535Y008714D03* +X018525Y008744D03* +X019155Y011824D03* +X019155Y011834D03* +X019155Y011844D03* +X019155Y012064D03* +X019165Y012094D03* +X019165Y012104D03* +X019175Y012114D03* +X019175Y012124D03* +X019345Y012774D03* +X019275Y013164D03* +X014335Y007084D03* +D28* +X017640Y007094D03* +X004870Y010004D03* +D29* +X004550Y009894D03* +X004540Y009874D03* +X004530Y009864D03* +X004530Y009854D03* +X004520Y009844D03* +X004520Y009834D03* +X004510Y009824D03* +X004500Y009804D03* +X004470Y009754D03* +X005560Y009304D03* +X005590Y009254D03* +X005610Y009224D03* +X005640Y009174D03* +X005660Y009144D03* +X005670Y009124D03* +X005690Y009094D03* +X005710Y009064D03* +X005720Y009044D03* +X005740Y009014D03* +X005750Y008994D03* +X005760Y008984D03* +X005770Y008964D03* +X005780Y008954D03* +X005790Y008934D03* +X005800Y008914D03* +X005810Y008904D03* +X005820Y008884D03* +X005830Y008874D03* +X005830Y008864D03* +X005840Y008854D03* +X005850Y008834D03* +X005860Y008824D03* +X005870Y008804D03* +X005880Y008794D03* +X005880Y008784D03* +X005890Y008774D03* +X005900Y008754D03* +X005910Y008744D03* +X005910Y008734D03* +X005920Y008724D03* +X005930Y008714D03* +X005930Y008704D03* +X005940Y008694D03* +X005940Y008684D03* +X005950Y008674D03* +X005960Y008664D03* +X005960Y008654D03* +X005970Y008644D03* +X005980Y008634D03* +X005980Y008624D03* +X005990Y008614D03* +X005990Y008604D03* +X006000Y008594D03* +X006010Y008584D03* +X006010Y008574D03* +X006020Y008564D03* +X006030Y008544D03* +X006040Y008534D03* +X006040Y008524D03* +X006050Y008514D03* +X006060Y008504D03* +X006060Y008494D03* +X006070Y008484D03* +X006080Y008464D03* +X006090Y008454D03* +X006090Y008444D03* +X006100Y008434D03* +X006110Y008414D03* +X006120Y008404D03* +X006130Y008384D03* +X006140Y008374D03* +X006140Y008364D03* +X006150Y008354D03* +X006160Y008334D03* +X006170Y008324D03* +X006180Y008304D03* +X006190Y008284D03* +X006200Y008274D03* +X006210Y008254D03* +X006220Y008244D03* +X006230Y008224D03* +X006240Y008204D03* +X006250Y008194D03* +X006260Y008174D03* +X006280Y008144D03* +X006300Y008114D03* +X006310Y008094D03* +X006330Y008064D03* +X006360Y008014D03* +X006410Y007934D03* +X005180Y010824D03* +X005190Y010844D03* +X005200Y010864D03* +X005210Y010874D03* +X005210Y010884D03* +X005220Y010894D03* +X005220Y010904D03* +X005230Y010914D03* +X005240Y010934D03* +X005250Y010954D03* +X005260Y010964D03* +X005270Y010984D03* +X005280Y011004D03* +X003270Y012944D03* +X003240Y012994D03* +X003210Y013044D03* +X003180Y013094D03* +X003150Y013144D03* +X003140Y013164D03* +X003120Y013194D03* +X003110Y013214D03* +X003100Y013224D03* +X003090Y013244D03* +X003080Y013264D03* +X003070Y013274D03* +X003060Y013294D03* +X003050Y013314D03* +X003040Y013324D03* +X003030Y013344D03* +X003020Y013364D03* +X003010Y013374D03* +X003000Y013394D03* +X002990Y013414D03* +X002980Y013424D03* +X002980Y013434D03* +X002970Y013444D03* +X002960Y013464D03* +X002950Y013474D03* +X002950Y013484D03* +X002940Y013494D03* +X002930Y013514D03* +X002920Y013524D03* +X002920Y013534D03* +X002910Y013544D03* +X002900Y013554D03* +X002900Y013564D03* +X002890Y013574D03* +X002890Y013584D03* +X002880Y013594D03* +X002870Y013604D03* +X002870Y013614D03* +X002860Y013624D03* +X002860Y013634D03* +X002850Y013644D03* +X002840Y013654D03* +X002840Y013664D03* +X002830Y013674D03* +X002830Y013684D03* +X013360Y011994D03* +X014320Y011594D03* +X014320Y011584D03* +X014320Y011574D03* +X014310Y011564D03* +X014310Y011554D03* +X014300Y011534D03* +X014300Y011524D03* +X014290Y011514D03* +X014290Y011504D03* +X014280Y011484D03* +X014280Y011474D03* +X014270Y011454D03* +X014260Y011434D03* +X014260Y011424D03* +X014250Y011404D03* +X014240Y011384D03* +X014240Y011374D03* +X014230Y011354D03* +X014220Y011324D03* +X014210Y011304D03* +X014200Y011274D03* +X014190Y011254D03* +X014180Y011224D03* +X014330Y011604D03* +X014330Y011614D03* +X014340Y011624D03* +X014340Y011634D03* +X014340Y011644D03* +X014350Y011654D03* +X014350Y011664D03* +X014360Y011674D03* +X014360Y011684D03* +X014360Y011694D03* +X014370Y011704D03* +X014370Y011714D03* +X014380Y011734D03* +X014380Y011744D03* +X014390Y011754D03* +X014390Y011764D03* +X016090Y011314D03* +X016120Y011234D03* +X015770Y009444D03* +X016790Y008804D03* +X016780Y008784D03* +X016780Y008774D03* +X016770Y008764D03* +X016770Y008754D03* +X016760Y008734D03* +X016750Y007884D03* +X016750Y007874D03* +X016760Y007854D03* +X016760Y007844D03* +X016770Y007824D03* +X016780Y007794D03* +X018520Y007824D03* +X018530Y007844D03* +X018530Y007854D03* +X018530Y007864D03* +X018540Y007874D03* +X018540Y007884D03* +X018540Y007894D03* +X018540Y008694D03* +X018530Y008724D03* +X018530Y008734D03* +X018520Y008754D03* +X018520Y008764D03* +X018510Y008784D03* +X019170Y011784D03* +X019170Y011794D03* +X019160Y011804D03* +X019160Y011814D03* +X019180Y012134D03* +X019190Y012144D03* +X019360Y012764D03* +X019280Y013174D03* +X019290Y013184D03* +X015160Y013684D03* +X015160Y013694D03* +X014340Y007104D03* +X014340Y007094D03* +D30* +X017645Y007104D03* +D31* +X017645Y007114D03* +D32* +X017645Y007124D03* +D33* +X017645Y007134D03* +D34* +X017645Y007144D03* +D35* +X017645Y007154D03* +D36* +X017645Y007164D03* +D37* +X016840Y007674D03* +X016830Y007694D03* +X016840Y008904D03* +X016850Y008924D03* +X016860Y008934D03* +X015750Y009374D03* +X015750Y009384D03* +X014360Y007164D03* +X018450Y007674D03* +X018460Y007694D03* +X018450Y008904D03* +X018440Y008914D03* +X018440Y008924D03* +X018430Y008934D03* +X019200Y011724D03* +X019230Y012204D03* +X019240Y012214D03* +X015160Y013634D03* +X015160Y013644D03* +X013340Y012034D03* +X012420Y012354D03* +X012420Y012364D03* +X012410Y012344D03* +X012410Y012334D03* +X012400Y012324D03* +X012390Y012314D03* +X012390Y012304D03* +X012380Y012294D03* +X012370Y012274D03* +X012360Y012264D03* +X012350Y012244D03* +D38* +X012955Y012584D03* +X012955Y012594D03* +X017645Y007174D03* +D39* +X016855Y007654D03* +X016845Y007664D03* +X016865Y008944D03* +X015745Y009364D03* +X014365Y007184D03* +X014365Y007174D03* +X018435Y007654D03* +X018445Y007664D03* +X018455Y007684D03* +X018425Y008944D03* +X018415Y008954D03* +X019215Y011714D03* +X019405Y012734D03* +X019315Y013224D03* +X019325Y013234D03* +X015165Y013624D03* +X013335Y012044D03* +X012375Y012284D03* +X012355Y012254D03* +X012345Y012234D03* +X012335Y012224D03* +X012335Y012214D03* +X012325Y012204D03* +X012315Y012194D03* +X012315Y012184D03* +X012305Y012174D03* +X012295Y012154D03* +X012285Y012144D03* +X012285Y012134D03* +X012275Y012124D03* +X012265Y012104D03* +X012245Y012074D03* +D40* +X012935Y012524D03* +X017645Y007184D03* +D41* +X017645Y007194D03* +X012925Y012494D03* +D42* +X013330Y012054D03* +X012300Y012164D03* +X012270Y012114D03* +X012260Y012094D03* +X012250Y012084D03* +X012240Y012064D03* +X012230Y012054D03* +X012230Y012044D03* +X012220Y012034D03* +X012210Y012024D03* +X012210Y012014D03* +X012200Y012004D03* +X012190Y011984D03* +X012180Y011974D03* +X012170Y011954D03* +X012160Y011934D03* +X012140Y011904D03* +X015160Y013604D03* +X015160Y013614D03* +X019250Y012224D03* +X019260Y012234D03* +X019220Y011704D03* +X018400Y008974D03* +X018410Y008964D03* +X018430Y007644D03* +X018420Y007634D03* +X016870Y007634D03* +X016860Y007644D03* +X016870Y008954D03* +X016880Y008964D03* +X016890Y008974D03* +X015740Y009344D03* +X015740Y009354D03* +X014370Y007204D03* +X014370Y007194D03* +X004870Y010364D03* +D43* +X012905Y012434D03* +X017645Y007204D03* +X020235Y011224D03* +D44* +X017645Y007214D03* +X012895Y012404D03* +D45* +X013325Y012064D03* +X012195Y011994D03* +X012175Y011964D03* +X012165Y011944D03* +X012155Y011924D03* +X012145Y011914D03* +X012135Y011894D03* +X012125Y011884D03* +X012125Y011874D03* +X012115Y011864D03* +X012105Y011854D03* +X012105Y011844D03* +X012095Y011834D03* +X012085Y011814D03* +X012065Y011784D03* +X012035Y011734D03* +X015735Y009334D03* +X015735Y009324D03* +X016895Y008984D03* +X016905Y008994D03* +X016875Y007624D03* +X016885Y007614D03* +X018415Y007624D03* +X014375Y007214D03* +X019225Y011694D03* +X019335Y013244D03* +X004865Y010354D03* +D46* +X012885Y012374D03* +X017645Y007224D03* +D47* +X016900Y007594D03* +X016890Y007604D03* +X018390Y007594D03* +X018400Y007604D03* +X018410Y007614D03* +X018390Y008984D03* +X018380Y008994D03* +X017650Y009544D03* +X015730Y009314D03* +X014380Y007234D03* +X014380Y007224D03* +X011960Y011614D03* +X011980Y011644D03* +X011990Y011664D03* +X012000Y011674D03* +X012010Y011694D03* +X012020Y011704D03* +X012020Y011714D03* +X012030Y011724D03* +X012040Y011744D03* +X012050Y011754D03* +X012050Y011764D03* +X012060Y011774D03* +X012070Y011794D03* +X012080Y011804D03* +X012090Y011824D03* +X013320Y012074D03* +X015160Y013584D03* +X015160Y013594D03* +X019430Y012724D03* +X004870Y010384D03* +X004870Y010374D03* +X004860Y010344D03* +D48* +X017645Y007234D03* +D49* +X017645Y007244D03* +D50* +X016915Y007574D03* +X016905Y007584D03* +X018375Y007574D03* +X018385Y007584D03* +X018375Y009004D03* +X018365Y009014D03* +X016925Y009014D03* +X016915Y009004D03* +X015725Y009294D03* +X015725Y009304D03* +X014385Y007244D03* +X011855Y011444D03* +X011875Y011474D03* +X011885Y011494D03* +X011895Y011504D03* +X011905Y011524D03* +X011915Y011534D03* +X011915Y011544D03* +X011925Y011554D03* +X011925Y011564D03* +X011935Y011574D03* +X011945Y011584D03* +X011945Y011594D03* +X011955Y011604D03* +X011965Y011624D03* +X011975Y011634D03* +X011985Y011654D03* +X012005Y011684D03* +X013315Y012084D03* +X012655Y012764D03* +X012645Y012774D03* +X012635Y012784D03* +X012625Y012794D03* +X012615Y012804D03* +X015165Y013574D03* +X019275Y012244D03* +X019235Y011684D03* +X004865Y010334D03* +D51* +X017645Y007254D03* +X020175Y013634D03* +D52* +X019350Y013254D03* +X019290Y012254D03* +X019250Y011674D03* +X016930Y009024D03* +X015720Y009274D03* +X015720Y009284D03* +X016920Y007564D03* +X016930Y007554D03* +X014390Y007264D03* +X014390Y007254D03* +X011770Y011304D03* +X011780Y011324D03* +X011790Y011334D03* +X011800Y011354D03* +X011810Y011374D03* +X011820Y011384D03* +X011830Y011404D03* +X011840Y011414D03* +X011840Y011424D03* +X011850Y011434D03* +X011860Y011454D03* +X011870Y011464D03* +X011880Y011484D03* +X011900Y011514D03* +X013310Y012094D03* +X012690Y012724D03* +X012680Y012734D03* +X012670Y012744D03* +X012660Y012754D03* +X012610Y012814D03* +X012600Y012824D03* +X012590Y012834D03* +X012580Y012844D03* +X012570Y012854D03* +X012560Y012864D03* +X012550Y012874D03* +X012540Y012884D03* +X012530Y012894D03* +X012520Y012904D03* +X012510Y012914D03* +X012500Y012924D03* +X015160Y013554D03* +X015160Y013564D03* +X004870Y010404D03* +X004870Y010394D03* +D53* +X017645Y009314D03* +X017645Y007264D03* +X020165Y013624D03* +D54* +X020145Y013604D03* +X020145Y011264D03* +X017645Y009294D03* +X017645Y007274D03* +D55* +X018355Y007554D03* +X018365Y007564D03* +X018355Y009024D03* +X018345Y009034D03* +X016945Y009034D03* +X015715Y009264D03* +X014395Y007284D03* +X014395Y007274D03* +X011715Y011214D03* +X011725Y011234D03* +X011735Y011244D03* +X011735Y011254D03* +X011745Y011264D03* +X011755Y011274D03* +X011755Y011284D03* +X011765Y011294D03* +X011775Y011314D03* +X011795Y011344D03* +X011805Y011364D03* +X011825Y011394D03* +X013305Y012104D03* +X012495Y012934D03* +X012485Y012944D03* +X012475Y012954D03* +X012465Y012964D03* +X012455Y012974D03* +X012445Y012984D03* +X012435Y012994D03* +X012425Y013004D03* +X012415Y013014D03* +X012405Y013024D03* +X012395Y013034D03* +X012385Y013044D03* +X012375Y013054D03* +X012375Y013064D03* +X012365Y013074D03* +X012355Y013084D03* +X012345Y013094D03* +X012335Y013104D03* +X012325Y013114D03* +X012315Y013124D03* +X012305Y013134D03* +X012295Y013144D03* +X012285Y013154D03* +X012275Y013164D03* +X012265Y013174D03* +X004865Y010324D03* +X004865Y010314D03* +D56* +X017645Y009284D03* +X017645Y007284D03* +X020135Y011274D03* +X020135Y013594D03* +D57* +X020125Y013584D03* +X020125Y012564D03* +X017645Y009274D03* +X017645Y007294D03* +D58* +X017645Y007304D03* +X017645Y009264D03* +X020115Y012504D03* +X020115Y012574D03* +D59* +X019375Y013264D03* +X019265Y011664D03* +X018325Y009054D03* +X016965Y009054D03* +X015705Y009224D03* +X015705Y009234D03* +X014405Y007314D03* +X014405Y007304D03* +X018335Y007534D03* +X018345Y007544D03* +X013295Y012124D03* +X012695Y012704D03* +X012145Y013304D03* +X012135Y013314D03* +X012125Y013324D03* +X012115Y013334D03* +X012105Y013344D03* +X012095Y013354D03* +X012035Y013424D03* +X012025Y013434D03* +X012015Y013444D03* +X012005Y013454D03* +X011995Y013464D03* +X011985Y013474D03* +X011975Y013484D03* +X011965Y013494D03* +X011955Y013504D03* +X011945Y013514D03* +X011935Y013524D03* +X011925Y013534D03* +X011915Y013544D03* +X011805Y013664D03* +X004865Y010304D03* +D60* +X017645Y009254D03* +X017645Y007314D03* +X020105Y013554D03* +D61* +X020095Y013544D03* +X020095Y012604D03* +X017645Y009244D03* +X017645Y007324D03* +X015155Y012174D03* +X015155Y012184D03* +D62* +X015160Y013504D03* +X015160Y013514D03* +X013290Y012134D03* +X011910Y013554D03* +X011900Y013564D03* +X011890Y013574D03* +X011880Y013584D03* +X011870Y013594D03* +X011860Y013604D03* +X011850Y013614D03* +X011840Y013624D03* +X011830Y013634D03* +X011820Y013644D03* +X011810Y013654D03* +X011800Y013674D03* +X011790Y013684D03* +X011780Y013694D03* +X015700Y009214D03* +X016980Y009064D03* +X018310Y009064D03* +X016960Y007524D03* +X014410Y007334D03* +X014410Y007324D03* +X019310Y012264D03* +X004870Y010434D03* +X004870Y010424D03* +D63* +X012665Y008554D03* +X012665Y008544D03* +X012665Y008534D03* +X012665Y008524D03* +X012665Y008514D03* +X012665Y008504D03* +X012665Y008494D03* +X012665Y008484D03* +X012665Y008474D03* +X012665Y008464D03* +X012665Y008454D03* +X012665Y008444D03* +X012665Y008434D03* +X012665Y008424D03* +X012665Y008414D03* +X012665Y008404D03* +X012665Y008394D03* +X012665Y008384D03* +X012665Y008374D03* +X012665Y008364D03* +X012665Y008354D03* +X012665Y008344D03* +X012665Y008334D03* +X012665Y008324D03* +X012665Y008314D03* +X012665Y008304D03* +X012665Y008294D03* +X012665Y008284D03* +X012665Y008274D03* +X012665Y008264D03* +X012665Y008254D03* +X012665Y008244D03* +X012665Y008234D03* +X012665Y008224D03* +X012665Y008214D03* +X012665Y008204D03* +X012665Y008194D03* +X012665Y008184D03* +X012665Y008174D03* +X012665Y008164D03* +X012665Y008154D03* +X012665Y008144D03* +X017645Y007334D03* +X017645Y009234D03* +X020085Y011324D03* +X020085Y012474D03* +X020085Y013524D03* +X017585Y012724D03* +X017585Y012714D03* +X017585Y012704D03* +X017585Y012694D03* +X017585Y012684D03* +X017585Y012674D03* +X017585Y012664D03* +X017585Y012654D03* +X017585Y012644D03* +X017585Y012634D03* +X017585Y012624D03* +X017585Y012614D03* +X017585Y012604D03* +X017585Y012594D03* +X017585Y012584D03* +X017585Y012574D03* +X017585Y012564D03* +X017585Y012554D03* +X017585Y012544D03* +X017585Y012534D03* +X017585Y012524D03* +X017585Y012514D03* +X017585Y012504D03* +X017585Y012494D03* +X017585Y012484D03* +X017585Y012474D03* +X017585Y012464D03* +X017585Y012454D03* +X017585Y012444D03* +X017585Y012434D03* +X017585Y012424D03* +X017585Y012414D03* +X017585Y012404D03* +X017585Y012394D03* +X017585Y012384D03* +X017585Y012374D03* +X017585Y012364D03* +X017585Y012354D03* +X017585Y012344D03* +X017585Y012334D03* +X017585Y012324D03* +X017585Y012314D03* +X017585Y012304D03* +X015155Y012154D03* +D64* +X015155Y012134D03* +X015155Y012124D03* +X017645Y009224D03* +X017645Y007344D03* +X020075Y011334D03* +X020075Y012464D03* +X020075Y012634D03* +X020075Y013504D03* +D65* +X019285Y011654D03* +X015695Y009204D03* +X015695Y009194D03* +X016975Y007514D03* +X018315Y007514D03* +X018325Y007524D03* +X014415Y007344D03* +X013285Y012144D03* +X012695Y012694D03* +X004865Y010294D03* +X004865Y010284D03* +D66* +X015155Y012104D03* +X017645Y009214D03* +X017645Y007354D03* +X020065Y011344D03* +X020065Y012454D03* +X020065Y012654D03* +X020065Y013484D03* +D67* +X015160Y013484D03* +X015160Y013474D03* +X015160Y013494D03* +X013280Y012154D03* +X012690Y012684D03* +X015690Y009184D03* +X016990Y009074D03* +X017650Y009534D03* +X018300Y009074D03* +X018300Y007504D03* +X016990Y007504D03* +X014420Y007364D03* +X014420Y007354D03* +X004870Y010444D03* +X004870Y010454D03* +D68* +X015155Y012074D03* +X015155Y012084D03* +X017645Y009204D03* +X017645Y007374D03* +X017645Y007364D03* +X020055Y011364D03* +X020055Y012444D03* +X020055Y012674D03* +X020055Y013464D03* +D69* +X015685Y009174D03* +X015685Y009164D03* +X017005Y009084D03* +X018285Y009084D03* +X014425Y007374D03* +X013275Y012164D03* +X013275Y012174D03* +X012695Y012674D03* +X004865Y010274D03* +X004865Y010264D03* +D70* +X015155Y012054D03* +X017645Y009194D03* +X017645Y007384D03* +X020045Y012424D03* +X020045Y012704D03* +X020045Y013434D03* +D71* +X015160Y013454D03* +X015160Y013464D03* +X013270Y012184D03* +X015680Y009154D03* +X015680Y009144D03* +X017020Y009094D03* +X017000Y007494D03* +X018290Y007494D03* +X014430Y007394D03* +X014430Y007384D03* +X004870Y010464D03* +D72* +X015155Y012024D03* +X017645Y009174D03* +X017645Y007394D03* +X020035Y011394D03* +X020035Y012414D03* +X020035Y013404D03* +X020035Y013414D03* +D73* +X020025Y013384D03* +X020025Y013374D03* +X020025Y012394D03* +X020025Y011414D03* +X017645Y009164D03* +X017645Y007404D03* +X015155Y012004D03* +D74* +X013265Y012194D03* +X012695Y012664D03* +X015675Y009134D03* +X014435Y007414D03* +X014435Y007404D03* +X017015Y007484D03* +X018275Y007484D03* +X018265Y009094D03* +X019345Y012274D03* +X004865Y010254D03* +D75* +X015155Y011954D03* +X017645Y009144D03* +X017645Y007424D03* +X020005Y011454D03* +X020005Y011464D03* +X020005Y012354D03* +X020005Y013294D03* +X020005Y013304D03* +D76* +X019310Y011644D03* +X015670Y009124D03* +X015670Y009114D03* +X014440Y007424D03* +X013260Y012204D03* +X015160Y013424D03* +X015160Y013434D03* +X015160Y013444D03* +X004870Y010484D03* +X004870Y010474D03* +D77* +X015155Y011924D03* +X019995Y012334D03* +X019995Y011484D03* +X017645Y007434D03* +D78* +X018185Y007444D03* +X017655Y009514D03* +X015615Y008944D03* +X015615Y008934D03* +X014495Y007604D03* +X014495Y007594D03* +X013205Y012314D03* +X015155Y013294D03* +X004865Y010154D03* +D79* +X004870Y010164D03* +X004870Y010564D03* +X013210Y012304D03* +X015160Y013304D03* +X015160Y013314D03* +X015620Y008954D03* +X014490Y007584D03* +X017100Y007444D03* +D80* +X017070Y007454D03* +X015640Y009014D03* +X015640Y009024D03* +X014470Y007524D03* +X014470Y007514D03* +X013230Y012264D03* +X012700Y012624D03* +X015160Y013354D03* +X015160Y013364D03* +X004870Y010534D03* +X004870Y010524D03* +D81* +X004870Y010494D03* +X012700Y012644D03* +X013250Y012224D03* +X015160Y013404D03* +X015160Y013414D03* +X015660Y009084D03* +X014450Y007454D03* +X018250Y009104D03* +D82* +X018230Y009114D03* +X017060Y009114D03* +X015650Y009054D03* +X014460Y007494D03* +X014460Y007484D03* +X018240Y007464D03* +X013240Y012244D03* +X012700Y012634D03* +X015160Y013374D03* +X015160Y013384D03* +X015160Y013394D03* +X004870Y010514D03* +X004870Y010214D03* +D83* +X004865Y010224D03* +X004865Y010504D03* +X013245Y012234D03* +X015655Y009074D03* +X015655Y009064D03* +X014455Y007474D03* +X014455Y007464D03* +X017045Y007464D03* +X019425Y013274D03* +D84* +X017655Y009524D03* +X015645Y009044D03* +X015645Y009034D03* +X014465Y007504D03* +X013235Y012254D03* +X004865Y010204D03* +D85* +X004870Y010544D03* +X013220Y012284D03* +X015160Y013324D03* +X015160Y013334D03* +X015630Y008994D03* +X015630Y008984D03* +X014480Y007554D03* +D86* +X014485Y007564D03* +X014485Y007574D03* +X015625Y008964D03* +X015625Y008974D03* +X013215Y012294D03* +X019405Y012284D03* +X004865Y010554D03* +X004865Y010174D03* +D87* +X004870Y010574D03* +X004870Y010584D03* +X013200Y012324D03* +X015160Y013274D03* +X015160Y013284D03* +X015610Y008924D03* +X015610Y008914D03* +X014500Y007624D03* +X014500Y007614D03* +D88* +X014510Y007644D03* +X014510Y007654D03* +X015600Y008884D03* +X015600Y008894D03* +X013190Y012344D03* +X015160Y013254D03* +X015160Y013264D03* +X004870Y010594D03* +X004870Y010134D03* +D89* +X004865Y010124D03* +X004865Y010604D03* +X013185Y012354D03* +X015155Y013244D03* +X015595Y008874D03* +X014515Y007664D03* +D90* +X014520Y007674D03* +X014520Y007684D03* +X015590Y008854D03* +X015590Y008864D03* +X013180Y012364D03* +X015160Y013224D03* +X015160Y013234D03* +X004870Y010614D03* +X004870Y010114D03* +D91* +X004865Y010104D03* +X014525Y007704D03* +X014525Y007694D03* +X015585Y008834D03* +X015585Y008844D03* +X017655Y009504D03* +D92* +X015580Y008824D03* +X014530Y007714D03* +X019410Y011634D03* +X015160Y013204D03* +X015160Y013214D03* +X004870Y010634D03* +X004870Y010624D03* +D93* +X004870Y010644D03* +X004870Y010084D03* +X014540Y007754D03* +X014540Y007744D03* +X015570Y008784D03* +X015570Y008794D03* +X015160Y013174D03* +X015160Y013184D03* +D94* +X015155Y013164D03* +X015565Y008774D03* +X014545Y007764D03* +X004865Y010074D03* +X004865Y010654D03* +D95* +X004870Y010664D03* +X014550Y007784D03* +X014550Y007774D03* +X015560Y008754D03* +X015560Y008764D03* +X015160Y013154D03* +D96* +X015155Y013144D03* +X017655Y009494D03* +X015555Y008744D03* +X014555Y007794D03* +X004865Y010064D03* +D97* +X004870Y010054D03* +X004870Y010674D03* +X004870Y010684D03* +X014560Y007814D03* +X014560Y007804D03* +X015550Y008724D03* +X015550Y008734D03* +X015160Y013124D03* +X015160Y013134D03* +D98* +X015160Y013104D03* +X015540Y008694D03* +X014570Y007844D03* +X004870Y010034D03* +X004870Y010694D03* +D99* +X004865Y010704D03* +X004865Y010024D03* +X014575Y007864D03* +X014575Y007854D03* +X015535Y008674D03* +X015535Y008684D03* +X015155Y013094D03* +D100* +X017650Y009484D03* +X015530Y008664D03* +X015530Y008654D03* +X014580Y007874D03* +X004870Y010714D03* +D101* +X004865Y010014D03* +X014585Y007894D03* +X014585Y007884D03* +X015525Y008644D03* +D102* +X014305Y008644D03* +X014305Y008634D03* +X014305Y008624D03* +X014305Y008614D03* +X014305Y008604D03* +X014305Y008594D03* +X014305Y008584D03* +X014305Y008574D03* +X014305Y008564D03* +X014305Y008554D03* +X014305Y008544D03* +X014305Y008534D03* +X014305Y008524D03* +X014305Y008514D03* +X014305Y008504D03* +X014305Y008494D03* +X014305Y008484D03* +X014305Y008474D03* +X014305Y008464D03* +X014305Y008454D03* +X014305Y008444D03* +X014305Y008434D03* +X014305Y008424D03* +X014305Y008414D03* +X014305Y008404D03* +X014305Y008394D03* +X014305Y008384D03* +X014305Y008374D03* +X014305Y008364D03* +X014305Y008354D03* +X014305Y008344D03* +X014305Y008334D03* +X014305Y008324D03* +X014305Y008314D03* +X014305Y008304D03* +X014305Y008294D03* +X014305Y008284D03* +X014305Y008274D03* +X014305Y008264D03* +X014305Y008254D03* +X014305Y008244D03* +X014305Y008234D03* +X014305Y008224D03* +X014305Y008214D03* +X014305Y008204D03* +X014305Y008194D03* +X014305Y008184D03* +X014305Y008174D03* +X014305Y008164D03* +X014305Y008154D03* +X014305Y008144D03* +X014305Y008134D03* +X014305Y008124D03* +X014305Y008114D03* +X014305Y008104D03* +X014305Y008094D03* +X014305Y008084D03* +X014305Y008074D03* +X014305Y008064D03* +X014305Y008054D03* +X014305Y008044D03* +X014305Y008034D03* +X014305Y008024D03* +X014305Y008014D03* +X014305Y008004D03* +X014305Y007994D03* +X014305Y007984D03* +X014305Y007974D03* +X014305Y007964D03* +X014305Y007954D03* +X014305Y007944D03* +X014305Y007934D03* +X014305Y007924D03* +X014305Y007914D03* +X014305Y007904D03* +X014305Y008654D03* +X014305Y008664D03* +X014305Y008674D03* +X014305Y008684D03* +X014305Y008694D03* +X014305Y008704D03* +X014305Y008714D03* +X014305Y008724D03* +X014305Y008734D03* +X014305Y008744D03* +X014305Y008754D03* +X014305Y008764D03* +X014305Y008774D03* +X014305Y008784D03* +X014305Y008794D03* +X014305Y008804D03* +X014305Y008814D03* +X014305Y008824D03* +X014305Y008834D03* +X014305Y008844D03* +X014305Y008854D03* +X014305Y008864D03* +X014305Y008874D03* +X014305Y008884D03* +X014305Y008894D03* +X014305Y008904D03* +X014305Y008914D03* +X014305Y008924D03* +X014305Y008934D03* +X014305Y008944D03* +X014305Y008954D03* +X014305Y008964D03* +X014305Y008974D03* +X014305Y008984D03* +X014305Y008994D03* +X014305Y009004D03* +X014305Y009014D03* +X014305Y009024D03* +X014305Y009034D03* +X014305Y009044D03* +X014305Y009054D03* +X014305Y009064D03* +X014305Y009074D03* +X014305Y009084D03* +X014305Y009094D03* +X014305Y009104D03* +X014305Y009114D03* +X014305Y009124D03* +X014305Y009134D03* +X014305Y009144D03* +X014305Y009154D03* +X014305Y009164D03* +X014305Y009174D03* +X014305Y009184D03* +X014305Y009194D03* +X014305Y009204D03* +X014305Y009214D03* +X014305Y009224D03* +X014305Y009234D03* +X014305Y009244D03* +X014305Y009254D03* +X014305Y009264D03* +X014305Y009274D03* +X014305Y009284D03* +X014305Y009294D03* +X014305Y009304D03* +X014305Y009314D03* +X014305Y009324D03* +X014305Y009334D03* +X014305Y009344D03* +X014305Y009354D03* +X014305Y009364D03* +X014305Y009374D03* +X014305Y009384D03* +X014305Y009394D03* +X014305Y009404D03* +X014305Y009414D03* +X014305Y009424D03* +X014305Y009434D03* +X014305Y009444D03* +X014305Y009454D03* +X014305Y009464D03* +X014305Y009474D03* +X014305Y009484D03* +X014305Y009494D03* +X014305Y009504D03* +X014305Y009514D03* +X014305Y009524D03* +D103* +X015150Y011864D03* +X019970Y011564D03* +X020060Y009524D03* +X020060Y009514D03* +X020060Y009504D03* +X020060Y009494D03* +X020060Y009484D03* +X020060Y009474D03* +X020060Y009464D03* +X020060Y009454D03* +X020060Y009444D03* +X020060Y009434D03* +X020060Y009424D03* +X020060Y009414D03* +X020060Y009404D03* +X020060Y009394D03* +X020060Y009384D03* +X020060Y009374D03* +X020060Y009364D03* +X020060Y009354D03* +X020060Y009344D03* +X020060Y009334D03* +X020060Y009324D03* +X020060Y009314D03* +X020060Y009304D03* +X020060Y009294D03* +X020060Y009284D03* +X020060Y009274D03* +X020060Y009264D03* +X020060Y009254D03* +X020060Y009244D03* +X020060Y009234D03* +X020060Y009224D03* +X020060Y009214D03* +X020060Y009204D03* +X020060Y009194D03* +X020060Y009184D03* +X020060Y009174D03* +X020060Y009164D03* +X020060Y009154D03* +X020060Y009144D03* +X020060Y009134D03* +X020060Y009124D03* +X020060Y009114D03* +D104* +X017650Y009184D03* +X020040Y011384D03* +X017540Y013274D03* +X017540Y013284D03* +X017540Y013294D03* +X017540Y013304D03* +X017540Y013314D03* +X017540Y013324D03* +X017540Y013334D03* +X017540Y013344D03* +X017540Y013354D03* +X017540Y013364D03* +X017540Y013374D03* +X017540Y013384D03* +X017540Y013394D03* +X017540Y013404D03* +X017540Y013414D03* +X017540Y013424D03* +X017540Y013434D03* +X017540Y013444D03* +X017540Y013454D03* +X017540Y013464D03* +X017540Y013474D03* +X017540Y013484D03* +X017540Y013494D03* +X017540Y013504D03* +X017540Y013514D03* +X017540Y013524D03* +X017540Y013534D03* +X017540Y013544D03* +X017540Y013554D03* +X017540Y013564D03* +X017540Y013574D03* +X017540Y013584D03* +X017540Y013594D03* +X017540Y013604D03* +X017540Y013614D03* +X017540Y013624D03* +X017540Y013634D03* +X017540Y013644D03* +X017540Y013654D03* +X017540Y013664D03* +X017540Y013674D03* +X017540Y013684D03* +X017540Y013694D03* +X020040Y013424D03* +X015150Y012044D03* +X015150Y012034D03* +X012620Y009524D03* +X012620Y009514D03* +X012620Y009504D03* +X012620Y009494D03* +X012620Y009484D03* +X012620Y009474D03* +X012620Y009464D03* +X012620Y009454D03* +X012620Y009444D03* +X012620Y009434D03* +X012620Y009424D03* +X012620Y009414D03* +X012620Y009404D03* +X012620Y009394D03* +X012620Y009384D03* +X012620Y009374D03* +X012620Y009364D03* +X012620Y009354D03* +X012620Y009344D03* +X012620Y009334D03* +X012620Y009324D03* +X012620Y009314D03* +X012620Y009304D03* +X012620Y009294D03* +X012620Y009284D03* +X012620Y009274D03* +X012620Y009264D03* +X012620Y009254D03* +X012620Y009244D03* +X012620Y009234D03* +X012620Y009224D03* +X012620Y009214D03* +X012620Y009204D03* +X012620Y009194D03* +X012620Y009184D03* +X012620Y009174D03* +X012620Y009164D03* +X012620Y009154D03* +X012620Y009144D03* +X012620Y009134D03* +X012620Y009124D03* +X012620Y009114D03* +D105* +X017645Y009304D03* +D106* +X017650Y009324D03* +X020180Y011244D03* +D107* +X017650Y009334D03* +X020190Y013644D03* +D108* +X020205Y013654D03* +X020205Y011234D03* +X017645Y009344D03* +D109* +X017650Y009354D03* +X012890Y012384D03* +X012890Y012394D03* +X020220Y013664D03* +D110* +X017650Y009364D03* +X012900Y012414D03* +X012900Y012424D03* +D111* +X012920Y012474D03* +X012920Y012484D03* +X017650Y009374D03* +D112* +X017650Y009384D03* +X012930Y012504D03* +X012930Y012514D03* +D113* +X012945Y012554D03* +X012945Y012564D03* +X017655Y009394D03* +D114* +X017650Y009404D03* +X012960Y012604D03* +D115* +X017650Y009414D03* +D116* +X017655Y009424D03* +D117* +X017650Y009434D03* +D118* +X017650Y009444D03* +D119* +X017650Y009454D03* +D120* +X017655Y009464D03* +D121* +X017655Y009474D03* +D122* +X015795Y009524D03* +X015565Y012704D03* +X015555Y012734D03* +X015545Y012754D03* +X015535Y012784D03* +X015525Y012814D03* +X015515Y012834D03* +X015515Y012844D03* +X015505Y012864D03* +X015495Y012884D03* +X015495Y012894D03* +X015485Y012914D03* +X015485Y012924D03* +X015475Y012934D03* +X015475Y012944D03* +X015475Y012954D03* +X015465Y012964D03* +X015465Y012974D03* +X015455Y012984D03* +X015455Y012994D03* +X015455Y013004D03* +X015445Y013014D03* +X015445Y013024D03* +X015445Y013034D03* +X015435Y013044D03* +X015435Y013054D03* +X015425Y013074D03* +X015425Y013084D03* +X019255Y013084D03* +X019255Y013074D03* +X019255Y013064D03* +X019255Y013054D03* +X019255Y013044D03* +X019275Y012884D03* +X019285Y012864D03* +X019295Y012844D03* +X019295Y012834D03* +X019305Y012824D03* +X004575Y010754D03* +X004545Y010804D03* +X004535Y010824D03* +X004525Y010834D03* +X004525Y010844D03* +X004515Y010854D03* +X004505Y010874D03* +X004495Y010884D03* +X004495Y010894D03* +X004485Y010904D03* +X004485Y010914D03* +X004475Y010924D03* +X004465Y010944D03* +X004455Y010954D03* +X004455Y010964D03* +X004445Y010974D03* +X004435Y010994D03* +X004425Y011014D03* +X004395Y011064D03* +D123* +X017650Y009564D03* +D124* +X017650Y009574D03* +D125* +X009690Y010104D03* +X009690Y010114D03* +X009690Y010124D03* +X009690Y010134D03* +X009690Y010144D03* +X009690Y010154D03* +X009690Y010164D03* +X009690Y010174D03* +X009690Y010184D03* +X009690Y010194D03* +X009690Y010204D03* +X009690Y010214D03* +X009690Y010224D03* +X009690Y010234D03* +X009690Y010244D03* +X009690Y010254D03* +X009690Y010264D03* +X009690Y010274D03* +X009690Y010284D03* +X009690Y010294D03* +X009690Y010304D03* +X009690Y010314D03* +X009690Y010324D03* +X009690Y010334D03* +X009690Y010344D03* +X009690Y010354D03* +X009690Y010364D03* +X009690Y010374D03* +X009690Y010384D03* +X009690Y010394D03* +X009690Y010404D03* +X009690Y010414D03* +X009690Y010424D03* +X009690Y010434D03* +X009690Y010444D03* +X009690Y010454D03* +X009690Y010464D03* +X009690Y010474D03* +X009690Y010484D03* +X009690Y010494D03* +X009690Y010504D03* +X009690Y010514D03* +X009690Y010524D03* +D126* +X004590Y010724D03* +X004590Y010734D03* +X004580Y010744D03* +X004570Y010764D03* +X004560Y010774D03* +X004560Y010784D03* +X004550Y010794D03* +X004540Y010814D03* +X004510Y010864D03* +X015430Y013064D03* +X019250Y013034D03* +X019250Y013024D03* +X019250Y013014D03* +X019250Y013004D03* +X019250Y012994D03* +X019250Y012984D03* +X019250Y012974D03* +X019250Y012964D03* +X019260Y012954D03* +X019260Y012944D03* +X019260Y012934D03* +X019260Y012924D03* +X019260Y012914D03* +X019270Y012904D03* +X019270Y012894D03* +X019280Y012874D03* +X019290Y012854D03* +D127* +X020320Y011214D03* +D128* +X020160Y011254D03* +D129* +X020120Y011284D03* +X020120Y013574D03* +D130* +X020110Y013564D03* +X020110Y012584D03* +X020110Y011294D03* +D131* +X020100Y011304D03* +X020100Y012494D03* +X020100Y012594D03* +X015150Y012194D03* +D132* +X015150Y012164D03* +X020090Y012484D03* +X020090Y012614D03* +X020090Y013534D03* +X020090Y011314D03* +D133* +X020060Y011354D03* +X020060Y012664D03* +X020060Y013474D03* +X015150Y012094D03* +D134* +X015150Y012064D03* +X020050Y012434D03* +X020050Y012684D03* +X020050Y012694D03* +X020050Y013444D03* +X020050Y013454D03* +X020050Y011374D03* +D135* +X020030Y011404D03* +X020030Y012404D03* +X020030Y013394D03* +X015150Y012014D03* +D136* +X015150Y011994D03* +X015150Y011984D03* +X020020Y011424D03* +X020020Y013354D03* +X020020Y013364D03* +D137* +X020010Y013324D03* +X020010Y013314D03* +X020010Y012374D03* +X020010Y012364D03* +X020010Y011444D03* +X015150Y011964D03* +D138* +X015150Y011944D03* +X015150Y011934D03* +X020000Y012344D03* +X020000Y011474D03* +X020000Y013284D03* +D139* +X019990Y012324D03* +X019990Y011504D03* +X019990Y011494D03* +X015150Y011914D03* +D140* +X015155Y011904D03* +X019985Y011514D03* +X019985Y012314D03* +D141* +X019980Y012304D03* +X019980Y011534D03* +X019980Y011524D03* +X015150Y011884D03* +X015150Y011894D03* +D142* +X015155Y011874D03* +X019975Y011554D03* +X019975Y011544D03* +X019975Y012294D03* +D143* +X019965Y011584D03* +X019965Y011574D03* +X015155Y011854D03* +D144* +X015150Y011844D03* +X015150Y011834D03* +X019960Y011604D03* +X019960Y011594D03* +D145* +X019955Y011614D03* +X019955Y011624D03* +X015155Y011824D03* +D146* +X015155Y011774D03* +D147* +X015150Y011784D03* +X015150Y011794D03* +D148* +X015150Y011804D03* +X015150Y011814D03* +D149* +X015150Y012114D03* +X020070Y012644D03* +X020070Y013494D03* +D150* +X020080Y013514D03* +X020080Y012624D03* +X015150Y012144D03* +D151* +X012910Y012444D03* +X012910Y012454D03* +D152* +X012915Y012464D03* +X020245Y013674D03* +D153* +X020130Y012554D03* +X020130Y012514D03* +D154* +X020140Y012524D03* +X020140Y012544D03* +D155* +X020150Y012534D03* +X020150Y013614D03* +D156* +X012940Y012544D03* +X012940Y012534D03* +D157* +X012950Y012574D03* +X020280Y013684D03* +D158* +X009595Y013684D03* +X009595Y013674D03* +X009595Y013664D03* +X009595Y013654D03* +X009595Y013644D03* +X009595Y013634D03* +X009595Y013624D03* +X009595Y013614D03* +X009595Y013604D03* +X009595Y013594D03* +X009595Y013584D03* +X009595Y013574D03* +X009595Y013564D03* +X009595Y013554D03* +X009595Y013544D03* +X009595Y013534D03* +X009595Y013524D03* +X009595Y013514D03* +X009595Y013504D03* +X009595Y013494D03* +X009595Y013484D03* +X009595Y013474D03* +X009595Y013464D03* +X009595Y013454D03* +X009595Y013444D03* +X009595Y013434D03* +X009595Y013424D03* +X009595Y013414D03* +X009595Y013404D03* +X009595Y013394D03* +X009595Y013384D03* +X009595Y013374D03* +X009595Y013364D03* +X009595Y013354D03* +X009595Y013344D03* +X009595Y013334D03* +X009595Y013324D03* +X009595Y013314D03* +X009595Y013304D03* +X009595Y013294D03* +X009595Y013284D03* +X009595Y013274D03* +X009595Y013264D03* +D159* +X020345Y013694D03* +D160* +X022869Y013789D02* +X022869Y007639D01* +M02* diff --git a/gerber/tests/resources/ncdrill.DRD b/gerber/tests/resources/ncdrill.DRD new file mode 100644 index 0000000..ced00ca --- /dev/null +++ b/gerber/tests/resources/ncdrill.DRD @@ -0,0 +1,51 @@ +% +M48 +M72 +T01C0.0236 +T02C0.0354 +T03C0.0400 +T04C0.1260 +T05C0.1280 +% +T01 +X9250Y4064 +X12100Y5314 +X13500Y6864 +X15650Y6264 +X15200Y4514 +X13550Y8764 +X13350Y10114 +X13300Y11464 +X11650Y13164 +X10000Y15114 +X6500Y13714 +X4150Y11564 +X14250Y14964 +X15850Y9914 +T02 +X17200Y9464 +X18200Y9964 +X18200Y10964 +X17200Y10464 +X17200Y11464 +X18200Y11964 +T03 +X18350Y16814 +X17350Y16814 +X7350Y16964 +X6350Y16964 +X5350Y16964 +X1500Y12564 +X1500Y11564 +X1500Y10564 +X1500Y9564 +X1500Y8564 +T04 +X2350Y5114 +X2300Y16064 +X20800Y16064 +X20800Y5064 +T05 +X20700Y8714 +X20700Y12714 +M30 diff --git a/gerber/tests/resources/top_copper.GTL b/gerber/tests/resources/top_copper.GTL new file mode 100644 index 0000000..cedd2fd --- /dev/null +++ b/gerber/tests/resources/top_copper.GTL @@ -0,0 +1,3457 @@ +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* diff --git a/gerber/tests/resources/top_mask.GTS b/gerber/tests/resources/top_mask.GTS new file mode 100644 index 0000000..a3886f5 --- /dev/null +++ b/gerber/tests/resources/top_mask.GTS @@ -0,0 +1,162 @@ +G75* +%MOIN*% +%OFA0B0*% +%FSLAX24Y24*% +%IPPOS*% +%LPD*% +%AMOC8* +5,1,8,0,0,1.08239X$1,22.5* +% +%ADD10R,0.0340X0.0880*% +%ADD11R,0.0671X0.0237*% +%ADD12R,0.4178X0.4332*% +%ADD13R,0.0930X0.0500*% +%ADD14R,0.0710X0.1655*% +%ADD15R,0.0671X0.0592*% +%ADD16R,0.0592X0.0671*% +%ADD17R,0.0710X0.1615*% +%ADD18R,0.1419X0.0828*% +%ADD19C,0.0634*% +%ADD20C,0.1360*% +%ADD21R,0.0474X0.0580*% +%ADD22C,0.0680*% +%ADD23R,0.0552X0.0552*% +%ADD24C,0.1340*% +%ADD25C,0.0476*% +D10* +X005000Y010604D03* +X005500Y010604D03* +X006000Y010604D03* +X006500Y010604D03* +X006500Y013024D03* +X006000Y013024D03* +X005500Y013024D03* +X005000Y013024D03* +D11* +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* +D12* +X009350Y010114D03* +D13* +X012630Y010114D03* +X012630Y010784D03* +X012630Y011454D03* +X012630Y009444D03* +X012630Y008774D03* +D14* +X010000Y013467D03* +X010000Y016262D03* +D15* +X004150Y012988D03* +X004150Y012240D03* +X009900Y005688D03* +X009900Y004940D03* +X015000Y006240D03* +X015000Y006988D03* +D16* +X014676Y008364D03* +X015424Y008364D03* +X017526Y004514D03* +X018274Y004514D03* +X010674Y004064D03* +X009926Y004064D03* +X004174Y009564D03* +X003426Y009564D03* +X005376Y014564D03* +X006124Y014564D03* +D17* +X014250Y016088D03* +X014250Y012741D03* +D18* +X014250Y010982D03* +X014250Y009447D03* +D19* +X017200Y009464D03* +X018200Y009964D03* +X018200Y010964D03* +X017200Y010464D03* +X017200Y011464D03* +X018200Y011964D03* +D20* +X020700Y012714D03* +X020700Y008714D03* +D21* +X005004Y003814D03* +X005004Y004864D03* +X005004Y005864D03* +X005004Y006914D03* +X008696Y006914D03* +X008696Y005864D03* +X008696Y004864D03* +X008696Y003814D03* +D22* +X001800Y008564D02* +X001200Y008564D01* +X001200Y009564D02* +X001800Y009564D01* +X001800Y010564D02* +X001200Y010564D01* +X001200Y011564D02* +X001800Y011564D01* +X001800Y012564D02* +X001200Y012564D01* +X005350Y016664D02* +X005350Y017264D01* +X006350Y017264D02* +X006350Y016664D01* +X007350Y016664D02* +X007350Y017264D01* +X017350Y017114D02* +X017350Y016514D01* +X018350Y016514D02* +X018350Y017114D01* +D23* +X016613Y004514D03* +X015787Y004514D03* +D24* +X020800Y005064D03* +X020800Y016064D03* +X002300Y016064D03* +X002350Y005114D03* +D25* +X009250Y004064D03* +X012100Y005314D03* +X013500Y006864D03* +X015650Y006264D03* +X015200Y004514D03* +X013550Y008764D03* +X013350Y010114D03* +X013300Y011464D03* +X011650Y013164D03* +X010000Y015114D03* +X006500Y013714D03* +X004150Y011564D03* +X014250Y014964D03* +X015850Y009914D03* +M02* diff --git a/gerber/tests/resources/top_silk.GTO b/gerber/tests/resources/top_silk.GTO new file mode 100644 index 0000000..ea46f80 --- /dev/null +++ b/gerber/tests/resources/top_silk.GTO @@ -0,0 +1,2099 @@ +G75* +%MOIN*% +%OFA0B0*% +%FSLAX24Y24*% +%IPPOS*% +%LPD*% +%AMOC8* +5,1,8,0,0,1.08239X$1,22.5* +% +%ADD10C,0.0000*% +%ADD11C,0.0060*% +%ADD12C,0.0020*% +%ADD13C,0.0050*% +%ADD14C,0.0080*% +%ADD15C,0.0040*% +%ADD16R,0.0660X0.0380*% +%ADD17C,0.0030*% +%ADD18C,0.0004*% +%ADD19R,0.0450X0.0364*% +%ADD20C,0.0025*% +%ADD21C,0.0098*% +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* +X019450Y005064D02* +X019452Y005137D01* +X019458Y005210D01* +X019468Y005282D01* +X019482Y005354D01* +X019499Y005425D01* +X019521Y005495D01* +X019546Y005564D01* +X019575Y005631D01* +X019607Y005696D01* +X019643Y005760D01* +X019683Y005822D01* +X019725Y005881D01* +X019771Y005938D01* +X019820Y005992D01* +X019872Y006044D01* +X019926Y006093D01* +X019983Y006139D01* +X020042Y006181D01* +X020104Y006221D01* +X020168Y006257D01* +X020233Y006289D01* +X020300Y006318D01* +X020369Y006343D01* +X020439Y006365D01* +X020510Y006382D01* +X020582Y006396D01* +X020654Y006406D01* +X020727Y006412D01* +X020800Y006414D01* +X020873Y006412D01* +X020946Y006406D01* +X021018Y006396D01* +X021090Y006382D01* +X021161Y006365D01* +X021231Y006343D01* +X021300Y006318D01* +X021367Y006289D01* +X021432Y006257D01* +X021496Y006221D01* +X021558Y006181D01* +X021617Y006139D01* +X021674Y006093D01* +X021728Y006044D01* +X021780Y005992D01* +X021829Y005938D01* +X021875Y005881D01* +X021917Y005822D01* +X021957Y005760D01* +X021993Y005696D01* +X022025Y005631D01* +X022054Y005564D01* +X022079Y005495D01* +X022101Y005425D01* +X022118Y005354D01* +X022132Y005282D01* +X022142Y005210D01* +X022148Y005137D01* +X022150Y005064D01* +X022148Y004991D01* +X022142Y004918D01* +X022132Y004846D01* +X022118Y004774D01* +X022101Y004703D01* +X022079Y004633D01* +X022054Y004564D01* +X022025Y004497D01* +X021993Y004432D01* +X021957Y004368D01* +X021917Y004306D01* +X021875Y004247D01* +X021829Y004190D01* +X021780Y004136D01* +X021728Y004084D01* +X021674Y004035D01* +X021617Y003989D01* +X021558Y003947D01* +X021496Y003907D01* +X021432Y003871D01* +X021367Y003839D01* +X021300Y003810D01* +X021231Y003785D01* +X021161Y003763D01* +X021090Y003746D01* +X021018Y003732D01* +X020946Y003722D01* +X020873Y003716D01* +X020800Y003714D01* +X020727Y003716D01* +X020654Y003722D01* +X020582Y003732D01* +X020510Y003746D01* +X020439Y003763D01* +X020369Y003785D01* +X020300Y003810D01* +X020233Y003839D01* +X020168Y003871D01* +X020104Y003907D01* +X020042Y003947D01* +X019983Y003989D01* +X019926Y004035D01* +X019872Y004084D01* +X019820Y004136D01* +X019771Y004190D01* +X019725Y004247D01* +X019683Y004306D01* +X019643Y004368D01* +X019607Y004432D01* +X019575Y004497D01* +X019546Y004564D01* +X019521Y004633D01* +X019499Y004703D01* +X019482Y004774D01* +X019468Y004846D01* +X019458Y004918D01* +X019452Y004991D01* +X019450Y005064D01* +X019798Y007044D02* +X019904Y007044D01* +X020011Y007151D01* +X020011Y007685D01* +X019904Y007685D02* +X020118Y007685D01* +X020335Y007471D02* +X020549Y007685D01* +X020549Y007044D01* +X020762Y007044D02* +X020335Y007044D01* +X019798Y007044D02* +X019691Y007151D01* +X019450Y016064D02* +X019452Y016137D01* +X019458Y016210D01* +X019468Y016282D01* +X019482Y016354D01* +X019499Y016425D01* +X019521Y016495D01* +X019546Y016564D01* +X019575Y016631D01* +X019607Y016696D01* +X019643Y016760D01* +X019683Y016822D01* +X019725Y016881D01* +X019771Y016938D01* +X019820Y016992D01* +X019872Y017044D01* +X019926Y017093D01* +X019983Y017139D01* +X020042Y017181D01* +X020104Y017221D01* +X020168Y017257D01* +X020233Y017289D01* +X020300Y017318D01* +X020369Y017343D01* +X020439Y017365D01* +X020510Y017382D01* +X020582Y017396D01* +X020654Y017406D01* +X020727Y017412D01* +X020800Y017414D01* +X020873Y017412D01* +X020946Y017406D01* +X021018Y017396D01* +X021090Y017382D01* +X021161Y017365D01* +X021231Y017343D01* +X021300Y017318D01* +X021367Y017289D01* +X021432Y017257D01* +X021496Y017221D01* +X021558Y017181D01* +X021617Y017139D01* +X021674Y017093D01* +X021728Y017044D01* +X021780Y016992D01* +X021829Y016938D01* +X021875Y016881D01* +X021917Y016822D01* +X021957Y016760D01* +X021993Y016696D01* +X022025Y016631D01* +X022054Y016564D01* +X022079Y016495D01* +X022101Y016425D01* +X022118Y016354D01* +X022132Y016282D01* +X022142Y016210D01* +X022148Y016137D01* +X022150Y016064D01* +X022148Y015991D01* +X022142Y015918D01* +X022132Y015846D01* +X022118Y015774D01* +X022101Y015703D01* +X022079Y015633D01* +X022054Y015564D01* +X022025Y015497D01* +X021993Y015432D01* +X021957Y015368D01* +X021917Y015306D01* +X021875Y015247D01* +X021829Y015190D01* +X021780Y015136D01* +X021728Y015084D01* +X021674Y015035D01* +X021617Y014989D01* +X021558Y014947D01* +X021496Y014907D01* +X021432Y014871D01* +X021367Y014839D01* +X021300Y014810D01* +X021231Y014785D01* +X021161Y014763D01* +X021090Y014746D01* +X021018Y014732D01* +X020946Y014722D01* +X020873Y014716D01* +X020800Y014714D01* +X020727Y014716D01* +X020654Y014722D01* +X020582Y014732D01* +X020510Y014746D01* +X020439Y014763D01* +X020369Y014785D01* +X020300Y014810D01* +X020233Y014839D01* +X020168Y014871D01* +X020104Y014907D01* +X020042Y014947D01* +X019983Y014989D01* +X019926Y015035D01* +X019872Y015084D01* +X019820Y015136D01* +X019771Y015190D01* +X019725Y015247D01* +X019683Y015306D01* +X019643Y015368D01* +X019607Y015432D01* +X019575Y015497D01* +X019546Y015564D01* +X019521Y015633D01* +X019499Y015703D01* +X019482Y015774D01* +X019468Y015846D01* +X019458Y015918D01* +X019452Y015991D01* +X019450Y016064D01* +X018850Y016564D02* +X018600Y016314D01* +X018100Y016314D01* +X017850Y016564D01* +X017600Y016314D01* +X017100Y016314D01* +X016850Y016564D01* +X016850Y017064D01* +X017100Y017314D01* +X017600Y017314D01* +X017850Y017064D01* +X018100Y017314D01* +X018600Y017314D01* +X018850Y017064D01* +X018850Y016564D01* +X017850Y016564D02* +X017850Y017064D01* +X007850Y017214D02* +X007850Y016714D01* +X007600Y016464D01* +X007100Y016464D01* +X006850Y016714D01* +X006600Y016464D01* +X006100Y016464D01* +X005850Y016714D01* +X005600Y016464D01* +X005100Y016464D01* +X004850Y016714D01* +X004850Y017214D01* +X005100Y017464D01* +X005600Y017464D01* +X005850Y017214D01* +X006100Y017464D01* +X006600Y017464D01* +X006850Y017214D01* +X007100Y017464D01* +X007600Y017464D01* +X007850Y017214D01* +X006850Y017214D02* +X006850Y016714D01* +X005850Y016714D02* +X005850Y017214D01* +X000950Y016064D02* +X000952Y016137D01* +X000958Y016210D01* +X000968Y016282D01* +X000982Y016354D01* +X000999Y016425D01* +X001021Y016495D01* +X001046Y016564D01* +X001075Y016631D01* +X001107Y016696D01* +X001143Y016760D01* +X001183Y016822D01* +X001225Y016881D01* +X001271Y016938D01* +X001320Y016992D01* +X001372Y017044D01* +X001426Y017093D01* +X001483Y017139D01* +X001542Y017181D01* +X001604Y017221D01* +X001668Y017257D01* +X001733Y017289D01* +X001800Y017318D01* +X001869Y017343D01* +X001939Y017365D01* +X002010Y017382D01* +X002082Y017396D01* +X002154Y017406D01* +X002227Y017412D01* +X002300Y017414D01* +X002373Y017412D01* +X002446Y017406D01* +X002518Y017396D01* +X002590Y017382D01* +X002661Y017365D01* +X002731Y017343D01* +X002800Y017318D01* +X002867Y017289D01* +X002932Y017257D01* +X002996Y017221D01* +X003058Y017181D01* +X003117Y017139D01* +X003174Y017093D01* +X003228Y017044D01* +X003280Y016992D01* +X003329Y016938D01* +X003375Y016881D01* +X003417Y016822D01* +X003457Y016760D01* +X003493Y016696D01* +X003525Y016631D01* +X003554Y016564D01* +X003579Y016495D01* +X003601Y016425D01* +X003618Y016354D01* +X003632Y016282D01* +X003642Y016210D01* +X003648Y016137D01* +X003650Y016064D01* +X003648Y015991D01* +X003642Y015918D01* +X003632Y015846D01* +X003618Y015774D01* +X003601Y015703D01* +X003579Y015633D01* +X003554Y015564D01* +X003525Y015497D01* +X003493Y015432D01* +X003457Y015368D01* +X003417Y015306D01* +X003375Y015247D01* +X003329Y015190D01* +X003280Y015136D01* +X003228Y015084D01* +X003174Y015035D01* +X003117Y014989D01* +X003058Y014947D01* +X002996Y014907D01* +X002932Y014871D01* +X002867Y014839D01* +X002800Y014810D01* +X002731Y014785D01* +X002661Y014763D01* +X002590Y014746D01* +X002518Y014732D01* +X002446Y014722D01* +X002373Y014716D01* +X002300Y014714D01* +X002227Y014716D01* +X002154Y014722D01* +X002082Y014732D01* +X002010Y014746D01* +X001939Y014763D01* +X001869Y014785D01* +X001800Y014810D01* +X001733Y014839D01* +X001668Y014871D01* +X001604Y014907D01* +X001542Y014947D01* +X001483Y014989D01* +X001426Y015035D01* +X001372Y015084D01* +X001320Y015136D01* +X001271Y015190D01* +X001225Y015247D01* +X001183Y015306D01* +X001143Y015368D01* +X001107Y015432D01* +X001075Y015497D01* +X001046Y015564D01* +X001021Y015633D01* +X000999Y015703D01* +X000982Y015774D01* +X000968Y015846D01* +X000958Y015918D01* +X000952Y015991D01* +X000950Y016064D01* +X001250Y013064D02* +X001000Y012814D01* +X001000Y012314D01* +X001250Y012064D01* +X001000Y011814D01* +X001000Y011314D01* +X001250Y011064D01* +X001750Y011064D01* +X002000Y011314D01* +X002000Y011814D01* +X001750Y012064D01* +X001250Y012064D01* +X001750Y012064D02* +X002000Y012314D01* +X002000Y012814D01* +X001750Y013064D01* +X001250Y013064D01* +X001250Y011064D02* +X001000Y010814D01* +X001000Y010314D01* +X001250Y010064D01* +X001000Y009814D01* +X001000Y009314D01* +X001250Y009064D01* +X001000Y008814D01* +X001000Y008314D01* +X001250Y008064D01* +X001750Y008064D01* +X002000Y008314D01* +X002000Y008814D01* +X001750Y009064D01* +X001250Y009064D01* +X001750Y009064D02* +X002000Y009314D01* +X002000Y009814D01* +X001750Y010064D01* +X001250Y010064D01* +X001750Y010064D02* +X002000Y010314D01* +X002000Y010814D01* +X001750Y011064D01* +X004750Y011194D02* +X004750Y011614D01* +X004750Y012014D01* +X004750Y012434D01* +X004752Y012457D01* +X004757Y012480D01* +X004766Y012502D01* +X004779Y012522D01* +X004794Y012540D01* +X004812Y012555D01* +X004832Y012568D01* +X004854Y012577D01* +X004877Y012582D01* +X004900Y012584D01* +X006600Y012584D01* +X006623Y012582D01* +X006646Y012577D01* +X006668Y012568D01* +X006688Y012555D01* +X006706Y012540D01* +X006721Y012522D01* +X006734Y012502D01* +X006743Y012480D01* +X006748Y012457D01* +X006750Y012434D01* +X006750Y011194D01* +X006748Y011171D01* +X006743Y011148D01* +X006734Y011126D01* +X006721Y011106D01* +X006706Y011088D01* +X006688Y011073D01* +X006668Y011060D01* +X006646Y011051D01* +X006623Y011046D01* +X006600Y011044D01* +X004900Y011044D01* +X004877Y011046D01* +X004854Y011051D01* +X004832Y011060D01* +X004812Y011073D01* +X004794Y011088D01* +X004779Y011106D01* +X004766Y011126D01* +X004757Y011148D01* +X004752Y011171D01* +X004750Y011194D01* +X004750Y011614D02* +X004777Y011616D01* +X004804Y011621D01* +X004830Y011631D01* +X004854Y011643D01* +X004876Y011659D01* +X004896Y011677D01* +X004913Y011699D01* +X004928Y011722D01* +X004938Y011747D01* +X004946Y011773D01* +X004950Y011800D01* +X004950Y011828D01* +X004946Y011855D01* +X004938Y011881D01* +X004928Y011906D01* +X004913Y011929D01* +X004896Y011951D01* +X004876Y011969D01* +X004854Y011985D01* +X004830Y011997D01* +X004804Y012007D01* +X004777Y012012D01* +X004750Y012014D01* +X001000Y005114D02* +X001002Y005187D01* +X001008Y005260D01* +X001018Y005332D01* +X001032Y005404D01* +X001049Y005475D01* +X001071Y005545D01* +X001096Y005614D01* +X001125Y005681D01* +X001157Y005746D01* +X001193Y005810D01* +X001233Y005872D01* +X001275Y005931D01* +X001321Y005988D01* +X001370Y006042D01* +X001422Y006094D01* +X001476Y006143D01* +X001533Y006189D01* +X001592Y006231D01* +X001654Y006271D01* +X001718Y006307D01* +X001783Y006339D01* +X001850Y006368D01* +X001919Y006393D01* +X001989Y006415D01* +X002060Y006432D01* +X002132Y006446D01* +X002204Y006456D01* +X002277Y006462D01* +X002350Y006464D01* +X002423Y006462D01* +X002496Y006456D01* +X002568Y006446D01* +X002640Y006432D01* +X002711Y006415D01* +X002781Y006393D01* +X002850Y006368D01* +X002917Y006339D01* +X002982Y006307D01* +X003046Y006271D01* +X003108Y006231D01* +X003167Y006189D01* +X003224Y006143D01* +X003278Y006094D01* +X003330Y006042D01* +X003379Y005988D01* +X003425Y005931D01* +X003467Y005872D01* +X003507Y005810D01* +X003543Y005746D01* +X003575Y005681D01* +X003604Y005614D01* +X003629Y005545D01* +X003651Y005475D01* +X003668Y005404D01* +X003682Y005332D01* +X003692Y005260D01* +X003698Y005187D01* +X003700Y005114D01* +X003698Y005041D01* +X003692Y004968D01* +X003682Y004896D01* +X003668Y004824D01* +X003651Y004753D01* +X003629Y004683D01* +X003604Y004614D01* +X003575Y004547D01* +X003543Y004482D01* +X003507Y004418D01* +X003467Y004356D01* +X003425Y004297D01* +X003379Y004240D01* +X003330Y004186D01* +X003278Y004134D01* +X003224Y004085D01* +X003167Y004039D01* +X003108Y003997D01* +X003046Y003957D01* +X002982Y003921D01* +X002917Y003889D01* +X002850Y003860D01* +X002781Y003835D01* +X002711Y003813D01* +X002640Y003796D01* +X002568Y003782D01* +X002496Y003772D01* +X002423Y003766D01* +X002350Y003764D01* +X002277Y003766D01* +X002204Y003772D01* +X002132Y003782D01* +X002060Y003796D01* +X001989Y003813D01* +X001919Y003835D01* +X001850Y003860D01* +X001783Y003889D01* +X001718Y003921D01* +X001654Y003957D01* +X001592Y003997D01* +X001533Y004039D01* +X001476Y004085D01* +X001422Y004134D01* +X001370Y004186D01* +X001321Y004240D01* +X001275Y004297D01* +X001233Y004356D01* +X001193Y004418D01* +X001157Y004482D01* +X001125Y004547D01* +X001096Y004614D01* +X001071Y004683D01* +X001049Y004753D01* +X001032Y004824D01* +X001018Y004896D01* +X001008Y004968D01* +X001002Y005041D01* +X001000Y005114D01* +D12* +X004750Y011184D02* +X006750Y011184D01* +D13* +X006929Y012889D02* +X007079Y012889D01* +X007154Y012964D01* +X007154Y013340D01* +X007315Y013265D02* +X007390Y013340D01* +X007540Y013340D01* +X007615Y013265D01* +X007615Y013190D01* +X007540Y013115D01* +X007615Y013039D01* +X007615Y012964D01* +X007540Y012889D01* +X007390Y012889D01* +X007315Y012964D01* +X007465Y013115D02* +X007540Y013115D01* +X006929Y012889D02* +X006854Y012964D01* +X006854Y013340D01* +X006216Y015659D02* +X005916Y016110D01* +X005756Y016110D02* +X005756Y015659D01* +X005916Y015659D02* +X006216Y016110D01* +X005756Y016110D02* +X005606Y015960D01* +X005455Y016110D01* +X005455Y015659D01* +X005295Y015734D02* +X005295Y016035D01* +X005220Y016110D01* +X004995Y016110D01* +X004995Y015659D01* +X005220Y015659D01* +X005295Y015734D01* +X002695Y012963D02* +X002695Y012812D01* +X002695Y012887D02* +X002245Y012887D01* +X002245Y012812D02* +X002245Y012963D01* +X002320Y012652D02* +X002245Y012577D01* +X002245Y012352D01* +X002695Y012352D01* +X002695Y012577D01* +X002620Y012652D01* +X002320Y012652D01* +X002245Y012195D02* +X002245Y012045D01* +X002245Y012120D02* +X002695Y012120D01* +X002695Y012045D02* +X002695Y012195D01* +X002695Y011885D02* +X002245Y011885D01* +X002395Y011735D01* +X002245Y011585D01* +X002695Y011585D01* +X016845Y017559D02* +X016845Y018010D01* +X017070Y018010D01* +X017145Y017935D01* +X017145Y017785D01* +X017070Y017709D01* +X016845Y017709D01* +X017305Y017559D02* +X017305Y018010D01* +X017606Y018010D02* +X017606Y017559D01* +X017456Y017709D01* +X017305Y017559D01* +X017766Y017559D02* +X017766Y018010D01* +X017991Y018010D01* +X018066Y017935D01* +X018066Y017785D01* +X017991Y017709D01* +X017766Y017709D01* +X017916Y017709D02* +X018066Y017559D01* +D14* +X020131Y016064D02* +X020133Y016115D01* +X020139Y016166D01* +X020149Y016216D01* +X020162Y016266D01* +X020180Y016314D01* +X020200Y016361D01* +X020225Y016406D01* +X020253Y016449D01* +X020284Y016490D01* +X020318Y016528D01* +X020355Y016563D01* +X020394Y016596D01* +X020436Y016626D01* +X020480Y016652D01* +X020526Y016674D01* +X020574Y016694D01* +X020623Y016709D01* +X020673Y016721D01* +X020723Y016729D01* +X020774Y016733D01* +X020826Y016733D01* +X020877Y016729D01* +X020927Y016721D01* +X020977Y016709D01* +X021026Y016694D01* +X021074Y016674D01* +X021120Y016652D01* +X021164Y016626D01* +X021206Y016596D01* +X021245Y016563D01* +X021282Y016528D01* +X021316Y016490D01* +X021347Y016449D01* +X021375Y016406D01* +X021400Y016361D01* +X021420Y016314D01* +X021438Y016266D01* +X021451Y016216D01* +X021461Y016166D01* +X021467Y016115D01* +X021469Y016064D01* +X021467Y016013D01* +X021461Y015962D01* +X021451Y015912D01* +X021438Y015862D01* +X021420Y015814D01* +X021400Y015767D01* +X021375Y015722D01* +X021347Y015679D01* +X021316Y015638D01* +X021282Y015600D01* +X021245Y015565D01* +X021206Y015532D01* +X021164Y015502D01* +X021120Y015476D01* +X021074Y015454D01* +X021026Y015434D01* +X020977Y015419D01* +X020927Y015407D01* +X020877Y015399D01* +X020826Y015395D01* +X020774Y015395D01* +X020723Y015399D01* +X020673Y015407D01* +X020623Y015419D01* +X020574Y015434D01* +X020526Y015454D01* +X020480Y015476D01* +X020436Y015502D01* +X020394Y015532D01* +X020355Y015565D01* +X020318Y015600D01* +X020284Y015638D01* +X020253Y015679D01* +X020225Y015722D01* +X020200Y015767D01* +X020180Y015814D01* +X020162Y015862D01* +X020149Y015912D01* +X020139Y015962D01* +X020133Y016013D01* +X020131Y016064D01* +X023764Y013422D02* +X016441Y013422D01* +X016441Y008007D01* +X023764Y008007D01* +X023764Y013422D01* +X013874Y007472D02* +X013874Y003456D01* +X011826Y003456D01* +X011826Y007472D01* +X011484Y008109D02* +X011484Y012120D01* +X008060Y007206D02* +X005640Y007206D01* +X005640Y003522D01* +X008060Y003522D01* +X008060Y007206D01* +X001681Y005114D02* +X001683Y005165D01* +X001689Y005216D01* +X001699Y005266D01* +X001712Y005316D01* +X001730Y005364D01* +X001750Y005411D01* +X001775Y005456D01* +X001803Y005499D01* +X001834Y005540D01* +X001868Y005578D01* +X001905Y005613D01* +X001944Y005646D01* +X001986Y005676D01* +X002030Y005702D01* +X002076Y005724D01* +X002124Y005744D01* +X002173Y005759D01* +X002223Y005771D01* +X002273Y005779D01* +X002324Y005783D01* +X002376Y005783D01* +X002427Y005779D01* +X002477Y005771D01* +X002527Y005759D01* +X002576Y005744D01* +X002624Y005724D01* +X002670Y005702D01* +X002714Y005676D01* +X002756Y005646D01* +X002795Y005613D01* +X002832Y005578D01* +X002866Y005540D01* +X002897Y005499D01* +X002925Y005456D01* +X002950Y005411D01* +X002970Y005364D01* +X002988Y005316D01* +X003001Y005266D01* +X003011Y005216D01* +X003017Y005165D01* +X003019Y005114D01* +X003017Y005063D01* +X003011Y005012D01* +X003001Y004962D01* +X002988Y004912D01* +X002970Y004864D01* +X002950Y004817D01* +X002925Y004772D01* +X002897Y004729D01* +X002866Y004688D01* +X002832Y004650D01* +X002795Y004615D01* +X002756Y004582D01* +X002714Y004552D01* +X002670Y004526D01* +X002624Y004504D01* +X002576Y004484D01* +X002527Y004469D01* +X002477Y004457D01* +X002427Y004449D01* +X002376Y004445D01* +X002324Y004445D01* +X002273Y004449D01* +X002223Y004457D01* +X002173Y004469D01* +X002124Y004484D01* +X002076Y004504D01* +X002030Y004526D01* +X001986Y004552D01* +X001944Y004582D01* +X001905Y004615D01* +X001868Y004650D01* +X001834Y004688D01* +X001803Y004729D01* +X001775Y004772D01* +X001750Y004817D01* +X001730Y004864D01* +X001712Y004912D01* +X001699Y004962D01* +X001689Y005012D01* +X001683Y005063D01* +X001681Y005114D01* +X001631Y016064D02* +X001633Y016115D01* +X001639Y016166D01* +X001649Y016216D01* +X001662Y016266D01* +X001680Y016314D01* +X001700Y016361D01* +X001725Y016406D01* +X001753Y016449D01* +X001784Y016490D01* +X001818Y016528D01* +X001855Y016563D01* +X001894Y016596D01* +X001936Y016626D01* +X001980Y016652D01* +X002026Y016674D01* +X002074Y016694D01* +X002123Y016709D01* +X002173Y016721D01* +X002223Y016729D01* +X002274Y016733D01* +X002326Y016733D01* +X002377Y016729D01* +X002427Y016721D01* +X002477Y016709D01* +X002526Y016694D01* +X002574Y016674D01* +X002620Y016652D01* +X002664Y016626D01* +X002706Y016596D01* +X002745Y016563D01* +X002782Y016528D01* +X002816Y016490D01* +X002847Y016449D01* +X002875Y016406D01* +X002900Y016361D01* +X002920Y016314D01* +X002938Y016266D01* +X002951Y016216D01* +X002961Y016166D01* +X002967Y016115D01* +X002969Y016064D01* +X002967Y016013D01* +X002961Y015962D01* +X002951Y015912D01* +X002938Y015862D01* +X002920Y015814D01* +X002900Y015767D01* +X002875Y015722D01* +X002847Y015679D01* +X002816Y015638D01* +X002782Y015600D01* +X002745Y015565D01* +X002706Y015532D01* +X002664Y015502D01* +X002620Y015476D01* +X002574Y015454D01* +X002526Y015434D01* +X002477Y015419D01* +X002427Y015407D01* +X002377Y015399D01* +X002326Y015395D01* +X002274Y015395D01* +X002223Y015399D01* +X002173Y015407D01* +X002123Y015419D01* +X002074Y015434D01* +X002026Y015454D01* +X001980Y015476D01* +X001936Y015502D01* +X001894Y015532D01* +X001855Y015565D01* +X001818Y015600D01* +X001784Y015638D01* +X001753Y015679D01* +X001725Y015722D01* +X001700Y015767D01* +X001680Y015814D01* +X001662Y015862D01* +X001649Y015912D01* +X001639Y015962D01* +X001633Y016013D01* +X001631Y016064D01* +X020131Y005064D02* +X020133Y005115D01* +X020139Y005166D01* +X020149Y005216D01* +X020162Y005266D01* +X020180Y005314D01* +X020200Y005361D01* +X020225Y005406D01* +X020253Y005449D01* +X020284Y005490D01* +X020318Y005528D01* +X020355Y005563D01* +X020394Y005596D01* +X020436Y005626D01* +X020480Y005652D01* +X020526Y005674D01* +X020574Y005694D01* +X020623Y005709D01* +X020673Y005721D01* +X020723Y005729D01* +X020774Y005733D01* +X020826Y005733D01* +X020877Y005729D01* +X020927Y005721D01* +X020977Y005709D01* +X021026Y005694D01* +X021074Y005674D01* +X021120Y005652D01* +X021164Y005626D01* +X021206Y005596D01* +X021245Y005563D01* +X021282Y005528D01* +X021316Y005490D01* +X021347Y005449D01* +X021375Y005406D01* +X021400Y005361D01* +X021420Y005314D01* +X021438Y005266D01* +X021451Y005216D01* +X021461Y005166D01* +X021467Y005115D01* +X021469Y005064D01* +X021467Y005013D01* +X021461Y004962D01* +X021451Y004912D01* +X021438Y004862D01* +X021420Y004814D01* +X021400Y004767D01* +X021375Y004722D01* +X021347Y004679D01* +X021316Y004638D01* +X021282Y004600D01* +X021245Y004565D01* +X021206Y004532D01* +X021164Y004502D01* +X021120Y004476D01* +X021074Y004454D01* +X021026Y004434D01* +X020977Y004419D01* +X020927Y004407D01* +X020877Y004399D01* +X020826Y004395D01* +X020774Y004395D01* +X020723Y004399D01* +X020673Y004407D01* +X020623Y004419D01* +X020574Y004434D01* +X020526Y004454D01* +X020480Y004476D01* +X020436Y004502D01* +X020394Y004532D01* +X020355Y004565D01* +X020318Y004600D01* +X020284Y004638D01* +X020253Y004679D01* +X020225Y004722D01* +X020200Y004767D01* +X020180Y004814D01* +X020162Y004862D01* +X020149Y004912D01* +X020139Y004962D01* +X020133Y005013D01* +X020131Y005064D01* +D15* +X018017Y003995D02* +X017710Y003995D01* +X017710Y003765D01* +X017863Y003841D01* +X017940Y003841D01* +X018017Y003765D01* +X018017Y003611D01* +X017940Y003534D01* +X017786Y003534D01* +X017710Y003611D01* +X017556Y003534D02* +X017403Y003688D01* +X017479Y003688D02* +X017249Y003688D01* +X017249Y003534D02* +X017249Y003995D01* +X017479Y003995D01* +X017556Y003918D01* +X017556Y003765D01* +X017479Y003688D01* +X016918Y003628D02* +X016611Y003628D01* +X016764Y003628D02* +X016764Y004088D01* +X016611Y003935D01* +X016457Y004012D02* +X016457Y003705D01* +X016380Y003628D01* +X016150Y003628D01* +X016150Y004088D01* +X016380Y004088D01* +X016457Y004012D01* +X015997Y004088D02* +X015690Y004088D01* +X015690Y003628D01* +X015997Y003628D01* +X015843Y003858D02* +X015690Y003858D01* +X015536Y003628D02* +X015229Y003628D01* +X015229Y004088D01* +X015596Y006214D02* +X015903Y006214D01* +X015980Y006290D01* +X015980Y006444D01* +X015903Y006520D01* +X015903Y006674D02* +X015980Y006751D01* +X015980Y006904D01* +X015903Y006981D01* +X015750Y006981D01* +X015673Y006904D01* +X015673Y006827D01* +X015750Y006674D01* +X015520Y006674D01* +X015520Y006981D01* +X015596Y006520D02* +X015520Y006444D01* +X015520Y006290D01* +X015596Y006214D01* +X012602Y007640D02* +X012295Y007640D01* +X012602Y007947D01* +X012602Y008024D01* +X012525Y008101D01* +X012372Y008101D01* +X012295Y008024D01* +X012142Y008101D02* +X012142Y007717D01* +X012065Y007640D01* +X011911Y007640D01* +X011835Y007717D01* +X011835Y008101D01* +X010261Y006645D02* +X010030Y006415D01* +X010337Y006415D01* +X010261Y006645D02* +X010261Y006184D01* +X009877Y006184D02* +X009723Y006338D01* +X009800Y006338D02* +X009570Y006338D01* +X009570Y006184D02* +X009570Y006645D01* +X009800Y006645D01* +X009877Y006568D01* +X009877Y006415D01* +X009800Y006338D01* +X009847Y003695D02* +X009770Y003618D01* +X009770Y003311D01* +X009847Y003234D01* +X010000Y003234D01* +X010077Y003311D01* +X010230Y003465D02* +X010537Y003465D01* +X010461Y003695D02* +X010461Y003234D01* +X010230Y003465D02* +X010461Y003695D01* +X010077Y003618D02* +X010000Y003695D01* +X009847Y003695D01* +X006311Y007384D02* +X006311Y007845D01* +X006080Y007615D01* +X006387Y007615D01* +X005927Y007461D02* +X005927Y007845D01* +X005620Y007845D02* +X005620Y007461D01* +X005697Y007384D01* +X005850Y007384D01* +X005927Y007461D01* +X004261Y010084D02* +X004107Y010084D01* +X004030Y010161D01* +X003877Y010084D02* +X003723Y010238D01* +X003800Y010238D02* +X003570Y010238D01* +X003570Y010084D02* +X003570Y010545D01* +X003800Y010545D01* +X003877Y010468D01* +X003877Y010315D01* +X003800Y010238D01* +X004030Y010468D02* +X004107Y010545D01* +X004261Y010545D01* +X004337Y010468D01* +X004337Y010391D01* +X004261Y010315D01* +X004337Y010238D01* +X004337Y010161D01* +X004261Y010084D01* +X004261Y010315D02* +X004184Y010315D01* +X004207Y013484D02* +X004130Y013561D01* +X004207Y013484D02* +X004361Y013484D01* +X004437Y013561D01* +X004437Y013638D01* +X004361Y013715D01* +X004284Y013715D01* +X004361Y013715D02* +X004437Y013791D01* +X004437Y013868D01* +X004361Y013945D01* +X004207Y013945D01* +X004130Y013868D01* +X003977Y013868D02* +X003900Y013945D01* +X003747Y013945D01* +X003670Y013868D01* +X003670Y013561D01* +X003747Y013484D01* +X003900Y013484D01* +X003977Y013561D01* +X006649Y014334D02* +X006649Y014795D01* +X006879Y014795D01* +X006956Y014718D01* +X006956Y014565D01* +X006879Y014488D01* +X006649Y014488D01* +X006803Y014488D02* +X006956Y014334D01* +X007110Y014334D02* +X007417Y014334D01* +X007263Y014334D02* +X007263Y014795D01* +X007110Y014641D01* +X008386Y014156D02* +X008386Y016479D01* +X009606Y016479D01* +X010394Y016479D02* +X011614Y016479D01* +X011614Y014156D01* +X010709Y013250D01* +X010394Y013250D01* +X009606Y013250D02* +X009291Y013250D01* +X008386Y014156D01* +X009646Y013348D02* +X009569Y013368D01* +X009494Y013391D01* +X009420Y013419D01* +X009348Y013450D01* +X009277Y013485D01* +X009208Y013523D01* +X009142Y013565D01* +X009077Y013610D01* +X009015Y013658D01* +X008955Y013710D01* +X008898Y013764D01* +X008844Y013821D01* +X008792Y013881D01* +X008744Y013943D01* +X008699Y014008D01* +X008658Y014075D01* +X008620Y014144D01* +X008585Y014215D01* +X008554Y014287D01* +X008526Y014361D01* +X008503Y014436D01* +X008483Y014512D01* +X008467Y014590D01* +X008455Y014668D01* +X008447Y014746D01* +X008443Y014825D01* +X008443Y014903D01* +X008447Y014982D01* +X008455Y015060D01* +X008467Y015138D01* +X008483Y015216D01* +X008503Y015292D01* +X008526Y015367D01* +X008554Y015441D01* +X008585Y015513D01* +X008620Y015584D01* +X008658Y015653D01* +X008699Y015720D01* +X008744Y015785D01* +X008792Y015847D01* +X008844Y015907D01* +X008898Y015964D01* +X008955Y016018D01* +X009015Y016070D01* +X009077Y016118D01* +X009142Y016163D01* +X009208Y016205D01* +X009277Y016243D01* +X009348Y016278D01* +X009420Y016309D01* +X009494Y016337D01* +X009569Y016360D01* +X009646Y016380D01* +X010354Y016380D02* +X010431Y016360D01* +X010506Y016337D01* +X010580Y016309D01* +X010652Y016278D01* +X010723Y016243D01* +X010792Y016205D01* +X010858Y016163D01* +X010923Y016118D01* +X010985Y016070D01* +X011045Y016018D01* +X011102Y015964D01* +X011156Y015907D01* +X011208Y015847D01* +X011256Y015785D01* +X011301Y015720D01* +X011342Y015653D01* +X011380Y015584D01* +X011415Y015513D01* +X011446Y015441D01* +X011474Y015367D01* +X011497Y015292D01* +X011517Y015216D01* +X011533Y015138D01* +X011545Y015060D01* +X011553Y014982D01* +X011557Y014903D01* +X011557Y014825D01* +X011553Y014746D01* +X011545Y014668D01* +X011533Y014590D01* +X011517Y014512D01* +X011497Y014436D01* +X011474Y014361D01* +X011446Y014287D01* +X011415Y014215D01* +X011380Y014144D01* +X011342Y014075D01* +X011301Y014008D01* +X011256Y013943D01* +X011208Y013881D01* +X011156Y013821D01* +X011102Y013764D01* +X011045Y013710D01* +X010985Y013658D01* +X010923Y013610D01* +X010858Y013565D01* +X010792Y013523D01* +X010723Y013485D01* +X010652Y013450D01* +X010580Y013419D01* +X010506Y013391D01* +X010431Y013368D01* +X010354Y013348D01* +X011749Y012395D02* +X011749Y012011D01* +X011826Y011934D01* +X011979Y011934D01* +X012056Y012011D01* +X012056Y012395D01* +X012210Y012241D02* +X012363Y012395D01* +X012363Y011934D01* +X012210Y011934D02* +X012517Y011934D01* +X013148Y012406D02* +X012242Y013312D01* +X012242Y016422D01* +X013856Y016422D01* +X014644Y016422D02* +X016258Y016422D01* +X016258Y013312D01* +X015352Y012406D01* +X014644Y012406D01* +X013856Y012406D02* +X013148Y012406D01* +X014849Y010645D02* +X014849Y010184D01* +X015156Y010184D01* +X015310Y010184D02* +X015617Y010184D01* +X015463Y010184D02* +X015463Y010645D01* +X015310Y010491D01* +X015320Y009295D02* +X015550Y009295D01* +X015627Y009218D01* +X015627Y009065D01* +X015550Y008988D01* +X015320Y008988D01* +X015473Y008988D02* +X015627Y008834D01* +X015780Y008834D02* +X016087Y009141D01* +X016087Y009218D01* +X016011Y009295D01* +X015857Y009295D01* +X015780Y009218D01* +X015780Y008834D02* +X016087Y008834D01* +X015320Y008834D02* +X015320Y009295D01* +X014644Y012504D02* +X014729Y012524D01* +X014813Y012547D01* +X014896Y012574D01* +X014978Y012605D01* +X015058Y012639D01* +X015137Y012678D01* +X015214Y012719D01* +X015289Y012764D01* +X015362Y012812D01* +X015433Y012864D01* +X015501Y012918D01* +X015567Y012976D01* +X015630Y013036D01* +X015690Y013099D01* +X015748Y013165D01* +X015802Y013234D01* +X015854Y013304D01* +X015902Y013377D01* +X015946Y013453D01* +X015988Y013530D01* +X016026Y013608D01* +X016060Y013689D01* +X016091Y013771D01* +X016118Y013854D01* +X016141Y013938D01* +X016160Y014023D01* +X016176Y014109D01* +X016188Y014196D01* +X016196Y014283D01* +X016200Y014370D01* +X016200Y014458D01* +X016196Y014545D01* +X016188Y014632D01* +X016176Y014719D01* +X016160Y014805D01* +X016141Y014890D01* +X016118Y014974D01* +X016091Y015057D01* +X016060Y015139D01* +X016026Y015220D01* +X015988Y015298D01* +X015946Y015375D01* +X015902Y015451D01* +X015854Y015524D01* +X015802Y015594D01* +X015748Y015663D01* +X015690Y015729D01* +X015630Y015792D01* +X015567Y015852D01* +X015501Y015910D01* +X015433Y015964D01* +X015362Y016016D01* +X015289Y016064D01* +X015214Y016109D01* +X015137Y016150D01* +X015058Y016189D01* +X014978Y016223D01* +X014896Y016254D01* +X014813Y016281D01* +X014729Y016304D01* +X014644Y016324D01* +X013856Y016324D02* +X013771Y016304D01* +X013687Y016281D01* +X013604Y016254D01* +X013522Y016223D01* +X013442Y016189D01* +X013363Y016150D01* +X013286Y016109D01* +X013211Y016064D01* +X013138Y016016D01* +X013067Y015964D01* +X012999Y015910D01* +X012933Y015852D01* +X012870Y015792D01* +X012810Y015729D01* +X012752Y015663D01* +X012698Y015594D01* +X012646Y015524D01* +X012598Y015451D01* +X012554Y015375D01* +X012512Y015298D01* +X012474Y015220D01* +X012440Y015139D01* +X012409Y015057D01* +X012382Y014974D01* +X012359Y014890D01* +X012340Y014805D01* +X012324Y014719D01* +X012312Y014632D01* +X012304Y014545D01* +X012300Y014458D01* +X012300Y014370D01* +X012304Y014283D01* +X012312Y014196D01* +X012324Y014109D01* +X012340Y014023D01* +X012359Y013938D01* +X012382Y013854D01* +X012409Y013771D01* +X012440Y013689D01* +X012474Y013608D01* +X012512Y013530D01* +X012554Y013453D01* +X012598Y013377D01* +X012646Y013304D01* +X012698Y013234D01* +X012752Y013165D01* +X012810Y013099D01* +X012870Y013036D01* +X012933Y012976D01* +X012999Y012918D01* +X013067Y012864D01* +X013138Y012812D01* +X013211Y012764D01* +X013286Y012719D01* +X013363Y012678D01* +X013442Y012639D01* +X013522Y012605D01* +X013604Y012574D01* +X013687Y012547D01* +X013771Y012524D01* +X013856Y012504D01* +D16* +X011780Y011454D03* +X011780Y010784D03* +X011780Y010114D03* +X011780Y009444D03* +X011780Y008774D03* +D17* +X015534Y016610D02* +X015657Y016610D01* +X015719Y016672D01* +X015841Y016610D02* +X016088Y016857D01* +X016088Y016919D01* +X016026Y016981D01* +X015902Y016981D01* +X015841Y016919D01* +X015719Y016919D02* +X015657Y016981D01* +X015534Y016981D01* +X015472Y016919D01* +X015472Y016672D01* +X015534Y016610D01* +X015841Y016610D02* +X016088Y016610D01* +X011491Y016701D02* +X011244Y016701D01* +X011368Y016701D02* +X011368Y017071D01* +X011244Y016948D01* +X011123Y017010D02* +X011061Y017071D01* +X010938Y017071D01* +X010876Y017010D01* +X010876Y016763D01* +X010938Y016701D01* +X011061Y016701D01* +X011123Y016763D01* +D18* +X022869Y013789D02* +X022869Y007639D01* +D19* +X022634Y007796D03* +X022634Y013633D03* +D20* +X016200Y004573D02* +X016259Y004514D01* +X016190Y004445D01* +X016131Y004504D01* +X016200Y004573D01* +D21* +X016092Y004672D03* +M02* diff --git a/gerber/tests/test_cam.py b/gerber/tests/test_cam.py index 4af1984..ce4ec44 100644 --- a/gerber/tests/test_cam.py +++ b/gerber/tests/test_cam.py @@ -7,12 +7,6 @@ from ..cam import CamFile, FileSettings from tests import * -def test_smoke_filesettings(): - """ Smoke test FileSettings class - """ - fs = FileSettings() - - def test_filesettings_defaults(): """ Test FileSettings default values """ @@ -37,14 +31,38 @@ def test_filesettings_assign(): """ Test FileSettings attribute assignment """ fs = FileSettings() - fs.units = 'test' - fs.notation = 'test' - fs.zero_suppression = 'test' - fs.format = 'test' - assert_equal(fs.units, 'test') - assert_equal(fs.notation, 'test') - assert_equal(fs.zero_suppression, 'test') - assert_equal(fs.format, 'test') - -def test_smoke_camfile(): - cf = CamFile + fs.units = 'test1' + fs.notation = 'test2' + fs.zero_suppression = 'test3' + fs.format = 'test4' + assert_equal(fs.units, 'test1') + assert_equal(fs.notation, 'test2') + assert_equal(fs.zero_suppression, 'test3') + assert_equal(fs.format, 'test4') + + +def test_filesettings_dict_assign(): + """ Test FileSettings dict-style attribute assignment + """ + fs = FileSettings() + fs['units'] = 'metric' + fs['notation'] = 'incremental' + fs['zero_suppression'] = 'leading' + fs['format'] = (1, 2) + assert_equal(fs.units, 'metric') + assert_equal(fs.notation, 'incremental') + 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()) + + \ No newline at end of file diff --git a/gerber/tests/test_common.py b/gerber/tests/test_common.py new file mode 100644 index 0000000..1e1efe5 --- /dev/null +++ b/gerber/tests/test_common.py @@ -0,0 +1,24 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# Author: Hamilton Kibbe +from ..common import read +from ..excellon import ExcellonFile +from ..rs274x import GerberFile +from tests import * + +import os + + +NCDRILL_FILE = os.path.join(os.path.dirname(__file__), + 'resources/ncdrill.DRD') +TOP_COPPER_FILE = os.path.join(os.path.dirname(__file__), + '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)) diff --git a/gerber/tests/test_excellon.py b/gerber/tests/test_excellon.py new file mode 100644 index 0000000..72e3d7d --- /dev/null +++ b/gerber/tests/test_excellon.py @@ -0,0 +1,32 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# Author: Hamilton Kibbe +from ..excellon import read, detect_excellon_format, ExcellonFile +from tests import * + +import os + +NCDRILL_FILE = os.path.join(os.path.dirname(__file__), + 'resources/ncdrill.DRD') + +def test_format_detection(): + """ Test file type detection + """ + settings = detect_excellon_format(NCDRILL_FILE) + assert_equal(settings['format'], (2, 4)) + assert_equal(settings['zero_suppression'], 'leading') + +def test_read(): + ncdrill = read(NCDRILL_FILE) + assert(isinstance(ncdrill, ExcellonFile)) + +def test_read_settings(): + ncdrill = read(NCDRILL_FILE) + assert_equal(ncdrill.settings.format, (2, 4)) + assert_equal(ncdrill.settings.zero_suppression, 'leading') + + + + + diff --git a/gerber/tests/test_excellon_statements.py b/gerber/tests/test_excellon_statements.py index 5e5e8dc..f2e17ee 100644 --- a/gerber/tests/test_excellon_statements.py +++ b/gerber/tests/test_excellon_statements.py @@ -5,14 +5,15 @@ from .tests import assert_equal, assert_raises from ..excellon_statements import * +from ..cam import FileSettings def test_excellontool_factory(): """ Test ExcellonTool factory method """ exc_line = 'T8F00S00C0.12500' - settings = {'format': (2, 5), 'zero_suppression': 'trailing', - 'units': 'inch', 'notation': 'absolute'} + settings = FileSettings(format=(2, 5), zero_suppression='trailing', + units='inch', notation='absolute') tool = ExcellonTool.from_excellon(exc_line, settings) assert_equal(tool.diameter, 0.125) assert_equal(tool.feed_rate, 0) @@ -25,16 +26,16 @@ def test_excellontool_dump(): exc_lines = ['T1F00S00C0.01200', 'T2F00S00C0.01500', 'T3F00S00C0.01968', 'T4F00S00C0.02800', 'T5F00S00C0.03300', 'T6F00S00C0.03800', 'T7F00S00C0.04300', 'T8F00S00C0.12500', 'T9F00S00C0.13000', ] - settings = {'format': (2, 5), 'zero_suppression': 'trailing', - 'units': 'inch', 'notation': 'absolute'} + settings = FileSettings(format=(2, 5), zero_suppression='trailing', + units='inch', notation='absolute') for line in exc_lines: tool = ExcellonTool.from_excellon(line, settings) assert_equal(tool.to_excellon(), line) def test_excellontool_order(): - settings = {'format': (2, 5), 'zero_suppression': 'trailing', - 'units': 'inch', 'notation': 'absolute'} + settings = FileSettings(format=(2, 5), zero_suppression='trailing', + units='inch', notation='absolute') line = 'T8F00S00C0.12500' tool1 = ExcellonTool.from_excellon(line, settings) line = 'T8C0.12500F00S00' diff --git a/gerber/tests/test_rs274x.py b/gerber/tests/test_rs274x.py new file mode 100644 index 0000000..f66a09e --- /dev/null +++ b/gerber/tests/test_rs274x.py @@ -0,0 +1,16 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# Author: Hamilton Kibbe +from ..rs274x import read, GerberFile +from tests import * + +import os + +TOP_COPPER_FILE = os.path.join(os.path.dirname(__file__), + 'resources/top_copper.GTL') + + +def test_read(): + top_copper = read(TOP_COPPER_FILE) + assert(isinstance(top_copper, GerberFile)) diff --git a/gerber/utils.py b/gerber/utils.py index fce6369..31ff196 100644 --- a/gerber/utils.py +++ b/gerber/utils.py @@ -44,6 +44,10 @@ def parse_gerber_value(value, format=(2, 5), zero_suppression='trailing'): The specified value as a floating-point number. """ + # Handle excellon edge case with explicit decimal. "That was easy!" + if '.' in value: + return float(value) + # Format precision integer_digits, decimal_digits = format MAX_DIGITS = integer_digits + decimal_digits @@ -55,23 +59,20 @@ 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.strip(' +') + #value = value.strip() + value = value.lstrip('+') negative = '-' in value if negative: - value = value.strip(' -') + value = value.lstrip('-') - # Handle excellon edge case with explicit decimal. "That was easy!" - if '.' in value: - return float(value) - digits = [digit for digit in '0' * MAX_DIGITS] + 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 result = float(''.join(digits[:integer_digits] + ['.'] + digits[integer_digits:])) - return -1.0 * result if negative else result + return -result if negative else result def write_gerber_value(value, format=(2, 5), zero_suppression='trailing'): -- cgit From 4f076d7b769b0f488888d268a9a199b7545afdd7 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Sun, 26 Oct 2014 17:59:57 -0400 Subject: Merge aperture fixses from upstream --- gerber/gerber_statements.py | 8 ++++---- gerber/rs274x.py | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) (limited to 'gerber') diff --git a/gerber/gerber_statements.py b/gerber/gerber_statements.py index 44eeee0..e392ec5 100644 --- a/gerber/gerber_statements.py +++ b/gerber/gerber_statements.py @@ -308,9 +308,6 @@ class ADParamStmt(ParamStmt): d = int(stmt_dict.get('d')) shape = stmt_dict.get('shape') modifiers = stmt_dict.get('modifiers') - if modifiers is not None: - modifiers = [[float(x) for x in m.split('X')] - for m in modifiers.split(',')] return cls(param, d, shape, modifiers) def __init__(self, param, d, shape, modifiers): @@ -339,7 +336,10 @@ class ADParamStmt(ParamStmt): ParamStmt.__init__(self, param) self.d = d self.shape = shape - self.modifiers = modifiers + if modifiers is not None: + self.modifiers = [[x for x in m.split("X")] for m in modifiers.split(",")] + else: + self.modifiers = [] def to_gerber(self, settings): return '%ADD{0}{1},{2}*%'.format(self.d, self.shape, diff --git a/gerber/rs274x.py b/gerber/rs274x.py index 739c253..f18a35d 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -118,7 +118,7 @@ class GerberParser(object): NUMBER = r"[\+-]?\d+" DECIMAL = r"[\+-]?\d+([.]?\d+)?" STRING = r"[a-zA-Z0-9_+\-/!?<>”’(){}.\|&@# :]+" - NAME = "[a-zA-Z_$][a-zA-Z_$0-9]+" + NAME = r"[a-zA-Z_$][a-zA-Z_$0-9]+" FUNCTION = r"G\d{2}" COORD_OP = r"D[0]?[123]" @@ -128,10 +128,10 @@ class GerberParser(object): IP = r"(?PIP)(?P(POS|NEG))" LP = r"(?PLP)(?P(D|C))" AD_CIRCLE = r"(?PAD)D(?P\d+)(?PC)[,](?P[^,]*)" - AD_RECT = r"(?PAD)D(?P\d+)(?PR)[,](?P[^,]*)" + AD_RECT = r"(?PAD)D(?P\d+)(?PR)[,]?(?P[^,]+)?" AD_OBROUND = r"(?PAD)D(?P\d+)(?PO)[,](?P[^,]*)" AD_POLY = r"(?PAD)D(?P\d+)(?PP)[,](?P[^,]*)" - AD_MACRO = r"(?PAD)D(?P\d+)+(?P{name})[,](?P[^,]*)".format(name=NAME) + AD_MACRO = r"(?PAD)D(?P\d+)(?P{name})[,]?(?P[^,]+)?".format(name=NAME) AM = r"(?PAM)(?P{name})\*(?P.*)".format(name=NAME) # begin deprecated @@ -140,7 +140,7 @@ class GerberParser(object): LN = r"(?PLN)(?P.*)" # end deprecated - PARAMS = (FS, MO, IP, LP, AD_CIRCLE, AD_RECT, AD_OBROUND, AD_MACRO, AD_POLY, AM, OF, IN, LN) + 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] COORD_STMT = re.compile(( -- cgit